param(
    [Parameter(Mandatory = $true)]
    [string]$ExecutablePath,
    [Parameter(Mandatory = $true)]
    [string]$OutputDirectory,
    [string[]]$LaunchArguments = @(
        '-windowed',
        '-ResX=1280',
        '-ResY=720'
    ),
    [int]$StartupTimeoutSeconds = 45,
    [int]$LayoutSettleSeconds = 5,
    [int]$SampleStride = 6,
    [double]$MinimumNonBlackRatio = 0.02,
    [double]$MinimumBrightRatio = 0.001,
    [double]$MinimumLuminanceDeviation = 2.5,
    [double]$MinimumLuminanceRange = 20.0,
    [double]$ClickNormalizedX = -1.0,
    [double]$ClickNormalizedY = -1.0,
    [ValidateSet('PhysicalMouse', 'WindowMessage')]
    [string]$ClickInjectionMode = 'PhysicalMouse',
    [int]$PostClickSettleSeconds = 5,
    [string]$ExpectedWindowClassName = 'UnrealWindow',
    [string]$ExpectedStartupDiagnosticsResult = '',
    [string]$ExpectedSemanticReadyCaptureName = '',
    [int]$SemanticReadyCaptureTimeoutSeconds = 30,
    [switch]$KeepProcess
)

$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
    }

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

function Write-HyperTwistVisualCheckpoint {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [string]$Stage
    )

    $Line = "{0}`t{1}{2}" -f `
        [DateTime]::UtcNow.ToString('o'), `
        $Stage, `
        [Environment]::NewLine
    [System.IO.File]::AppendAllText(
        $Path,
        $Line,
        (New-Object System.Text.UTF8Encoding($false))
    )
}

function Get-HyperTwistClientBounds {
    param(
        [Parameter(Mandatory = $true)]
        [IntPtr]$WindowHandle
    )

    $ClientRect = New-Object HyperTwistVisualGate.NativeMethods+RECT
    if (-not [HyperTwistVisualGate.NativeMethods]::GetClientRect($WindowHandle, [ref]$ClientRect))
    {
        throw 'GetClientRect failed for the HyperTwist window.'
    }

    $ClientOrigin = New-Object HyperTwistVisualGate.NativeMethods+POINT
    $ClientOrigin.X = 0
    $ClientOrigin.Y = 0
    if (-not [HyperTwistVisualGate.NativeMethods]::ClientToScreen($WindowHandle, [ref]$ClientOrigin))
    {
        throw 'ClientToScreen failed for the HyperTwist window.'
    }

    return [ordered]@{
        x = $ClientOrigin.X
        y = $ClientOrigin.Y
        width = $ClientRect.Right - $ClientRect.Left
        height = $ClientRect.Bottom - $ClientRect.Top
    }
}

function Set-HyperTwistWindowForeground {
    param(
        [Parameter(Mandatory = $true)]
        [IntPtr]$WindowHandle,
        [switch]$RequireForeground
    )

    for ($Attempt = 1; $Attempt -le 3; $Attempt += 1)
    {
        [void][HyperTwistVisualGate.NativeMethods]::ShowWindowAsync($WindowHandle, 9)
        # Pulse topmost status to expose an obscured game window, then restore
        # normal z-order before capture.
        [void][HyperTwistVisualGate.NativeMethods]::SetWindowPos(
            $WindowHandle,
            [IntPtr](-1),
            0,
            0,
            0,
            0,
            0x0043
        )
        [void][HyperTwistVisualGate.NativeMethods]::SetWindowPos(
            $WindowHandle,
            [IntPtr](-2),
            0,
            0,
            0,
            0,
            0x0043
        )
        [void][HyperTwistVisualGate.NativeMethods]::BringWindowToTop($WindowHandle)
        [void][HyperTwistVisualGate.NativeMethods]::SetForegroundWindow($WindowHandle)
        Start-Sleep -Milliseconds 300
        if ([HyperTwistVisualGate.NativeMethods]::GetForegroundWindow() -eq $WindowHandle)
        {
            return "topmost-attempt-$Attempt"
        }

        Start-Sleep -Milliseconds 300
    }

    if ($RequireForeground)
    {
        throw 'Could not focus the HyperTwist window before interactive visual capture.'
    }

    return 'topmost-foreground-unconfirmed'
}

function Measure-HyperTwistBitmap {
    param(
        [Parameter(Mandatory = $true)]
        [System.Drawing.Bitmap]$Bitmap,
        [int]$Stride = 6
    )

    $SafeStride = [Math]::Max($Stride, 1)
    $SampleCount = 0
    $NonBlackCount = 0
    $BrightCount = 0
    $LuminanceSum = 0.0
    $LuminanceSquaredSum = 0.0
    $MinimumLuminance = 255.0
    $MaximumLuminance = 0.0
    $ColorBuckets = New-Object 'System.Collections.Generic.HashSet[string]'

    for ($Y = 0; $Y -lt $Bitmap.Height; $Y += $SafeStride)
    {
        for ($X = 0; $X -lt $Bitmap.Width; $X += $SafeStride)
        {
            $Pixel = $Bitmap.GetPixel($X, $Y)
            $Luminance = (0.2126 * $Pixel.R) + (0.7152 * $Pixel.G) + (0.0722 * $Pixel.B)
            $SampleCount += 1
            $LuminanceSum += $Luminance
            $LuminanceSquaredSum += ($Luminance * $Luminance)
            $MinimumLuminance = [Math]::Min($MinimumLuminance, $Luminance)
            $MaximumLuminance = [Math]::Max($MaximumLuminance, $Luminance)
            if ($Luminance -gt 10.0)
            {
                $NonBlackCount += 1
            }
            if ($Luminance -gt 35.0)
            {
                $BrightCount += 1
            }

            $Bucket = '{0}-{1}-{2}' -f `
                [Math]::Floor($Pixel.R / 16), `
                [Math]::Floor($Pixel.G / 16), `
                [Math]::Floor($Pixel.B / 16)
            [void]$ColorBuckets.Add($Bucket)
        }
    }

    if ($SampleCount -le 0)
    {
        throw 'The captured HyperTwist client image contained no samples.'
    }

    $AverageLuminance = $LuminanceSum / $SampleCount
    $Variance = [Math]::Max(
        ($LuminanceSquaredSum / $SampleCount) - ($AverageLuminance * $AverageLuminance),
        0.0
    )

    return [ordered]@{
        sampleStride = $SafeStride
        sampleCount = $SampleCount
        nonBlackRatio = $NonBlackCount / $SampleCount
        brightRatio = $BrightCount / $SampleCount
        averageLuminance = $AverageLuminance
        luminanceDeviation = [Math]::Sqrt($Variance)
        minimumLuminance = $MinimumLuminance
        maximumLuminance = $MaximumLuminance
        luminanceRange = $MaximumLuminance - $MinimumLuminance
        quantizedColorBucketCount = $ColorBuckets.Count
    }
}

