Tests/ParamLevel.Verify.ps1

# ==============================================================================
# ParamLevel.Verify.ps1 - Parameter-level completion verification script (run in a normal session)
# Path: Tests\
# Purpose: Pester runspaces cannot verify the "parameter-level forwarding + engine direct
# call" path (see the 5 skipped cases in the test file), so this script verifies the
# same logic in a normal pwsh session (with a host) — it maps 1:1 to the Skipped cases
# in LazyCompletions.Tests.ps1.
# Run: pwsh -File .\Modules\LazyCompletions\Tests\ParamLevel.Verify.ps1
# ==============================================================================

$moduleRoot = Split-Path $PSScriptRoot -Parent
$passed = 0
$failed = 0

function Assert-Case([string]$Name, [scriptblock]$Body) {
    $err = $null
    try { & $Body } catch { $err = $_.Exception.Message }
    if ($err) {
        Write-Host " [FAIL] $Name`n $err" -ForegroundColor Red
        $script:failed++
    } else {
        Write-Host " [PASS] $Name" -ForegroundColor Green
        $script:passed++
    }
}

# Note: the parameter name cannot be $Input (PowerShell automatic variable; binding fails to an
# empty value, measured); the first+second Tab must share the same $ps (the engine direct call
# on the second Tab depends on the same completion session, measured)
function Test-Tab([string]$Line, [int]$Column, $PS) {
    $r = [System.Management.Automation.CommandCompletion]::CompleteInput($Line, $Column, $null, $PS)
    return @($r.CompletionMatches | ForEach-Object { $_.CompletionText })
}

function New-PS {
    return [powershell]::Create([System.Management.Automation.RunspaceMode]::CurrentRunspace)
}

# reset the module state per case (no reload: after completers are registered, Import-Module
# -Force corrupts later completions, measured; leftover engine placeholders are harmless —
# later registrations win)
function Import-Case([string]$Dir) {
    $m = Get-Module LazyCompletions
    if (-not $m) {
        Import-Module (Join-Path $moduleRoot 'LazyCompletions.psd1') -Force
        $m = Get-Module LazyCompletions
    }
    & $m {
        $script:LazyCompletionsDirs = @($script:DefaultCompletionsDir)
        $script:LazyCompletionsDir = $script:DefaultCompletionsDir
        $script:LazyCompleterStates = @{}
        $script:CommandToScript = @{}
        $script:StatesByDir = @{}
        $script:CacheEnabled = $false
        $script:CacheUpdateMode = 'Sync'
        $script:CacheChangeMode = 'Full'
        $script:CacheLastError = $null
        $script:CacheLastUpdate = $null
        $script:FilterSystemFilesEnabled = $true
        $global:LC_FilterSystemFilesEnabled = $true
        $global:LC_TabWrapperDeferred = $false
    }
    Set-LazyCache -Disable | Out-Null
    Set-LazyCompletionsPath $Dir
    Import-LazyCompletion
}

Write-Host "=== Parameter-level completion verification (normal session) ===" -ForegroundColor Cyan

# test functions must be defined at script scope: functions defined inside scriptblocks are
# invisible to engine command discovery (measured: the engine treats local functions as unknown
# commands and only calls the general completer) — unrelated to the Pester runspace
function lc-multitest { param([string]$One, [string]$Two) }
function lc-boundtest { param([string]$One, [string]$Mode) }
function lc-abbrtest { param([string]$Mode) }
function lc-hotparam { param([string]$Mode) }
function lc-proxykey { param([string]$Old, [string]$New) }

