param(
    [string]$PackageRoot = 'C:\HyperTwist\packaged\classic-cube',
    [string]$MapUrl = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining',
    [int]$SmokeSeconds = 30,
    [int]$ResX = 1600,
    [int]$ResY = 900,
    [string]$ReportPath = '',
    [switch]$KeepRunning,
    [switch]$UseNullRHI,
    [switch]$NoSound
)

$ErrorActionPreference = 'Stop'

function Write-Utf8JsonFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [object]$Value
    )

    $ParentPath = Split-Path -Parent $Path
    if (-not [string]::IsNullOrWhiteSpace($ParentPath))
    {
        New-Item -ItemType Directory -Force -Path $ParentPath | Out-Null
    }

    $Json = $Value | ConvertTo-Json -Depth 8
    $Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllText($Path, $Json, $Utf8NoBom)
}

function Get-FatalRuntimeLogMatches {
    param(
        [Parameter(Mandatory = $true)]
        [string]$RuntimeLogPath
    )

    if (-not (Test-Path -LiteralPath $RuntimeLogPath))
    {
        return @()
    }

    $FatalPatterns = @(
        'Fatal error:',
        'LowLevelFatalError',
        'Assertion failed:',
        'Critical error:',
        'appError called:',
        'DXGI_ERROR_NOT_CURRENTLY_AVAILABLE',
        'CreateSwapChainResult failed',
        'Unhandled Exception:',
        'StaticShutdownAfterError'
    )

    return @(Get-Content -LiteralPath $RuntimeLogPath | Where-Object {
        $Line = $_
        foreach ($Pattern in $FatalPatterns)
        {
            if ($Line -like "*$Pattern*")
            {
                return $true
            }
        }

        return $false
    })
}

function Get-OwnedPackageProcessIds {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResolvedPackageRoot,
        [int[]]$ExcludedProcessIds = @()
    )

    return @(
        Get-OwnedPackageProcessRecords `
            -ResolvedPackageRoot $ResolvedPackageRoot `
            -ExcludedProcessIds $ExcludedProcessIds |
            Select-Object -ExpandProperty ProcessId
    )
}

function Get-OwnedPackageProcessRecords {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResolvedPackageRoot,
        [int[]]$ExcludedProcessIds = @()
    )

    $RootPrefix = $ResolvedPackageRoot.TrimEnd(
        [System.IO.Path]::DirectorySeparatorChar,
        [System.IO.Path]::AltDirectorySeparatorChar
    ) + [System.IO.Path]::DirectorySeparatorChar

    return @(
        Get-CimInstance Win32_Process -Filter "Name = 'UnrealHyperTwist.exe'" -ErrorAction SilentlyContinue |
            Where-Object {
                -not [string]::IsNullOrWhiteSpace([string]$_.ExecutablePath) `
                    -and ([string]$_.ExecutablePath).StartsWith(
                        $RootPrefix,
                        [System.StringComparison]::OrdinalIgnoreCase
                    ) `
                    -and $ExcludedProcessIds -notcontains [int]$_.ProcessId
            }
    )
}

function Get-OwnedListeningTcpEndpoints {
    param(
        [int[]]$ProcessIds
    )

    if ($ProcessIds.Count -eq 0)
    {
        return @()
    }

    return @(
        Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
            Where-Object { $ProcessIds -contains [int]$_.OwningProcess } |
            Sort-Object OwningProcess, LocalPort |
            ForEach-Object {
                [pscustomobject][ordered]@{
                    processId = [int]$_.OwningProcess
                    localAddress = [string]$_.LocalAddress
                    localPort = [int]$_.LocalPort
                }
            }
    )
}

function Stop-OwnedPackageProcesses {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ResolvedPackageRoot,
        [int[]]$ExcludedProcessIds = @()
    )

    $StoppedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
    for ($Attempt = 0; $Attempt -lt 5; $Attempt += 1)
    {
        $OwnedProcessIds = @(
            Get-OwnedPackageProcessIds `
                -ResolvedPackageRoot $ResolvedPackageRoot `
                -ExcludedProcessIds $ExcludedProcessIds
        )
        if ($OwnedProcessIds.Count -eq 0)
        {
            break
        }

        foreach ($OwnedProcessId in $OwnedProcessIds)
        {
            Stop-Process -Id $OwnedProcessId -Force -ErrorAction SilentlyContinue
            [void]$StoppedProcessIds.Add([int]$OwnedProcessId)
        }
        Start-Sleep -Milliseconds 200
    }

    return @($StoppedProcessIds)
}

