###############################################################################
#### windows-container-smoke-tests.ps1 — Verify Windows container health   ####
#### Checks Docker/containerd service, container lifecycle, isolation      ####
#### modes, NAT networking, port mapping, volumes, and event logs.         ####
####                                                                       ####
#### Author:  Phil Connor                                                  ####
#### Contact: contact@mylinux.work                                         ####
#### License: MIT                                                          ####
#### Version: 1.0                                                          ####
####                                                                       ####
#### Usage:   .\windows-container-smoke-tests.ps1                          ####
####          .\windows-container-smoke-tests.ps1 -ContainerRuntime docker ####
####          .\windows-container-smoke-tests.ps1 -OutputFormat tap        ####
####                                                                       ####
#### See -Help for all options.                                            ####
###############################################################################

[CmdletBinding()]
param(
    [ValidateSet("docker","containerd")]
    [string]$ContainerRuntime = "docker",
    [string]$TestImage = "mcr.microsoft.com/windows/nanoserver:ltsc2022",
    [ValidateSet("text","tap")]
    [string]$OutputFormat = "text",
    [switch]$NoColor,
    [switch]$Help
)

$ErrorActionPreference = "Continue"

# ============================================================================
# HELP
# ============================================================================

if ($Help) {
    @"
Usage: .\windows-container-smoke-tests.ps1 [OPTIONS]

Smoke-test Windows container infrastructure. PowerShell 5.1+.
Designed for Windows Server or Windows 10/11 with containers enabled.

Parameters:
  -ContainerRuntime RT   Container runtime: docker (default), containerd
  -TestImage IMAGE       Test image (default: mcr.microsoft.com/windows/nanoserver:ltsc2022)
  -OutputFormat FORMAT   Output: text (default), tap
  -NoColor               Disable coloured output
  -Verbose               Show debug output
  -Help                  Show this help

Examples:
  .\windows-container-smoke-tests.ps1
  .\windows-container-smoke-tests.ps1 -ContainerRuntime containerd
  .\windows-container-smoke-tests.ps1 -TestImage mcr.microsoft.com/windows/servercore:ltsc2022
  .\windows-container-smoke-tests.ps1 -OutputFormat tap -NoColor
"@
    exit 0
}

# ============================================================================
# STATE
# ============================================================================

$script:Pass = 0
$script:Fail = 0
$script:Skip = 0
$script:Total = 0
$script:Results = @()
$script:StartTime = Get-Date

# ============================================================================
# COLORS
# ============================================================================

function Write-Color {
    param([string]$Text, [string]$Color = "White")
    if ($NoColor) {
        Write-Host $Text
    } else {
        Write-Host $Text -ForegroundColor $Color
    }
}

function Write-Log   { param([string]$Msg) Write-Color "[INFO] $Msg" "Cyan" }
function Write-Warn  { param([string]$Msg) Write-Color "[WARN] $Msg" "Yellow" }
function Write-Err   { param([string]$Msg) Write-Color "[ERROR] $Msg" "Red" }

# ============================================================================
# TEST RESULT RECORDING
# ============================================================================

function Record-Pass {
    param([string]$Name, [string]$Detail = "")
    $script:Pass++
    $script:Total++
    $script:Results += [PSCustomObject]@{ Status="PASS"; Name=$Name; Detail=$Detail }
    if ($OutputFormat -eq "tap") {
        Write-Host "ok $($script:Total) - $Name$(if($Detail){" ($Detail)"})"
    } else {
        $mark = if ($NoColor) { "[PASS]" } else { [char]0x2713 }
        $msg = "  $mark $Name"
        if ($Detail) { $msg += " - $Detail" }
        Write-Color $msg "Green"
    }
}

function Record-Fail {
    param([string]$Name, [string]$Detail = "")
    $script:Fail++
    $script:Total++
    $script:Results += [PSCustomObject]@{ Status="FAIL"; Name=$Name; Detail=$Detail }
    if ($OutputFormat -eq "tap") {
        Write-Host "not ok $($script:Total) - $Name"
        if ($Detail) { Write-Host "  # $Detail" }
    } else {
        $mark = if ($NoColor) { "[FAIL]" } else { [char]0x2717 }
        $msg = "  $mark $Name"
        if ($Detail) { $msg += " - $Detail" }
        Write-Color $msg "Red"
    }
}

