tests/PowerCraft.AI.Tests.ps1

BeforeAll {
    # Mock PowerCraft.Secrets dependency
    if (-not (Get-Module PowerCraft.Secrets -ErrorAction SilentlyContinue)) {
        # Create a minimal mock module so the manifest dependency is satisfied
        $mockModule = New-Module -Name PowerCraft.Secrets -ScriptBlock {
            function Get-PCSecret { param([string]$Name) return $null }
            function Get-PCSecretNames { return @() }
            Export-ModuleMember -Function *
        }
        Import-Module $mockModule -Force
    }

    $modulePath = Split-Path $PSScriptRoot -Parent
    Import-Module $modulePath -Force
}

Describe 'Module loads correctly' {
    It 'Exports expected functions' {
        $expected = @(
            'Invoke-PCCompletion'
            'Invoke-PCCompletionCached'
            'Get-PCProviders'
            'Test-PCProvider'
        )
        $exported = (Get-Module PowerCraft.AI).ExportedFunctions.Keys
        foreach ($fn in $expected) {
            $exported | Should -Contain $fn
        }
    }
}

Describe 'Provider configuration' {
    It 'Has openai provider defined' {
        InModuleScope PowerCraft.AI {
            $script:Providers.ContainsKey('openai') | Should -BeTrue
            $script:Providers.openai.DefaultModel | Should -Not -BeNullOrEmpty
        }
    }

    It 'Has gemini provider defined' {
        InModuleScope PowerCraft.AI {
            $script:Providers.ContainsKey('gemini') | Should -BeTrue
            $script:Providers.gemini.DefaultModel | Should -Not -BeNullOrEmpty
        }
    }

    It 'Has anthropic provider defined' {
        InModuleScope PowerCraft.AI {
            $script:Providers.ContainsKey('anthropic') | Should -BeTrue
            $script:Providers.anthropic.DefaultModel | Should -Not -BeNullOrEmpty
        }
    }
}

Describe 'Resolve-PCApiKey' {
    It 'Returns key from secrets store' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return 'secret-key-123' } -ParameterFilter { $Name -eq 'openai' }
            $result = Resolve-PCApiKey -Provider 'openai'
            $result | Should -Be 'secret-key-123'
        }
    }

    It 'Returns null when no key in secrets store' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return $null }
            $result = Resolve-PCApiKey -Provider 'openai'
            $result | Should -BeNullOrEmpty
        }
    }

    It 'Uses correct secret name per provider' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return 'gemini-key' } -ParameterFilter { $Name -eq 'gemini' }
            $result = Resolve-PCApiKey -Provider 'gemini'
            $result | Should -Be 'gemini-key'
        }
    }

    It 'Resolves anthropic provider' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return 'anthropic-key' } -ParameterFilter { $Name -eq 'anthropic' }
            $result = Resolve-PCApiKey -Provider 'anthropic'
            $result | Should -Be 'anthropic-key'
        }
    }
}

Describe 'Get-PCProviders' {
    It 'Returns providers with configured keys' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return 'test-key' } -ParameterFilter { $Name -eq 'openai' }
            $result = Get-PCProviders
            $result | Where-Object Provider -eq 'openai' | Should -Not -BeNullOrEmpty
        }
    }

    It 'Shows DefaultModel for each provider' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return 'test-key' } -ParameterFilter { $Name -eq 'gemini' }
            $result = Get-PCProviders
            $gemini = $result | Where-Object Provider -eq 'gemini'
            $gemini.DefaultModel | Should -Not -BeNullOrEmpty
        }
    }
}

Describe 'Invoke-PCCompletion parameter validation' {
    It 'Throws when no provider can be resolved' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return $null }
            { Invoke-PCCompletion -UserPrompt "test" } | Should -Throw '*No AI provider*'
        }
    }

    It 'Throws when API key missing for specified provider' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return $null }
            { Invoke-PCCompletion -Provider openai -UserPrompt "test" } | Should -Throw '*No API key*'
        }
    }

    It 'Auto-detects provider from model name' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return 'fake-key' } -ParameterFilter { $Name -eq 'gemini' }
            # This will throw on API call but should correctly pick gemini provider
            { Invoke-PCCompletion -Model 'gemini-2.5-flash' -UserPrompt "test" } | Should -Throw
        }
    }
}

