######################################################################################
#### windows-network-smoke-tests.ps1 — Verify Windows network configuration      ####
#### Checks NIC status, IP config, gateway, DNS, DC, firewall, NTP, link speed.  ####
####                                                                              ####
#### Author:  Phil Connor                                                         ####
#### Contact: contact@mylinux.work                                                ####
#### License: MIT                                                                 ####
#### Version: 1.0                                                                 ####
####                                                                              ####
#### Usage:   .\windows-network-smoke-tests.ps1                                   ####
####          .\windows-network-smoke-tests.ps1 -Gateway 10.0.0.1                 ####
####          .\windows-network-smoke-tests.ps1 -OutputFormat tap                  ####
####                                                                              ####
#### See -Help for all options.                                                   ####
######################################################################################

[CmdletBinding()]
param(
    [string]$Gateway,
    [string]$DnsServer,
    [string]$DomainController,
    [int]$ExpectedLinkSpeedMbps = 1000,
    [string]$NtpServer,
    [string]$TestUrl = "http://www.msftconnecttest.com/connecttest.txt",
    [ValidateSet("text", "tap")]
    [string]$OutputFormat = "text",
    [switch]$NoColor,
    [switch]$Help
)

$ErrorActionPreference = "Continue"

# ============================================================================
# STATE & HELPERS
# ============================================================================

$script:Pass = 0; $script:Fail = 0; $script:Skip = 0; $script:Total = 0
$script:Results = @(); $script:StartTime = $null

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" }

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"
    } else {
        $msg = "  $(if ($NoColor) { '[PASS]' } else { [char]0x2713 }) $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 {
        $msg = "  $(if ($NoColor) { '[FAIL]' } else { [char]0x2717 }) $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 {
        $msg = "  $(if ($NoColor) { '[SKIP]' } else { [char]0x2298 }) $Name"
        if ($Reason) { $msg += " - $Reason" }
        Write-Color $msg "Yellow"
    }
}

function Write-TapHeader { Write-Host "TAP version 13" }

function Write-TapFooter {
    Write-Host "1..$($script:Total)"
    Write-Host "# pass $($script:Pass)"
    Write-Host "# fail $($script:Fail)"
    Write-Host "# skip $($script:Skip)"
}

function Write-Summary {
    $duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
    Write-Host ""
    $separator = [string]::new([char]0x2500, 50)
    Write-Color $separator "White"
    Write-Color "Windows Network Smoke Tests - Summary" "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" }
}

function Show-Usage {
    @"
Usage: .\windows-network-smoke-tests.ps1 [OPTIONS]

Smoke-test Windows network configuration. PowerShell 5.1+, no modules.
Auto-detects gateway, DNS, DC, and NTP when not specified.

Parameters:
  -Gateway STR              Default gateway IP (default: auto-detect)
  -DnsServer STR            DNS server IP (default: auto-detect)
  -DomainController STR     Domain controller hostname/IP (default: auto-detect)
  -ExpectedLinkSpeedMbps N  Minimum link speed in Mbps (default: 1000)
  -NtpServer STR            NTP server (default: auto-detect from w32tm)
  -TestUrl STR              URL for internet test (default: msftconnecttest.com)
  -OutputFormat FORMAT      Output: text (default), tap
  -NoColor                  Disable colored output
  -Help                     Show this help message

Examples:
  .\windows-network-smoke-tests.ps1
  .\windows-network-smoke-tests.ps1 -Gateway 10.0.0.1 -DnsServer 10.0.0.53
  .\windows-network-smoke-tests.ps1 -DomainController dc01.corp.local
  .\windows-network-smoke-tests.ps1 -ExpectedLinkSpeedMbps 10000
  .\windows-network-smoke-tests.ps1 -OutputFormat tap
  .\windows-network-smoke-tests.ps1 -NoColor
"@
}

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

function Test-NicStatus {
    try {
        $adapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" }
        if ($adapters -and @($adapters).Count -gt 0) {
            $first = @($adapters)[0].Name
            Record-Pass "NIC status ($first is Up)"
        } else {
            Record-Fail "NIC status" "no adapter is Up"
        }
    } catch { Record-Fail "NIC status" $_.Exception.Message }
}

function Test-IpConfiguration {
    try {
        $ips = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
            Where-Object { $_.IPAddress -ne "127.0.0.1" -and $_.PrefixOrigin -ne "WellKnown" }
        if ($ips) {
            $first = @($ips)[0]
            Record-Pass "IPv4 address assigned ($($first.IPAddress)/$($first.PrefixLength))"
        } else {
            Record-Fail "IPv4 address assigned" "no IPv4 address found"
        }
    } catch { Record-Fail "IPv4 address assigned" $_.Exception.Message }
}

