#Requires -Version 7.0
<#
.SYNOPSIS
  End-to-end CLI test for the FOCAS (Fanuc CNC) driver.

.DESCRIPTION
  Runs the CLI against either the managed wire client (default — Driver.FOCAS.Cli
  dials the CNC on TCP:8193 directly, no native dependencies) or the focas-mock
  Docker fixture. Hardware-gated by default because the default CncHost is
  127.0.0.1; set FOCAS_TRUST_WIRE=1 once -CncHost points at a real CNC, or pass
  -ProfileName to run against the Docker sim.

  The script also supports three nice-to-have modes shipped 2026-04-24:

  -Series   — per-series matrix mode. Accepts a comma-separated list; the
              core stages are run once per series, swapping the -Address to
              the supplied per-series probe. Fails fast if any series's
              configured address is outside the documented range (the driver
              itself enforces that at InitializeAsync).

  -ProfileName — for use with the Python Docker simulator (see
              docs/v2/implementation/focas-simulator-plan.md). Selects a
              docker-compose profile + matching -Series. When set, the
              FOCAS_TRUST_WIRE gate is considered satisfied because the sim
              is a legitimate non-hardware target.

  -HandleLeakCycles <int> — stress stage that opens + closes <N> sessions
              via the CLI's `probe` command with a short sleep between
              cycles. Exercises the Tier-C supervisor's handle-recycle path
              without touching user data. Typical values: 100–1000. A CNC's
              FWLIB handle pool is finite (~5–10), so this shakes out
              handle-leak bugs if either side forgets to free.

.PARAMETER CncHost
  IP or hostname of the CNC. Default 127.0.0.1 — override for real runs.

.PARAMETER CncPort
  FOCAS TCP port. Default 8193.

.PARAMETER Address
  FOCAS address to exercise. Default R100 (PMC R-file register). Ignored
  when -Series is set and the series profile supplies its own probe.

.PARAMETER Series
  Comma-separated list of CNC series to run the matrix against. Known:
  ZeroI_D, ZeroI_F, ZeroI_MF, ZeroI_TF, Sixteen_i, Thirty_i, ThirtyOne_i,
  ThirtyTwo_i, PowerMotion_i. When empty the script runs a single pass
  without a series constraint.

.PARAMETER ProfileName
  docker-compose profile name from tests/.../Docker/profiles/. When set,
  the script assumes the Python simulator is the target + un-gates
  FOCAS_TRUST_WIRE.

.PARAMETER HandleLeakCycles
  Run a handle-leak stress stage with <N> open/close cycles. 0 = skip.

.PARAMETER OpcUaUrl
  OtOpcUa server endpoint.

.PARAMETER BridgeNodeId
  NodeId at which the server publishes the Address.
#>

param(
    [string]$CncHost = "127.0.0.1",
    [int]$CncPort = 8193,
    [string]$Address = "R100",
    [string]$Series = "",
    [string]$ProfileName = "",
    [int]$HandleLeakCycles = 0,
    [string]$OpcUaUrl = "opc.tcp://localhost:4840",
    [Parameter(Mandatory)] [string]$BridgeNodeId
)

$ErrorActionPreference = "Stop"
. "$PSScriptRoot/_common.ps1"

$simGated = -not [string]::IsNullOrWhiteSpace($ProfileName)
if (-not $simGated -and -not ($env:FOCAS_TRUST_WIRE -eq "1" -or $env:FOCAS_TRUST_WIRE -eq "true")) {
    Write-Skip "FOCAS_TRUST_WIRE not set. Pass -ProfileName <profile> to run against the Docker mock in tests/.../Driver.FOCAS.IntegrationTests/Docker/, or set FOCAS_TRUST_WIRE=1 when -CncHost points at a real CNC."
    exit 0
}

if ($simGated) {
    Write-Info "Sim mode — profile '$ProfileName'. FOCAS_TRUST_WIRE gate bypassed."
}

# Per-series probe addresses — each one is inside the authoritative range for
# that series (docs/v2/focas-version-matrix.md). Picking one representative per
# kind (PMC / parameter / macro) is enough to exercise the driver's validator.
$seriesProbes = @{
    "ZeroI_D"      = "R100"
    "ZeroI_F"      = "R100"
    "ZeroI_MF"     = "R100"
    "ZeroI_TF"     = "R100"
    "Sixteen_i"    = "R100"
    "Thirty_i"     = "R100"
    "ThirtyOne_i"  = "R100"
    "ThirtyTwo_i"  = "R100"
    "PowerMotion_i"= "R100"
}