# ---- 1. multi-registration: general + parameter-level; second Tab calls the engine directly ----
Assert-Case 'multi-registration: second Tab calls parameter-level directly (one/two-result)' {
    $dir = Join-Path $env:TEMP 'lc-verify-multi'
    Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path $dir | Out-Null
    @'
Register-ArgumentCompleter -CommandName 'lc-multitest' -ParameterName 'One' -ScriptBlock { param($c,$p,$w,$a,$f) 'one-result' }
Register-ArgumentCompleter -CommandName 'lc-multitest' -ParameterName 'Two' -ScriptBlock { param($c,$p,$w,$a,$f) 'two-result' }
Register-ArgumentCompleter -CommandName 'lc-multitest' -ScriptBlock { param($w,$a,$c) 'gen-result' }
'@
 | Set-Content (Join-Path $dir 'multi1.ps1') -Encoding UTF8
    Import-Case $dir
    $ps = New-PS
    try {
        $null = Test-Tab 'lc-multitest -One ' 18 $ps    # first: trigger loading
        $m4 = Test-Tab 'lc-multitest -One ' 18 $ps
        $m5 = Test-Tab 'lc-multitest -Two ' 18 $ps
    } finally { $ps.Dispose() }
    if ($m4 -notcontains 'one-result') { throw "second -One expected one-result, got [$($m4 -join ',')]" }
    if ($m5 -notcontains 'two-result') { throw "second -Two expected two-result, got [$($m5 -join ',')]" }
    Remove-Item $dir -Recurse -Force
}

# ---- 2. fakeBoundParameters is the engine real value ----
Assert-Case 'fakeBoundParameters is the engine real binding (bound-result)' {
    $dir = Join-Path $env:TEMP 'lc-verify-bound'
    Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path $dir | Out-Null
    @'
Register-ArgumentCompleter -CommandName 'lc-boundtest' -ParameterName 'Mode' -ScriptBlock {
    param($c,$p,$w,$a,$f)
    if ($f -and $f.ContainsKey('One')) { 'bound-result' } else { 'nobound-result' }
}
'@
 | Set-Content (Join-Path $dir 'prox1.ps1') -Encoding UTF8
    Import-Case $dir
    Add-LazyCompletion (Join-Path $dir 'prox1.ps1') | Out-Null
    $ps = New-PS
    try {
        $null = Test-Tab 'lc-boundtest -One x -Mode ' 26 $ps   # first: trigger loading
        $m2 = Test-Tab 'lc-boundtest -One x -Mode ' 26 $ps
    } finally { $ps.Dispose() }
    if ($m2 -notcontains 'bound-result') { throw "expected bound-result (real fakeBoundParameters), got [$($m2 -join ',')]" }
    if ($m2 -contains 'nobound-result') { throw "nobound-result must not appear" }
    Remove-Item function:lc-boundtest -ErrorAction SilentlyContinue
    Remove-Item $dir -Recurse -Force
}

# ---- 3. parameter abbreviation resolved by the engine on the second Tab ----
Assert-Case 'parameter abbreviation -Mo -> Mode resolved by the engine (abbr-ok)' {
    $dir = Join-Path $env:TEMP 'lc-verify-abbr'
    Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path $dir | Out-Null
    @'
Register-ArgumentCompleter -CommandName 'lc-abbrtest' -ParameterName 'Mode' -ScriptBlock { param($c,$p,$w,$a,$f) 'abbr-ok' }
'@
 | Set-Content (Join-Path $dir 'abbr1.ps1') -Encoding UTF8
    Import-Case $dir
    Add-LazyCompletion (Join-Path $dir 'abbr1.ps1') | Out-Null
    $ps = New-PS
    try {
        $null = Test-Tab 'lc-abbrtest -Mo ' 16 $ps            # first: trigger loading
        $m2 = Test-Tab 'lc-abbrtest -Mo ' 16 $ps
    } finally { $ps.Dispose() }
    if ($m2 -notcontains 'abbr-ok') { throw "expected abbr-ok (engine resolves -Mo to Mode), got [$($m2 -join ',')]" }
    Remove-Item function:lc-abbrtest -ErrorAction SilentlyContinue
    Remove-Item $dir -Recurse -Force
}