function Test-DefaultGateway {
    $gw = $Gateway
    if (-not $gw) {
        try {
            $route = Get-NetRoute -DestinationPrefix "0.0.0.0/0" -ErrorAction Stop | Select-Object -First 1
            $gw = $route.NextHop
        } catch {
            Record-Fail "default gateway exists" "no default route found"
            return
        }
    }
    if (-not $gw -or $gw -eq "0.0.0.0") {
        Record-Fail "default gateway exists" "no gateway configured"
        return
    }
    Record-Pass "default gateway exists ($gw)"
    try {
        $ping = Test-Connection -ComputerName $gw -Count 2 -Quiet -ErrorAction Stop
        if ($ping) { Record-Pass "default gateway reachable" }
        else { Record-Fail "default gateway reachable" "$gw did not respond" }
    } catch { Record-Fail "default gateway reachable" $_.Exception.Message }
}

function Test-DnsServers {
    $dns = $DnsServer
    if (-not $dns) {
        try {
            $dnsAddrs = Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction Stop |
                Where-Object { $_.ServerAddresses.Count -gt 0 } |
                Select-Object -ExpandProperty ServerAddresses -First 1
            if ($dnsAddrs) { $dns = @($dnsAddrs)[0] }
        } catch { }
    }
    if ($dns) { Record-Pass "DNS servers configured ($dns)" }
    else { Record-Fail "DNS servers configured" "no DNS servers found" }
}

function Test-DnsResolution {
    try {
        $result = Resolve-DnsName -Name "www.microsoft.com" -Type A -DnsOnly -ErrorAction Stop
        if ($result) { Record-Pass "DNS resolution (www.microsoft.com)" }
        else { Record-Fail "DNS resolution" "no result for www.microsoft.com" }
    } catch { Record-Fail "DNS resolution" $_.Exception.Message }
}

function Test-DomainController {
    $dc = $DomainController
    if (-not $dc) {
        try {
            $nltest = nltest /dsgetdc: 2>&1
            $dcLine = $nltest | Select-String "DC: \\\\"
            if ($dcLine) { $dc = ($dcLine -replace ".*DC: \\\\", "").Trim() }
        } catch { }
    }
    if (-not $dc) {
        $isDomain = $false
        try {
            $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
            if ($cs.PartOfDomain) { $isDomain = $true }
        } catch { }
        if (-not $isDomain) {
            Record-Skip "domain controller reachable" "not domain-joined"
            Record-Skip "domain controller LDAP port 389" "not domain-joined"
        } else {
            Record-Fail "domain controller reachable" "domain-joined but DC not found"
            Record-Skip "domain controller LDAP port 389" "DC not identified"
        }
        return
    }
    try {
        $ping = Test-Connection -ComputerName $dc -Count 2 -Quiet -ErrorAction Stop
        if ($ping) { Record-Pass "domain controller reachable ($dc)" }
        else { Record-Fail "domain controller reachable" "$dc did not respond" }
    } catch { Record-Fail "domain controller reachable" $_.Exception.Message }
    try {
        $ldap = Test-NetConnection -ComputerName $dc -Port 389 -WarningAction SilentlyContinue -ErrorAction Stop
        if ($ldap.TcpTestSucceeded) { Record-Pass "domain controller LDAP port 389" }
        else { Record-Fail "domain controller LDAP port 389" "TCP 389 connection failed" }
    } catch { Record-Fail "domain controller LDAP port 389" $_.Exception.Message }
}

function Test-FirewallProfile {
    try {
        $profiles = Get-NetFirewallProfile -ErrorAction Stop | Where-Object { $_.Enabled -eq $true }
        if (-not $profiles) {
            Record-Fail "firewall profile" "no firewall profile is enabled"
            return
        }
        $activeNames = ($profiles | ForEach-Object { $_.Name }) -join ", "
        $hasPublic = $profiles | Where-Object { $_.Name -eq "Public" }
        if ($hasPublic) { Record-Fail "firewall profile ($activeNames)" "Public profile is active - expected Domain or Private" }
        else { Record-Pass "firewall profile ($activeNames)" }
    } catch { Record-Fail "firewall profile" $_.Exception.Message }
}

function Test-FirewallRules {
    $checks = @(
        @{ Name = "RDP";   Pattern = "*Remote Desktop*" }
        @{ Name = "WinRM"; Pattern = "*Windows Remote Management*" }
        @{ Name = "ICMP";  Pattern = "*ICMPv4*" }
    )
    $found = @(); $missing = @()
    foreach ($check in $checks) {
        try {
            $rule = Get-NetFirewallRule -Enabled True -ErrorAction SilentlyContinue |
                Where-Object { $_.DisplayName -like $check.Pattern } | Select-Object -First 1
            if ($rule) { $found += $check.Name } else { $missing += $check.Name }
        } catch { $missing += $check.Name }
    }
    if ($missing.Count -eq 0) { Record-Pass "firewall rules ($($found -join ', '))" }
    elseif ($found.Count -gt 0) { Record-Fail "firewall rules" "missing: $($missing -join ', '); found: $($found -join ', ')" }
    else { Record-Fail "firewall rules" "none of RDP, WinRM, ICMP rules found enabled" }
}

