param(
  [int]$Port = 0,
  [string[]]$Command = @('node', './scripts/run-cdp-smoke.mjs')
)

$ErrorActionPreference = 'Stop'

if (-not $Command -or $Command.Count -eq 0) {
  throw 'Command must not be empty'
}

function Test-UrlReady {
  param(
    [Parameter(Mandatory = $true)][string]$Url
  )

  try {
    $response = Invoke-WebRequest $Url -UseBasicParsing -TimeoutSec 2
    return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500
  } catch {
    return $false
  }
}

function Wait-UrlReady {
  param(
    [Parameter(Mandatory = $true)][string]$Url,
    [Parameter(Mandatory = $true)][string]$Label,
    [int]$RetryCount = 60,
    [int]$DelayMs = 500
  )

  for ($i = 0; $i -lt $RetryCount; $i++) {
    if (Test-UrlReady -Url $Url) {
      return
    }
    Start-Sleep -Milliseconds $DelayMs
  }

  throw "$Label did not become ready: $Url"
}

function Get-FreeTcpPort {
  $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
  $listener.Start()
  try {
    return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
  } finally {
    $listener.Stop()
  }
}

function Resolve-BrowserPath {
  if ($env:E2E_BROWSER_PATH) {
    return $env:E2E_BROWSER_PATH
  }

  if ($env:CHROME_HEADLESS_SHELL_PATH) {
    return $env:CHROME_HEADLESS_SHELL_PATH
  }

  if ($env:PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
    return $env:PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
  }

  $baseDir = Join-Path $env:LOCALAPPDATA 'ms-playwright'
  $candidate = Get-ChildItem $baseDir -Directory -Filter 'chromium_headless_shell-*' |
    Sort-Object Name -Descending |
    Select-Object -First 1

  if ($candidate) {
    return (Join-Path $candidate.FullName 'chrome-headless-shell-win64\chrome-headless-shell.exe')
  }

  foreach ($fallback in @(
    'C:\Program Files\Google\Chrome\Application\chrome.exe',
    'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
    'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
    'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
  )) {
    if (Test-Path $fallback) {
      return $fallback
    }
  }

  throw 'No compatible browser found; set E2E_BROWSER_PATH or CHROME_HEADLESS_SHELL_PATH explicitly if needed'
}

function Test-HeadlessShellBrowser {
  param(
    [Parameter(Mandatory = $true)][string]$BrowserPath
  )

  return [System.IO.Path]::GetFileName($BrowserPath).ToLowerInvariant().Contains('headless-shell')
}

function Get-BrowserArguments {
  param(
    [Parameter(Mandatory = $true)][string]$BrowserPath,
    [Parameter(Mandatory = $true)][int]$Port,
    [Parameter(Mandatory = $true)][string]$ProfileDir
  )

  $arguments = @(
    "--remote-debugging-port=$Port",
    "--user-data-dir=$ProfileDir",
    '--no-sandbox'
  )

  if (Test-HeadlessShellBrowser -BrowserPath $BrowserPath) {
    $arguments += '--single-process'
  } else {
    $arguments += @(
      '--disable-dev-shm-usage',
      '--disable-background-networking',
      '--disable-background-timer-throttling',
      '--disable-renderer-backgrounding',
      '--disable-sync',
      '--headless=new'
    )
  }

  $arguments += 'about:blank'
  return $arguments
}

function Get-BrowserProcessIds {
  param(
    [Parameter(Mandatory = $true)][string]$BrowserPath
  )

  $processName = [System.IO.Path]::GetFileNameWithoutExtension($BrowserPath)
  try {
    return @(Get-Process -Name $processName -ErrorAction Stop | Select-Object -ExpandProperty Id)
  } catch {
    return @()
  }
}

function Get-BrowserProcessesByProfile {
  param(
    [Parameter(Mandatory = $true)][string]$BrowserPath,
    [Parameter(Mandatory = $true)][string]$ProfileDir
  )

  $processFileName = [System.IO.Path]::GetFileName($BrowserPath)
  $profileFragment = $ProfileDir.ToLowerInvariant()

  try {
    return @(
      Get-CimInstance Win32_Process -Filter ("Name = '{0}'" -f $processFileName) -ErrorAction Stop |
        Where-Object {
          $commandLine = $_.CommandLine
          $commandLine -and $commandLine.ToLowerInvariant().Contains($profileFragment)
        }
    )
  } catch {
    return @()
  }
}