function Record-Skip {
    param([string]$Name, [string]$Reason = "")
    $script:Skip++
    $script:Total++
    $script:Results += [PSCustomObject]@{ Status="SKIP"; Name=$Name; Detail=$Reason }
    if ($OutputFormat -eq "tap") {
        Write-Host "ok $($script:Total) - $Name # SKIP $Reason"
    } else {
        $mark = if ($NoColor) { "[SKIP]" } else { [char]0x2298 }
        $msg = "  $mark $Name"
        if ($Reason) { $msg += " - $Reason" }
        Write-Color $msg "Yellow"
    }
}

# ============================================================================
# HELPERS
# ============================================================================

function Test-CommandExists {
    param([string]$Command)
    $null -ne (Get-Command $Command -ErrorAction SilentlyContinue)
}

function Write-Section {
    param([string]$Name)
    if ($OutputFormat -eq "text") {
        Write-Host ""
        Write-Color $Name "White"
    }
}

function Remove-TestContainer {
    param([string]$Name)
    try { docker rm -f $Name 2>&1 | Out-Null } catch {}
}

function Test-DockerReady {
    param([string]$TestName, [switch]$NeedImage)
    if (-not (Test-CommandExists "docker")) {
        Record-Skip $TestName "docker command not found"; return $false
    }
    if ($NeedImage -and -not $script:ImageAvailable) {
        Record-Skip $TestName "test image not available"; return $false
    }
    return $true
}

$script:ImageAvailable = $false
$script:TestContainerPrefix = "smoketest-container"

# ============================================================================
# TESTS
# ============================================================================

# -- 1. Container Service Running -------------------------------------------

function Test-ContainerService {
    Write-Section "Service"

    $serviceName = if ($ContainerRuntime -eq "containerd") { "containerd" } else { "docker" }

    try {
        $svc = Get-Service -Name $serviceName -ErrorAction Stop
        if ($svc.Status -eq "Running") {
            Record-Pass "Container service running" "$serviceName ($($svc.Status))"
        } else {
            Record-Fail "Container service running" "$serviceName is $($svc.Status)"
        }
    } catch {
        Record-Fail "Container service running" "$serviceName service not found"
    }
}

# -- 2. Daemon Responding ---------------------------------------------------

function Test-DaemonHealth {
    if ($ContainerRuntime -eq "containerd") {
        if (Test-CommandExists "ctr") {
            try {
                $output = ctr version 2>&1 | Out-String
                if ($output -match "Version:" -or $output -match "Revision:") {
                    Record-Pass "Daemon responding" "containerd responding"
                } else {
                    Record-Fail "Daemon responding" "ctr version returned unexpected output"
                }
            } catch {
                Record-Fail "Daemon responding" "ctr error - $($_.Exception.Message)"
            }
        } else {
            Record-Fail "Daemon responding" "ctr command not found"
        }
        return
    }

    if (-not (Test-CommandExists "docker")) {
        Record-Fail "Daemon responding" "docker command not found"
        return
    }

    try {
        $output = docker info --format '{{.ServerVersion}}' 2>&1 | Out-String
        if ($LASTEXITCODE -eq 0 -and $output -match "\d+\.\d+") {
            Record-Pass "Daemon responding" "Docker Engine v$($output.Trim())"
        } else {
            Record-Fail "Daemon responding" "Docker daemon not responding"
        }
    } catch {
        Record-Fail "Daemon responding" "docker error - $($_.Exception.Message)"
    }
}

# -- 3. Container Runtime Mode ----------------------------------------------

function Test-RuntimeMode {
    Write-Section "Runtime"

    if ($ContainerRuntime -eq "containerd") {
        Record-Skip "Container runtime mode" "mode detection not applicable to containerd"
        return
    }
    if (-not (Test-DockerReady "Container runtime mode")) { return }

    try {
        $info = docker info 2>&1 | Out-String
        if ($info -match "OSType:\s*windows") {
            Record-Pass "Container runtime mode" "Windows containers"
        } elseif ($info -match "OSType:\s*linux") {
            Record-Fail "Container runtime mode" "Linux containers mode - switch to Windows containers"
        } else {
            Record-Fail "Container runtime mode" "unable to determine container mode"
        }
    } catch {
        Record-Fail "Container runtime mode" "docker info error - $($_.Exception.Message)"
    }
}

# -- 4. Image Pull -----------------------------------------------------------