function Update-HyperTwistOwnedProcessIds {
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [System.Collections.Generic.HashSet[int]]$OwnedProcessIds,
        [Parameter(Mandatory = $true)]
        [string]$ResolvedExecutableRoot,
        [int[]]$IgnoredProcessIds = @()
    )

    $RootPrefix = $ResolvedExecutableRoot.TrimEnd(
        [System.IO.Path]::DirectorySeparatorChar,
        [System.IO.Path]::AltDirectorySeparatorChar
    ) + [System.IO.Path]::DirectorySeparatorChar
    $ProcessSnapshots = @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue)
    foreach ($ProcessSnapshot in $ProcessSnapshots)
    {
        $ExecutablePath = [string]$ProcessSnapshot.ExecutablePath
        if ($IgnoredProcessIds -notcontains [int]$ProcessSnapshot.ProcessId `
            -and ([string]$ProcessSnapshot.Name) -like 'UnrealHyperTwist*.exe' `
            -and -not [string]::IsNullOrWhiteSpace($ExecutablePath) `
            -and $ExecutablePath.StartsWith(
                $RootPrefix,
                [System.StringComparison]::OrdinalIgnoreCase
            ))
        {
            [void]$OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId)
        }
    }

    # Preserve ownership across the bootstrap-to-Shipping handoff even when
    # Windows temporarily withholds a child's ExecutablePath during startup.
    $AddedDescendant = $true
    while ($AddedDescendant)
    {
        $AddedDescendant = $false
        foreach ($ProcessSnapshot in $ProcessSnapshots)
        {
            if ($IgnoredProcessIds -notcontains [int]$ProcessSnapshot.ProcessId `
                -and $OwnedProcessIds.Contains([int]$ProcessSnapshot.ParentProcessId) `
                -and $OwnedProcessIds.Add([int]$ProcessSnapshot.ProcessId))
            {
                $AddedDescendant = $true
            }
        }
    }
}

if (-not (Test-Path -LiteralPath $ExecutablePath -PathType Leaf))
{
    throw "HyperTwist executable '$ExecutablePath' does not exist."
}

New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
$ResolvedExecutablePath = (Resolve-Path -LiteralPath $ExecutablePath).Path
$ResolvedOutputDirectory = (Resolve-Path -LiteralPath $OutputDirectory).Path
$ExecutableDirectory = Split-Path -Parent $ResolvedExecutablePath
$CaptureToken = [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ')
$ScreenshotPath = Join-Path $ResolvedOutputDirectory "HyperTwist-window-$CaptureToken.png"
$ReportPath = Join-Path $ResolvedOutputDirectory "HyperTwist-window-$CaptureToken.json"
$ProgressPath = Join-Path $ResolvedOutputDirectory "HyperTwist-window-$CaptureToken.progress.log"
$RuntimeDiagnosticsPath = Join-Path $ResolvedOutputDirectory "HyperTwist-window-$CaptureToken.hyperdiagnostics.log"
$EffectiveLaunchArguments = @($LaunchArguments)
if (-not @($EffectiveLaunchArguments | Where-Object {
    $_ -like '-HyperTwistDiagnosticsLog=*'
}).Count)
{
    $EffectiveLaunchArguments += "-HyperTwistDiagnosticsLog=`"$RuntimeDiagnosticsPath`""
}
$SemanticReadyCaptureCandidates = @()
if (-not [string]::IsNullOrWhiteSpace($ExpectedSemanticReadyCaptureName))
{
    if ([System.IO.Path]::GetFileName($ExpectedSemanticReadyCaptureName) `
        -ne $ExpectedSemanticReadyCaptureName `
        -or [System.IO.Path]::GetExtension($ExpectedSemanticReadyCaptureName) -ne '.png')
    {
        throw 'ExpectedSemanticReadyCaptureName must be one PNG file name without a path.'
    }

    $SemanticReadyCaptureCandidates = @(
        (Join-Path `
            $env:LOCALAPPDATA `
            "UnrealHyperTwist\Saved\Screenshots\HyperTwistDiagnostics\$ExpectedSemanticReadyCaptureName"),
        (Join-Path `
            $ExecutableDirectory `
            "UnrealHyperTwist\Saved\Screenshots\HyperTwistDiagnostics\$ExpectedSemanticReadyCaptureName"),
        (Join-Path `
            $ExecutableDirectory `
            "Saved\Screenshots\HyperTwistDiagnostics\$ExpectedSemanticReadyCaptureName")
    ) | Select-Object -Unique
}
$ExistingOwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'
Update-HyperTwistOwnedProcessIds `
    -OwnedProcessIds $ExistingOwnedProcessIds `
    -ResolvedExecutableRoot $ExecutableDirectory
$ExistingProcessIds = @($ExistingOwnedProcessIds)

Add-Type -AssemblyName System.Drawing
if (-not ('HyperTwistVisualGate.NativeMethods' -as [type]))
{
    Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace HyperTwistVisualGate
{
    public static class NativeMethods
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct POINT
        {
            public int X;
            public int Y;
        }

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool GetClientRect(IntPtr hWnd, ref RECT lpRect);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);

        [DllImport("user32.dll")]
        public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetForegroundWindow(IntPtr hWnd);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool BringWindowToTop(IntPtr hWnd);

        [DllImport("user32.dll")]
        public static extern IntPtr GetForegroundWindow();

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetWindowPos(
            IntPtr hWnd,
            IntPtr hWndInsertAfter,
            int x,
            int y,
            int cx,
            int cy,
            uint flags
        );

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool SetCursorPos(int x, int y);

        [DllImport("user32.dll")]
        public static extern void mouse_event(
            uint dwFlags,
            uint dx,
            uint dy,
            uint dwData,
            UIntPtr dwExtraInfo
        );

        [DllImport("user32.dll")]
        public static extern IntPtr SendMessage(
            IntPtr hWnd,
            uint msg,
            IntPtr wParam,
            IntPtr lParam
        );

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern int GetClassName(
            IntPtr hWnd,
            StringBuilder lpClassName,
            int nMaxCount
        );

        public static string ReadWindowClassName(IntPtr hWnd)
        {
            var className = new StringBuilder(256);
            return GetClassName(hWnd, className, className.Capacity) > 0
                ? className.ToString()
                : String.Empty;
        }
    }
}
'@
}