$seriesList = @()
if (-not [string]::IsNullOrWhiteSpace($Series)) {
    $seriesList = @($Series.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    $unknown = @($seriesList | Where-Object { -not $seriesProbes.ContainsKey($_) })
    if ($unknown.Count -gt 0) {
        Write-Fail "Unknown -Series entries: $($unknown -join ', '). Known: $($seriesProbes.Keys -join ', ')."
        exit 2
    }
}

$focasCli = Get-CliInvocation `
    -ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli" `
    -ExeName "otopcua-focas-cli"
$opcUaCli = Get-CliInvocation `
    -ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
    -ExeName "otopcua-cli"

$allResults = @()

function Invoke-FocasCore {
    param(
        [string]$Label,
        [string]$ProbeAddress
    )

    Write-Header "FOCAS stages — $Label"
    $commonFocas = @("-h", $CncHost, "-p", $CncPort)
    $results = @()

    $results += Test-Probe `
        -Cli $focasCli `
        -ProbeArgs (@("probe") + $commonFocas + @("-a", $ProbeAddress, "--type", "Int16"))

    $writeValue = Get-Random -Minimum 1 -Maximum 9999
    $results += Test-DriverLoopback `
        -Cli $focasCli `
        -WriteArgs (@("write") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16", "-v", $writeValue)) `
        -ReadArgs  (@("read")  + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16")) `
        -ExpectedValue "$writeValue"

    $bridgeValue = Get-Random -Minimum 10000 -Maximum 19999
    $results += Test-ServerBridge `
        -DriverCli $focasCli `
        -DriverWriteArgs (@("write") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16", "-v", $bridgeValue)) `
        -OpcUaCli $opcUaCli `
        -OpcUaUrl $OpcUaUrl `
        -OpcUaNodeId $BridgeNodeId `
        -ExpectedValue "$bridgeValue"

    $reverseValue = Get-Random -Minimum 20000 -Maximum 29999
    $results += Test-OpcUaWriteBridge `
        -OpcUaCli $opcUaCli `
        -OpcUaUrl $OpcUaUrl `
        -OpcUaNodeId $BridgeNodeId `
        -DriverCli $focasCli `
        -DriverReadArgs (@("read") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16")) `
        -ExpectedValue "$reverseValue"

    $subValue = Get-Random -Minimum 30000 -Maximum 32766
    $results += Test-SubscribeSeesChange `
        -OpcUaCli $opcUaCli `
        -OpcUaUrl $OpcUaUrl `
        -OpcUaNodeId $BridgeNodeId `
        -DriverCli $focasCli `
        -DriverWriteArgs (@("write") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16", "-v", $subValue)) `
        -ExpectedValue "$subValue"

    return $results
}

function Invoke-HandleLeakStage {
    param([int]$Cycles)

    Write-Header "FOCAS handle-leak stress — $Cycles cycles"
    $commonFocas = @("-h", $CncHost, "-p", $CncPort)
    $failed = 0
    for ($i = 1; $i -le $Cycles; $i++) {
        $probe = Test-Probe `
            -Cli $focasCli `
            -ProbeArgs (@("probe") + $commonFocas + @("-a", $Address, "--type", "Int16"))
        if (-not $probe.Passed) {
            $failed++
            # First 3 failures are informative; the rest just tally.
            if ($failed -le 3) {
                Write-Fail "cycle $i failed: $($probe.Reason)"
            }
        }
        # Tiny delay so a broken loop can't DDoS the CNC; FWLIB handles take a
        # few tens of ms to recycle in practice.
        Start-Sleep -Milliseconds 50
    }
    $passed = $Cycles - $failed
    if ($failed -eq 0) {
        Write-Pass "handle-leak stress: $passed/$Cycles cycles succeeded"
        return @{ Passed = $true; Reason = "$passed/$Cycles" }
    } else {
        Write-Fail "handle-leak stress: $failed/$Cycles cycles failed"
        return @{ Passed = $false; Reason = "$failed/$Cycles failed" }
    }
}

if ($seriesList.Count -eq 0) {
    $allResults += Invoke-FocasCore -Label "single" -ProbeAddress $Address
} else {
    foreach ($series in $seriesList) {
        $probeAddr = $seriesProbes[$series]
        Write-Info "Running matrix pass for series '$series' with address $probeAddr"
        $allResults += Invoke-FocasCore -Label $series -ProbeAddress $probeAddr
    }
}

if ($HandleLeakCycles -gt 0) {
    $allResults += Invoke-HandleLeakStage -Cycles $HandleLeakCycles
}

Write-Summary -Title "FOCAS e2e" -Results $allResults
if ($allResults | Where-Object { -not $_.Passed }) { exit 1 }
