tests/TabCompletionHooks.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/TestSupport.ps1"
    foreach ($f in Get-DFTestModuleFile) { . $f }
    $script:PSFzfCompanion = Join-Path $PSScriptRoot '../Tools/PSFzf.ps1'
    $script:CarapaceCompanion = Join-Path $PSScriptRoot '../Tools/carapace.ps1'
    $script:InshellisenseCompanion = Join-Path $PSScriptRoot '../Tools/inshellisense.ps1'
    # PSFzf is not installed in the test environment; Pester can only mock commands
    # that exist, so stub them. (Assert-MockCalled must not be used: Pester 6 removed
    # it, and calling it auto-imports Windows PowerShell's Pester 3.4.0.)
    function Set-PsFzfOption { param([Parameter(ValueFromRemainingArguments)]$Rest) }
    function Invoke-FzfTabCompletion { }
}

Describe 'tab-completion sidecar hooks' {
    BeforeEach { Reset-DFTestSession;
        $script:SavedFzfDefaultOpts = $Env:FZF_DEFAULT_OPTS
        $script:SavedCarapaceBridges = $Env:CARAPACE_BRIDGES
        Set-DFTestXdg
        Remove-Item function:Initialize-DFRoleTabCompletion -ErrorAction Ignore
        Remove-DFTestGlobal -Function Start-DFInshellisense
    }
    AfterEach {
        if ($null -eq $script:SavedFzfDefaultOpts) { Remove-Item Env:FZF_DEFAULT_OPTS -ErrorAction Ignore } else { $Env:FZF_DEFAULT_OPTS = $script:SavedFzfDefaultOpts }
        if ($null -eq $script:SavedCarapaceBridges) { Remove-Item Env:CARAPACE_BRIDGES -ErrorAction Ignore } else { $Env:CARAPACE_BRIDGES = $script:SavedCarapaceBridges }
        Remove-Item function:Initialize-DFRoleTabCompletion, function:Enable-DFCarapaceInshellisenseBridge -ErrorAction Ignore
        Remove-DFTestGlobal -Function Start-DFInshellisense
        Restore-DFTestXdg
    }

    Context 'PSFzf' {
        BeforeEach {
            Mock Import-Module { } -ParameterFilter { $Name -eq 'PSFzf' }
            Mock Get-Module { [pscustomobject]@{ Name = 'PSFzf' } } -ParameterFilter { $Name -eq 'PSFzf' }
            Mock Set-PsFzfOption { }
            Mock Set-PSReadLineKeyHandler { }
        }

        It 'binds Tab to a ScriptBlock that invokes Invoke-FzfTabCompletion' {
            Mock Get-Command { $null } -ParameterFilter { $Name -eq 'carapace' }
            . $script:PSFzfCompanion
            Initialize-DFRoleTabCompletion -Tool ([pscustomobject]@{}) -Role 'tab-completion'

            Should -Invoke Set-PSReadLineKeyHandler -Times 1 -Exactly -ParameterFilter {
                $Key -eq 'Tab' -and $ScriptBlock -is [scriptblock] -and $ScriptBlock.ToString() -match 'Invoke-FzfTabCompletion'
            }
        }

        It 'adds --ansi only once when carapace is resolvable and preserves existing options' {
            $Env:FZF_DEFAULT_OPTS = '--height 40% --ansi --layout reverse'
            Mock Get-Command { [pscustomobject]@{ Name = 'carapace' } } -ParameterFilter { $Name -eq 'carapace' }
            . $script:PSFzfCompanion
            Initialize-DFRoleTabCompletion -Tool ([pscustomobject]@{}) -Role 'tab-completion'

            $Env:FZF_DEFAULT_OPTS | Should -Be '--height 40% --ansi --layout reverse'
        }

        It 'leaves FZF_DEFAULT_OPTS unchanged when carapace is absent' {
            $Env:FZF_DEFAULT_OPTS = '--height 40% --layout reverse'
            Mock Get-Command { $null } -ParameterFilter { $Name -eq 'carapace' }
            . $script:PSFzfCompanion
            Initialize-DFRoleTabCompletion -Tool ([pscustomobject]@{}) -Role 'tab-completion'

            $Env:FZF_DEFAULT_OPTS | Should -Be '--height 40% --layout reverse'
        }
    }

    Context 'carapace' {
        BeforeEach {
            Mock Get-DFXdgPath { Join-Path $TestDrive 'xdg-config' }
            Mock New-DFDirectory { }
            Mock Get-DFCachedCommandOutput { 'return' }
            Mock Invoke-Expression { }
            Mock Get-Command { $null }
            Mock Get-DFRole { [pscustomobject]@{ Winner = 'carapace' } }
            Mock Set-PSReadLineKeyHandler { }
        }

        It 'binds Tab to MenuComplete' {
            . $script:CarapaceCompanion
            Initialize-DFRoleTabCompletion -Tool ([pscustomobject]@{}) -Role 'tab-completion'
            Should -Invoke Set-PSReadLineKeyHandler -Times 1 -Exactly -ParameterFilter { $Key -eq 'Tab' -and $Function -eq 'MenuComplete' }
        }

        It 'looks up the tab-completion winner once per startup' {
            # The bridge itself is covered below; this guards the startup cost (one Get-DFRole call).
            Mock Get-Command { [pscustomobject]@{ Name = 'is' } } -ParameterFilter { $Name -eq 'is' }
            . $script:CarapaceCompanion
            Should -Invoke Get-DFRole -Times 1 -Exactly -ParameterFilter { $Name -eq 'tab-completion' }
        }

        It 'merges inshellisense into user bridges without duplicates when it did not win Tab' {
            $Env:CARAPACE_BRIDGES = 'zsh,InShelliSense,bash,ZSH'
            Mock Get-Command { [pscustomobject]@{ Name = 'is' } } -ParameterFilter { $Name -eq 'is' }
            . $script:CarapaceCompanion
            Enable-DFCarapaceInshellisenseBridge -TabCompletionWinner 'carapace' | Should -BeTrue
            $Env:CARAPACE_BRIDGES | Should -Be 'zsh,InShelliSense,bash'
        }

        It 'does not bridge when inshellisense won Tab' {
            Mock Get-Command { [pscustomobject]@{ Name = 'is' } } -ParameterFilter { $Name -eq 'is' }
            . $script:CarapaceCompanion
            $Env:CARAPACE_BRIDGES = 'zsh'
            Enable-DFCarapaceInshellisenseBridge -TabCompletionWinner 'inshellisense' | Should -BeFalse
            $Env:CARAPACE_BRIDGES | Should -Be 'zsh'
        }

        It 'does not bridge when inshellisense is not installed' {
            . $script:CarapaceCompanion
            $Env:CARAPACE_BRIDGES = 'zsh'
            Enable-DFCarapaceInshellisenseBridge -TabCompletionWinner 'carapace' | Should -BeFalse
            $Env:CARAPACE_BRIDGES | Should -Be 'zsh'
        }
    }

    It 'starts inshellisense without binding Tab' {
        Mock Set-PSReadLineKeyHandler { }
        . $script:InshellisenseCompanion
        Mock Start-DFInshellisense { }
        Initialize-DFRoleTabCompletion -Tool ([pscustomobject]@{}) -Role 'tab-completion'
        Should -Invoke Start-DFInshellisense -Times 1 -Exactly
        Should -Invoke Set-PSReadLineKeyHandler -Times 0 -Exactly -ParameterFilter { $Key -eq 'Tab' }
    }
}