# CopyFromScreen uses physical pixels. Match the calling thread to modern UE
# per-monitor DPI coordinates so the measured rectangle cannot drift onto the
# surrounding desktop at display scaling values above 100 percent.
$PreviousDpiAwarenessContext = [HyperTwistVisualGate.NativeMethods]::SetThreadDpiAwarenessContext(
    [IntPtr](-4)
)
if ($PreviousDpiAwarenessContext -eq [IntPtr]::Zero)
{
    throw 'Could not enable per-monitor-v2 DPI awareness for the visual capture thread.'
}

$Result = [ordered]@{
    reportVersion = 'ht-packaged-window-visual/v2'
    generatedAtUtc = [DateTime]::UtcNow.ToString('o')
    launchStartedAtUtc = $null
    executablePath = $ResolvedExecutablePath
    executableSha256 = (Get-FileHash -LiteralPath $ResolvedExecutablePath -Algorithm SHA256).Hash.ToLowerInvariant()
    launchArguments = @($EffectiveLaunchArguments)
    perMonitorV2DpiAware = $true
    processId = $null
    windowTitle = $null
    windowClassName = $null
    expectedWindowClassName = $ExpectedWindowClassName
    clientBounds = $null
    screenshotPath = $ScreenshotPath
    progressPath = $ProgressPath
    screenshotSha256 = $null
    captureMethod = $null
    expectedSemanticReadyCaptureName = $ExpectedSemanticReadyCaptureName
    semanticReadyCaptureSourcePath = $null
    semanticReadyCaptureSourceSha256 = $null
    metrics = $null
    thresholds = [ordered]@{
        minimumNonBlackRatio = $MinimumNonBlackRatio
        minimumBrightRatio = $MinimumBrightRatio
        minimumLuminanceDeviation = $MinimumLuminanceDeviation
        minimumLuminanceRange = $MinimumLuminanceRange
    }
    interaction = [ordered]@{
        requested = $ClickNormalizedX -ge 0.0 -or $ClickNormalizedY -ge 0.0
        normalizedX = $ClickNormalizedX
        normalizedY = $ClickNormalizedY
        screenX = $null
        screenY = $null
        clientX = $null
        clientY = $null
        injectionMethod = $null
        clicked = $false
        expectedStartupDiagnosticsResult = $ExpectedStartupDiagnosticsResult
    }
    startupDiagnosticsPath = $null
    startupDiagnosticsLastWriteUtc = $null
    startupDiagnosticsResult = $null
    runtimeDiagnosticsPath = $RuntimeDiagnosticsPath
    runtimeDiagnosticsExists = $false
    runtimeDiagnosticsLines = @()
    processCommandLines = @()
    ownedListeningTcpEndpoints = @()
    traceControlListenerLines = @()
    traceControlListeningEndpoints = @()
    focusMethod = $null
    result = 'failed'
    error = $null
}

