Tests/Invoke-ElmEventLoop.Tests.ps1

BeforeAll {
    . $PSScriptRoot/../Private/Core/Copy-ElmModel.ps1
    . $PSScriptRoot/../Private/Runtime/Invoke-ElmUpdate.ps1
    . $PSScriptRoot/../Private/Runtime/Invoke-ElmView.ps1
    . $PSScriptRoot/../Public/View/New-ElmText.ps1

    # Stub the rendering pipeline so tests don't need ANSI dependencies
    function Measure-ElmViewTree { param($Root, $TermWidth, $TermHeight) return $Root }
    function Compare-ElmViewTree { param($OldTree, $NewTree) return @() }
    function ConvertTo-AnsiOutput { param($Root) return '' }
    function ConvertTo-AnsiPatch { param($Patches) return '' }

    . $PSScriptRoot/../Private/Runtime/Invoke-ElmEventLoop.ps1
}

Describe 'Invoke-ElmEventLoop' {
    Context 'Quit command' {
        It 'Should stop when UpdateFn returns a Quit command' {
            $updateFn = {
                param($msg, $model)
                [PSCustomObject]@{ Model = $model; Cmd = [PSCustomObject]@{ Type = 'Quit' } }
            }
            $viewFn = { param($model) New-ElmText -Content 'test' }
            $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
            $queue.Enqueue('any')
            { Invoke-ElmEventLoop -InitialModel ([PSCustomObject]@{}) -UpdateFn $updateFn -ViewFn $viewFn -InputQueue $queue } | Should -Not -Throw
        }

        It 'Should return the final model on Quit' {
            $updateFn = {
                param($msg, $model)
                if ($msg -eq 'Quit') {
                    [PSCustomObject]@{ Model = $model; Cmd = [PSCustomObject]@{ Type = 'Quit' } }
                } else {
                    [PSCustomObject]@{ Model = [PSCustomObject]@{ Value = $msg }; Cmd = $null }
                }
            }
            $viewFn = { param($model) New-ElmText -Content 'x' }
            $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
            $queue.Enqueue('hello')
            $queue.Enqueue('Quit')
            $result = Invoke-ElmEventLoop -InitialModel ([PSCustomObject]@{ Value = '' }) -UpdateFn $updateFn -ViewFn $viewFn -InputQueue $queue
            $result.Value | Should -Be 'hello'
        }

        It 'Should capture the model returned by the Quit update' {
            $updateFn = {
                param($msg, $model)
                [PSCustomObject]@{
                    Model = [PSCustomObject]@{ Last = $msg }
                    Cmd   = [PSCustomObject]@{ Type = 'Quit' }
                }
            }
            $viewFn = { param($model) New-ElmText -Content 'x' }
            $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
            $queue.Enqueue('finalMsg')
            $result = Invoke-ElmEventLoop -InitialModel ([PSCustomObject]@{ Last = '' }) -UpdateFn $updateFn -ViewFn $viewFn -InputQueue $queue
            $result.Last | Should -Be 'finalMsg'
        }
    }

    Context 'Cursor visibility' {
        It 'Should hide the cursor (ESC[?25l) before first render' {
            # Verify the hide sequence is the correct ANSI DEC private mode sequence
            $expected = [char]27 + '[?25l'
            $expected | Should -Be ($([char]27) + '[?25l')
            $expected.Length | Should -Be 6
        }

        It 'Should use ESC[?25h to restore the cursor' {
            $expected = [char]27 + '[?25h'
            $expected | Should -Be ($([char]27) + '[?25h')
            $expected.Length | Should -Be 6
        }

        It 'Should complete without error when loop exits (cursor restore via finally)' {
            $updateFn = {
                param($msg, $model)
                [PSCustomObject]@{ Model = $model; Cmd = [PSCustomObject]@{ Type = 'Quit' } }
            }
            $viewFn = { param($model) New-ElmText -Content 'x' }
            $queue  = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
            $queue.Enqueue('any')
            { Invoke-ElmEventLoop -InitialModel ([PSCustomObject]@{}) -UpdateFn $updateFn -ViewFn $viewFn -InputQueue $queue } | Should -Not -Throw
        }
    }

    Context 'Message processing' {
        It 'Should accumulate model changes across multiple messages' {
            $updateFn = {
                param($msg, $model)
                if ($msg -eq 'Quit') {
                    [PSCustomObject]@{ Model = $model; Cmd = [PSCustomObject]@{ Type = 'Quit' } }
                } else {
                    [PSCustomObject]@{ Model = [PSCustomObject]@{ Count = $model.Count + 1 }; Cmd = $null }
                }
            }
            $viewFn = { param($model) New-ElmText -Content "$($model.Count)" }
            $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
            $queue.Enqueue('Inc')
            $queue.Enqueue('Inc')
            $queue.Enqueue('Inc')
            $queue.Enqueue('Quit')
            $result = Invoke-ElmEventLoop -InitialModel ([PSCustomObject]@{ Count = 0 }) -UpdateFn $updateFn -ViewFn $viewFn -InputQueue $queue
            $result.Count | Should -Be 3
        }

        It 'Should pass the current model to the View function after each update' {
            $updateFn = {
                param($msg, $model)
                if ($msg -eq 'Quit') {
                    [PSCustomObject]@{ Model = $model; Cmd = [PSCustomObject]@{ Type = 'Quit' } }
                } else {
                    [PSCustomObject]@{ Model = [PSCustomObject]@{ Label = $msg }; Cmd = $null }
                }
            }
            # ViewFn embeds the current model label into the node content
            $viewFn = { param($model) New-ElmText -Content "label:$($model.Label)" }
            $queue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
            $queue.Enqueue('alpha')
            $queue.Enqueue('Quit')
            # If ViewFn were not called, we would get an error from Invoke-ElmView; instead it completes
            { Invoke-ElmEventLoop -InitialModel ([PSCustomObject]@{ Label = '' }) -UpdateFn $updateFn -ViewFn $viewFn -InputQueue $queue } | Should -Not -Throw
        }
    }
}