[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$SmokeDll,

    [Parameter(Mandatory = $true)]
    [string]$SceneRoot,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedNativeSha256,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedInteropSha256,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedSmokeSha256,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedPlayoutSha256,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedCoreSha256,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedDepsSha256,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedRuntimeConfigSha256,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedWindowsSdkSha256,

    [Parameter(Mandatory = $true)]
    [ValidatePattern('^[0-9A-Fa-f]{64}$')]
    [string]$ApprovedWinRtSha256,

    [string]$ExpectedPgmWindowTitle = 'PGM',

    [ValidateRange(1, 65535)]
    [int]$Port = 30001,

    [string]$EvidenceDirectory,

    [switch]$IUnderstandPgmWillRenderCuts
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$acknowledgement =
    '--i-understand-this-will-render-current-pgm-tornado-cuts-5001-then-5006-and-take-them-out'
$expectedCuts = @{
    '5001.t2s' = '99CE3B689A42D8C42BEB09A86FA10C2D7C1AEF4F50D324D81276C1A1E4C4D8A7'
    '5006.t2s' = '25CD0AE931F51E4E3B84CE3E6FD21A40DB85464F157A23CC3511D63B336D8757'
}

if (-not $IUnderstandPgmWillRenderCuts) {
    throw 'The explicit PGM render acknowledgement switch is required.'
}

$playoutOverrides = @(Get-ChildItem Env: | Where-Object {
    $_.Name -like 'MBN_STOCK_PLAYOUT_*'
})
if ($playoutOverrides.Count -ne 0) {
    throw 'MBN_STOCK_PLAYOUT environment overrides must be absent.'
}

$runtimeInjectionOverrides = @(Get-ChildItem Env: | Where-Object {
    $_.Name -in @(
        'DOTNET_STARTUP_HOOKS',
        'DOTNET_ADDITIONAL_DEPS',
        'DOTNET_SHARED_STORE',
        'DOTNET_HOST_PATH',
        'CORECLR_ENABLE_PROFILING',
        'COR_ENABLE_PROFILING') -or
    $_.Name -like 'DOTNET_ROOT*' -or
    $_.Name -like 'CORECLR_*PROFILER*' -or
    $_.Name -like 'COR_*PROFILER*'
})
if ($runtimeInjectionOverrides.Count -ne 0) {
    throw '.NET startup-hook, additional-dependency, or profiler overrides must be absent.'
}

$SmokeDll = [IO.Path]::GetFullPath($SmokeDll)
$SceneRoot = [IO.Path]::GetFullPath($SceneRoot)
if (-not (Test-Path -LiteralPath $SmokeDll -PathType Leaf)) {
    throw 'The playout smoke assembly does not exist.'
}

if ([IO.Path]::GetFileName($SmokeDll) -cne 'MBN_STOCK_WEBVIEW.PlayoutSmoke.dll') {
    throw 'The expected playout smoke assembly name is required.'
}

$runnerDirectory = [IO.Path]::GetDirectoryName($SmokeDll)
$runnerFiles = [ordered]@{
    $SmokeDll = $ApprovedSmokeSha256
    (Join-Path $runnerDirectory 'MBN_STOCK_WEBVIEW.Playout.dll') = $ApprovedPlayoutSha256
    (Join-Path $runnerDirectory 'MBN_STOCK_WEBVIEW.Core.dll') = $ApprovedCoreSha256
    (Join-Path $runnerDirectory 'MBN_STOCK_WEBVIEW.PlayoutSmoke.deps.json') = $ApprovedDepsSha256
    (Join-Path $runnerDirectory 'MBN_STOCK_WEBVIEW.PlayoutSmoke.runtimeconfig.json') = $ApprovedRuntimeConfigSha256
    (Join-Path $runnerDirectory 'Microsoft.Windows.SDK.NET.dll') = $ApprovedWindowsSdkSha256
    (Join-Path $runnerDirectory 'WinRT.Runtime.dll') = $ApprovedWinRtSha256
}

foreach ($entry in $expectedCuts.GetEnumerator()) {
    $path = Join-Path $SceneRoot $entry.Key
    if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
        throw "Required cut file is missing: $($entry.Key)"
    }

    $actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash
    if ($actual -cne $entry.Value) {
        throw "Required cut file hash does not match: $($entry.Key)"
    }
}

