tests/psreadline.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/../Public/New-DFDirectory.ps1"
    . "$PSScriptRoot/../Private/Invoke-DFFzf.ps1"
    . "$PSScriptRoot/../Public/Invoke-DFPicker.ps1"
    . "$PSScriptRoot/../Private/Test-DFToolSchema.ps1"
    . "$PSScriptRoot/../Private/ConvertTo-DFPath.ps1"
    . "$PSScriptRoot/../Private/Expand-DFXdgPath.ps1"
    . "$PSScriptRoot/../Private/Import-DFToolDb.ps1"
    . "$PSScriptRoot/../Private/Invoke-DFTopoSort.ps1"
    . "$PSScriptRoot/../Private/Test-DFToolAvailable.ps1"
    . "$PSScriptRoot/../Public/Get-DFTool.ps1"
    . "$PSScriptRoot/../Public/Find-DFTool.ps1"
    # Register-DFTool calls Get-DFCommandConflict for its shadowed-command warning.
    . "$PSScriptRoot/../Private/Get-DFCoreutilsShadowSet.ps1"
    . "$PSScriptRoot/../Public/Get-DFCommandConflict.ps1"
    . "$PSScriptRoot/../Private/Initialize-DFCompletionStack.ps1"
    . "$PSScriptRoot/../Private/Get-DFConfiguredTheme.ps1"
    . "$PSScriptRoot/../Private/Resolve-DFThemeName.ps1"
    . "$PSScriptRoot/../Private/Set-DFToolXdgConfig.ps1"
    . "$PSScriptRoot/../Private/Register-DFToolAliases.ps1"
    . "$PSScriptRoot/../Private/New-DFToolPickerFunction.ps1"
    . "$PSScriptRoot/../Private/Invoke-DFToolCompanion.ps1"
    . "$PSScriptRoot/../Private/Start-DFModulePrewarm.ps1"
    . "$PSScriptRoot/../Public/Register-DFTool.ps1"
}