function Test-ImagePull {
    Write-Section "Images"

    if (-not (Test-DockerReady "Image pull")) { return }

    try {
        # Check if image already exists locally
        $existing = docker images --format '{{.Repository}}:{{.Tag}}' 2>&1 | Out-String
        if ($existing -match [regex]::Escape($TestImage)) {
            $script:ImageAvailable = $true
            Record-Pass "Image pull" "$TestImage (already present)"
            return
        }

        # Attempt pull
        $pullOutput = docker pull $TestImage 2>&1 | Out-String
        if ($LASTEXITCODE -eq 0) {
            $script:ImageAvailable = $true
            Record-Pass "Image pull" $TestImage
        } elseif ($pullOutput -match "timeout|network|unreachable|no match") {
            Record-Skip "Image pull" "network unavailable or image not found"
        } else {
            Record-Fail "Image pull" "pull failed - $($pullOutput.Trim())"
        }
    } catch {
        Record-Skip "Image pull" "pull error - $($_.Exception.Message)"
    }
}

# -- 5. Container Lifecycle --------------------------------------------------

function Test-ContainerLifecycle {
    Write-Section "Lifecycle"

    if (-not (Test-DockerReady "Container lifecycle" -NeedImage)) { return }

    $name = "$($script:TestContainerPrefix)-lifecycle"
    Remove-TestContainer $name

    try {
        # Create
        docker create --name $name $TestImage cmd /c "echo smoketest" 2>&1 | Out-Null
        if ($LASTEXITCODE -ne 0) {
            Record-Fail "Container lifecycle" "create failed"
            Remove-TestContainer $name
            return
        }
        docker start $name 2>&1 | Out-Null
        Start-Sleep -Seconds 2
        docker stop $name -t 5 2>&1 | Out-Null
        docker rm $name 2>&1 | Out-Null
        if ($LASTEXITCODE -eq 0) {
            Record-Pass "Container lifecycle" "create, start, stop, remove"
        } else {
            Record-Fail "Container lifecycle" "remove failed"
        }
    } catch {
        Record-Fail "Container lifecycle" $_.Exception.Message
        Remove-TestContainer $name
    }
}

# -- 6. Process Isolation Mode -----------------------------------------------

function Test-ProcessIsolation {
    Write-Section "Isolation"

    if (-not (Test-DockerReady "Process isolation mode" -NeedImage)) { return }

    $name = "$($script:TestContainerPrefix)-process"
    Remove-TestContainer $name
    try {
        $output = docker run --rm --isolation=process --name $name $TestImage cmd /c "echo process-ok" 2>&1 | Out-String
        if ($LASTEXITCODE -eq 0 -and $output -match "process-ok") {
            Record-Pass "Process isolation mode" "process isolation available"
        } elseif ($output -match "not supported|not available|HCS") {
            Record-Skip "Process isolation mode" "not supported on this host (requires Windows Server)"
        } else {
            Record-Fail "Process isolation mode" "unexpected result - $($output.Trim())"
        }
    } catch {
        Record-Fail "Process isolation mode" $_.Exception.Message
    } finally {
        Remove-TestContainer $name
    }
}

# -- 7. Hyper-V Isolation Mode -----------------------------------------------

function Test-HyperVIsolation {
    if (-not (Test-DockerReady "Hyper-V isolation mode" -NeedImage)) { return }

    # Check if Hyper-V is installed (client vs server detection)
    $hypervEnabled = $false
    try {
        $hyperv = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -ErrorAction Stop
        $hypervEnabled = ($hyperv.State -eq "Enabled")
    } catch {
        try {
            $hypervRole = Get-WindowsFeature -Name Hyper-V -ErrorAction Stop
            $hypervEnabled = $hypervRole.Installed
        } catch {}
    }
    if (-not $hypervEnabled) {
        Record-Skip "Hyper-V isolation mode" "Hyper-V not installed"
        return
    }

    $name = "$($script:TestContainerPrefix)-hyperv"
    Remove-TestContainer $name
    try {
        $output = docker run --rm --isolation=hyperv --name $name $TestImage cmd /c "echo hyperv-ok" 2>&1 | Out-String
        if ($LASTEXITCODE -eq 0 -and $output -match "hyperv-ok") {
            Record-Pass "Hyper-V isolation mode" "Hyper-V isolation available"
        } elseif ($output -match "not supported|not available|HCS") {
            Record-Skip "Hyper-V isolation mode" "Hyper-V isolation not available"
        } else {
            Record-Fail "Hyper-V isolation mode" "unexpected result - $($output.Trim())"
        }
    } catch {
        Record-Fail "Hyper-V isolation mode" $_.Exception.Message
    } finally {
        Remove-TestContainer $name
    }
}