# ---- 4. -Force hot update: parameter-level placeholder overrides the old real ----
Assert-Case '-Force hot update (old-mode -> new-mode)' {
    $dir = Join-Path $env:TEMP 'lc-verify-hotparam'
    Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path $dir | Out-Null
    $f = Join-Path $dir 'hot1.ps1'
    @'
Register-ArgumentCompleter -CommandName 'lc-hotparam' -ParameterName 'Mode' -ScriptBlock { param($c,$p,$w,$a,$f) 'old-mode' }
'@
 | Set-Content $f -Encoding UTF8
    Import-Case $dir
    Add-LazyCompletion $f | Out-Null
    $ps = New-PS
    try {
        $null = Test-Tab 'lc-hotparam -Mode ' 18 $ps          # first: trigger loading
        $m2 = Test-Tab 'lc-hotparam -Mode ' 18 $ps
    } finally { $ps.Dispose() }
    if ($m2 -notcontains 'old-mode') { throw "second expected old-mode, got [$($m2 -join ',')]" }
    @'
Register-ArgumentCompleter -CommandName 'lc-hotparam' -ParameterName 'Mode' -ScriptBlock { param($c,$p,$w,$a,$f) 'new-mode' }
'@
 | Set-Content $f -Encoding UTF8
    Add-LazyCompletion $f -Force | Out-Null
    $ps = New-PS
    try {
        $null = Test-Tab 'lc-hotparam -Mode ' 18 $ps          # trigger reload
        $m4 = Test-Tab 'lc-hotparam -Mode ' 18 $ps
    } finally { $ps.Dispose() }
    if ($m4 -notcontains 'new-mode') { throw "after -Force expected new-mode, got [$($m4 -join ',')]" }
    Remove-Item function:lc-hotparam -ErrorAction SilentlyContinue
    Remove-Item $dir -Recurse -Force
}

# ---- 5. -Force parameter rename: the old parameter no longer yields old results ----
Assert-Case '-Force parameter rename (Old -> New)' {
    $dir = Join-Path $env:TEMP 'lc-verify-proxykey'
    Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
    New-Item -ItemType Directory -Path $dir | Out-Null
    $f = Join-Path $dir 'proxy.ps1'
    @'
Register-ArgumentCompleter -CommandName 'lc-proxykey' -ParameterName 'Old' -ScriptBlock { 'old' }
'@
 | Set-Content $f -Encoding UTF8
    Import-Case $dir
    Add-LazyCompletion $f | Out-Null
    $ps = New-PS
    try {
        $null = Test-Tab 'lc-proxykey -Old ' 17 $ps           # first: trigger loading
        $m2 = Test-Tab 'lc-proxykey -Old ' 17 $ps
    } finally { $ps.Dispose() }
    if ($m2 -notcontains 'old') { throw "second expected old, got [$($m2 -join ',')]" }
    @'
Register-ArgumentCompleter -CommandName 'lc-proxykey' -ParameterName 'New' -ScriptBlock { 'new' }
'@
 | Set-Content $f -Encoding UTF8
    Add-LazyCompletion $f -Force | Out-Null
    $ps = New-PS
    try {
        $null = Test-Tab 'lc-proxykey -New ' 17 $ps           # trigger reload
        $m4 = Test-Tab 'lc-proxykey -New ' 17 $ps
        $m5 = Test-Tab 'lc-proxykey -Old ' 17 $ps
    } finally { $ps.Dispose() }
    if ($m4 -notcontains 'new') { throw "-New expected new, got [$($m4 -join ',')]" }
    if ($m5 -contains 'old') { throw "-Old must not yield old anymore, got [$($m5 -join ',')]" }
    Remove-Item function:lc-proxykey -ErrorAction SilentlyContinue
    Remove-Item $dir -Recurse -Force
}

Write-Host ""
Write-Host "=== Result: $passed passed / $failed failed ===" -ForegroundColor Cyan
exit $failed