Tests/PSAICloudflareClef.Tests.ps1

# Import the module and define request fixtures while replacing the HTTP boundary with a mock.
BeforeAll {
    # Resolve the module manifest from this repository's test folder.
    $modulePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'PSAICloudflareClef.psd1'

    # Import the module under test into the Pester session.
    Import-Module $modulePath -Force

    # Use fake credentials that are only inspected by the mock.
    $testToken = 'offline-test-token'
    $testAccount = 'account-123'

    # Define representative context and all three documented typed question kinds.
    $testState = @{ incident = 'Checkout returns 503.' }
    $testQuestions = [ordered]@{
        urgent = @{ type = 'noul'; instructions = 'Is this urgent?' }
        owner = @{ type = 'choice'; instructions = 'Which team owns this?'; criteria = @{ support = 'Customer issue'; billing = 'Invoice issue' } }
        severity = @{ type = 'score'; instructions = 'How severe is the impact?'; criteria = @('Minor', 'Major', 'Critical') }
    }
}

# Exercise request building, helper conversion, input validation, and safe failures entirely offline.
Describe 'Invoke-CloudflareClefDecision' {
    # Provide a representative parsed API response for all tests unless a case replaces it.
    BeforeEach {
        Mock Invoke-RestMethod -ModuleName PSAICloudflareClef {
            [pscustomobject]@{ success = $true; result = [pscustomobject]@{ model = 'clef'; answers = [pscustomobject]@{ urgent = [pscustomobject]@{ noul = 0.93 }; owner = [pscustomobject]@{ choice = 'support' }; severity = [pscustomobject]@{ score = 1.7 } } } }
        }
    }

    # Verify the URL, authorization, timeout, JSON request fields, and typed questions.
    It 'posts the Clef payload and returns the parsed response' {
        $response = Invoke-CloudflareClefDecision -State $testState -Questions $testQuestions -AccountId $testAccount -Token $testToken -TimeoutSec 45
        $response.answers.urgent.noul | Should -Be 0.93
        Should -Invoke Invoke-RestMethod -ModuleName PSAICloudflareClef -Times 1 -ParameterFilter {
            $Uri -eq 'https://api.cloudflare.com/client/v4/accounts/account-123/ai/run/@cf/cloudflare/clef' -and
            $Method -eq 'Post' -and $Headers.Authorization -eq 'Bearer offline-test-token' -and
            $ContentType -eq 'application/json' -and $TimeoutSec -eq 45 -and
            (($Body | ConvertFrom-Json).model -eq 'clef') -and
            (($Body | ConvertFrom-Json).questions.owner.type -eq 'choice') -and
            (($Body | ConvertFrom-Json).questions.severity.criteria[2] -eq 'Critical')
        }
    }

    # Verify that helper objects become exactly the named API question shape.
    It 'converts helper-created typed questions' {
        $questions = @(
            New-CloudflareClefQuestion -Name urgent -Type Noul -Instructions 'Should this be escalated?'
            New-CloudflareClefQuestion -Name category -Type Choice -Instructions 'Which category?' -Criteria @{ bug = 'A software issue'; request = 'A feature request' }
            New-CloudflareClefQuestion -Name severity -Type Score -Instructions 'How severe?' -Criteria @('Low', 'High')
        )
        $null = Invoke-CloudflareClefDecision -State $testState -Question $questions -AccountId $testAccount -Token $testToken
        Should -Invoke Invoke-RestMethod -ModuleName PSAICloudflareClef -Times 1 -ParameterFilter {
            $payload = $Body | ConvertFrom-Json
            $payload.questions.urgent.type -eq 'noul' -and $payload.questions.urgent.instructions -eq 'Should this be escalated?' -and
            $payload.questions.category.type -eq 'choice' -and $payload.questions.category.criteria.bug -eq 'A software issue' -and
            $payload.questions.severity.type -eq 'score' -and $payload.questions.severity.criteria[1] -eq 'High'
        }
    }

    # Verify the Flash selection changes both the URL path and model field.
    It 'supports the documented Clef Flash model option' {
        $null = Invoke-CloudflareClefDecision -State $testState -Questions @{ urgent = @{ type = 'noul'; instructions = 'Urgent?' } } -AccountId $testAccount -Token $testToken -Model ClefFlash
        Should -Invoke Invoke-RestMethod -ModuleName PSAICloudflareClef -Times 1 -ParameterFilter {
            $Uri -like '*/@cf/cloudflare/clef-flash' -and (($Body | ConvertFrom-Json).model -eq 'clef-flash')
        }
    }

    # Reject malformed question IDs, unsupported types, and excess counts before HTTP.
    It 'rejects invalid question identifiers before HTTP' {
        { Invoke-CloudflareClefDecision -State $testState -Questions @{ 'bad id' = @{ type = 'noul'; instructions = 'Valid?' } } -AccountId $testAccount -Token $testToken } | Should -Throw '*Question ID*'
        Should -Invoke Invoke-RestMethod -ModuleName PSAICloudflareClef -Times 0
    }

    # Reject invalid criteria containers to prevent sending malformed typed answers.
    It 'rejects invalid choice and score criteria before HTTP' {
        { Invoke-CloudflareClefDecision -State $testState -Questions @{ choose = @{ type = 'choice'; instructions = 'Pick'; criteria = @('one') } } -AccountId $testAccount -Token $testToken } | Should -Throw '*Choice question*'
        { Invoke-CloudflareClefDecision -State $testState -Questions @{ rate = @{ type = 'score'; instructions = 'Rate'; criteria = @('only one') } } -AccountId $testAccount -Token $testToken } | Should -Throw '*Score question*'
        Should -Invoke Invoke-RestMethod -ModuleName PSAICloudflareClef -Times 0
    }

    # Verify an embedded PNG data URI is retained in the request JSON.
    It 'includes valid embedded images and rejects remote image URLs' {
        $pngData = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/lN8AAAAASUVORK5CYII='
        $dataUri = "data:image/png;base64,$pngData"
        $null = Invoke-CloudflareClefDecision -State 'What is visible?' -Questions @{ scene = @{ type = 'noul'; instructions = 'Is a person visible?' } } -AccountId $testAccount -Token $testToken -Image $dataUri
        Should -Invoke Invoke-RestMethod -ModuleName PSAICloudflareClef -Times 1 -ParameterFilter { (($Body | ConvertFrom-Json).images[0] -like 'data:image/png;base64,*') }
        { Invoke-CloudflareClefDecision -State 'Check this' -Questions @{ q = @{ type = 'noul'; instructions = 'Yes?' } } -AccountId $testAccount -Token $testToken -Image 'https://example.com/image.png' } | Should -Throw '*embedded base64*'
    }

    # Confirm that missing credentials stop before any mocked HTTP call.
    It 'requires a token and account ID before HTTP' {
        { Invoke-CloudflareClefDecision -State $testState -Questions $testQuestions -AccountId '' -Token $testToken } | Should -Throw '*AccountId is required*'
        { Invoke-CloudflareClefDecision -State $testState -Questions $testQuestions -AccountId $testAccount -Token '' } | Should -Throw '*API token is required*'
        Should -Invoke Invoke-RestMethod -ModuleName PSAICloudflareClef -Times 0
    }

    # Simulate a 400 response and verify its status and body reach callers safely.
    It 'includes useful HTTP status and response details' {
        Mock Invoke-RestMethod -ModuleName PSAICloudflareClef {
            $response = [System.Net.Http.HttpResponseMessage]::new([System.Net.HttpStatusCode]::BadRequest)
            $response.Content = [System.Net.Http.StringContent]::new('{"error":"invalid request"}')
            $exception = [System.Net.Http.HttpRequestException]::new('Bad request')
            $exception | Add-Member -MemberType NoteProperty -Name Response -Value $response -Force
            throw $exception
        }
        { Invoke-CloudflareClefDecision -State $testState -Questions $testQuestions -AccountId $testAccount -Token $testToken } | Should -Throw '*HTTP 400*invalid request*'
    }
}