Describe 'psreadline tool sidecar' {
    BeforeEach {
        $script:DFToolDb = $null
        $script:DFToolAvailability = @{}
        $script:SavedConfigHome = $Env:XDG_CONFIG_HOME
        $script:SavedStateHome  = $Env:XDG_STATE_HOME
        $Env:XDG_CONFIG_HOME    = Join-Path $TestDrive 'config'
        $Env:XDG_STATE_HOME     = Join-Path $TestDrive 'state'

        # Point at the real Tools directory
        $script:RealTools = Join-Path $PSScriptRoot '../Tools'
    }

    AfterEach {
        $Env:XDG_CONFIG_HOME = $script:SavedConfigHome
        $Env:XDG_STATE_HOME  = $script:SavedStateHome
        $script:DFToolDb     = $null

        Remove-Variable DFConfig              -Scope Global -ErrorAction Ignore
        Remove-Variable DFPSReadLineColors    -Scope Global -ErrorAction Ignore
        Remove-Item 'function:global:Select-PSReadLineTheme'        -ErrorAction Ignore
        Remove-Item 'function:global:Invoke-DFApplyPSReadLineTheme' -ErrorAction Ignore
        Remove-Alias fprl -Scope Global -Force -ErrorAction Ignore
    }

    It 'registers Select-PSReadLineTheme as a global function' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        Test-Path 'function:global:Select-PSReadLineTheme' | Should -BeTrue
    }

    It 'registers fprl as an alias for Select-PSReadLineTheme' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        Get-Alias fprl -ErrorAction Ignore | Should -Not -BeNullOrEmpty
    }

    It 'registers Invoke-DFApplyPSReadLineTheme as a global function' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        Test-Path 'function:global:Invoke-DFApplyPSReadLineTheme' | Should -BeTrue
    }

    It 'applies PSReadLine settings from tool JSON' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        (Get-PSReadLineOption).BellStyle                    | Should -Be 'None'
        (Get-PSReadLineOption).HistoryNoDuplicates          | Should -BeTrue
        (Get-PSReadLineOption).HistorySearchCursorMovesToEnd | Should -BeTrue
        (Get-PSReadLineOption).MaximumHistoryCount | Should -Be 10000
    }

    It 'relocates HistorySavePath under $XDG_STATE_HOME/psreadline' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        (Get-PSReadLineOption).HistorySavePath | Should -Be (Join-Path $Env:XDG_STATE_HOME 'psreadline' 'history')
    }

    It 'creates the $XDG_STATE_HOME/psreadline directory' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        Test-Path (Join-Path $Env:XDG_STATE_HOME 'psreadline') | Should -BeTrue
    }

    It 'defaults EditMode to Emacs when $DFConfig[PSReadLineEditMode] is not set' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        (Get-PSReadLineOption).EditMode | Should -Be 'Emacs'
    }

    It 'lets $DFConfig[PSReadLineEditMode] override the default to Windows' {
        $Global:DFConfig = @{ PSReadLineEditMode = 'Windows' }
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        (Get-PSReadLineOption).EditMode | Should -Be 'Windows'
    }

    It 'binds Ctrl+p to HistorySearchBackward' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        $handler = Get-PSReadLineKeyHandler -Bound | Where-Object { $_.Key -eq 'Ctrl+p' }
        $handler.Function | Should -Be 'HistorySearchBackward'
    }

    It 'binds Ctrl+n to HistorySearchForward' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        $handler = Get-PSReadLineKeyHandler -Bound | Where-Object { $_.Key -eq 'Ctrl+n' }
        $handler.Function | Should -Be 'HistorySearchForward'
    }

    It 'applies the catppuccin-mocha theme by default' {
        # NOTE: Get-PSReadLineOption.Colors returns $null when output is redirected
        # (PSReadLine disables color support without VT). The sidecar also stores the
        # applied colors in $global:DFPSReadLineColors for testability.
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        $colors = (Get-PSReadLineOption).Colors
        $commandColor = if ($colors) { $colors.Command } else { $global:DFPSReadLineColors['Command'] }
        # catppuccin-mocha Command color is #cba6f7 -> VT contains "203;166;247"
        $commandColor | Should -Match '203;166;247'
    }

    It 'applies the theme named in $DFConfig[PSReadLineTheme]' {
        # NOTE: Same VT/redirect limitation — fall back to $global:DFPSReadLineColors.
        $Global:DFConfig = @{ PSReadLineTheme = 'light' }
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        $colors = (Get-PSReadLineOption).Colors
        # Light theme Command color is #0000ff — VT sequence contains "0;0;255"
        $commandColor = if ($colors) { $colors.Command } else { $global:DFPSReadLineColors['Command'] }
        $commandColor | Should -Match '0;0;255'
    }

    It 'follows the shared $DFConfig[Theme] key (catppuccin-mocha -> mocha)' {
        # Same VT/redirect limitation — fall back to $global:DFPSReadLineColors.
        $Global:DFConfig = @{ Theme = 'catppuccin-mocha' }
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        $colors = (Get-PSReadLineOption).Colors
        $commandColor = if ($colors) { $colors.Command } else { $global:DFPSReadLineColors['Command'] }
        # catppuccin-mocha Command color is #cba6f7 -> VT contains "203;166;247"
        $commandColor | Should -Match '203;166;247'
    }

    It 'lets PSReadLineTheme override the shared Theme key' {
        $Global:DFConfig = @{ Theme = 'catppuccin-mocha'; PSReadLineTheme = 'light' }
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        $colors = (Get-PSReadLineOption).Colors
        $commandColor = if ($colors) { $colors.Command } else { $global:DFPSReadLineColors['Command'] }
        # light theme Command color is #0000ff -> VT contains "0;0;255"
        $commandColor | Should -Match '0;0;255'
    }

    It 'applies a theme from XDG user dir, overriding bundled name' {
        # NOTE: Same VT/redirect limitation — fall back to $global:DFPSReadLineColors.
        # Force the theme name explicitly rather than relying on the sidecar's
        # ambient default (now catppuccin-mocha, not dark) — this test is about
        # XDG-user-dir-beats-bundled resolution for a *named* theme, independent
        # of whatever the default happens to be.
        $Global:DFConfig = @{ PSReadLineTheme = 'dark' }
        $userDir = Join-Path $Env:XDG_CONFIG_HOME 'psreadline' 'themes'
        New-Item -ItemType Directory -Force -Path $userDir | Out-Null
        @'
{
  "name": "dark",
  "colors": {
    "Command": "#ff0000",
    "Parameter": "#00ff00",
    "String": "#0000ff",
    "Operator": "#ffffff",
    "Variable": "#00ff00",
    "Comment": "#888888",
    "Keyword": "#ff00ff",
    "Error": "#ff0000",
    "InlinePrediction": "#444444",
    "ListPrediction": "#00ffff"
  }
}
'@
 | Set-Content (Join-Path $userDir 'dark.json')

        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        $colors = (Get-PSReadLineOption).Colors
        # User dark overrides bundled: Command = #ff0000 → VT contains "255;0;0"
        $commandColor = if ($colors) { $colors.Command } else { $global:DFPSReadLineColors['Command'] }
        $commandColor | Should -Match '255;0;0'
    }

    It 'warns and continues when an invalid hex color is in the theme' {
        $Global:DFConfig = @{ PSReadLineTheme = 'badcolors' }
        $userDir = Join-Path $Env:XDG_CONFIG_HOME 'psreadline' 'themes'
        New-Item -ItemType Directory -Force -Path $userDir | Out-Null
        @'
{
  "name": "badcolors",
  "colors": {
    "Command": "notahex",
    "Parameter": "#9cdcfe"
  }
}
'@
 | Set-Content (Join-Path $userDir 'badcolors.json')

        $warnings = Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools 3>&1 |
            Where-Object { $_ -is [System.Management.Automation.WarningRecord] }
        $warnings | Where-Object { $_ -match 'invalid color' } | Should -Not -BeNullOrEmpty
    }

    It 'warns when named theme is not found' {
        $Global:DFConfig = @{ PSReadLineTheme = 'nonexistent-theme' }
        $warnings = Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools 3>&1 |
            Where-Object { $_ -is [System.Management.Automation.WarningRecord] }
        $warnings | Where-Object { $_ -match 'not found' } | Should -Not -BeNullOrEmpty
    }

    It 'tolerates $DFConfig being set to $null' {
        # Regression: guarding on the variable's existence rather than its value
        # threw "Cannot index into a null array" for a profile with $DFConfig = $null.
        $Global:DFConfig = $null
        { Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools } | Should -Not -Throw
        Test-Path 'function:global:Invoke-DFApplyPSReadLineTheme' | Should -BeTrue
    }

    It 'Invoke-DFApplyPSReadLineTheme accepts an absolute path directly' {
        Register-DFTool -Name 'psreadline' -ToolsPath $script:RealTools
        $themePath = Join-Path $script:RealTools 'psreadline' 'light.json'
        { Invoke-DFApplyPSReadLineTheme -Name $themePath } | Should -Not -Throw
        $colors = if ((Get-PSReadLineOption).Colors.Command) {
            (Get-PSReadLineOption).Colors.Command
        } else { $global:DFPSReadLineColors['Command'] }
        $colors | Should -Match '0;0;255'
    }
}