$tornado = @(Get-Process -Name 'Tornado2*' -ErrorAction SilentlyContinue)
if ($tornado.Count -ne 1 -or $tornado[0].MainWindowTitle -cne $ExpectedPgmWindowTitle) {
    throw 'Exactly one Tornado2 process with the expected PGM title is required.'
}

$listener = @(Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction Stop)
if ($listener.Count -ne 1 -or $listener[0].OwningProcess -ne $tornado[0].Id) {
    throw 'The exact Tornado2 process must own the requested KTAP listener.'
}

if ([string]::IsNullOrWhiteSpace($EvidenceDirectory)) {
    $stamp = Get-Date -Format 'yyyyMMdd_HHmmss'
    $EvidenceDirectory = Join-Path $env:TEMP "MBN_STOCK_WEBVIEW_PGM_EVIDENCE_$stamp"
}

$EvidenceDirectory = [IO.Path]::GetFullPath($EvidenceDirectory)
if (Test-Path -LiteralPath $EvidenceDirectory) {
    throw 'The evidence directory must not already exist.'
}

[void](New-Item -ItemType Directory -Path $EvidenceDirectory)

Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
Add-Type @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;

public static class PgmEvidenceWindows
{
    private const int GwlExStyle = -20;
    private const long WsExTopmost = 0x00000008L;
    public const int SwRestore = 9;
    public static readonly IntPtr HwndTopmost = new IntPtr(-1);
    public static readonly IntPtr HwndNotTopmost = new IntPtr(-2);
    public const uint SwpNoSize = 0x0001;
    public const uint SwpNoMove = 0x0002;
    public const uint SwpNoActivate = 0x0010;
    public const uint SwpShowWindow = 0x0040;

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

    [StructLayout(LayoutKind.Sequential)]
    public struct Rect { public int Left; public int Top; public int Right; public int Bottom; }

    [StructLayout(LayoutKind.Sequential)]
    public struct WindowPlacement
    {
        public int Length;
        public int Flags;
        public int ShowCommand;
        public Point MinimumPosition;
        public Point MaximumPosition;
        public Rect NormalPosition;
    }

    private delegate bool EnumWindowsCallback(IntPtr window, IntPtr parameter);

    [DllImport("user32.dll")]
    private static extern bool EnumWindows(EnumWindowsCallback callback, IntPtr parameter);

    [DllImport("user32.dll", CharSet = CharSet.Unicode)]
    private static extern int GetWindowText(IntPtr window, StringBuilder text, int count);

    [DllImport("user32.dll")]
    private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);

    [DllImport("user32.dll")]
    public static extern bool GetWindowPlacement(IntPtr window, ref WindowPlacement placement);

    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr window, out Rect rectangle);

    [DllImport("user32.dll")]
    public static extern bool SetWindowPlacement(IntPtr window, ref WindowPlacement placement);

    [DllImport("user32.dll")]
    public static extern bool ShowWindow(IntPtr window, int command);

    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(
        IntPtr window,
        IntPtr insertAfter,
        int x,
        int y,
        int width,
        int height,
        uint flags);

    [DllImport("user32.dll", EntryPoint = "GetWindowLong")]
    private static extern int GetWindowLong32(IntPtr window, int index);

    [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
    private static extern IntPtr GetWindowLongPtr64(IntPtr window, int index);

    public static IntPtr FindTopLevelWindow(uint processId, string exactTitle)
    {
        IntPtr match = IntPtr.Zero;
        EnumWindows((window, parameter) =>
        {
            uint owner;
            GetWindowThreadProcessId(window, out owner);
            if (owner != processId)
            {
                return true;
            }

            var title = new StringBuilder(512);
            GetWindowText(window, title, title.Capacity);
            if (String.Equals(title.ToString(), exactTitle, StringComparison.Ordinal))
            {
                match = window;
                return false;
            }

            return true;
        }, IntPtr.Zero);
        return match;
    }

    public static bool IsTopmost(IntPtr window)
    {
        var style = IntPtr.Size == 8
            ? GetWindowLongPtr64(window, GwlExStyle).ToInt64()
            : GetWindowLong32(window, GwlExStyle);
        return (style & WsExTopmost) != 0;
    }

    public static string QuoteArgument(string argument)
    {
        if (argument == null)
        {
            throw new ArgumentNullException("argument");
        }

        if (argument.Length > 0 &&
            argument.IndexOfAny(new[] { ' ', '\t', '\n', '\v', '"' }) < 0)
        {
            return argument;
        }

        var commandLine = new StringBuilder();
        commandLine.Append('"');
        var backslashes = 0;
        foreach (var character in argument)
        {
            if (character == '\\')
            {
                backslashes++;
                continue;
            }

            if (character == '"')
            {
                commandLine.Append('\\', backslashes * 2 + 1);
                commandLine.Append('"');
                backslashes = 0;
                continue;
            }

            commandLine.Append('\\', backslashes);
            backslashes = 0;
            commandLine.Append(character);
        }

        commandLine.Append('\\', backslashes * 2);
        commandLine.Append('"');
        return commandLine.ToString();
    }
}
'@