Describe 'Invoke-PCCompletionCached' {
    BeforeAll {
        $cacheDir = Join-Path $TestDrive 'ai-cache'
    }

    It 'Creates cache file on miss' {
        # Pre-populate a cache file to simulate a hit (we cannot call real APIs)
        $cacheFile = Join-Path $cacheDir 'test-item.json'
        New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
        @{
            schema       = 'ai-response-cache'
            version      = '1.0'
            cache_key    = 'test-item'
            metadata     = @{
                model       = 'gpt-4o'
                provider    = 'openai'
                timestamp   = (Get-Date).ToString('o')
                duration_ms = 1000
                max_tokens  = 4096
                response_length = 5
            }
            raw_response = 'Hello'
        } | ConvertTo-Json -Depth 5 | Set-Content $cacheFile

        $result = Invoke-PCCompletionCached -UserPrompt "test" -CachePath $cacheDir -CacheKey 'test-item' -Provider openai
        $result | Should -Be 'Hello'
    }

    It 'Returns cached response on hit' {
        $cacheFile = Join-Path $cacheDir 'cached-hit.json'
        @{
            schema       = 'ai-response-cache'
            version      = '1.0'
            cache_key    = 'cached-hit'
            metadata     = @{
                model       = 'gpt-4o'
                provider    = 'openai'
                timestamp   = (Get-Date).ToString('o')
                duration_ms = 500
                max_tokens  = 4096
                response_length = 11
            }
            raw_response = 'cached reply'
        } | ConvertTo-Json -Depth 5 | Set-Content $cacheFile

        $result = Invoke-PCCompletionCached -UserPrompt "anything" -CachePath $cacheDir -CacheKey 'cached-hit' -Provider openai
        $result | Should -Be 'cached reply'
    }

    It 'Respects cache TTL — returns cached if not expired' {
        $cacheFile = Join-Path $cacheDir 'ttl-fresh.json'
        @{
            schema       = 'ai-response-cache'
            version      = '1.0'
            cache_key    = 'ttl-fresh'
            metadata     = @{
                model       = 'gpt-4o'
                provider    = 'openai'
                timestamp   = (Get-Date).ToString('o')
                duration_ms = 100
                max_tokens  = 4096
                response_length = 5
            }
            raw_response = 'fresh'
        } | ConvertTo-Json -Depth 5 | Set-Content $cacheFile

        $result = Invoke-PCCompletionCached -UserPrompt "x" -CachePath $cacheDir -CacheKey 'ttl-fresh' -CacheTTLHours 24
        $result | Should -Be 'fresh'
    }

    It 'Expires cache when TTL exceeded' {
        $cacheFile = Join-Path $cacheDir 'ttl-expired.json'
        @{
            schema       = 'ai-response-cache'
            version      = '1.0'
            cache_key    = 'ttl-expired'
            metadata     = @{
                model       = 'gpt-4o'
                provider    = 'openai'
                timestamp   = (Get-Date).AddHours(-48).ToString('o')
                duration_ms = 100
                max_tokens  = 4096
                response_length = 7
            }
            raw_response = 'expired'
        } | ConvertTo-Json -Depth 5 | Set-Content $cacheFile

        # Will try to call API and fail (no real key), which proves cache was skipped
        InModuleScope PowerCraft.AI -Parameters @{ cacheDir = $cacheDir } {
            Mock Get-PCSecret { return $null }
            { Invoke-PCCompletionCached -UserPrompt "x" -CachePath $cacheDir -CacheKey 'ttl-expired' -CacheTTLHours 1 -Provider openai } | Should -Throw '*No API key*'
        }
    }
}

Describe 'Test-PCProvider' {
    It 'Returns false when no key configured' {
        InModuleScope PowerCraft.AI {
            Mock Get-PCSecret { return $null }
            $result = Test-PCProvider -Name openai -WarningAction SilentlyContinue
            $result | Should -BeFalse
        }
    }
}