param(
    [string]$RmbHost = "127.0.0.1",
    [int]$TerminalPort = 2323,
    [int]$DiscoveryPort = 2324,
    [switch]$Raw,
    [switch]$BroadcastDiscovery
)

$ErrorActionPreference = "Stop"

function Write-Pass([string]$Message) {
    Write-Host "[PASS] $Message" -ForegroundColor Green
}

function Write-Fail([string]$Message) {
    Write-Host "[FAIL] $Message" -ForegroundColor Red
    throw $Message
}

function Read-AvailableBytes([System.Net.Sockets.NetworkStream]$Stream, [int]$TimeoutMs = 2500) {
    $bytes = New-Object System.Collections.Generic.List[byte]
    $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMs)
    $lastData = [DateTime]::UtcNow
    $buffer = New-Object byte[] 4096

    while ([DateTime]::UtcNow -lt $deadline) {
        if ($Stream.DataAvailable) {
            $read = $Stream.Read($buffer, 0, $buffer.Length)
            if ($read -le 0) { break }
            for ($i = 0; $i -lt $read; $i++) { $bytes.Add($buffer[$i]) }
            $lastData = [DateTime]::UtcNow
            continue
        }

        if ($bytes.Count -gt 0 -and ([DateTime]::UtcNow - $lastData).TotalMilliseconds -gt 200) {
            break
        }
        Start-Sleep -Milliseconds 25
    }

    return $bytes.ToArray()
}

function Contains-Sequence([byte[]]$Haystack, [byte[]]$Needle) {
    if ($Needle.Length -eq 0) { return $true }
    for ($i = 0; $i -le $Haystack.Length - $Needle.Length; $i++) {
        $match = $true
        for ($j = 0; $j -lt $Needle.Length; $j++) {
            if ($Haystack[$i + $j] -ne $Needle[$j]) { $match = $false; break }
        }
        if ($match) { return $true }
    }
    return $false
}

Write-Host "RetroModem Bridge FujiNet network smoke test"
Write-Host "RMB host: $RmbHost  TCP: $TerminalPort  UDP discovery: $DiscoveryPort"
Write-Host "Mode: $(if ($Raw) { 'Raw TCP' } else { 'Telnet' })"
Write-Host ""

# 1. UDP discovery
$udp = [System.Net.Sockets.UdpClient]::new()
try {
    $udp.Client.ReceiveTimeout = 2500
    $target = if ($BroadcastDiscovery) { "255.255.255.255" } else { $RmbHost }
    if ($BroadcastDiscovery) { $udp.EnableBroadcast = $true }
    $request = [Text.Encoding]::ASCII.GetBytes("RMB_DISCOVER_V1")
    [void]$udp.Send($request, $request.Length, $target, $DiscoveryPort)
    $remote = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
    $reply = $udp.Receive([ref]$remote)
    $replyText = [Text.Encoding]::ASCII.GetString($reply)
    Write-Host "Discovery reply from $($remote.Address):$($remote.Port): $replyText"
    if ($replyText -notmatch '^RMB_HERE_V1\|[^|]+\|\d+$') {
        Write-Fail "Discovery response format was not RMB_HERE_V1|computer-name|tcp-port"
    }
    $parts = $replyText.Split('|')
    if ([int]$parts[2] -ne $TerminalPort) {
        Write-Fail "Discovery advertised TCP port $($parts[2]), expected $TerminalPort"
    }
    Write-Pass "UDP discovery response format and advertised TCP port"
}
finally {
    $udp.Dispose()
}