function Get-Placement([IntPtr]$Handle) {
    $placement = New-Object PgmEvidenceWindows+WindowPlacement
    $placement.Length = [Runtime.InteropServices.Marshal]::SizeOf($placement)
    if (-not [PgmEvidenceWindows]::GetWindowPlacement($Handle, [ref]$placement)) {
        throw 'Unable to read Tornado window placement.'
    }

    return $placement
}

function Open-ApprovedFileLease(
    [string]$Path,
    [string]$ApprovedSha256) {
    if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
        throw "Required runner file is missing: $([IO.Path]::GetFileName($Path))"
    }

    $stream = [IO.File]::Open(
        $Path,
        [IO.FileMode]::Open,
        [IO.FileAccess]::Read,
        [IO.FileShare]::Read)
    try {
        $sha = [Security.Cryptography.SHA256]::Create()
        try {
            $bytes = $sha.ComputeHash($stream)
        }
        finally {
            $sha.Dispose()
        }

        $actual = -join ($bytes | ForEach-Object { $_.ToString('X2') })
        if ($actual -cne $ApprovedSha256.ToUpperInvariant()) {
            throw "Runner file hash does not match: $([IO.Path]::GetFileName($Path))"
        }

        return [pscustomobject]@{
            Path = $Path
            Sha256 = $actual
            Stream = $stream
        }
    }
    catch {
        $stream.Dispose()
        throw
    }
}

