#===========================================================================
# Tests - General Sanity
#===========================================================================

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path

BeforeAll {
    $script:repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path

    function script:Test-WinUtilParser {
        param([string]$Path)

        $tokens = $null
        $syntaxErrors = $null
        [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$syntaxErrors) | Out-Null

        if ($syntaxErrors.Count -ne 0) {
            throw ($syntaxErrors | Out-String)
        }
    }

    function script:Invoke-WindowsPowerShellParser {
        param([string[]]$Path)

        $windowsPowerShell = Get-Command powershell.exe -ErrorAction SilentlyContinue
        if (-not $windowsPowerShell) {
            Set-ItResult -Skipped -Because "powershell.exe is not available on this platform."
            return
        }

        $previousPaths = $env:WINUTIL_TEST_PARSE_PATHS
        try {
            $env:WINUTIL_TEST_PARSE_PATHS = @($Path) -join [Environment]::NewLine
            $parseScript = @'
$ErrorActionPreference = 'Stop'
$paths = $env:WINUTIL_TEST_PARSE_PATHS -split "`r?`n" | Where-Object { $_ }
$failed = @()

foreach ($path in $paths) {
    $tokens = $null
    $syntaxErrors = $null
    [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$syntaxErrors) | Out-Null

    if ($syntaxErrors.Count -ne 0) {
        $messages = $syntaxErrors | ForEach-Object { $_.Message }
        $failed += "[$path] $($messages -join '; ')"
    }
}

if ($failed.Count -gt 0) {
    $failed -join [Environment]::NewLine
    exit 1
}
'@

            $encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($parseScript))
            $output = & $windowsPowerShell.Source -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedCommand 2>&1
            if ($LASTEXITCODE -ne 0) {
                throw "Windows PowerShell parser failed:`n$($output | Out-String)"
            }
        } finally {
            if ($null -eq $previousPaths) {
                Remove-Item Env:\WINUTIL_TEST_PARSE_PATHS -ErrorAction SilentlyContinue
            } else {
                $env:WINUTIL_TEST_PARSE_PATHS = $previousPaths
            }
        }
    }
}

Describe "PowerShell source sanity" {
    $scriptCases = @(
        "Compile.ps1",
        "windev.ps1",
        "scripts\start.ps1",
        "scripts\main.ps1"
    ) | ForEach-Object {
        @{
            Name = $_
            Path = Join-Path $repoRoot $_
        }
    }

    foreach ($scriptCase in $scriptCases) {
        It "parses $($scriptCase.Name) with the current PowerShell parser" -TestCases $scriptCase {
            param([string]$Path)

            Test-WinUtilParser -Path $Path
        }
    }

    It "parses WinUtil source files with Windows PowerShell when available" {
        $sourcePaths = @(
            Join-Path $script:repoRoot "Compile.ps1"
            Join-Path $script:repoRoot "windev.ps1"
            Join-Path $script:repoRoot "scripts\start.ps1"
            Join-Path $script:repoRoot "scripts\main.ps1"
            Get-ChildItem -Path (Join-Path $script:repoRoot "functions") -Filter *.ps1 -Recurse | Select-Object -ExpandProperty FullName
        )

        Invoke-WindowsPowerShellParser -Path $sourcePaths
    }
}

