param(
  [Parameter(Mandatory = $true)] [string] $Manifest,
  [Parameter(Mandatory = $true)] [string] $OutputDirectory,
  [string] $Cli = 'build/windows-ucrt-gui-release/smartcut-cli.exe',
  [string] $Worker = 'build/windows-ucrt-gui-release/smartcut-worker.exe',
  [string] $Ffmpeg = 'C:/ffmpeg_latest/ffmpeg.exe',
  [string] $Ffprobe = 'C:/ffmpeg_latest/ffprobe.exe',
  [string] $MpcBe = 'C:/Program Files/MPC-BE x64/mpc-be64.exe'
)

$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent $PSScriptRoot
Push-Location $repo
try {
  foreach ($required in @($Manifest, $Cli, $Worker, $Ffmpeg, $Ffprobe, $MpcBe)) {
    if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing required file: $required" }
  }
  New-Item -ItemType Directory -Force -Path $OutputDirectory | Out-Null
  $OutputDirectory = (Resolve-Path -LiteralPath $OutputDirectory).Path

  Add-Type -AssemblyName System.Drawing
  Add-Type @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public static class SmartCutPlaybackWindow {
  [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left, Top, Right, Bottom; }
  private delegate bool EnumWindowsCallback(IntPtr window, IntPtr parameter);
  [DllImport("user32.dll")] private static extern bool EnumWindows(EnumWindowsCallback callback, IntPtr parameter);
  [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr window, out uint processId);
  [DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr window);
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr window, out Rect rect);
  [DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr window, IntPtr target, uint flags);
  [DllImport("user32.dll")] public static extern bool IsHungAppWindow(IntPtr window);
  public static IntPtr LargestVisibleWindow(uint processId) {
    IntPtr best = IntPtr.Zero;
    long bestArea = 0;
    EnumWindows(delegate(IntPtr window, IntPtr parameter) {
      uint owner;
      GetWindowThreadProcessId(window, out owner);
      Rect rect;
      if (owner == processId && IsWindowVisible(window) && GetWindowRect(window, out rect)) {
        long width = Math.Max(0, rect.Right - rect.Left);
        long height = Math.Max(0, rect.Bottom - rect.Top);
        long area = width * height;
        if (area > bestArea) { bestArea = area; best = window; }
      }
      return true;
    }, IntPtr.Zero);
    return best;
  }
}
'@

  function ConvertTo-NativeArgument([string] $Value) {
    if ($Value -notmatch '[\s"]') { return $Value }
    return '"' + ($Value -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"'
  }

  function Invoke-Native([string] $FilePath, [string[]] $Arguments) {
    $start = New-Object Diagnostics.ProcessStartInfo
    $start.FileName = $FilePath
    $start.Arguments = (($Arguments | ForEach-Object { ConvertTo-NativeArgument ([string]$_) }) -join ' ')
    $start.UseShellExecute = $false
    $start.CreateNoWindow = $true
    $start.RedirectStandardOutput = $true
    $start.RedirectStandardError = $true
    $process = New-Object Diagnostics.Process
    $process.StartInfo = $start
    if (-not $process.Start()) { throw "Could not start native process: $FilePath" }
    $stdout = $process.StandardOutput.ReadToEndAsync()
    $stderr = $process.StandardError.ReadToEndAsync()
    $process.WaitForExit()
    $result = [pscustomobject]@{
      ExitCode = $process.ExitCode
      StdOut = $stdout.Result
      StdErr = $stderr.Result
    }
    $process.Dispose()
    return $result
  }

  function Invoke-JsonProbe([string[]] $Arguments) {
    $probe = Invoke-Native $Ffprobe $Arguments
    if ($probe.ExitCode -ne 0 -or -not $probe.StdOut) { throw "ffprobe failed: $($Arguments -join ' '): $($probe.StdErr)" }
    return ($probe.StdOut | ConvertFrom-Json)
  }

  function Get-StreamSignature([string] $Path) {
    $probe = Invoke-JsonProbe @('-v', 'error', '-show_entries',
      'stream=index,codec_type,codec_name,profile,pix_fmt,bits_per_raw_sample,channels', '-of', 'json', '--', $Path)
    $video = @($probe.streams | Where-Object codec_type -eq 'video' | Select-Object -First 1)
    $audio = @($probe.streams | Where-Object codec_type -eq 'audio')
    if (-not $video) { throw "No video stream: $Path" }
    return [pscustomobject]@{
      VideoCodec = [string]$video.codec_name
      VideoProfile = [string]$video.profile
      PixelFormat = [string]$video.pix_fmt
      BitDepth = [string]$video.bits_per_raw_sample
      AudioCodecs = (@($audio | ForEach-Object { [string]$_.codec_name }) -join '+')
      AudioTracks = $audio.Count
      StreamTypes = (@($probe.streams | ForEach-Object { [string]$_.codec_type }) -join '+')
    }
  }

  function Get-MidGopRange([string] $Path) {
    $indexed = Invoke-Native $Cli @('frame-index', '--input', $Path)
    if ($indexed.ExitCode -ne 0 -or -not $indexed.StdOut) { throw "SmartCut frame indexing failed: $Path $($indexed.StdErr)" }
    $index = ($indexed.StdOut | ConvertFrom-Json)
    $keys = @($index.keyframes | ForEach-Object { [int64]$_ })
    $stream = Invoke-JsonProbe @('-v', 'error', '-select_streams', 'v:0', '-show_entries',
      'stream=avg_frame_rate', '-of', 'json', '--', $Path)
    $rateParts = ([string]$stream.streams[0].avg_frame_rate) -split '/'
    $rate = if ($rateParts.Count -eq 2 -and [double]$rateParts[1] -ne 0) {
      [double]$rateParts[0] / [double]$rateParts[1]
    } else { 25.0 }
    if ($keys.Count -ge 3) {
      $first = $keys[0] + 1
      $minimumGap = [Math]::Ceiling(12 * $rate)
      $lastKey = @($keys | Where-Object { $_ -gt $keys[0] -and $_ - $keys[0] -ge $minimumGap } | Select-Object -First 1)
      if ($lastKey) {
        $end = [int64]$lastKey[0] + 1
        if ($end -lt [int64]$index.frame_count -and $first -notin $keys -and $end -notin $keys -and
            @($keys | Where-Object { $_ -gt $first -and $_ -lt $end }).Count -ge 2) {
          return [pscustomobject]@{ First = $first; End = $end; FrameCount = [int64]$index.frame_count; KeyframesInside = @($keys | Where-Object { $_ -gt $first -and $_ -lt $end }).Count }
        }
      }
    }
    throw "Could not find two non-keyframe boundaries surrounding copyable GOPs: $Path"
  }

  function Get-WindowHash([IntPtr] $Window, [string] $CapturePath) {
    $rect = New-Object SmartCutPlaybackWindow+Rect
    if (-not [SmartCutPlaybackWindow]::GetWindowRect($Window, [ref]$rect)) { throw 'GetWindowRect failed' }
    $width = [Math]::Max(1, $rect.Right - $rect.Left)
    $height = [Math]::Max(1, $rect.Bottom - $rect.Top)
    $bitmap = New-Object System.Drawing.Bitmap $width, $height
    $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
    try {
      $target = $graphics.GetHdc()
      try {
        if (-not [SmartCutPlaybackWindow]::PrintWindow($Window, $target, 2)) { throw 'PrintWindow failed' }
      } finally { $graphics.ReleaseHdc($target) }
      $stream = New-Object System.IO.MemoryStream
      try {
        $bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
        if (-not [string]::IsNullOrWhiteSpace($CapturePath)) {
          [IO.File]::WriteAllBytes($CapturePath, $stream.ToArray())
        }
        $sha = [Security.Cryptography.SHA256]::Create()
        try { return (($sha.ComputeHash($stream.ToArray()) | ForEach-Object { $_.ToString('x2') }) -join '') }
        finally { $sha.Dispose() }
      } finally { $stream.Dispose() }
    } finally { $graphics.Dispose(); $bitmap.Dispose() }
  }

  function Test-MpcPlayback([string] $Path) {
    $quotedPath = '"' + $Path + '"'
    $player = Start-Process -FilePath $MpcBe -ArgumentList @('/new', '/play', '/volume', '0', $quotedPath) -PassThru
    try {
      $deadline = [DateTime]::UtcNow.AddSeconds(15)
      $window = [IntPtr]::Zero
      do {
        Start-Sleep -Milliseconds 250
        $player.Refresh()
        if (-not $player.HasExited) { $window = [SmartCutPlaybackWindow]::LargestVisibleWindow([uint32]$player.Id) }
      } while (-not $player.HasExited -and $window -eq [IntPtr]::Zero -and [DateTime]::UtcNow -lt $deadline)
      if ($player.HasExited -or $window -eq [IntPtr]::Zero) { throw 'MPC-BE did not create a visible playback window' }
      $hashes = @()
      $responsive = $true
      $capture = 0
      foreach ($delay in @(2, 3, 3)) {
        Start-Sleep -Seconds $delay
        $player.Refresh()
        if ($player.HasExited) { throw 'MPC-BE exited before playback evidence was collected' }
        $window = [SmartCutPlaybackWindow]::LargestVisibleWindow([uint32]$player.Id)
        if ($window -eq [IntPtr]::Zero) { throw 'MPC-BE playback window disappeared' }
        $responsive = $responsive -and $player.Responding -and
          -not [SmartCutPlaybackWindow]::IsHungAppWindow($window)
        ++$capture
        $captureName = [IO.Path]::GetFileNameWithoutExtension($Path) + ".mpc-$capture.png"
        $hashes += Get-WindowHash $window (Join-Path $OutputDirectory $captureName)
      }
      return [pscustomobject]@{ Passed = $responsive -and (@($hashes | Sort-Object -Unique).Count -ge 2); Responsive = $responsive; DistinctFrames = @($hashes | Sort-Object -Unique).Count; Hashes = ($hashes -join ',') }
    } finally {
      if (-not $player.HasExited) {
        $null = $player.CloseMainWindow()
        if (-not $player.WaitForExit(3000)) { Stop-Process -Id $player.Id -Force -ErrorAction SilentlyContinue }
      }
    }
  }

  $results = @()
  foreach ($case in @(Import-Csv -LiteralPath $Manifest)) {
    $name = [string]$case.Name
    $input = [string]$case.Input
    Write-Host "Testing $name"
    $result = [ordered]@{ Name = $name; Input = $input; Status = 'FAILED'; Failure = '' }
    try {
      if (-not (Test-Path -LiteralPath $input -PathType Leaf)) { throw "Missing input: $input" }
      $source = Get-StreamSignature $input
      $range = Get-MidGopRange $input
      $delivery = [string]$case.Delivery
      $extension = if ([string]::IsNullOrWhiteSpace([string]$case.OutputExtension)) { '.mkv' } else { [string]$case.OutputExtension }
      if (-not $extension.StartsWith('.')) { $extension = '.' + $extension }
      $output = Join-Path $OutputDirectory ($name + $extension)
      Remove-Item -LiteralPath $output -Force -ErrorAction SilentlyContinue
      $exportLog = Join-Path $OutputDirectory ($name + '.export.log')
      if ($delivery -eq 'edit-export') {
        $cuts = '0:' + $range.First + ',' + $range.End + ':' + $range.FrameCount
        $exported = Invoke-Native $Worker @('--export-edits', '--input', $input, '--output', $output,
                                           '--frame-count', [string]$range.FrameCount, '--cuts', $cuts,
                                           '--mode', 'smart')
      } else {
        $exported = Invoke-Native $Cli @('export-range-mkv', '--input', $input, '--output', $output,
                                        '--first-frame', [string]$range.First, '--end-frame', [string]$range.End)
      }
      [IO.File]::WriteAllText($exportLog, $exported.StdErr, [Text.Encoding]::UTF8)
      $raw = @($exported.StdOut -split "`r?`n")
      if ($exported.ExitCode -ne 0) {
        throw "Export process failed with exit code $($exported.ExitCode): $($exported.StdOut) $($exported.StdErr)"
      }
      if ($delivery -eq 'edit-export') {
        $eventLine = @($raw | Where-Object { [string]$_ -match '^\{"schema":"worker-edit-export-v1","event":"completed"' } | Select-Object -Last 1)
        if (-not $eventLine) { throw "No worker export report: $($raw -join [Environment]::NewLine)" }
        $event = ([string]$eventLine | ConvertFrom-Json)
        if ($event.status -ne 'PASSED') { throw "Edit delivery failed: $eventLine" }
        $export = @($event.report.segment_reports)[0]
        $jsonLine = $export | ConvertTo-Json -Compress -Depth 20
      } else {
        $jsonLine = @($raw | Where-Object { [string]$_ -match '^\{"schema":"export-range-report' } | Select-Object -Last 1)
        if (-not $jsonLine) { throw "No export report: $($raw -join [Environment]::NewLine)" }
        $export = ([string]$jsonLine | ConvertFrom-Json)
      }
      if ($export.status -ne 'PASSED' -or $export.render_mode -ne 'smart-render' -or
          [int64]$export.encoded_frames -le 0 -or [int64]$export.copied_frames -le 0) {
        throw "Export did not exercise both boundary encoding and packet copy: $jsonLine"
      }
      $rendered = Get-StreamSignature $output
      if ($source.VideoCodec -ne $rendered.VideoCodec -or $source.VideoProfile -ne $rendered.VideoProfile -or
          $source.PixelFormat -ne $rendered.PixelFormat -or $source.AudioCodecs -ne $rendered.AudioCodecs -or
          $source.AudioTracks -ne $rendered.AudioTracks -or $source.StreamTypes -ne $rendered.StreamTypes) {
        throw "Stream signature changed: $($source | ConvertTo-Json -Compress) -> $($rendered | ConvertTo-Json -Compress)"
      }
      $decodeLog = Join-Path $OutputDirectory ($name + '.decode.log')
      $decoded = Invoke-Native $Ffmpeg @('-v', 'error', '-i', $output, '-map', '0:v:0', '-map', '0:a?', '-f', 'null', 'NUL')
      [IO.File]::WriteAllText($decodeLog, $decoded.StdErr, [Text.Encoding]::UTF8)
      if ($decoded.ExitCode -ne 0) { throw "Complete FFmpeg decode failed: $($decoded.StdErr)" }
      $playback = Test-MpcPlayback $output
      if (-not $playback.Passed) { throw "MPC-BE did not visibly advance (responsive=$($playback.Responsive), distinct=$($playback.DistinctFrames))" }
      $result.Status = 'PASSED'
      $result.Output = $output
      $result.Video = $source.VideoCodec
      $result.PixelFormat = $source.PixelFormat
      $result.Audio = $source.AudioCodecs
      $result.AudioTracks = $source.AudioTracks
      $result.FirstFrame = $range.First
      $result.EndFrame = $range.End
      $result.CopiedFrames = [int64]$export.copied_frames
      $result.EncodedFrames = [int64]$export.encoded_frames
      $result.AnalysisMs = [int64]$export.analysis_ms
      $result.ExportMs = [int64]$export.encode_ms + [int64]$export.copy_ms + [int64]$export.join_ms + [int64]$export.mux_ms + [int64]$export.validation_ms
      $result.MpcDistinctFrames = $playback.DistinctFrames
      $result.MpcResponsive = $playback.Responsive
    } catch {
      $result.Failure = $_.Exception.Message
      Write-Warning "$name failed: $($result.Failure)"
    }
    $results += [pscustomobject]$result
  }
  $csv = Join-Path $OutputDirectory 'mpc-midgop-results.csv'
  $json = Join-Path $OutputDirectory 'mpc-midgop-results.json'
  $results | Export-Csv -LiteralPath $csv -NoTypeInformation -Encoding UTF8
  [IO.File]::WriteAllText($json, ($results | ConvertTo-Json -Depth 6), [Text.Encoding]::UTF8)
  $results | Format-Table -AutoSize Name,Status,Video,PixelFormat,Audio,CopiedFrames,EncodedFrames,AnalysisMs,ExportMs,MpcDistinctFrames
  $failed = @($results | Where-Object Status -ne 'PASSED')
  Write-Host "MPC-BE mid-GOP playback matrix: $($results.Count - $failed.Count)/$($results.Count) passed"
  if ($failed) { exit 1 }
} finally { Pop-Location }