function Save-Frame([int]$Index, [long]$ElapsedMilliseconds) {
    $name = 'frame_{0:D4}_{1:D7}ms.png' -f $Index, $ElapsedMilliseconds
    $path = Join-Path $EvidenceDirectory $name
    $bitmap = New-Object Drawing.Bitmap `
        $script:captureWidth, `
        $script:captureHeight, `
        ([Drawing.Imaging.PixelFormat]::Format24bppRgb)
    try {
        $graphics = [Drawing.Graphics]::FromImage($bitmap)
        try {
            $graphics.Clear([Drawing.Color]::Black)
            $graphics.CopyFromScreen(
                $script:networkCaptureRect.X,
                $script:networkCaptureRect.Y,
                0,
                0,
                $script:networkCaptureRect.Size)
            $graphics.CopyFromScreen(
                $script:pgmCaptureRect.X,
                $script:pgmCaptureRect.Y,
                $script:networkCaptureRect.Width + 20,
                0,
                $script:pgmCaptureRect.Size)
        }
        finally {
            $graphics.Dispose()
        }

        $bitmap.Save($path, [Drawing.Imaging.ImageFormat]::Png)
    }
    finally {
        $bitmap.Dispose()
    }

    return [pscustomobject]@{
        file = $name
        elapsedMilliseconds = $ElapsedMilliseconds
        sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash
    }
}

$pgmHandle = [IntPtr]$tornado[0].MainWindowHandle
$networkTitle = -join ([char[]]@(
    0xB124, 0xD2B8, 0xC6CC, 0xD06C, 0x20,
    0xBAA8, 0xB2C8, 0xD130, 0xB9C1, 0x20, 0xCC3D))
$networkHandle = [PgmEvidenceWindows]::FindTopLevelWindow(
    [uint32]$tornado[0].Id,
    $networkTitle)
if ($pgmHandle -eq [IntPtr]::Zero -or $networkHandle -eq [IntPtr]::Zero) {
    throw 'Both the PGM and Network Monitoring windows must already be open.'
}

$script:networkCaptureRect = New-Object Drawing.Rectangle 20, 20, 1183, 648
$script:pgmCaptureRect = New-Object Drawing.Rectangle 1760, 70, 720, 543
$script:captureWidth =
    $script:networkCaptureRect.Width + 20 + $script:pgmCaptureRect.Width
$script:captureHeight = [Math]::Max(
    $script:networkCaptureRect.Height,
    $script:pgmCaptureRect.Height)
$primaryBounds = [Windows.Forms.Screen]::PrimaryScreen.Bounds
if (-not $primaryBounds.Contains($script:networkCaptureRect) -or
    -not $primaryBounds.Contains($script:pgmCaptureRect)) {
    throw 'The primary display cannot contain both evidence windows.'
}

function Assert-WindowRectangle(
    [IntPtr]$Handle,
    [Drawing.Rectangle]$Expected) {
    $actual = New-Object PgmEvidenceWindows+Rect
    if (-not [PgmEvidenceWindows]::GetWindowRect($Handle, [ref]$actual) -or
        $actual.Left -ne $Expected.Left -or
        $actual.Top -ne $Expected.Top -or
        ($actual.Right - $actual.Left) -ne $Expected.Width -or
        ($actual.Bottom - $actual.Top) -ne $Expected.Height) {
        throw 'A Tornado evidence window was not placed at the verified capture rectangle.'
    }
}

$pgmPlacement = Get-Placement $pgmHandle
$networkPlacement = Get-Placement $networkHandle
$pgmWasTopmost = [PgmEvidenceWindows]::IsTopmost($pgmHandle)
$networkWasTopmost = [PgmEvidenceWindows]::IsTopmost($networkHandle)
$dotnet = 'C:\Program Files\dotnet\dotnet.exe'
if (-not (Test-Path -LiteralPath $dotnet -PathType Leaf)) {
    throw 'The trusted 64-bit dotnet host was not found.'
}

$dotnetSignature = Get-AuthenticodeSignature -LiteralPath $dotnet
if ($dotnetSignature.Status -ne [System.Management.Automation.SignatureStatus]::Valid -or
    $null -eq $dotnetSignature.SignerCertificate -or
    $dotnetSignature.SignerCertificate.Subject -notmatch
        '(^|,\s*)O=Microsoft Corporation(,|$)') {
    throw 'The dotnet host signature is not trusted.'
}

$runnerLeases = [Collections.Generic.List[object]]::new()
try {
    foreach ($runnerFile in $runnerFiles.GetEnumerator()) {
        $runnerLeases.Add((Open-ApprovedFileLease `
            ([string]$runnerFile.Key) `
            ([string]$runnerFile.Value)))
    }
}
catch {
    foreach ($lease in $runnerLeases) {
        $lease.Stream.Dispose()
    }

    throw
}

$frames = [Collections.Generic.List[object]]::new()
$output = [Collections.Generic.List[object]]::new()
$captureErrors = [Collections.Generic.List[string]]::new()
$restorationErrors = [Collections.Generic.List[string]]::new()
$process = $null
$exitCode = $null
$evidenceExitCode = $null
$result = $null
$stopwatch = [Diagnostics.Stopwatch]::StartNew()

try {
    [void][PgmEvidenceWindows]::ShowWindow($networkHandle, [PgmEvidenceWindows]::SwRestore)
    [void][PgmEvidenceWindows]::ShowWindow($pgmHandle, [PgmEvidenceWindows]::SwRestore)
    if (-not [PgmEvidenceWindows]::SetWindowPos(
        $networkHandle,
        [PgmEvidenceWindows]::HwndTopmost,
        $script:networkCaptureRect.X,
        $script:networkCaptureRect.Y,
        $script:networkCaptureRect.Width,
        $script:networkCaptureRect.Height,
        [PgmEvidenceWindows]::SwpNoActivate -bor
            [PgmEvidenceWindows]::SwpShowWindow)) {
        throw 'Unable to place the Network Monitoring window for evidence capture.'
    }

    if (-not [PgmEvidenceWindows]::SetWindowPos(
        $pgmHandle,
        [PgmEvidenceWindows]::HwndTopmost,
        $script:pgmCaptureRect.X,
        $script:pgmCaptureRect.Y,
        $script:pgmCaptureRect.Width,
        $script:pgmCaptureRect.Height,
        [PgmEvidenceWindows]::SwpNoActivate -bor
            [PgmEvidenceWindows]::SwpShowWindow)) {
        throw 'Unable to place the PGM window for evidence capture.'
    }

    Assert-WindowRectangle $networkHandle $script:networkCaptureRect
    Assert-WindowRectangle $pgmHandle $script:pgmCaptureRect
    Start-Sleep -Milliseconds 750
    Assert-WindowRectangle $networkHandle $script:networkCaptureRect
    Assert-WindowRectangle $pgmHandle $script:pgmCaptureRect

    for ($index = 0; $index -lt 4; $index++) {
        $frames.Add((Save-Frame $index $stopwatch.ElapsedMilliseconds))
        Start-Sleep -Milliseconds 500
    }

    $start = [Diagnostics.ProcessStartInfo]::new()
    $start.FileName = $dotnet
    $start.UseShellExecute = $false
    $start.CreateNoWindow = $true
    $start.RedirectStandardOutput = $true
    $start.RedirectStandardError = $true
    $runnerArguments = @(
        $SmokeDll,
        '--pgm-cuts-sequence',
        $acknowledgement,
        '--host',
        '127.0.0.1',
        '--port',
        $Port.ToString([Globalization.CultureInfo]::InvariantCulture),
        '--expected-pgm-window-title',
        $ExpectedPgmWindowTitle,
        '--scene-root',
        $SceneRoot,
        '--observe-ms',
        '5000')
    $start.Arguments = ($runnerArguments | ForEach-Object {
        [PgmEvidenceWindows]::QuoteArgument([string]$_)
    }) -join ' '

    $start.EnvironmentVariables['MBN_STOCK_K3D_NATIVE_SHA256'] =
        $ApprovedNativeSha256.ToUpperInvariant()
    $start.EnvironmentVariables['MBN_STOCK_K3D_INTEROP_SHA256'] =
        $ApprovedInteropSha256.ToUpperInvariant()

    $process = [Diagnostics.Process]::new()
    $process.StartInfo = $start
    if (-not $process.Start()) {
        throw 'Unable to launch the PGM sequence runner.'
    }

    $processStartElapsed = $stopwatch.ElapsedMilliseconds
    $frameIndex = 4
    $captureEnabled = $true
    try {
        $stdoutRead = $process.StandardOutput.ReadLineAsync()
        $stderrRead = $process.StandardError.ReadLineAsync()
        $nextFrameAt = $stopwatch.ElapsedMilliseconds
        $stdoutEnded = $false
        $stderrEnded = $false

        while (-not $process.HasExited -or -not $stdoutEnded -or -not $stderrEnded) {
        if (-not $stdoutEnded -and $stdoutRead.IsCompleted) {
            try {
                $line = $stdoutRead.GetAwaiter().GetResult()
            }
            catch {
                $captureErrors.Add('stdout-read-failed')
                $stdoutEnded = $true
                $line = $null
            }

            if ($null -eq $line) {
                $stdoutEnded = $true
            }
            else {
                $output.Add([pscustomobject]@{
                    stream = 'stdout'
                    elapsedMilliseconds = $stopwatch.ElapsedMilliseconds
                    line = $line
                })
                $stdoutRead = $process.StandardOutput.ReadLineAsync()
            }
        }

        if (-not $stderrEnded -and $stderrRead.IsCompleted) {
            try {
                $line = $stderrRead.GetAwaiter().GetResult()
            }
            catch {
                $captureErrors.Add('stderr-read-failed')
                $stderrEnded = $true
                $line = $null
            }

            if ($null -eq $line) {
                $stderrEnded = $true
            }
            else {
                $output.Add([pscustomobject]@{
                    stream = 'stderr'
                    elapsedMilliseconds = $stopwatch.ElapsedMilliseconds
                    line = $line
                })
                $stderrRead = $process.StandardError.ReadLineAsync()
            }
        }

        if ($captureEnabled -and $stopwatch.ElapsedMilliseconds -ge $nextFrameAt) {
            try {
                $frames.Add((Save-Frame $frameIndex $stopwatch.ElapsedMilliseconds))
                $frameIndex++
            }
            catch {
                $captureErrors.Add('frame-capture-failed')
                $captureEnabled = $false
            }

            $nextFrameAt += 500
        }

            Start-Sleep -Milliseconds 20
        }
    }
    catch {
        $captureErrors.Add('supervision-loop-failed')
        while (-not $process.HasExited) {
            Start-Sleep -Milliseconds 100
        }
    }

    $process.WaitForExit()
    $exitCode = $process.ExitCode
    Start-Sleep -Milliseconds 500
    if ($captureEnabled) {
        try {
            $frames.Add((Save-Frame $frameIndex $stopwatch.ElapsedMilliseconds))
        }
        catch {
            $captureErrors.Add('final-frame-capture-failed')
        }
    }

    $validationErrors = [Collections.Generic.List[string]]::new()
    $stdoutEntries = @($output | Where-Object { $_.stream -eq 'stdout' })
    $markers = [Collections.Generic.List[object]]::new()
    $lastMarkerIndex = -1
    for ($index = 0; $index -lt $stdoutEntries.Count; $index++) {
        $entry = $stdoutEntries[$index]
        if ($entry.line -notmatch '"event"\s*:\s*"observation-start"') {
            continue
        }

        try {
            $marker = $entry.line | ConvertFrom-Json -ErrorAction Stop
            $markers.Add([pscustomobject]@{
                stage = [string]$marker.stage
                elapsedMilliseconds = [long]$entry.elapsedMilliseconds
            })
            $lastMarkerIndex = $index
        }
        catch {
            $validationErrors.Add('observation-marker-json-invalid')
        }
    }

    $markerStages = @($markers | ForEach-Object { $_.stage })
    $expectedMarkerStages = @('first', 'next', 'take-out')
    if ($markerStages.Count -ne $expectedMarkerStages.Count -or
        ($markerStages -join '|') -cne ($expectedMarkerStages -join '|')) {
        $validationErrors.Add('observation-marker-order-invalid')
    }

    $terminalEntries = if ($lastMarkerIndex + 1 -lt $stdoutEntries.Count) {
        @($stdoutEntries[($lastMarkerIndex + 1)..($stdoutEntries.Count - 1)])
    }
    else {
        @()
    }
    $terminal = $null
    if ($terminalEntries.Count -ne 0) {
        try {
            $terminal = ($terminalEntries.line -join [Environment]::NewLine) |
                ConvertFrom-Json -ErrorAction Stop
        }
        catch {
            $validationErrors.Add('terminal-report-json-invalid')
        }
    }

    $observationIntervals = @()
    if ($markers.Count -eq 3 -and $terminalEntries.Count -ne 0) {
        $observationIntervals = @(
            ([long]$markers[1].elapsedMilliseconds -
                [long]$markers[0].elapsedMilliseconds),
            ([long]$markers[2].elapsedMilliseconds -
                [long]$markers[1].elapsedMilliseconds),
            ([long]$terminalEntries[0].elapsedMilliseconds -
                [long]$markers[2].elapsedMilliseconds)
        )
        if (@($observationIntervals | Where-Object { $_ -lt 4900 }).Count -ne 0) {
            $validationErrors.Add('observation-duration-too-short')
        }
    }
    else {
        $validationErrors.Add('observation-duration-unavailable')
    }

    if ($null -eq $terminal -or $terminal.command -ne 'pgm-cuts-sequence') {
        $validationErrors.Add('terminal-report-missing-or-invalid')
    }
    else {
        $actualCommandStages = @($terminal.commands | ForEach-Object {
            '{0}:{1}' -f $_.stage, $_.code
        })
        $expectedCommandStages = @(
            'connect:Success',
            'prepare-first:Success',
            'take-in:Success',
            'next:Success',
            'take-out:Success',
            'disconnect:Success')
        if ($actualCommandStages.Count -ne $expectedCommandStages.Count -or
            ($actualCommandStages -join '|') -cne ($expectedCommandStages -join '|')) {
            $validationErrors.Add('terminal-command-order-invalid')
        }

        if ($terminal.completed -ne $true -or
            $terminal.outcomeUnknown -ne $false -or
            $terminal.renderCommandAttempted -ne $true -or
            $terminal.disconnectAttempted -ne $true -or
            $terminal.quarantineAttempted -ne $false) {
            $validationErrors.Add('terminal-safety-state-invalid')
        }
    }

    if ($exitCode -ne 0) {
        $validationErrors.Add('runner-exit-code-nonzero')
    }

    if ($frames.Count -lt 30) {
        $validationErrors.Add('insufficient-frame-count')
    }

    if ($captureErrors.Count -ne 0) {
        $validationErrors.Add('evidence-capture-incomplete')
    }

    $terminalValidated = $validationErrors.Count -eq 0
    $evidenceExitCode = if ($exitCode -ne 0) { $exitCode } elseif (
        -not $terminalValidated) { 6 } else { 0 }

    $manifest = [ordered]@{
        schemaVersion = 1
        createdAtUtc = [DateTimeOffset]::UtcNow.ToString('O')
        processStartElapsedMilliseconds = $processStartElapsed
        runnerExitCode = $exitCode
        evidenceExitCode = $evidenceExitCode
        terminalValidated = $terminalValidated
        runnerFilesSha256 = @($runnerLeases | ForEach-Object {
            [ordered]@{
                file = [IO.Path]::GetFileName($_.Path)
                sha256 = $_.Sha256
            }
        })
        cutSha256 = $expectedCuts
        frames = $frames
        captureErrors = $captureErrors
        validationErrors = $validationErrors
        observationIntervalsMilliseconds = $observationIntervals
        runnerOutput = $output
    }
    $manifestPath = Join-Path $EvidenceDirectory 'manifest.json'
    [IO.File]::WriteAllText(
        $manifestPath,
        ($manifest | ConvertTo-Json -Depth 8),
        [Text.UTF8Encoding]::new($false))
    $manifestHash = (Get-FileHash -LiteralPath $manifestPath -Algorithm SHA256).Hash

    $result = [pscustomobject]@{
        EvidenceDirectory = $EvidenceDirectory
        RunnerExitCode = $exitCode
        TerminalValidated = $terminalValidated
        FrameCount = $frames.Count
        ManifestSha256 = $manifestHash
    }
}
finally {
    if ($null -ne $process) {
        $process.Dispose()
    }

    foreach ($lease in $runnerLeases) {
        $lease.Stream.Dispose()
    }

    if (-not [PgmEvidenceWindows]::SetWindowPos(
        $networkHandle,
        $(if ($networkWasTopmost) {
            [PgmEvidenceWindows]::HwndTopmost
        } else {
            [PgmEvidenceWindows]::HwndNotTopmost
        }),
        0,
        0,
        0,
        0,
        [PgmEvidenceWindows]::SwpNoActivate -bor
            [PgmEvidenceWindows]::SwpNoMove -bor
            [PgmEvidenceWindows]::SwpNoSize)) {
        $restorationErrors.Add('network-topmost-restore-failed')
    }

    if (-not [PgmEvidenceWindows]::SetWindowPos(
        $pgmHandle,
        $(if ($pgmWasTopmost) {
            [PgmEvidenceWindows]::HwndTopmost
        } else {
            [PgmEvidenceWindows]::HwndNotTopmost
        }),
        0,
        0,
        0,
        0,
        [PgmEvidenceWindows]::SwpNoActivate -bor
            [PgmEvidenceWindows]::SwpNoMove -bor
            [PgmEvidenceWindows]::SwpNoSize)) {
        $restorationErrors.Add('pgm-topmost-restore-failed')
    }

    if (-not [PgmEvidenceWindows]::SetWindowPlacement(
        $networkHandle,
        [ref]$networkPlacement)) {
        $restorationErrors.Add('network-placement-restore-failed')
    }

    if (-not [PgmEvidenceWindows]::SetWindowPlacement(
        $pgmHandle,
        [ref]$pgmPlacement)) {
        $restorationErrors.Add('pgm-placement-restore-failed')
    }
}

if ($null -ne $result) {
    $result | Add-Member -NotePropertyName RestorationSucceeded `
        -NotePropertyValue ($restorationErrors.Count -eq 0)
}

if ($restorationErrors.Count -ne 0) {
    Write-Warning ($restorationErrors -join ',')
}

$result
if ($evidenceExitCode -ne 0) {
    exit $evidenceExitCode
}