$CandidateExecutablePaths = @(
    (Join-Path $PackageRoot 'Windows\UnrealHyperTwist.exe'),
    (Join-Path $PackageRoot 'WindowsNoEditor\UnrealHyperTwist.exe'),
    (Join-Path $PackageRoot 'UnrealHyperTwist.exe')
)

$LauncherExecutablePath = $CandidateExecutablePaths |
    Where-Object { Test-Path $_ } |
    Select-Object -First 1
if ($null -eq $LauncherExecutablePath)
{
    throw "No packaged UnrealHyperTwist executable was found beneath '$PackageRoot'."
}

$PackagedExecutableDirectory = Split-Path -Parent $LauncherExecutablePath
$ClassicDefaultMap = '/Game/HyperTwistTraining/Maps/L_HyperTwist_ClassicTraining'
$StartupRouteByMap = @{
    $ClassicDefaultMap = ''
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_FollowAlongTraining' = 'follow-along-training'
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_2x2x2x2Training' = 'magic-cube-4d-2x2x2x2'
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_3x3x3x3Training' = 'magic-cube-4d-3x3x3x3'
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_4x4x4x4Training' = 'magic-cube-4d-4x4x4x4'
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_5x5x5x5Training' = 'magic-cube-4d-5x5x5x5'
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_6x6x6x6Training' = 'magic-cube-4d-6x6x6x6'
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining' = 'magic-120-cell-training'
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining' = 'magic-cube-5d-training'
}
$FourDimensionalOrderByMap = @{
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_2x2x2x2Training' = 2
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_3x3x3x3Training' = 3
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_4x4x4x4Training' = 4
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_5x5x5x5Training' = 5
    '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube4D_6x6x6x6Training' = 6
}
if (-not $StartupRouteByMap.ContainsKey($MapUrl))
{
    throw "No first-party packaged startup route is registered for smoke map '$MapUrl'."
}
$StartupRouteId = [string]$StartupRouteByMap[$MapUrl]
$LaunchMode = if ($MapUrl -eq $ClassicDefaultMap)
{
    'bootstrap-default-map'
}
else
{
    'first-party-startup-route'
}
$ExecutablePath = $LauncherExecutablePath

$LogDirectory = Join-Path $PackageRoot 'validation\logs'
New-Item -ItemType Directory -Force -Path $LogDirectory | Out-Null
$LogToken = if ([string]::IsNullOrWhiteSpace($MapUrl))
{
    'classic-cube-runtime'
}
else
{
    ($MapUrl -replace '[\\/:*?"<>| ]', '_')
}
$RuntimeLogPath = Join-Path $LogDirectory ("{0}.log" -f $LogToken)
$RuntimeDiagnosticsPath = Join-Path $LogDirectory ("{0}.hyperdiagnostics.log" -f $LogToken)
Remove-Item -LiteralPath $RuntimeLogPath -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $RuntimeDiagnosticsPath -Force -ErrorAction SilentlyContinue