# -- 8. NAT Network Exists ---------------------------------------------------

function Test-NATNetwork {
    Write-Section "Networking"

    if (-not (Test-DockerReady "NAT network exists")) { return }

    try {
        $networks = docker network ls --format '{{.Name}}' 2>&1 | Out-String
        if ($networks -match "(?m)^nat$") {
            Record-Pass "NAT network exists" "default nat network present"
        } else {
            Record-Fail "NAT network exists" "nat network not found"
        }
    } catch {
        Record-Fail "NAT network exists" $_.Exception.Message
    }
}

# -- 9. Outbound Connectivity -----------------------------------------------

function Test-OutboundConnectivity {
    if (-not (Test-DockerReady "Container outbound connectivity" -NeedImage)) { return }

    $name = "$($script:TestContainerPrefix)-outbound"
    Remove-TestContainer $name
    try {
        $output = docker run --rm --name $name $TestImage cmd /c "ping -n 1 8.8.8.8" 2>&1 | Out-String
        if ($LASTEXITCODE -eq 0 -and $output -match "Reply from|bytes=") {
            Record-Pass "Container outbound connectivity" "ping to 8.8.8.8 succeeded"
        } elseif ($output -match "timed out|unreachable|could not find|General failure") {
            Record-Fail "Container outbound connectivity" "no outbound connectivity"
        } else {
            Record-Fail "Container outbound connectivity" "exit code $LASTEXITCODE"
        }
    } catch {
        Record-Fail "Container outbound connectivity" $_.Exception.Message
    } finally {
        Remove-TestContainer $name
    }
}

# -- 10. Port Mapping --------------------------------------------------------

function Test-PortMapping {
    if (-not (Test-DockerReady "Port mapping" -NeedImage)) { return }

    $name = "$($script:TestContainerPrefix)-portmap"
    $hostPort = 48199
    Remove-TestContainer $name
    try {
        docker run -d --rm --name $name -p "${hostPort}:80" $TestImage cmd /c "ping -n 30 127.0.0.1 >nul" 2>&1 | Out-Null
        Start-Sleep -Seconds 3

        $state = docker inspect --format '{{.State.Running}}' $name 2>&1 | Out-String
        if ($state.Trim() -eq "true") {
            $portInfo = docker port $name 2>&1 | Out-String
            if ($portInfo -match "$hostPort" -or $portInfo -match "80/tcp") {
                Record-Pass "Port mapping" "port $hostPort mapped to container"
            } else {
                Record-Pass "Port mapping" "container running with port binding"
            }
        } else {
            Record-Fail "Port mapping" "container did not stay running for port test"
        }
    } catch {
        Record-Fail "Port mapping" $_.Exception.Message
    } finally {
        Remove-TestContainer $name
    }
}

# -- 11. Volume Mount --------------------------------------------------------

function Test-VolumeMount {
    Write-Section "Storage"

    if (-not (Test-DockerReady "Volume mount" -NeedImage)) { return }

    $name = "$($script:TestContainerPrefix)-volume"
    $tempDir = Join-Path $env:TEMP "container-smoke-test-vol"
    Remove-TestContainer $name
    try {
        if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force }
        New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
        Set-Content -Path (Join-Path $tempDir "testfile.txt") -Value "smoke-test-data"

        $output = docker run --rm --name $name -v "${tempDir}:C:\testmount" $TestImage cmd /c "type C:\testmount\testfile.txt" 2>&1 | Out-String
        if ($LASTEXITCODE -eq 0 -and $output -match "smoke-test-data") {
            Record-Pass "Volume mount" "bind mount read successful"
        } else {
            Record-Fail "Volume mount" "bind mount failed - $($output.Trim())"
        }
    } catch {
        Record-Fail "Volume mount" $_.Exception.Message
    } finally {
        Remove-TestContainer $name
        if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue }
    }
}

# -- 12. Container Logs ------------------------------------------------------