function Get-ChildProcessIds {
  param(
    [Parameter(Mandatory = $true)][int]$ParentId
  )

  $pending = [System.Collections.Generic.Queue[int]]::new()
  $seen = [System.Collections.Generic.HashSet[int]]::new()
  $pending.Enqueue($ParentId)

  while ($pending.Count -gt 0) {
    $currentParentId = $pending.Dequeue()

    try {
      $children = @(Get-CimInstance Win32_Process -Filter ("ParentProcessId = {0}" -f $currentParentId) -ErrorAction Stop)
    } catch {
      $children = @()
    }

    foreach ($child in $children) {
      if ($seen.Add([int]$child.ProcessId)) {
        $pending.Enqueue([int]$child.ProcessId)
      }
    }
  }

  return @($seen)
}

function Get-BrowserCleanupIds {
  param(
    [Parameter(Mandatory = $true)]$Handle
  )

  $ids = [System.Collections.Generic.HashSet[int]]::new()

  if ($Handle.Process) {
    $null = $ids.Add([int]$Handle.Process.Id)
    foreach ($childId in Get-ChildProcessIds -ParentId $Handle.Process.Id) {
      $null = $ids.Add([int]$childId)
    }
  }

  foreach ($processInfo in Get-BrowserProcessesByProfile -BrowserPath $Handle.BrowserPath -ProfileDir $Handle.ProfileDir) {
    $null = $ids.Add([int]$processInfo.ProcessId)
  }

  $liveIds = @()
  foreach ($processId in $ids) {
    try {
      Get-Process -Id $processId -ErrorAction Stop | Out-Null
      $liveIds += $processId
    } catch {
      # Process already exited.
    }
  }

  return @($liveIds | Sort-Object -Unique)
}

function Start-BrowserProcess {
  param(
    [Parameter(Mandatory = $true)][string]$BrowserPath,
    [Parameter(Mandatory = $true)][int]$Port,
    [Parameter(Mandatory = $true)][string]$ProfileDir
  )

  $baselineIds = Get-BrowserProcessIds -BrowserPath $BrowserPath
  $arguments = Get-BrowserArguments -BrowserPath $BrowserPath -Port $Port -ProfileDir $ProfileDir
  $stdoutPath = Join-Path $ProfileDir 'browser-stdout.log'
  $stderrPath = Join-Path $ProfileDir 'browser-stderr.log'
  Remove-Item $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue

  $process = Start-Process `
    -FilePath $BrowserPath `
    -ArgumentList $arguments `
    -PassThru `
    -WindowStyle Hidden `
    -RedirectStandardOutput $stdoutPath `
    -RedirectStandardError $stderrPath

  return [pscustomobject]@{
    BrowserPath = $BrowserPath
    BaselineIds = $baselineIds
    ProfileDir = $ProfileDir
    Process = $process
    StdOut = $stdoutPath
    StdErr = $stderrPath
  }
}

function Show-BrowserLogs {
  param(
    [Parameter(Mandatory = $false)]$Handle
  )

  if (-not $Handle) {
    return
  }

  foreach ($path in @($Handle.StdOut, $Handle.StdErr)) {
    if (-not [string]::IsNullOrWhiteSpace($path) -and (Test-Path $path)) {
      Get-Content $path -ErrorAction SilentlyContinue
    }
  }
}