Describe "Compiled WinUtil sanity" {
    BeforeAll {
        $script:compiledPath = Join-Path $script:repoRoot "winutil.ps1"

        Push-Location $script:repoRoot
        try {
            & (Join-Path $script:repoRoot "Compile.ps1")
        } finally {
            Pop-Location
        }
    }

    It "generates winutil.ps1" {
        Test-Path -Path $script:compiledPath | Should -BeTrue
    }

    It "parses compiled winutil.ps1 with the current PowerShell parser" {
        Test-WinUtilParser -Path $script:compiledPath
    }

    It "parses compiled winutil.ps1 with Windows PowerShell when available" {
        Invoke-WindowsPowerShellParser -Path $script:compiledPath
    }

    It "contains embedded configs, XAML, autounattend XML, and runspace bootstrap" {
        $content = Get-Content -Path $script:compiledPath -Raw
        $requiredSnippets = @(
            ('$sync.configs.applications = @' + "'"),
            ('$inputXML = @' + "'"),
            ('$WinUtilAutounattendXml = @' + "'"),
            "SessionStateVariableEntry -ArgumentList 'sync'",
            "SessionStateFunctionEntry",
            "[runspacefactory]::CreateRunspacePool",
            "function Invoke-WPFRunspace"
        )

        foreach ($snippet in $requiredSnippets) {
            if (-not $content.Contains($snippet)) {
                throw "Compiled script is missing expected content: $snippet"
            }
        }
    }

    It "transforms applications config keys with WPFInstall prefixes" {
        $content = Get-Content -Path $script:compiledPath -Raw
        $configMatch = [regex]::Match(
            $content,
            "(?s)\`$sync\.configs\.applications = @'\r?\n(?<json>.*?)\r?\n'@ \| ConvertFrom-Json"
        )

        if (-not $configMatch.Success) {
            throw "Compiled script is missing embedded applications config."
        }

        $sourceApps = Get-Content -Path (Join-Path $script:repoRoot "config\applications.json") -Raw | ConvertFrom-Json
        $compiledApps = $configMatch.Groups["json"].Value | ConvertFrom-Json

        foreach ($sourceApp in $sourceApps.PSObject.Properties) {
            $compiledKey = "WPFInstall$($sourceApp.Name)"
            if ($compiledApps.PSObject.Properties.Name -notcontains $compiledKey) {
                throw "Compiled applications config is missing transformed key: $compiledKey"
            }
            if ($compiledApps.PSObject.Properties.Name -contains $sourceApp.Name) {
                throw "Compiled applications config contains untransformed source key: $($sourceApp.Name)"
            }
        }
    }

    It "preserves compile source ordering" {
        $content = Get-Content -Path $script:compiledPath -Raw
        $orderedSnippets = @(
            '$sync.version =',
            'function Add-SelectedAppsMenuItem',
            ('$sync.configs.applications = @' + "'"),
            ('$inputXML = @' + "'"),
            ('$WinUtilAutounattendXml = @' + "'"),
            '$sync.SearchBarClearButton.Add_Click({'
        )

        $lastIndex = -1
        foreach ($snippet in $orderedSnippets) {
            $index = $content.IndexOf($snippet)
            if ($index -lt 0) {
                throw "Compiled script is missing expected ordered content: $snippet"
            }
            if ($index -le $lastIndex) {
                throw "Compiled script content is out of order near: $snippet"
            }

            $lastIndex = $index
        }
    }

    It "replaces the generated build date placeholder" {
        $content = Get-Content -Path $script:compiledPath -Raw
        $expectedBuildDate = Get-Date -Format "yy.MM.dd"

        $content | Should -Not -Match ([regex]::Escape("#{replaceme}"))
        $content | Should -Match ([regex]::Escape('$sync.version = "' + $expectedBuildDate + '"'))
    }
}

Describe "Runspace sanity" {
    BeforeAll {
        . (Join-Path $script:repoRoot "functions\public\Invoke-WPFRunspace.ps1")
        . (Join-Path $script:repoRoot "functions\private\Close-WinUtilRunspacePool.ps1")
        . (Join-Path $script:repoRoot "functions\private\Initialize-WinUtilRunspacePool.ps1")
    }

    It "returns a single async handle and runs a scriptblock with arguments in the shared runspace pool" {
        $script:sync = [Hashtable]::Synchronized(@{ SmokeValue = "shared" })
        $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()

        try {
            $script:sync.Result = $null
            $handle = Invoke-WPFRunspace -ArgumentList "argument" -ParameterList @(,("NamedValue", "parameter")) -ScriptBlock {
                param($ArgumentValue, [string]$NamedValue)

                Start-Sleep -Milliseconds 200
                $sync.Result = "$ArgumentValue|$NamedValue|$($sync.SmokeValue)"
            }

            ($handle -is [System.IAsyncResult]) | Should -BeTrue
            ($handle -is [array]) | Should -BeFalse
            $handle.AsyncWaitHandle.WaitOne(5000) | Should -BeTrue
            $script:sync.Result | Should -Be "argument|parameter|shared"
        } finally {
            if ($script:sync -and $script:sync.runspace) {
                $script:sync.runspace.Close()
                $script:sync.runspace.Dispose()
            }

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