$ArgumentList = @()
if (-not [string]::IsNullOrWhiteSpace($StartupRouteId))
{
    # Nondefault smokes use the same registered product route as the launch
    # menu. Generic startup URLs are overridden by the packaged first-run
    # game-mode authority and therefore are not valid route evidence.
    $ArgumentList += "-HyperTwistStartupRoute=$StartupRouteId"
}
$ArgumentList += @(
    "-ResX=$ResX",
    "-ResY=$ResY",
    '-windowed',
    '-log',
    '-FORCELOGFLUSH',
    "-abslog=$RuntimeLogPath",
    "-HyperTwistDiagnosticsLog=`"$RuntimeDiagnosticsPath`""
)

if ($UseNullRHI)
{
    $ArgumentList += '-NullRHI'
}

if ($NoSound)
{
    $ArgumentList += '-nosound'
}

Write-Host "Launching packaged classic-cube validation lane from '$ExecutablePath'..."
$ResolvedPackageRoot = (Resolve-Path -LiteralPath $PackageRoot).Path
$ExistingPackageProcessIds = @(
    Get-OwnedPackageProcessIds -ResolvedPackageRoot $ResolvedPackageRoot
)
$GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
$Process = $null
$RequiresClassicPresentation = $MapUrl -match '/L_HyperTwist_(Classic|FollowAlong)Training$'
$HigherDimensionalFamily = if ($MapUrl -match '/L_HyperTwist_Magic120CellTraining$')
{
    'magic120cell'
}
elseif ($MapUrl -match '/L_HyperTwist_MagicCube5DTraining$')
{
    'magiccube5d'
}
else
{
    ''
}
$ExpectedHigherDimensionalElementCount = if ($HigherDimensionalFamily -eq 'magic120cell')
{
    120
}
elseif ($HigherDimensionalFamily -eq 'magiccube5d')
{
    242
}
else
{
    0
}
$FourDimensionalOrder = if ($FourDimensionalOrderByMap.ContainsKey($MapUrl))
{
    [int]$FourDimensionalOrderByMap[$MapUrl]
}
else
{
    0
}
$ExpectedFourDimensionalStatePieceCount = if ($FourDimensionalOrder -gt 0)
{
    [int][Math]::Pow($FourDimensionalOrder, 4)
}
else
{
    0
}
$ExpectedFourDimensionalVisiblePieceViewCount = if ($FourDimensionalOrder -eq 2)
{
    64
}
elseif ($FourDimensionalOrder -gt 0)
{
    [int][Math]::Pow($FourDimensionalOrder, 3)
}
else
{
    0
}
$Report = [ordered]@{
    reportVersion = 'ht-desktop-map-package-smoke/v8'
    generatedAtUtc = $GeneratedAtUtc
    packageRoot = $ResolvedPackageRoot
    launchMode = $LaunchMode
    startupRouteId = $StartupRouteId
    launcherExecutablePath = $LauncherExecutablePath
    executablePath = $ExecutablePath
    mapUrl = $MapUrl
    runtimeLogPath = $RuntimeLogPath
    runtimeLogExists = $false
    runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
    runtimeDiagnosticsExists = $false
    detectedFatalLogLines = @()
    mapLoadLogLines = @()
    requiresClassicPresentation = $RequiresClassicPresentation
    classicPresentationLogLines = @()
    classicScrambleSettlementLogLines = @()
    higherDimensionalFamily = $HigherDimensionalFamily
    expectedHigherDimensionalElementCount = $ExpectedHigherDimensionalElementCount
    higherDimensionalPresentationLogLines = @()
    higherDimensionalPaletteLogLines = @()
    fourDimensionalOrder = $FourDimensionalOrder
    expectedFourDimensionalStatePieceCount = $ExpectedFourDimensionalStatePieceCount
    expectedFourDimensionalVisiblePieceViewCount = $ExpectedFourDimensionalVisiblePieceViewCount
    fourDimensionalPresentationLogLines = @()
    smokeSeconds = $SmokeSeconds
    resolution = [ordered]@{
        width = $ResX
        height = $ResY
    }
    keepRunning = [bool]$KeepRunning
    useNullRhi = [bool]$UseNullRHI
    noSound = [bool]$NoSound
    result = 'failed'
    processId = $null
    processIds = @()
    processCommandLines = @()
    ownedListeningTcpEndpoints = @()
    traceControlListenerLines = @()
    traceControlListeningEndpoints = @()
    startupRouteCommandLineEvidence = @()
    startupRouteEvidenceLines = @()
    processStopped = $false
    stoppedProcessIds = @()
    exitCode = $null
    error = $null
}

try
{
    $Process = Start-Process `
        -FilePath $ExecutablePath `
        -ArgumentList $ArgumentList `
        -WorkingDirectory (Split-Path -Parent $ExecutablePath) `
        -PassThru
    Start-Sleep -Seconds $SmokeSeconds

    $Process.Refresh()
    $Report.processId = $Process.Id
    $OwnedProcessRecords = @(
        Get-OwnedPackageProcessRecords `
            -ResolvedPackageRoot $ResolvedPackageRoot `
            -ExcludedProcessIds $ExistingPackageProcessIds
    )
    $Report.processIds = @($OwnedProcessRecords | Select-Object -ExpandProperty ProcessId)
    $Report.processCommandLines = @(
        $OwnedProcessRecords |
            ForEach-Object { [string]$_.CommandLine } |
            Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    )
    $Report.ownedListeningTcpEndpoints = @(
        Get-OwnedListeningTcpEndpoints -ProcessIds $Report.processIds
    )
    $Report.traceControlListeningEndpoints = @(
        $Report.ownedListeningTcpEndpoints |
            Where-Object { [int]$_.localPort -eq 1985 }
    )

    Start-Sleep -Milliseconds 500
    $Report.runtimeLogExists = Test-Path -LiteralPath $RuntimeLogPath
    $Report.runtimeDiagnosticsExists = Test-Path -LiteralPath $RuntimeDiagnosticsPath
    $Report.detectedFatalLogLines = @(
        @(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeLogPath) +
        @(Get-FatalRuntimeLogMatches -RuntimeLogPath $RuntimeDiagnosticsPath)
    )
    if (-not $Report.runtimeDiagnosticsExists)
    {
        throw 'Packaged launch did not write its direct HyperTwist diagnostics log.'
    }
    if ($Report.detectedFatalLogLines.Count -gt 0)
    {
        throw (
            "Packaged classic-cube runtime log recorded fatal startup evidence: " +
            ($Report.detectedFatalLogLines -join ' | ')
        )
    }

    $CurrentRuntimeLogLines = @(
        if ($Report.runtimeLogExists)
        {
            Get-Content -LiteralPath $RuntimeLogPath
        }
    )
    $CurrentRuntimeDiagnosticsLines = @(
        if ($Report.runtimeDiagnosticsExists)
        {
            Get-Content -LiteralPath $RuntimeDiagnosticsPath
        }
    )
    $CurrentEvidenceLines = @($CurrentRuntimeLogLines + $CurrentRuntimeDiagnosticsLines)
    $Report.traceControlListenerLines = @(
        $CurrentEvidenceLines |
            Where-Object {
                $_.ToString().IndexOf(
                    'Control listening on port',
                    [System.StringComparison]::OrdinalIgnoreCase
                ) -ge 0
            } |
            ForEach-Object { $_.ToString() }
    )
    if ($Report.traceControlListenerLines.Count -gt 0 `
        -or $Report.traceControlListeningEndpoints.Count -gt 0)
    {
        throw 'Packaged runtime opened an Unreal trace-control listener.'
    }

    if (-not [string]::IsNullOrWhiteSpace($StartupRouteId))
    {
        $ExpectedStartupRouteArgument = "-HyperTwistStartupRoute=$StartupRouteId"
        $Report.startupRouteCommandLineEvidence = @(
            $Report.processCommandLines |
                Where-Object {
                    $_.IndexOf(
                        $ExpectedStartupRouteArgument,
                        [System.StringComparison]::OrdinalIgnoreCase
                    ) -ge 0
                }
        )
        if ($Report.startupRouteCommandLineEvidence.Count -eq 0)
        {
            throw (
                "Packaged bootstrap did not propagate first-party startup route argument " +
                "'$ExpectedStartupRouteArgument'."
            )
        }

        $ExpectedStartupRouteMarker = "Accepted packaged startup route '$StartupRouteId'"
        $Report.startupRouteEvidenceLines = @(
            $CurrentEvidenceLines |
                Where-Object { $_ -like "*$ExpectedStartupRouteMarker*" } |
                ForEach-Object { $_.ToString() }
        )
        if ($Report.startupRouteEvidenceLines.Count -eq 0)
        {
            throw (
                "Packaged runtime did not accept first-party startup route '$StartupRouteId' " +
                'through the launch controller.'
            )
        }
    }

    $ExpectedMapLeaf = ($MapUrl -split '/')[-1]
    $Report.mapLoadLogLines = @(
        $CurrentEvidenceLines |
            Where-Object {
                $_ -like "*LogLoad: LoadMap: $MapUrl*" `
                    -or $_ -like "*Bringing World $MapUrl*up for play*" `
                    -or $_ -like "*Runtime map ready:*$ExpectedMapLeaf*"
            } |
            ForEach-Object { $_.ToString() }
    )
    if ($Report.mapLoadLogLines.Count -eq 0)
    {
        throw "Packaged launch stayed alive but did not load target map '$MapUrl' before the smoke interval ended."
    }

    if ($FourDimensionalOrder -gt 0)
    {
        $ExpectedFourDimensionalMarker = (
            'Four-dimensional presentation initialized for order {0} with {1} exact state pieces ' +
            'and {2} visible projected piece views; state valid, projection valid.'
        ) -f (
            $FourDimensionalOrder,
            $ExpectedFourDimensionalStatePieceCount,
            $ExpectedFourDimensionalVisiblePieceViewCount
        )
        $Report.fourDimensionalPresentationLogLines = @(
            $CurrentEvidenceLines |
                Where-Object {
                    $_.ToString().IndexOf(
                        '[FourDimensionalPresentation]',
                        [System.StringComparison]::Ordinal
                    ) -ge 0
                } |
                ForEach-Object { $_.ToString() }
        )
        $MatchingFourDimensionalPresentationLines = @(
            $Report.fourDimensionalPresentationLogLines |
                Where-Object { $_ -like "*$ExpectedFourDimensionalMarker*" }
        )
        if ($MatchingFourDimensionalPresentationLines.Count -eq 0)
        {
            throw (
                "Packaged map '$MapUrl' loaded, but the runtime did not confirm order " +
                "$FourDimensionalOrder with $ExpectedFourDimensionalStatePieceCount exact state pieces " +
                "and $ExpectedFourDimensionalVisiblePieceViewCount visible projected piece views."
            )
        }
    }

    $Report.classicPresentationLogLines = @(
        $CurrentEvidenceLines |
            Where-Object {
                $_ -like '*Classic cube presentation initialized with 26 renderable pieces*'
            } |
            ForEach-Object { $_.ToString() }
    )
    if ($RequiresClassicPresentation -and $Report.classicPresentationLogLines.Count -eq 0)
    {
        throw (
            "Packaged map '$MapUrl' loaded, but the runtime did not confirm its complete 26-cubie " +
            'Classic cube presentation.'
        )
    }

    $Report.classicScrambleSettlementLogLines = @(
        $CurrentEvidenceLines |
            Where-Object {
                $_ -match 'Classic cube scramble settled with \d+ completed quarter turns, 0 queued rotations, and 26 renderable pieces; renderable state valid\.'
            } |
            ForEach-Object { $_.ToString() }
    )
    if ($RequiresClassicPresentation -and $Report.classicScrambleSettlementLogLines.Count -eq 0)
    {
        throw (
            "Packaged map '$MapUrl' loaded its Classic presentation, but the initial scramble " +
            'did not settle into a coherent 26-cubie state before the smoke interval ended.'
        )
    }

    if (-not [string]::IsNullOrWhiteSpace($HigherDimensionalFamily))
    {
        $ExpectedPresentationMarker = (
            'Higher-dimensional presentation initialized with {0} renderable elements ' +
            '(canonical={0}) for family {1}.'
        ) -f $ExpectedHigherDimensionalElementCount, $HigherDimensionalFamily
        $Report.higherDimensionalPresentationLogLines = @(
            $CurrentEvidenceLines |
                Where-Object { $_ -like "*$ExpectedPresentationMarker*" } |
                ForEach-Object { $_.ToString() }
        )
        if ($Report.higherDimensionalPresentationLogLines.Count -eq 0)
        {
            throw (
                "Packaged map '$MapUrl' loaded, but the runtime did not confirm its " +
                "$ExpectedHigherDimensionalElementCount-element $HigherDimensionalFamily presentation."
            )
        }

        $Report.higherDimensionalPaletteLogLines = @(
            $CurrentEvidenceLines |
                Where-Object {
                    $_ -like '*Higher-dimensional projection palette initialized with 6 distinct layer materials.*'
                } |
                ForEach-Object { $_.ToString() }
        )
        if ($Report.higherDimensionalPaletteLogLines.Count -eq 0)
        {
            throw (
                "Packaged map '$MapUrl' loaded its projection, but the runtime did not confirm " +
                'six distinct first-party layer materials.'
            )
        }
    }

    if ($Report.processIds.Count -eq 0)
    {
        if ($Process.HasExited)
        {
            $Report.exitCode = $Process.ExitCode
        }
        throw "Packaged classic-cube executable exited before the smoke interval completed."
    }

    $Report.result = 'passed'
}
catch
{
    $Report.error = $_.Exception.Message
    if ($null -ne $Process -and $Process.HasExited)
    {
        $Report.exitCode = $Process.ExitCode
    }
    if (-not $KeepRunning)
    {
        $Report.stoppedProcessIds = @(
            Stop-OwnedPackageProcesses `
                -ResolvedPackageRoot $ResolvedPackageRoot `
                -ExcludedProcessIds $ExistingPackageProcessIds
        )
        $Report.processStopped = $Report.stoppedProcessIds.Count -gt 0
    }

    if (-not [string]::IsNullOrWhiteSpace($ReportPath))
    {
        Write-Utf8JsonFile -Path $ReportPath -Value $Report
    }

    throw
}

Write-Host "Packaged classic-cube smoke launch succeeded (PID $($Process.Id))."
if (-not $KeepRunning)
{
    $Report.stoppedProcessIds = @(
        Stop-OwnedPackageProcesses `
            -ResolvedPackageRoot $ResolvedPackageRoot `
            -ExcludedProcessIds $ExistingPackageProcessIds
    )
    $Report.processStopped = $Report.stoppedProcessIds.Count -gt 0
    Write-Host 'Stopped the complete packaged classic-cube smoke process tree after successful launch validation.'
}

if (-not [string]::IsNullOrWhiteSpace($ReportPath))
{
    Write-Utf8JsonFile -Path $ReportPath -Value $Report
}