function Test-ContainerLogs {
    Write-Section "Logging"

    if (-not (Test-DockerReady "Container logs" -NeedImage)) { return }

    $name = "$($script:TestContainerPrefix)-logs"
    Remove-TestContainer $name
    try {
        docker run --name $name $TestImage cmd /c "echo log-output-test" 2>&1 | Out-Null
        Start-Sleep -Seconds 1

        $logs = docker logs $name 2>&1 | Out-String
        if ($logs -match "log-output-test") {
            Record-Pass "Container logs" "docker logs returned expected output"
        } else {
            Record-Fail "Container logs" "docker logs did not return expected output"
        }
    } catch {
        Record-Fail "Container logs" $_.Exception.Message
    } finally {
        Remove-TestContainer $name
    }
}

# -- 13. Image List ----------------------------------------------------------

function Test-ImageList {
    if (-not (Test-DockerReady "Image list")) { return }

    try {
        $images = docker images --format '{{.Repository}}:{{.Tag}}' 2>&1
        $imageCount = ($images | Where-Object { $_ -match "\S" -and $_ -notmatch "ERROR" } | Measure-Object).Count
        if ($imageCount -gt 0) {
            Record-Pass "Image list" "$imageCount image(s)"
        } else {
            Record-Fail "Image list" "no images found"
        }
    } catch {
        Record-Fail "Image list" $_.Exception.Message
    }
}

# -- 14. Event Log -----------------------------------------------------------

function Test-EventLog {
    Write-Section "Event Log"

    try {
        $containerErrors = @()

        foreach ($logName in @('System', 'Application')) {
            try {
                $events = Get-WinEvent -FilterHashtable @{
                    LogName   = $logName
                    Level     = 2  # Error
                    StartTime = (Get-Date).AddHours(-24)
                } -ErrorAction Stop | Where-Object {
                    $_.ProviderName -match "docker|container|Hyper-V" -or
                    $_.Message -match "container|docker|containerd"
                }
                if ($events) { $containerErrors += $events }
            } catch [Exception] {
                if ($_.Exception.Message -notmatch "No events were found") {
                    Write-Verbose "$logName log query error: $($_.Exception.Message)"
                }
            }
        }

        $errorCount = ($containerErrors | Measure-Object).Count
        if ($errorCount -eq 0) {
            Record-Pass "Event log" "no container errors in last 24h"
        } else {
            $latest = $containerErrors | Sort-Object TimeCreated -Descending | Select-Object -First 1
            $detail = "$errorCount error(s) in last 24h - latest: $($latest.ProviderName) - $($latest.Message.Substring(0, [Math]::Min(80, $latest.Message.Length)))"
            Record-Fail "Event log" $detail
        }
    } catch {
        Record-Skip "Event log" "unable to query event logs - $($_.Exception.Message)"
    }
}

# ============================================================================
# OUTPUT
# ============================================================================

function Write-Header {
    if ($OutputFormat -eq "tap") {
        Write-Host "TAP version 13"
    } else {
        Write-Host ""
        Write-Color "Windows Container Smoke Tests" "White"
        Write-Host "Runtime: $ContainerRuntime"
        Write-Host "Image:   $TestImage"
        Write-Host "Time:    $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
    }
}

function Write-Summary {
    $duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)

    if ($OutputFormat -eq "tap") {
        Write-Host "1..$($script:Total)"
        Write-Host "# pass $($script:Pass)"
        Write-Host "# fail $($script:Fail)"
        Write-Host "# skip $($script:Skip)"
    } else {
        Write-Host ""
        $separator = [string]::new([char]0x2500, 40)
        Write-Color $separator "White"
        Write-Color "Summary  $ContainerRuntime  ($TestImage)" "White"

        $summaryLine = "  $($script:Pass) passed  $($script:Fail) failed  $($script:Skip) skipped  (${duration}s)"
        Write-Host $summaryLine
        Write-Color $separator "White"

        if ($script:Fail -eq 0) {
            Write-Color "All tests passed." "Green"
        } else {
            Write-Color "$($script:Fail) test(s) failed." "Red"
        }
    }
}

# ============================================================================
# MAIN
# ============================================================================

Write-Header

# Run all tests
Test-ContainerService
Test-DaemonHealth
Test-RuntimeMode
Test-ImagePull
Test-ContainerLifecycle
Test-ProcessIsolation
Test-HyperVIsolation
Test-NATNetwork
Test-OutboundConnectivity
Test-PortMapping
Test-VolumeMount
Test-ContainerLogs
Test-ImageList
Test-EventLog

Write-Summary

if ($script:Fail -eq 0) { exit 0 } else { exit 1 }
