#===========================================================================
# Tests - Runspace Behavior
#===========================================================================

BeforeAll {
    $script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
    . (Join-Path $script:repoRoot "functions\private\Close-WinUtilRunspacePool.ps1")
    . (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1")
    . (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
    . (Join-Path $script:repoRoot "functions\public\Invoke-WPFFeatureInstall.ps1")
    . (Join-Path $script:repoRoot "functions\public\Invoke-WPFAppxRemoval.ps1")
    . (Join-Path $script:repoRoot "functions\public\Invoke-WPFundoall.ps1")

    function script:New-WinUtilRunspaceTestContext {
        param([hashtable]$InitialSync = @{})

        $script:sync = [Hashtable]::Synchronized($InitialSync)
        $initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
        $syncVariable = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList "sync", $script:sync, $null
        $initialSessionState.Variables.Add($syncVariable)
        $script:sync.runspace = [runspacefactory]::CreateRunspacePool(1, 2, $initialSessionState, $Host)
        $script:sync.runspace.Open()
    }

    function script:Clear-WinUtilRunspaceTestContext {
        if ($script:sync -and $script:sync.runspace) {
            $script:sync.runspace.Close()
            $script:sync.runspace.Dispose()
        }

        Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
    }

    function script:Assert-WinUtilAsyncHandle {
        param($Handle)

        ($Handle -is [System.IAsyncResult]) | Should -BeTrue
        ($Handle -is [array]) | Should -BeFalse
        $Handle.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue
    }
}

Describe "Invoke-WPFRunspace behavior" {
    BeforeEach {
        New-WinUtilRunspaceTestContext -InitialSync @{ Marker = "shared" }
    }

    AfterEach {
        Clear-WinUtilRunspaceTestContext
    }

    It "returns a single async handle with no argument list" {
        $script:sync.Result = $null

        $handle = Invoke-WPFRunspace -ScriptBlock {
            Start-Sleep -Milliseconds 100
            $sync.Result = "no-args|$($sync.Marker)"
        }

        Assert-WinUtilAsyncHandle -Handle $handle
        $script:sync.Result | Should -Be "no-args|shared"
    }

    It "passes one named parameter" {
        $script:sync.Result = $null

        $handle = Invoke-WPFRunspace -ParameterList @(,("Name", "value")) -ScriptBlock {
            param([string]$Name)

            Start-Sleep -Milliseconds 100
            $sync.Result = "Name=$Name"
        }

        Assert-WinUtilAsyncHandle -Handle $handle
        $script:sync.Result | Should -Be "Name=value"
    }

    It "passes multiple named parameters" {
        $script:sync.Result = $null

        $handle = Invoke-WPFRunspace -ParameterList @(
            ("First", "alpha"),
            ("Second", "beta")
        ) -ScriptBlock {
            param(
                [string]$First,
                [string]$Second
            )

            Start-Sleep -Milliseconds 100
            $sync.Result = "$First|$Second|$($sync.Marker)"
        }

        Assert-WinUtilAsyncHandle -Handle $handle
        $script:sync.Result | Should -Be "alpha|beta|shared"
    }

    It "keeps the shared runspace pool usable after scriptblock failures" {
        $handle = Invoke-WPFRunspace -ScriptBlock {
            Start-Sleep -Milliseconds 100
            throw "runspace failure"
        }

        Assert-WinUtilAsyncHandle -Handle $handle

        $script:sync.Result = $null
        $secondHandle = Invoke-WPFRunspace -ScriptBlock {
            $sync.Result = "after-failure"
        }

        Assert-WinUtilAsyncHandle -Handle $secondHandle
        $script:sync.Result | Should -Be "after-failure"
    }

    It "runs multiple queued invocations without shared PowerShell state" {
        $script:sync.FirstResult = $null
        $script:sync.SecondResult = $null

        $firstHandle = Invoke-WPFRunspace -ParameterList @(,("Value", "first")) -ScriptBlock {
            param([string]$Value)

            Start-Sleep -Milliseconds 150
            $sync.FirstResult = $Value
        }
        $secondHandle = Invoke-WPFRunspace -ParameterList @(,("Value", "second")) -ScriptBlock {
            param([string]$Value)

            $sync.SecondResult = $Value
        }

        Assert-WinUtilAsyncHandle -Handle $firstHandle
        Assert-WinUtilAsyncHandle -Handle $secondHandle
        $script:sync.FirstResult | Should -Be "first"
        $script:sync.SecondResult | Should -Be "second"
    }

    It "does not use script-scoped PowerShell or handle state" {
        $runspaceScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1") -Raw

        $runspaceScript | Should -Not -Match '\$script:powershell'
        $runspaceScript | Should -Not -Match '\$script:handle'
    }

    It "exposes a strongly typed cleanup callback" {
        ([WinUtilRunspaceCleanup]::Callback -is [System.Threading.WaitOrTimerCallback]) | Should -BeTrue
    }
}

Describe "Public runspace callers" {
    BeforeEach {
        $script:sync = [Hashtable]::Synchronized(@{
            ProcessRunning = $false
            selectedFeatures = [System.Collections.Generic.List[string]]::new()
            selectedTweaks = [System.Collections.Generic.List[string]]::new()
            selectedAppx = [System.Collections.Generic.List[string]]::new()
            configs = @{
                appxHashtable = @{}
            }
        })

        Mock Invoke-WPFRunspace { [pscustomobject]@{ MockHandle = $true } }
    }

    AfterEach {
        Remove-Variable -Name sync -Scope Script -ErrorAction SilentlyContinue
    }

    It "queues selected feature installation without executing the runspace body" {
        $script:sync.selectedFeatures.Add("WPFFeaturesSandbox")

        Invoke-WPFFeatureInstall

        Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
            $ScriptBlock -is [scriptblock] -and $null -eq $ArgumentList -and $null -eq $ParameterList
        }
    }

    It "passes selected tweaks as the runspace argument list for undo all" {
        $script:sync.selectedTweaks.Add("WPFTweaksTelemetry")
        $script:sync.selectedTweaks.Add("WPFTweaksServices")

        Invoke-WPFundoall

        Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
            $ScriptBlock -is [scriptblock] -and
                $ArgumentList.Count -eq 2 -and
                $ArgumentList[0] -eq "WPFTweaksTelemetry" -and
                $ArgumentList[1] -eq "WPFTweaksServices"
        }
    }

    It "passes selected AppX items and app metadata to the removal runspace" {
        $script:sync.selectedAppx.Add("WPFAppxExample")
        $script:sync.configs.appxHashtable["WPFAppxExample"] = [pscustomobject]@{
            Content = "Example"
            PackageId = "Example.Package"
        }

        Invoke-WPFAppxRemoval

        Should -Invoke -CommandName Invoke-WPFRunspace -Times 1 -Exactly -ParameterFilter {
            $ScriptBlock -is [scriptblock] -and
                $ParameterList.Count -eq 2 -and
                $ParameterList[0][0] -eq "selected" -and
                $ParameterList[0][1][0] -eq "WPFAppxExample" -and
                $ParameterList[1][0] -eq "apps" -and
                $ParameterList[1][1].ContainsKey("WPFAppxExample")
        }
    }
}