# 2. TCP connection and AT response
$tcp = [System.Net.Sockets.TcpClient]::new()
try {
    $tcp.NoDelay = $true
    $tcp.Connect($RmbHost, $TerminalPort)
    Write-Pass "TCP client connected"
    $stream = $tcp.GetStream()

    if (-not $Raw) {
        # WILL ECHO and DO SUPPRESS-GO-AHEAD. RMB should reject both without
        # exposing these bytes to the AT command parser.
        [byte[]]$negotiation = @(255, 251, 1, 255, 253, 3)
        $stream.Write($negotiation, 0, $negotiation.Length)
        $stream.Flush()
    }

    [byte[]]$at = [Text.Encoding]::ASCII.GetBytes("AT`r")
    $stream.Write($at, 0, $at.Length)
    $stream.Flush()

    $response = Read-AvailableBytes $stream
    if ($response.Length -eq 0) { Write-Fail "No response to AT command" }

    if (-not $Raw) {
        if (-not (Contains-Sequence $response ([byte[]]@(255, 254, 1)))) {
            Write-Fail "Missing IAC DONT ECHO response to WILL ECHO"
        }
        if (-not (Contains-Sequence $response ([byte[]]@(255, 252, 3)))) {
            Write-Fail "Missing IAC WONT SUPPRESS-GO-AHEAD response to DO"
        }
        Write-Pass "Telnet WILL/DO negotiation is rejected cleanly"
    }

    $ascii = [Text.Encoding]::ASCII.GetString($response)
    if ($ascii -notmatch 'OK') { Write-Fail "AT response did not contain OK" }
    Write-Pass "AT command reached the existing RMB session layer"

    if (-not $Raw) {
        # Enable local echo, then send one literal 0xFF encoded as IAC IAC.
        # RMB must decode it to one payload byte and re-escape the echoed byte
        # as FF FF on the Telnet connection.
        [byte[]]$echoOn = [Text.Encoding]::ASCII.GetBytes("ATE1`r")
        $stream.Write($echoOn, 0, $echoOn.Length)
        $stream.Flush()
        [void](Read-AvailableBytes $stream)

        [byte[]]$literalIac = @(255, 255, 13)
        $stream.Write($literalIac, 0, $literalIac.Length)
        $stream.Flush()
        $iacResponse = Read-AvailableBytes $stream
        if (-not (Contains-Sequence $iacResponse ([byte[]]@(255, 255)))) {
            Write-Fail "Literal 0xFF was not preserved/escaped as IAC IAC on output"
        }
        Write-Pass "Literal 0xFF survives Telnet decode/encode"

        [byte[]]$echoOff = [Text.Encoding]::ASCII.GetBytes("ATE0`r")
        $stream.Write($echoOff, 0, $echoOff.Length)
        $stream.Flush()
        [void](Read-AvailableBytes $stream)
    }

    # 3. Local RMB menu: verifies immediate multi-byte output and ANSI bytes.
    [byte[]]$menu = [Text.Encoding]::ASCII.GetBytes("ATDT MENU`r")
    $stream.Write($menu, 0, $menu.Length)
    $stream.Flush()
    $menuResponse = Read-AvailableBytes $stream 3500
    if ($menuResponse.Length -eq 0) { Write-Fail "No response to ATDT MENU" }
    if ($menuResponse -notcontains 27) { Write-Fail "Local menu did not contain ANSI ESC (0x1B)" }
    Write-Pass "ANSI escape bytes reached the network client"

    # RMB's local menu uses CP437 box drawing. Common bytes are DA/C4/BF/B3/C0/D9.
    $cp437BoxBytes = @(0xDA, 0xC4, 0xBF, 0xB3, 0xC0, 0xD9)
    $foundBoxByte = $false
    foreach ($boxByte in $cp437BoxBytes) {
        if ($menuResponse -contains [byte]$boxByte) { $foundBoxByte = $true; break }
    }
    if (-not $foundBoxByte) {
        Write-Fail "No expected CP437 box-drawing bytes were found in the local menu"
    }
    Write-Pass "Extended CP437 bytes are present"
}
finally {
    $tcp.Dispose()
}

Write-Host ""
Write-Host "All smoke tests passed." -ForegroundColor Green
Write-Host "Disconnect/reconnect test: run this script a second time without restarting RMB."