$LaunchedProcess = $null
$WindowProcess = $null
$OwnedProcessIds = New-Object 'System.Collections.Generic.HashSet[int]'

try
{
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'launch-starting'
    $LaunchStartedAtUtc = [DateTime]::UtcNow
    $Result.launchStartedAtUtc = $LaunchStartedAtUtc.ToString('o')
    $LaunchedProcess = Start-Process `
        -FilePath $ResolvedExecutablePath `
        -ArgumentList $EffectiveLaunchArguments `
        -WorkingDirectory $ExecutableDirectory `
        -PassThru
    [void]$OwnedProcessIds.Add($LaunchedProcess.Id)
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'launch-process-started'

    $Deadline = [DateTime]::UtcNow.AddSeconds([Math]::Max($StartupTimeoutSeconds, 1))
    while ([DateTime]::UtcNow -lt $Deadline)
    {
        Update-HyperTwistOwnedProcessIds `
            -OwnedProcessIds $OwnedProcessIds `
            -ResolvedExecutableRoot $ExecutableDirectory `
            -IgnoredProcessIds $ExistingProcessIds
        $CandidateProcesses = @($OwnedProcessIds) |
            Where-Object { $ExistingProcessIds -notcontains $_ } |
            ForEach-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue } |
            Where-Object { $null -ne $_ }
        foreach ($Candidate in $CandidateProcesses)
        {
            [void]$OwnedProcessIds.Add($Candidate.Id)
            $Candidate.Refresh()
            if ($Candidate.MainWindowHandle -ne [IntPtr]::Zero)
            {
                $CandidateWindowClass = [HyperTwistVisualGate.NativeMethods]::ReadWindowClassName(
                    $Candidate.MainWindowHandle
                )
                if ($CandidateWindowClass -eq 'ConsoleWindowClass')
                {
                    continue
                }
                if (-not [string]::IsNullOrWhiteSpace($ExpectedWindowClassName) `
                    -and $CandidateWindowClass -ne $ExpectedWindowClassName)
                {
                    continue
                }
                $WindowProcess = $Candidate
                break
            }
        }

        if ($null -ne $WindowProcess)
        {
            break
        }
        if ($LaunchedProcess.HasExited)
        {
            throw "HyperTwist exited with code $($LaunchedProcess.ExitCode) before creating a window."
        }

        Start-Sleep -Milliseconds 500
    }

    if ($null -eq $WindowProcess)
    {
        throw "HyperTwist did not expose a top-level window within $StartupTimeoutSeconds seconds."
    }

    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'window-detected'
    Start-Sleep -Seconds ([Math]::Max($LayoutSettleSeconds, 0))
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'initial-layout-settled'
    $WindowProcess.Refresh()
    if ($WindowProcess.HasExited -or $WindowProcess.MainWindowHandle -eq [IntPtr]::Zero)
    {
        throw 'The HyperTwist window closed before visual capture.'
    }
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'initial-window-refreshed'

    $Bounds = Get-HyperTwistClientBounds -WindowHandle $WindowProcess.MainWindowHandle
    if ($Bounds.width -lt 320 -or $Bounds.height -lt 200)
    {
        throw "HyperTwist exposed an invalid client area ($($Bounds.width)x$($Bounds.height))."
    }
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'initial-bounds-measured'
    $InteractionRequested = $ClickNormalizedX -ge 0.0 -or $ClickNormalizedY -ge 0.0
    $Result.focusMethod = Set-HyperTwistWindowForeground `
        -WindowHandle $WindowProcess.MainWindowHandle `
        -RequireForeground:$InteractionRequested
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'initial-window-focused'

    if ($InteractionRequested)
    {
        if ($ClickNormalizedX -lt 0.0 -or $ClickNormalizedX -gt 1.0 `
            -or $ClickNormalizedY -lt 0.0 -or $ClickNormalizedY -gt 1.0)
        {
            throw 'Provide both normalized click coordinates within the inclusive range 0.0 to 1.0.'
        }

        $ClickX = [int][Math]::Round(
            $Bounds.x + ([Math]::Max($Bounds.width - 1, 0) * $ClickNormalizedX)
        )
        $ClickY = [int][Math]::Round(
            $Bounds.y + ([Math]::Max($Bounds.height - 1, 0) * $ClickNormalizedY)
        )
        $ClientClickX = [int][Math]::Round(
            [Math]::Max($Bounds.width - 1, 0) * $ClickNormalizedX
        )
        $ClientClickY = [int][Math]::Round(
            [Math]::Max($Bounds.height - 1, 0) * $ClickNormalizedY
        )
        if (-not [HyperTwistVisualGate.NativeMethods]::SetCursorPos($ClickX, $ClickY))
        {
            throw 'Could not position the cursor for the requested HyperTwist interaction.'
        }
        Start-Sleep -Milliseconds 300

        if ($ClickInjectionMode -eq 'PhysicalMouse')
        {
            [HyperTwistVisualGate.NativeMethods]::mouse_event(
                0x0002,
                0,
                0,
                0,
                [UIntPtr]::Zero
            )
            Start-Sleep -Milliseconds 75
            [HyperTwistVisualGate.NativeMethods]::mouse_event(
                0x0004,
                0,
                0,
                0,
                [UIntPtr]::Zero
            )
            $Result.interaction.injectionMethod = 'physical-mouse-event'
        }
        else
        {
            $PackedClientPoint = [IntPtr](
                (($ClientClickY -band 0xffff) -shl 16) `
                    -bor ($ClientClickX -band 0xffff)
            )
            [void][HyperTwistVisualGate.NativeMethods]::SendMessage(
                $WindowProcess.MainWindowHandle,
                0x0200,
                [IntPtr]::Zero,
                $PackedClientPoint
            )
            [void][HyperTwistVisualGate.NativeMethods]::SendMessage(
                $WindowProcess.MainWindowHandle,
                0x0201,
                [IntPtr](1),
                $PackedClientPoint
            )
            Start-Sleep -Milliseconds 75
            [void][HyperTwistVisualGate.NativeMethods]::SendMessage(
                $WindowProcess.MainWindowHandle,
                0x0202,
                [IntPtr]::Zero,
                $PackedClientPoint
            )
            $Result.interaction.injectionMethod = 'window-client-message'
        }
        $Result.interaction.screenX = $ClickX
        $Result.interaction.screenY = $ClickY
        $Result.interaction.clientX = $ClientClickX
        $Result.interaction.clientY = $ClientClickY
        $Result.interaction.clicked = $true
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'interaction-injected'
        Start-Sleep -Seconds ([Math]::Max($PostClickSettleSeconds, 0))
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'post-interaction-settled'

        $WindowProcess.Refresh()
        if ($WindowProcess.HasExited -or $WindowProcess.MainWindowHandle -eq [IntPtr]::Zero)
        {
            throw 'The HyperTwist window closed after the requested interaction.'
        }
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'post-interaction-window-refreshed'
        $Bounds = Get-HyperTwistClientBounds -WindowHandle $WindowProcess.MainWindowHandle
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'post-interaction-bounds-measured'
    }
    # The initial focus is retained through physical interaction. Refocusing
    # after OpenLevel is both redundant and unsafe because Windows can block an
    # activation request while Unreal is replacing its viewport.
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'capture-window-ready'

    $SemanticReadyCaptureSourcePath = $null
    if ($SemanticReadyCaptureCandidates.Count -gt 0)
    {
        Write-HyperTwistVisualCheckpoint `
            -Path $ProgressPath `
            -Stage 'semantic-ready-capture-waiting'
        $SemanticReadyCaptureDeadline = [DateTime]::UtcNow.AddSeconds(
            [Math]::Max($SemanticReadyCaptureTimeoutSeconds, 1)
        )
        while ([DateTime]::UtcNow -lt $SemanticReadyCaptureDeadline)
        {
            $SemanticReadyCaptureSourcePath = $SemanticReadyCaptureCandidates |
                Where-Object {
                    (Test-Path -LiteralPath $_ -PathType Leaf) `
                        -and (Get-Item -LiteralPath $_).Length -gt 0 `
                        -and (Get-Item -LiteralPath $_).LastWriteTimeUtc `
                            -ge $LaunchStartedAtUtc.AddSeconds(-2)
                } |
                Select-Object -First 1
            if (-not [string]::IsNullOrWhiteSpace($SemanticReadyCaptureSourcePath))
            {
                break
            }

            Start-Sleep -Milliseconds 250
        }

        if ([string]::IsNullOrWhiteSpace($SemanticReadyCaptureSourcePath))
        {
            throw (
                "HyperTwist did not write fresh semantic-ready frame " +
                "'$ExpectedSemanticReadyCaptureName' within " +
                "$SemanticReadyCaptureTimeoutSeconds seconds."
            )
        }
        Write-HyperTwistVisualCheckpoint `
            -Path $ProgressPath `
            -Stage 'semantic-ready-capture-detected'
    }

    if (-not [string]::IsNullOrWhiteSpace($SemanticReadyCaptureSourcePath))
    {
        $SemanticReadyImage = [System.Drawing.Image]::FromFile(
            $SemanticReadyCaptureSourcePath
        )
        try
        {
            $Bitmap = New-Object System.Drawing.Bitmap($SemanticReadyImage)
        }
        finally
        {
            $SemanticReadyImage.Dispose()
        }
        $Result.captureMethod = 'hyper-twist-semantic-ready-frame'
        $Result.semanticReadyCaptureSourcePath = $SemanticReadyCaptureSourcePath
        $Result.semanticReadyCaptureSourceSha256 = (
            Get-FileHash `
                -LiteralPath $SemanticReadyCaptureSourcePath `
                -Algorithm SHA256
        ).Hash.ToLowerInvariant()
    }
    else
    {
        $Bitmap = New-Object System.Drawing.Bitmap(
            $Bounds.width,
            $Bounds.height,
            [System.Drawing.Imaging.PixelFormat]::Format32bppArgb
        )
        $Result.captureMethod = 'physical-client-screen-copy'
    }

    try
    {
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'capture-bitmap-created'
        if ([string]::IsNullOrWhiteSpace($SemanticReadyCaptureSourcePath))
        {
            $Graphics = [System.Drawing.Graphics]::FromImage($Bitmap)
            try
            {
                Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'screen-copy-starting'
                $Graphics.CopyFromScreen(
                    $Bounds.x,
                    $Bounds.y,
                    0,
                    0,
                    (New-Object System.Drawing.Size($Bounds.width, $Bounds.height)),
                    [System.Drawing.CopyPixelOperation]::SourceCopy
                )
                Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'screen-copy-complete'
            }
            finally
            {
                $Graphics.Dispose()
            }
        }

        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'bitmap-measurement-starting'
        $Metrics = Measure-HyperTwistBitmap -Bitmap $Bitmap -Stride $SampleStride
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'bitmap-measurement-complete'
        $Bitmap.Save($ScreenshotPath, [System.Drawing.Imaging.ImageFormat]::Png)
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'screenshot-saved'
    }
    finally
    {
        $Bitmap.Dispose()
    }

    $Result.processId = $WindowProcess.Id
    $Result.windowTitle = $WindowProcess.MainWindowTitle
    $Result.windowClassName = [HyperTwistVisualGate.NativeMethods]::ReadWindowClassName(
        $WindowProcess.MainWindowHandle
    )
    if (-not [string]::IsNullOrWhiteSpace($ExpectedWindowClassName) `
        -and $Result.windowClassName -ne $ExpectedWindowClassName)
    {
        throw (
            "Expected window class '$ExpectedWindowClassName' but observed " +
            "'$($Result.windowClassName)'."
        )
    }
    $Result.clientBounds = $Bounds
    $Result.screenshotSha256 = (Get-FileHash -LiteralPath $ScreenshotPath -Algorithm SHA256).Hash.ToLowerInvariant()
    $Result.metrics = $Metrics

    $StartupDiagnosticsCandidates = @(
        (Join-Path $ExecutableDirectory 'UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log'),
        (Join-Path $ExecutableDirectory 'Windows\UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log'),
        (Join-Path $env:LOCALAPPDATA 'UnrealHyperTwist\Saved\Logs\HyperTwistFirstRunLaunch-latest.log')
    )
    $StartupDiagnosticsPath = $StartupDiagnosticsCandidates |
        Where-Object {
            (Test-Path -LiteralPath $_ -PathType Leaf) `
                -and (Get-Item -LiteralPath $_).LastWriteTimeUtc -ge $LaunchStartedAtUtc.AddSeconds(-2)
        } |
        Select-Object -First 1
    if (-not [string]::IsNullOrWhiteSpace($StartupDiagnosticsPath))
    {
        $Result.startupDiagnosticsPath = $StartupDiagnosticsPath
        $Result.startupDiagnosticsLastWriteUtc = (
            Get-Item -LiteralPath $StartupDiagnosticsPath
        ).LastWriteTimeUtc.ToString('o')
        # Detach provider metadata before JSON serialization; decorated FileSystem
        # strings otherwise expand recursively under Windows PowerShell 5.1.
        $Result.startupDiagnosticsResult = [string](
            Get-Content -LiteralPath $StartupDiagnosticsPath |
                ForEach-Object { $_.ToString() } |
                Where-Object { $_ -like 'result=*' } |
                Select-Object -Last 1
        )
    }

    if (-not [string]::IsNullOrWhiteSpace($ExpectedStartupDiagnosticsResult) `
        -and $Result.startupDiagnosticsResult -ne "result=$ExpectedStartupDiagnosticsResult")
    {
        throw (
            "Expected startup diagnostics result '$ExpectedStartupDiagnosticsResult' but observed " +
            "'$($Result.startupDiagnosticsResult)'."
        )
    }

    $Result.runtimeDiagnosticsExists = Test-Path -LiteralPath $RuntimeDiagnosticsPath -PathType Leaf
    if (-not $Result.runtimeDiagnosticsExists)
    {
        throw 'HyperTwist did not write its direct Shipping-compatible runtime diagnostics file.'
    }
    $Result.runtimeDiagnosticsLines = @(
        Get-Content -LiteralPath $RuntimeDiagnosticsPath |
            ForEach-Object { $_.ToString() }
    )
    if (-not @($Result.runtimeDiagnosticsLines | Where-Object {
        $_ -like '*HyperTwist runtime module initialized.*'
    }).Count)
    {
        throw 'HyperTwist direct diagnostics did not confirm runtime-module initialization.'
    }

    Update-HyperTwistOwnedProcessIds `
        -OwnedProcessIds $OwnedProcessIds `
        -ResolvedExecutableRoot $ExecutableDirectory `
        -IgnoredProcessIds $ExistingProcessIds
    $OwnedProcessIdSnapshot = [int[]]@($OwnedProcessIds)
    $Result.processCommandLines = @(
        foreach ($OwnedProcessId in $OwnedProcessIdSnapshot)
        {
            Get-CimInstance Win32_Process `
                -Filter "ProcessId = $OwnedProcessId" `
                -ErrorAction SilentlyContinue |
                ForEach-Object { [string]$_.CommandLine }
        }
    ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
    $Result.ownedListeningTcpEndpoints = @(
        Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue |
            Where-Object { $OwnedProcessIdSnapshot -contains [int]$_.OwningProcess } |
            Sort-Object OwningProcess, LocalPort |
            ForEach-Object {
                [pscustomobject][ordered]@{
                    processId = [int]$_.OwningProcess
                    localAddress = [string]$_.LocalAddress
                    localPort = [int]$_.LocalPort
                }
            }
    )
    $Result.traceControlListenerLines = @(
        $Result.runtimeDiagnosticsLines |
            Where-Object {
                $_.IndexOf(
                    'Control listening on port',
                    [System.StringComparison]::OrdinalIgnoreCase
                ) -ge 0
            }
    )
    $Result.traceControlListeningEndpoints = @(
        $Result.ownedListeningTcpEndpoints |
            Where-Object { [int]$_.localPort -eq 1985 }
    )
    if ($Result.traceControlListenerLines.Count -gt 0 `
        -or $Result.traceControlListeningEndpoints.Count -gt 0)
    {
        throw 'HyperTwist opened a Development trace-control listener during the visual gate.'
    }

    $VisualPassed = $Metrics.nonBlackRatio -ge $MinimumNonBlackRatio `
        -and $Metrics.brightRatio -ge $MinimumBrightRatio `
        -and $Metrics.luminanceDeviation -ge $MinimumLuminanceDeviation `
        -and $Metrics.luminanceRange -ge $MinimumLuminanceRange
    if (-not $VisualPassed)
    {
        throw (
            'HyperTwist client capture is blank or visually uniform: ' +
            "nonBlack=$($Metrics.nonBlackRatio), bright=$($Metrics.brightRatio), " +
            "deviation=$($Metrics.luminanceDeviation), range=$($Metrics.luminanceRange)."
        )
    }

    $Result.result = 'visual-passed'
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'visual-gate-passed'
}
catch
{
    $Result.error = $_.Exception.Message
    Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'visual-gate-failed'
}
finally
{
    if (-not $KeepProcess)
    {
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'owned-process-scan-starting'
        Update-HyperTwistOwnedProcessIds `
            -OwnedProcessIds $OwnedProcessIds `
            -ResolvedExecutableRoot $ExecutableDirectory `
            -IgnoredProcessIds $ExistingProcessIds
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'owned-process-scan-complete'
        foreach ($OwnedProcessId in @($OwnedProcessIds))
        {
            Stop-Process -Id $OwnedProcessId -Force -ErrorAction SilentlyContinue
        }
        Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'owned-process-cleanup-complete'
    }
}

Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'report-write-starting'
Write-Utf8JsonFile -Path $ReportPath -Value $Result
Write-HyperTwistVisualCheckpoint -Path $ProgressPath -Stage 'report-write-complete'
$Result | ConvertTo-Json -Depth 12
if ($Result.result -ne 'visual-passed')
{
    throw $Result.error
}
