Tests/Get-JevScore.Tests.ps1

BeforeAll {
    Import-Module (Join-Path $PSScriptRoot '..' 'Jev.psd1') -Force
}

AfterAll {
    Remove-Module Jev -Force -ErrorAction SilentlyContinue
}

Describe 'Get-JevScore ordered ratings' {
    BeforeEach {
        $levels = @('Can wait', 'Needs attention soon', 'Needs attention now')
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State, $Questions)
            $Questions.Count | Should -Be 1
            $Questions.rating.type | Should -Be 'score'
            $Questions.rating.instructions | Should -Be 'How urgent is this?'
            $Questions.rating.criteria | Should -Be @('Can wait', 'Needs attention soon', 'Needs attention now')
            $score = switch ($State) {
                'No rush.' { 0.12 }
                'An event in two hours.' { 1.93 }
                default { 0.76 }
            }
            [pscustomobject]@{
                answers = @{ rating = @{ type = 'score'; score = $score; confidence = 0.8 } }
            }
        }
    }

    It 'binds quoted levels without commas and returns the unrounded weighted score' {
        $result = 'An event in two hours.' |
            Get-JevScore 'How urgent is this?' 'Can wait' 'Needs attention soon' 'Needs attention now'

        $result | Should -Be 1.93
        $result | Should -BeOfType [double]
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 1 -Scope It -ParameterFilter {
            $State -eq 'An event in two hours.'
        }
    }

    It 'returns one score per input in input order without sorting' {
        $results = @(@('An event in two hours.', 'No rush.', 'A current problem.') |
            Get-JevScore 'How urgent is this?' -Levels $levels)

        $results | Should -Be @(1.93, 0.12, 0.76)
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 3 -Scope It
    }

    It 'accepts fully named state, question, and levels' {
        Get-JevScore -State 'No rush.' -Question 'How urgent is this?' -Levels $levels |
            Should -Be 0.12
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 1 -Scope It
    }

    It 'accepts a named question and trailing levels with common parameters' {
        'An event in two hours.' | Get-JevScore -Question 'How urgent is this?' `
            'Can wait' 'Needs attention soon' 'Needs attention now' -ErrorAction Stop |
            Should -Be 1.93
    }

    It 'passes an object as the request state without altering its properties' {
        $record = [pscustomobject]@{ Id = 42; Message = 'A current problem.'; rating = 'Original field' }
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State, $Questions)
            [object]::ReferenceEquals($State, $record) | Should -BeTrue
            [pscustomobject]@{ answers = @{ rating = @{ score = 1.3 } } }
        }

        $record | Get-JevScore 'How urgent is this?' -Levels $levels | Should -Be 1.3
        $record.rating | Should -Be 'Original field'
        @($record.PSObject.Properties.Name) | Should -Be @('Id', 'Message', 'rating')
    }

    It 'keeps a named array as one state and accepts dictionary responses' {
        $stateArray = @('An event.', 'In two hours.')
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State, $Questions)
            [object]::ReferenceEquals($State, $stateArray) | Should -BeTrue
            @{ answers = @{ rating = @{ score = 1.9 } } }
        }

        Get-JevScore -State $stateArray 'How urgent is this?' -Levels $levels | Should -Be 1.9
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 1 -Scope It
    }

    It 'accepts a valid scale endpoint: <Score>' -TestCases @(
        @{ Score = 0 }
        @{ Score = 2 }
    ) {
        param($Score)
        Mock -ModuleName Jev Invoke-JevDecision {
            @{ answers = @{ rating = @{ score = $Score; confidence = 0.1 } } }
        }
        $result = 'A message.' | Get-JevScore 'How urgent is this?' -Levels $levels
        $result | Should -Be $Score
        $result | Should -BeOfType [double]
    }

    It 'supports the minimum two-level scale with a fractional score' {
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State, $Questions)
            $Questions.rating.criteria | Should -Be @('Low', 'High')
            @{ answers = @{ rating = @{ score = 0.65 } } }
        }
        'A message.' | Get-JevScore 'How urgent is this?' Low High | Should -Be 0.65
    }

    It 'supports ten levels and their zero-based maximum of nine' {
        $tenLevels = @(0..9 | ForEach-Object { "Level $_" })
        Mock -ModuleName Jev Invoke-JevDecision {
            param($State, $Questions)
            $Questions.rating.criteria.Count | Should -Be 10
            @{ answers = @{ rating = @{ score = 9 } } }
        }
        'A message.' | Get-JevScore 'How urgent is this?' -Levels $tenLevels | Should -Be 9
    }

    It 'makes no request for empty pipeline input' {
        @(@() | Get-JevScore 'How urgent is this?' -Levels $levels).Count | Should -Be 0
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 0 -Scope It
    }

    It 'rejects invalid levels before making a request: <Case>' -TestCases @(
        @{ Case = 'empty array'; Levels = @() }
        @{ Case = 'one level'; Levels = @('Only one') }
        @{ Case = 'eleven levels'; Levels = @('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10') }
        @{ Case = 'empty description'; Levels = @('Low', '') }
        @{ Case = 'whitespace description'; Levels = @('Low', ' ') }
    ) {
        param($Case, $Levels)
        { 'A message.' | Get-JevScore 'How urgent is this?' -Levels $Levels } | Should -Throw
        Should -Invoke Invoke-JevDecision -ModuleName Jev -Exactly 0 -Scope It
    }

    It 'rejects missing answers rather than returning zero: <Case>' -TestCases @(
        @{ Case = 'answers'; Response = [pscustomobject]@{ model = 'test' } }
        @{ Case = 'rating'; Response = [pscustomobject]@{ answers = @{} } }
        @{ Case = 'score'; Response = [pscustomobject]@{ answers = @{ rating = @{ confidence = 0.9 } } } }
        @{ Case = 'null score'; Response = [pscustomobject]@{ answers = @{ rating = @{ score = $null } } } }
    ) {
        param($Case, $Response)
        Mock -ModuleName Jev Invoke-JevDecision { $Response }
        { 'A message.' | Get-JevScore 'How urgent is this?' -Levels $levels } |
            Should -Throw '*Jev did not return*'
    }

    It 'rejects invalid returned scores: <Score>' -TestCases @(
        @{ Score = -0.1 }
        @{ Score = 2.1 }
        @{ Score = [double]::NaN }
        @{ Score = [double]::PositiveInfinity }
        @{ Score = 'not numeric' }
        @{ Score = '' }
        @{ Score = $true }
    ) {
        param($Score)
        Mock -ModuleName Jev Invoke-JevDecision {
            @{ answers = @{ rating = @{ score = $Score } } }
        }
        { 'A message.' | Get-JevScore 'How urgent is this?' -Levels $levels } |
            Should -Throw '*invalid score*'
    }

    It 'propagates request failures without fabricating a score' {
        Mock -ModuleName Jev Invoke-JevDecision { throw 'API unavailable.' }
        $emitted = [System.Collections.Generic.List[object]]::new()
        { 'A message.' | Get-JevScore 'How urgent is this?' -Levels $levels |
                ForEach-Object { $emitted.Add($_) } } | Should -Throw '*API unavailable*'
        $emitted.Count | Should -Be 0
    }
}