function Stop-BrowserProcess {
  param(
    [Parameter(Mandatory = $false)]$Handle
  )

  if (-not $Handle) {
    return
  }

  if ($Handle.Process -and -not $Handle.Process.HasExited) {
    foreach ($cleanupCommand in @(
      { param($id) taskkill /PID $id /T /F *> $null },
      { param($id) Stop-Process -Id $id -Force -ErrorAction Stop }
    )) {
      try {
        & $cleanupCommand $Handle.Process.Id
      } catch {
        # Ignore cleanup errors here; the residual PID check below is authoritative.
      }
    }
  }

  $residualIds = @()

  for ($attempt = 0; $attempt -lt 12; $attempt++) {
    $residualIds = @(Get-BrowserCleanupIds -Handle $Handle)

    foreach ($processId in $residualIds) {
      foreach ($cleanupCommand in @(
        { param($id) taskkill /PID $id /T /F *> $null },
        { param($id) Stop-Process -Id $id -Force -ErrorAction Stop }
      )) {
        try {
          & $cleanupCommand $processId
        } catch {
          # Ignore per-process cleanup errors during retry loop.
        }
      }
    }

    Start-Sleep -Milliseconds 500
    $residualIds = @(Get-BrowserCleanupIds -Handle $Handle)

    if ($residualIds.Count -eq 0) {
      break
    }
  }

  if ($residualIds.Count -gt 0) {
    throw "browser cleanup leaked PIDs: $($residualIds -join ', ')"
  }
}

function Remove-BrowserLogs {
  param(
    [Parameter(Mandatory = $false)]$Handle
  )

  if (-not $Handle) {
    return
  }

  $paths = @($Handle.StdOut, $Handle.StdErr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
  if ($paths.Count -gt 0) {
    Remove-Item $paths -Force -ErrorAction SilentlyContinue
  }
}

$browserPath = Resolve-BrowserPath
Write-Host "CDP browser: $browserPath"
$Port = if ($Port -gt 0) { $Port } else { Get-FreeTcpPort }
$profileRoot = Join-Path (Resolve-Path (Join-Path $PSScriptRoot '..')).Path '.cache\cdp-profiles'
New-Item -ItemType Directory -Force $profileRoot | Out-Null
$profileDir = Join-Path $profileRoot "pw-profile-cdp-smoke-win-$Port"
$browserReadyUrl = "http://127.0.0.1:$Port/json/version"
$browserCdpBaseUrl = "http://127.0.0.1:$Port"
$browserHandle = $null

try {
  for ($attempt = 1; $attempt -le 2; $attempt++) {
    Remove-Item -Recurse -Force $profileDir -ErrorAction SilentlyContinue
    $browserHandle = Start-BrowserProcess -BrowserPath $browserPath -Port $Port -ProfileDir $profileDir

    try {
      Wait-UrlReady -Url $browserReadyUrl -Label "browser CDP endpoint (attempt $attempt)"
      Write-Host "CDP endpoint ready: $browserReadyUrl"
      break
    } catch {
      Show-BrowserLogs $browserHandle
      Stop-BrowserProcess $browserHandle
      Remove-BrowserLogs $browserHandle
      $browserHandle = $null

      if ($attempt -eq 2) {
        throw
      }
    }
  }

  if (-not $env:E2E_COMMAND_TIMEOUT_MS) {
    $env:E2E_COMMAND_TIMEOUT_MS = '120000'
  }

  $env:E2E_SKIP_BROWSER_LAUNCH = '1'
  $env:E2E_CDP_PORT = "$Port"
  $env:E2E_CDP_BASE_URL = $browserCdpBaseUrl
  $env:E2E_PLAYWRIGHT_CDP_URL = $browserCdpBaseUrl
  $env:E2E_EXTERNAL_CDP = '1'

  $commandName = $Command[0]
  $commandArgs = @()
  if ($Command.Count -gt 1) {
    $commandArgs = $Command[1..($Command.Count - 1)]
  }

  Write-Host "Launching command: $commandName $($commandArgs -join ' ')"
  & $commandName @commandArgs
  if ($LASTEXITCODE -ne 0) {
    throw "command failed with exit code $LASTEXITCODE"
  }
} finally {
  Stop-BrowserProcess $browserHandle
  Remove-BrowserLogs $browserHandle
  Remove-Item Env:E2E_SKIP_BROWSER_LAUNCH -ErrorAction SilentlyContinue
  Remove-Item Env:E2E_CDP_PORT -ErrorAction SilentlyContinue
  Remove-Item Env:E2E_CDP_BASE_URL -ErrorAction SilentlyContinue
  Remove-Item Env:E2E_PLAYWRIGHT_CDP_URL -ErrorAction SilentlyContinue
  Remove-Item Env:E2E_EXTERNAL_CDP -ErrorAction SilentlyContinue
  Remove-Item -Recurse -Force $profileDir -ErrorAction SilentlyContinue
}
