tests/DFSessionConfig.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/TestSupport.ps1"
    foreach ($f in Get-DFTestModuleFile) { . $f }
}

Describe 'Session config' {
    BeforeEach { Reset-DFTestSession }
    AfterEach { Set-DFTestConfig $null }

    Context 'Get-DFConfig' {
        It 'returns -Default before any config is set' {
            Get-DFConfig Theme -Default 'x' | Should -Be 'x'
            Get-DFConfig Theme | Should -BeNullOrEmpty
        }
        It 'returns a configured value, including $false, and -Default for a missing or $null key' {
            Set-DFTestConfig @{ Theme = 'nord'; SkipConflictCheck = $false; Defaults = $null }
            Get-DFConfig Theme | Should -Be 'nord'
            Get-DFConfig SkipConflictCheck -Default $true | Should -BeFalse
            Get-DFConfig Defaults -Default 'd' | Should -Be 'd'
            Get-DFConfig ExcludeTools -Default 'e' | Should -Be 'e'
        }
        It 'never reads a global $DFConfig' {
            $global:DFConfig = @{ Theme = 'from-global' }
            try { Get-DFConfig Theme | Should -BeNullOrEmpty }
            finally { Remove-Variable DFConfig -Scope Global -ErrorAction Ignore }
        }
    }

    Context 'Set-DFSessionConfig' {
        It 'stores a copy, so later changes to the caller''s hashtable do not leak in' {
            $cfg = @{ Theme = 'nord' }
            Set-DFSessionConfig -Config $cfg
            $cfg.Theme = 'changed'
            Get-DFConfig Theme | Should -Be 'nord'
        }
        It 'stores a deep copy, so nested values (Defaults, lists) cannot be changed afterwards' {
            $cfg = @{ Defaults = @{ prompt = 'starship' }; SkipSetup = [System.Collections.Generic.List[string]]@('delta') }
            Set-DFSessionConfig -Config $cfg
            $cfg.Defaults.prompt = 'oh-my-posh'
            $cfg.SkipSetup.Add('mdv')
            (Get-DFConfig Defaults).prompt | Should -Be 'starship'
            @(Get-DFConfig SkipSetup) | Should -Be @('delta')
        }
        It 'warns about an unknown key, suggesting a known one' {
            Set-DFSessionConfig -Config @{ ExludeTools = @('x') } -WarningVariable w 3>$null
            "$w" | Should -Match "ExludeTools.*ExcludeTools"
        }
        It 'warns about an unknown key with no close match' {
            Set-DFSessionConfig -Config @{ Frobnicate = 1 } -WarningVariable w 3>$null
            "$w" | Should -Match "Frobnicate"
        }
        It 'explains removed keys and what replaced them' {
            Set-DFSessionConfig -Config @{ SkipTools = @('lsd'); CompletionMode = 'Native' } -WarningVariable w 3>$null
            "$w" | Should -Match 'SkipTools.*ExcludeTools'
            "$w" | Should -Match "CompletionMode.*tab-completion"
        }
        It 'accepts every known key without warnings' {
            $cfg = @{}
            foreach ($k in $script:DFConfigKeys.Keys) { $cfg[$k] = $null }
            Set-DFSessionConfig -Config $cfg -WarningVariable w 3>$null
            $w | Should -BeNullOrEmpty
        }
    }

    Context 'Assert-DFSessionConfigured (fail-closed migration guard)' {
        BeforeEach { $script:DFSessionConfigured = $false; $script:DFLegacyConfigWarned = $false }
        AfterEach { Remove-Variable DFConfig -Scope Global -ErrorAction Ignore }

        It 'warns loudly when a global $DFConfig exists but no session config was set, naming the protections being ignored' {
            $global:DFConfig = @{ SkipSetup = @('delta') }
            Assert-DFSessionConfigured -WarningVariable w 3>$null
            "$w" | Should -Match 'Start-DFSession -Config'
            "$w" | Should -Match 'SkipSetup'
        }
        It 'warns only once per session' {
            $global:DFConfig = @{}
            Assert-DFSessionConfigured 3>$null
            Assert-DFSessionConfigured -WarningVariable w 3>$null
            $w | Should -BeNullOrEmpty
        }
        It 'is silent once a session config is set, or when there is no global $DFConfig' {
            Assert-DFSessionConfigured -WarningVariable w 3>$null
            $w | Should -BeNullOrEmpty
            $global:DFConfig = @{}
            Set-DFSessionConfig -Config @{}
            Assert-DFSessionConfigured -WarningVariable w 3>$null
            $w | Should -BeNullOrEmpty
        }
    }

    Context 'known-key list' {
        It 'lists every key the code reads by name (Get-DFConfig and Get-DFConfiguredTheme -ToolKey)' {
            $root = Split-Path $PSScriptRoot -Parent
            $read = foreach ($file in Get-ChildItem (Join-Path $root 'Shared'), (Join-Path $root 'Private'), (Join-Path $root 'Public'), (Join-Path $root 'Modules' 'DotForge.Catalog' 'Private'), (Join-Path $root 'Modules' 'DotForge.Catalog' 'Public'), (Join-Path $root 'Modules' 'DotForge.Helpers' 'Private'), (Join-Path $root 'Modules' 'DotForge.Helpers' 'Public'), (Join-Path $root 'Tools') -Filter '*.ps1') {
                $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$null, [ref]$null)
                $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.CommandAst] -and
                        $n.GetCommandName() -in 'Get-DFConfig', 'Get-DFConfiguredTheme' }, $true) | ForEach-Object {
                    $els = $_.CommandElements
                    for ($i = 1; $i -lt $els.Count; $i++) {
                        $e = $els[$i]
                        $isKey = ($_.GetCommandName() -eq 'Get-DFConfig' -and $i -eq 1 -and $e -is [System.Management.Automation.Language.StringConstantExpressionAst]) -or
                                 ($i -gt 1 -and $els[$i - 1] -is [System.Management.Automation.Language.CommandParameterAst] -and
                                  $els[$i - 1].ParameterName -in 'ToolKey', 'Key' -and $e -is [System.Management.Automation.Language.StringConstantExpressionAst])
                        if ($isKey) { $e.Value }
                    }
                }
            }
            $unknown = @($read | Sort-Object -Unique | Where-Object { -not $script:DFConfigKeys.Contains($_) })
            $unknown | Should -BeNullOrEmpty -Because 'add new $DFConfig keys to $script:DFConfigKeys in Private/DFSessionConfig.ps1'
        }
        It 'no code in Private, Public or Tools reads a $DFConfig variable (the migration guard only tests that it exists)' {
            $root = Split-Path $PSScriptRoot -Parent
            $hits = foreach ($file in Get-ChildItem (Join-Path $root 'Shared'), (Join-Path $root 'Private'), (Join-Path $root 'Public'), (Join-Path $root 'Modules' 'DotForge.Catalog' 'Private'), (Join-Path $root 'Modules' 'DotForge.Catalog' 'Public'), (Join-Path $root 'Modules' 'DotForge.Helpers' 'Private'), (Join-Path $root 'Modules' 'DotForge.Helpers' 'Public'), (Join-Path $root 'Tools') -Filter '*.ps1') {
                $ast = [System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$null, [ref]$null)
                $ast.FindAll({ param($n)
                        ($n -is [System.Management.Automation.Language.VariableExpressionAst] -and $n.VariablePath.UserPath -match '^(global:)?DFConfig$') -or
                        ($n -is [System.Management.Automation.Language.CommandAst] -and $n.GetCommandName() -eq 'Get-Variable' -and $n.Extent.Text -match '\bDFConfig\b')
                    }, $true) | ForEach-Object { "$($file.Name):$($_.Extent.StartLineNumber)" }
            }
            @($hits) | Should -BeNullOrEmpty
        }
    }
}