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 env var when set' {
        $origKey = $env:OPENAI_API_KEY
        $env:OPENAI_API_KEY = 'test-key-123'
        try {
            InModuleScope PowerCraft.AI {
                $result = Resolve-PCApiKey -Provider 'openai'
                $result | Should -Be 'test-key-123'
            }
        }
        finally {
            if ($origKey) { $env:OPENAI_API_KEY = $origKey }
            else { Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue }
        }
    }

    It 'Returns null when no key available' {
        $origKeys = @{
            OPENAI_API_KEY = $env:OPENAI_API_KEY
        }
        Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue
        try {
            InModuleScope PowerCraft.AI {
                Mock Get-PCSecret { return $null }
                $result = Resolve-PCApiKey -Provider 'openai'
                $result | Should -BeNullOrEmpty
            }
        }
        finally {
            if ($origKeys.OPENAI_API_KEY) { $env:OPENAI_API_KEY = $origKeys.OPENAI_API_KEY }
        }
    }

    It 'Supports GEMINI_API_KEY env var' {
        $orig = $env:GEMINI_API_KEY
        $env:GEMINI_API_KEY = 'gemini-test-key'
        try {
            InModuleScope PowerCraft.AI {
                $result = Resolve-PCApiKey -Provider 'gemini'
                $result | Should -Be 'gemini-test-key'
            }
        }
        finally {
            if ($orig) { $env:GEMINI_API_KEY = $orig }
            else { Remove-Item env:GEMINI_API_KEY -ErrorAction SilentlyContinue }
        }
    }

    It 'Supports ANTHROPIC_API_KEY env var' {
        $orig = $env:ANTHROPIC_API_KEY
        $env:ANTHROPIC_API_KEY = 'claude-test-key'
        try {
            InModuleScope PowerCraft.AI {
                $result = Resolve-PCApiKey -Provider 'anthropic'
                $result | Should -Be 'claude-test-key'
            }
        }
        finally {
            if ($orig) { $env:ANTHROPIC_API_KEY = $orig }
            else { Remove-Item env:ANTHROPIC_API_KEY -ErrorAction SilentlyContinue }
        }
    }
}

Describe 'Get-PCProviders' {
    It 'Returns providers with configured keys' {
        $orig = $env:OPENAI_API_KEY
        $env:OPENAI_API_KEY = 'test-key'
        try {
            $result = Get-PCProviders
            $result | Where-Object Provider -eq 'openai' | Should -Not -BeNullOrEmpty
        }
        finally {
            if ($orig) { $env:OPENAI_API_KEY = $orig }
            else { Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue }
        }
    }

    It 'Shows DefaultModel for each provider' {
        $orig = $env:GEMINI_API_KEY
        $env:GEMINI_API_KEY = 'test-key'
        try {
            $result = Get-PCProviders
            $gemini = $result | Where-Object Provider -eq 'gemini'
            $gemini.DefaultModel | Should -Not -BeNullOrEmpty
        }
        finally {
            if ($orig) { $env:GEMINI_API_KEY = $orig }
            else { Remove-Item env:GEMINI_API_KEY -ErrorAction SilentlyContinue }
        }
    }
}

Describe 'Invoke-PCCompletion parameter validation' {
    It 'Throws when no provider can be resolved' {
        # Clear all env keys
        $origO = $env:OPENAI_API_KEY; $origG = $env:GEMINI_API_KEY; $origA = $env:ANTHROPIC_API_KEY
        Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue
        Remove-Item env:GEMINI_API_KEY -ErrorAction SilentlyContinue
        Remove-Item env:ANTHROPIC_API_KEY -ErrorAction SilentlyContinue
        try {
            { Invoke-PCCompletion -UserPrompt "test" } | Should -Throw '*No AI provider*'
        }
        finally {
            if ($origO) { $env:OPENAI_API_KEY = $origO }
            if ($origG) { $env:GEMINI_API_KEY = $origG }
            if ($origA) { $env:ANTHROPIC_API_KEY = $origA }
        }
    }

    It 'Throws when API key missing for specified provider' {
        $orig = $env:OPENAI_API_KEY
        Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue
        try {
            { Invoke-PCCompletion -Provider openai -UserPrompt "test" } | Should -Throw '*No API key*'
        }
        finally {
            if ($orig) { $env:OPENAI_API_KEY = $orig }
        }
    }

    It 'Auto-detects provider from model name' {
        $orig = $env:GEMINI_API_KEY
        $env:GEMINI_API_KEY = 'fake-key'
        try {
            # This will throw on API call but should correctly pick gemini provider
            { Invoke-PCCompletion -Model 'gemini-2.5-flash' -UserPrompt "test" } | Should -Throw
        }
        finally {
            if ($orig) { $env:GEMINI_API_KEY = $orig }
            else { Remove-Item env:GEMINI_API_KEY -ErrorAction SilentlyContinue }
        }
    }
}

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
        $origKey = $env:OPENAI_API_KEY
        Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue
        try {
            { Invoke-PCCompletionCached -UserPrompt "x" -CachePath $cacheDir -CacheKey 'ttl-expired' -CacheTTLHours 1 -Provider openai } | Should -Throw '*No API key*'
        }
        finally {
            if ($origKey) { $env:OPENAI_API_KEY = $origKey }
        }
    }
}

Describe 'Test-PCProvider' {
    It 'Returns false when no key configured' {
        $orig = $env:OPENAI_API_KEY
        Remove-Item env:OPENAI_API_KEY -ErrorAction SilentlyContinue
        try {
            $result = Test-PCProvider -Name openai -WarningAction SilentlyContinue
            $result | Should -BeFalse
        }
        finally {
            if ($orig) { $env:OPENAI_API_KEY = $orig }
        }
    }
}