Describe 'Register-DFTool tab-completion role activation' {
    BeforeEach { Reset-DFTestSession;
        $script:DFToolDb = $null
        $script:DFToolAvailability = @{}
        $script:TabCalls = @()
        $script:Tools = Join-Path $TestDrive "tools-$([guid]::NewGuid())"
        New-Item -ItemType Directory $script:Tools | Out-Null
        '{ "tab-completion": { "kind": "single", "hook": "Initialize-DFRoleTabCompletion" } }' | Set-Content (Join-Path $TestDrive 'roles-tab.json')
        $script:DFRoleDb = Get-DFRoleDb -Path (Join-Path $TestDrive 'roles-tab.json')
        Mock Test-DFToolAvailable { $true }
        Mock Write-DFConflictNotice { }
        Mock Set-PSReadLineKeyHandler { param($Key) if ($Key -eq 'Tab') { $script:TabCalls += 'Tab' } }
        function script:Write-TabTool([string]$Name, [int]$Priority) {
            "{ `"name`": `"$Name`", `"executable`": `"$Name.exe`", `"roles`": { `"tab-completion`": { `"priority`": $Priority } } }" | Set-Content (Join-Path $script:Tools "$Name.json")
            "function Initialize-DFRoleTabCompletion { param(`$Tool, `$Role) `$script:TabCalls += '$Name' }" | Set-Content (Join-Path $script:Tools "$Name.ps1")
        }
        Write-TabTool high 20
        Write-TabTool low 10
        '{ "name": "plain", "executable": "plain.exe" }' | Set-Content (Join-Path $script:Tools 'plain.json')
    }
    AfterEach { $script:DFRoleDb = $null; Remove-Variable TabCalls -Scope Script -ErrorAction Ignore }

    It 'runs only the highest-priority tab-completion hook' {
        Register-DFTool -Name high, low -ToolsPath $script:Tools
        $script:TabCalls | Should -Contain 'high'
        $script:TabCalls | Should -Not -Contain 'low'
    }

    It 'does not bind Tab when no registered tool is a tab-completion candidate' {
        Register-DFTool -Name plain -ToolsPath $script:Tools
        @($script:TabCalls | Where-Object { $_ -eq 'Tab' }).Count | Should -Be 0
    }
}