function Test-NtpSync {
    try {
        $status = w32tm /query /status 2>&1
        if ($LASTEXITCODE -ne 0) {
            Record-Fail "NTP synchronization" "w32tm query failed - is the Windows Time service running?"
            return
        }
        $sourceLine = $status | Select-String "Source:"
        $source = if ($sourceLine) { ($sourceLine -replace "Source:\s*", "").Trim() } else { "unknown" }
        if ($source -match "Free-Running|Local CMOS|VM IC") {
            Record-Fail "NTP synchronization" "source is $source - not synced to a remote server"
            return
        }
        $lastSync = $status | Select-String "Last Successful Sync Time:"
        if ($lastSync) { Record-Pass "NTP synchronized ($source)" }
        else { Record-Fail "NTP synchronization" "no successful sync recorded" }
    } catch { Record-Fail "NTP synchronization" $_.Exception.Message }
}

function Test-TimeDrift {
    $ntp = $NtpServer
    if (-not $ntp) {
        try {
            $cfg = w32tm /query /status 2>&1
            $sourceLine = $cfg | Select-String "Source:"
            if ($sourceLine) { $ntp = ($sourceLine -replace "Source:\s*", "").Trim() }
        } catch { }
    }
    if (-not $ntp -or $ntp -match "Free-Running|Local CMOS|VM IC") {
        Record-Skip "time drift" "no valid NTP source available"
        return
    }
    try {
        $strip = w32tm /stripchart /computer:$ntp /samples:1 /dataonly 2>&1
        if ($LASTEXITCODE -ne 0) {
            Record-Fail "time drift" "w32tm stripchart failed for $ntp"
            return
        }
        $offsetLine = $strip | Select-String "[\+\-]?\d+\.\d+s" | Select-Object -Last 1
        if ($offsetLine) {
            $match = [regex]::Match($offsetLine, "([\+\-]?\d+\.\d+)s")
            if ($match.Success) {
                $offset = [math]::Abs([double]$match.Groups[1].Value)
                if ($offset -le 1.0) { Record-Pass "time drift (${offset}s offset from $ntp)" }
                else { Record-Fail "time drift" "${offset}s offset exceeds 1.0s threshold" }
            } else { Record-Fail "time drift" "could not parse offset from w32tm output" }
        } else { Record-Fail "time drift" "no offset data in w32tm output" }
    } catch { Record-Fail "time drift" $_.Exception.Message }
}

function Test-LinkSpeed {
    try {
        $adapter = Get-NetAdapter | Where-Object { $_.Status -eq "Up" } | Select-Object -First 1
        if (-not $adapter) { Record-Fail "link speed" "no active adapter found"; return }
        $linkSpeedStr = $adapter.LinkSpeed
        $speedMbps = 0
        if ($linkSpeedStr -match "(\d+(\.\d+)?)\s*Gbps") { $speedMbps = [int]([double]$Matches[1] * 1000) }
        elseif ($linkSpeedStr -match "(\d+)\s*Mbps") { $speedMbps = [int]$Matches[1] }
        elseif ($linkSpeedStr -match "(\d+)\s*Kbps") { $speedMbps = [int]([double]$Matches[1] / 1000) }
        if ($speedMbps -ge $ExpectedLinkSpeedMbps) { Record-Pass "link speed ($speedMbps Mbps >= $ExpectedLinkSpeedMbps Mbps)" }
        else { Record-Fail "link speed" "$speedMbps Mbps is below expected $ExpectedLinkSpeedMbps Mbps" }
    } catch { Record-Fail "link speed" $_.Exception.Message }
}

function Test-VlanTagging {
    try {
        $adapters = Get-NetAdapter | Where-Object { $_.Status -eq "Up" }
        if (-not $adapters) { Record-Skip "VLAN tagging" "no active adapter"; return }
        $tagged = @($adapters) | Where-Object { $_.VlanID -and $_.VlanID -ne 0 }
        if ($tagged) {
            $first = @($tagged)[0]
            Record-Pass "VLAN tagging ($($first.Name) VLAN $($first.VlanID))"
        } else { Record-Skip "VLAN tagging" "adapter not VLAN-tagged" }
    } catch { Record-Skip "VLAN tagging" "VlanID property not available" }
}

function Test-InternetConnectivity {
    try {
        $response = Invoke-WebRequest -Uri $TestUrl -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
        if ($response.StatusCode -eq 200) { Record-Pass "internet connectivity (HTTP $($response.StatusCode))" }
        else { Record-Fail "internet connectivity" "HTTP $($response.StatusCode)" }
    } catch { Record-Fail "internet connectivity" $_.Exception.Message }
}

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

if ($Help) { Show-Usage; exit 0 }

$script:StartTime = Get-Date

if ($OutputFormat -eq "tap") {
    Write-TapHeader
} else {
    Write-Host ""
    Write-Color "Windows Network Smoke Tests" "White"
    Write-Host "Host:   $($env:COMPUTERNAME)"
    Write-Host "Time:   $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
    Write-Host ""
}

Test-NicStatus
Test-IpConfiguration
Test-DefaultGateway
Test-DnsServers
Test-DnsResolution
Test-DomainController
Test-FirewallProfile
Test-FirewallRules
Test-NtpSync
Test-TimeDrift
Test-LinkSpeed
Test-VlanTagging
Test-InternetConnectivity

if ($OutputFormat -eq "tap") { Write-TapFooter } else { Write-Summary }

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