###############################################################################
#### windows-iis-smoke-tests.ps1 — Verify IIS health                      ####
#### Checks W3SVC, WAS, app pools, site responses, SSL certs,             ####
#### bindings, ARR, URL Rewrite, logs, FREB, and event log.               ####
####                                                                       ####
#### Author:  Phil Connor                                                  ####
#### Contact: contact@mylinux.work                                         ####
#### License: MIT                                                          ####
#### Version: 1.0                                                          ####
####                                                                       ####
#### Usage:   .\windows-iis-smoke-tests.ps1                                ####
####          .\windows-iis-smoke-tests.ps1 -SiteNames "Default Web Site"  ####
####          .\windows-iis-smoke-tests.ps1 -OutputFormat tap              ####
####                                                                       ####
#### See -Help for all options.                                            ####
###############################################################################

[CmdletBinding()]
param(
    [string[]]$SiteNames = @(),
    [int]$CertExpiryDays = 30,
    [string]$TestUrl = "",
    [ValidateSet("text","tap")]
    [string]$OutputFormat = "text",
    [switch]$NoColor,
    [switch]$Help
)

$ErrorActionPreference = "Continue"

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

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

Smoke-test IIS (Internet Information Services). PowerShell 5.1+.
Requires the WebAdministration module (IIS Management Tools).

Parameters:
  -SiteNames "Site1","Site2"  Sites to test (default: all sites)
  -CertExpiryDays N           Warn if cert expires within N days (default: 30)
  -TestUrl URL                 Optional specific URL to test
  -OutputFormat FORMAT         Output: text (default), tap
  -NoColor                     Disable coloured output
  -Verbose                     Show debug output
  -Help                        Show this help

Examples:
  .\windows-iis-smoke-tests.ps1
  .\windows-iis-smoke-tests.ps1 -SiteNames "Default Web Site","IntranetApp"
  .\windows-iis-smoke-tests.ps1 -CertExpiryDays 60 -OutputFormat tap
  .\windows-iis-smoke-tests.ps1 -TestUrl "https://intranet.corp.local/health"
  .\windows-iis-smoke-tests.ps1 -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 Write-Section {
    param([string]$Name)
    if ($OutputFormat -eq "text") {
        Write-Host ""
        Write-Color $Name "White"
    }
}

function Get-TargetSites {
    if ($SiteNames.Count -gt 0) {
        return @(Get-Website | Where-Object { $SiteNames -contains $_.Name })
    }
    return @(Get-Website)
}

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

# -- 1. W3SVC Service -------------------------------------------------------

function Test-W3SVC {
    Write-Section "Services"

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

# -- 2. WAS Service ----------------------------------------------------------

function Test-WAS {
    try {
        $svc = Get-Service -Name WAS -ErrorAction Stop
        if ($svc.Status -eq "Running") {
            Record-Pass "WAS service running"
        } else {
            Record-Fail "WAS service running" "status: $($svc.Status)"
        }
    } catch {
        Record-Fail "WAS service running" "service not found"
    }
}

# -- 3. Application Pool States ---------------------------------------------

function Test-AppPoolStates {
    Write-Section "Application Pools"

    try {
        $pools = @(Get-WebAppPoolState -ErrorAction Stop)
        if ($pools.Count -eq 0) {
            Record-Skip "application pools" "no pools found"
            return
        }

        $started = @($pools | Where-Object { $_.Value -eq "Started" })
        $stopped = @($pools | Where-Object { $_.Value -ne "Started" })

        foreach ($p in $started) {
            $poolName = ($p.ItemXPath -replace ".*@name='([^']+)'.*", '$1')
            Record-Pass "app pool started" $poolName
        }

        foreach ($p in $stopped) {
            $poolName = ($p.ItemXPath -replace ".*@name='([^']+)'.*", '$1')
            Record-Fail "app pool stopped" "$poolName state: $($p.Value)"
        }
    } catch {
        Record-Fail "application pools" $_.Exception.Message
    }
}

# -- 4. Site HTTP Responses --------------------------------------------------

function Test-SiteResponses {
    Write-Section "Site Responses"

    $sites = Get-TargetSites
    if ($sites.Count -eq 0) {
        Record-Skip "site responses" "no sites found"
        return
    }

    foreach ($site in $sites) {
        if ($site.State -ne "Started") {
            Record-Fail "site responds ($($site.Name))" "site not started"
            continue
        }

        $bindings = @($site.Bindings.Collection)
        if ($bindings.Count -eq 0) {
            Record-Skip "site responds ($($site.Name))" "no bindings"
            continue
        }

        $binding = $bindings[0]
        $proto = $binding.protocol
        $info = $binding.bindingInformation -split ":"
        $port = if ($info.Count -ge 2) { $info[1] } else { if ($proto -eq "https") { "443" } else { "80" } }
        $host_ = if ($info.Count -ge 3 -and $info[2]) { $info[2] } else { "localhost" }

        $url = "${proto}://${host_}:${port}/"

        try {
            $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
            Record-Pass "site responds ($($site.Name))" "$url HTTP $($resp.StatusCode)"
        } catch {
            $code = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { "timeout" }
            if ($code -ge 200 -and $code -lt 500) {
                Record-Pass "site responds ($($site.Name))" "$url HTTP $code"
            } else {
                Record-Fail "site responds ($($site.Name))" "$url HTTP $code"
            }
        }
    }
}

# -- 5. SSL Certificate Validation ------------------------------------------

function Test-SSLCerts {
    Write-Section "SSL Certificates"

    $sites = Get-TargetSites
    $httpsBindings = @()

    foreach ($site in $sites) {
        foreach ($b in $site.Bindings.Collection) {
            if ($b.protocol -eq "https" -and $b.certificateHash) {
                $httpsBindings += [PSCustomObject]@{
                    SiteName = $site.Name
                    Hash     = $b.certificateHash
                    Store    = if ($b.certificateStoreName) { $b.certificateStoreName } else { "My" }
                    Binding  = $b.bindingInformation
                }
            }
        }
    }

    if ($httpsBindings.Count -eq 0) {
        Record-Skip "SSL certificates" "no HTTPS bindings found"
        return
    }

    $checked = @{}
    foreach ($hb in $httpsBindings) {
        if ($checked.ContainsKey($hb.Hash)) { continue }
        $checked[$hb.Hash] = $true

        try {
            $cert = Get-ChildItem "Cert:\LocalMachine\$($hb.Store)\$($hb.Hash)" -ErrorAction Stop
            $daysLeft = [math]::Floor(($cert.NotAfter - (Get-Date)).TotalDays)
            $cn = $cert.Subject -replace "CN=", "" -replace ",.*", ""

            if ($cert.NotAfter -lt (Get-Date)) {
                Record-Fail "SSL certificate ($cn)" "EXPIRED on $($cert.NotAfter.ToString('yyyy-MM-dd'))"
            } elseif ($daysLeft -lt $CertExpiryDays) {
                Record-Fail "certificate expiry ($cn)" "$daysLeft days remaining (threshold: $CertExpiryDays)"
            } else {
                Record-Pass "SSL certificate valid ($cn)" "expires $($cert.NotAfter.ToString('yyyy-MM-dd')), $daysLeft days"
            }
        } catch {
            Record-Fail "SSL certificate ($($hb.SiteName))" "cert hash $($hb.Hash) not found in $($hb.Store) store"
        }
    }
}

# -- 6. Site Bindings --------------------------------------------------------

function Test-SiteBindings {
    Write-Section "Configuration"

    $sites = Get-TargetSites
    $totalBindings = 0
    $validBindings = 0

    foreach ($site in $sites) {
        foreach ($b in $site.Bindings.Collection) {
            $totalBindings++
            $info = $b.bindingInformation
            $proto = $b.protocol
            if ($proto -in @("http","https") -and $info -match "^\*?:?\d+:") {
                $validBindings++
            } elseif ($proto -in @("net.tcp","net.pipe","net.msmq","msmq.formatname")) {
                $validBindings++
            }
        }
    }

    if ($totalBindings -eq 0) {
        Record-Skip "site bindings" "no bindings configured"
    } elseif ($validBindings -eq $totalBindings) {
        Record-Pass "site bindings valid" "$($sites.Count) sites, $totalBindings bindings"
    } else {
        $invalid = $totalBindings - $validBindings
        Record-Fail "site bindings" "$invalid invalid out of $totalBindings bindings"
    }
}

# -- 7. Default Document ----------------------------------------------------

function Test-DefaultDocument {
    $sites = Get-TargetSites
    if ($sites.Count -eq 0) { return }

    foreach ($site in $sites) {
        try {
            $docs = Get-WebConfiguration -Filter "system.webServer/defaultDocument/files/add" -PSPath "IIS:\Sites\$($site.Name)" -ErrorAction Stop
            if ($docs -and @($docs).Count -gt 0) {
                $first = @($docs)[0].value
                Record-Pass "default document ($($site.Name))" $first
            } else {
                Record-Fail "default document ($($site.Name))" "none configured"
            }
        } catch {
            Record-Skip "default document ($($site.Name))" "could not read config"
        }
    }
}

# -- 8. Custom URL Test -----------------------------------------------------

function Test-CustomUrl {
    if (-not $TestUrl) { return }

    Write-Section "Custom URL"

    try {
        $resp = Invoke-WebRequest -Uri $TestUrl -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
        Record-Pass "custom URL" "$TestUrl HTTP $($resp.StatusCode)"
    } catch {
        $code = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { "timeout" }
        Record-Fail "custom URL" "$TestUrl HTTP $code"
    }
}

# -- 9. ARR Health -----------------------------------------------------------

function Test-ARRHealth {
    Write-Section "Modules"

    try {
        $arr = Get-WebGlobalModule -Name "ApplicationRequestRouting" -ErrorAction Stop
        if (-not $arr) {
            Record-Skip "ARR health" "ARR not installed"
            return
        }
    } catch {
        Record-Skip "ARR health" "ARR not installed"
        return
    }

    try {
        $farms = Get-WebConfiguration -Filter "webFarms/webFarm" -PSPath "MACHINE/WEBROOT/APPHOST" -ErrorAction Stop
        if (-not $farms -or @($farms).Count -eq 0) {
            Record-Pass "ARR health" "ARR installed, no server farms configured"
            return
        }

        foreach ($farm in @($farms)) {
            $farmName = $farm.name
            $servers = @($farm.Collection)
            $healthy = @($servers | Where-Object { $_.enabled -eq $true })
            if ($healthy.Count -eq $servers.Count) {
                Record-Pass "ARR farm ($farmName)" "$($servers.Count) servers enabled"
            } else {
                $disabled = $servers.Count - $healthy.Count
                Record-Fail "ARR farm ($farmName)" "$disabled of $($servers.Count) servers disabled"
            }
        }
    } catch {
        Record-Fail "ARR health" $_.Exception.Message
    }
}

# -- 10. URL Rewrite Module -------------------------------------------------

function Test-URLRewrite {
    try {
        $mod = Get-WebGlobalModule -Name "RewriteModule" -ErrorAction Stop
        if ($mod) {
            Record-Pass "URL Rewrite module installed"
        } else {
            Record-Skip "URL Rewrite module" "not installed"
        }
    } catch {
        Record-Skip "URL Rewrite module" "not installed"
    }
}

# -- 11. Log Directory -------------------------------------------------------

function Test-LogDirectory {
    Write-Section "Logging"

    try {
        $logDir = (Get-WebConfigurationProperty -Filter "system.applicationHost/sites/siteDefaults/logFile" -Name "directory" -ErrorAction Stop).Value
        $expanded = [System.Environment]::ExpandEnvironmentVariables($logDir)

        if (Test-Path $expanded) {
            try {
                $testFile = Join-Path $expanded ".iis-smoke-test-$(Get-Random).tmp"
                [IO.File]::WriteAllText($testFile, "test")
                Remove-Item $testFile -Force
                Record-Pass "log directory accessible" $expanded
            } catch {
                Record-Fail "log directory writable" "$expanded - not writable"
            }
        } else {
            Record-Fail "log directory exists" "$expanded - not found"
        }
    } catch {
        Record-Skip "log directory" "could not read log configuration"
    }
}

# -- 12. Failed Request Tracing ----------------------------------------------

function Test-FREB {
    $sites = Get-TargetSites
    $frebFound = $false

    foreach ($site in $sites) {
        try {
            $tracing = Get-WebConfiguration -Filter "system.webServer/tracing/traceFailedRequests" -PSPath "IIS:\Sites\$($site.Name)" -ErrorAction Stop
            if ($tracing -and @($tracing).Count -gt 0) {
                $frebFound = $true
                $frebDir = "$($site.TraceFailedRequestsLogging.Directory)"
                if ($frebDir -and (Test-Path ([System.Environment]::ExpandEnvironmentVariables($frebDir)))) {
                    Record-Pass "Failed Request Tracing ($($site.Name))" "enabled, logs accessible"
                } else {
                    Record-Pass "Failed Request Tracing ($($site.Name))" "enabled"
                }
            }
        } catch { }
    }

    if (-not $frebFound) {
        Record-Skip "Failed Request Tracing" "FREB not enabled on any site"
    }
}

# -- 13. Event Log -----------------------------------------------------------

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

    try {
        $events = @()
        $sources = @("IIS", "W3SVC", "WAS", "Microsoft-Windows-IIS*")
        foreach ($src in $sources) {
            try {
                $filter = @{
                    LogName   = "System","Application"
                    Level     = 2
                    StartTime = (Get-Date).AddHours(-24)
                }
                $found = @(Get-WinEvent -FilterHashtable $filter -ErrorAction SilentlyContinue |
                    Where-Object { $_.ProviderName -like "*IIS*" -or $_.ProviderName -like "W3SVC" -or $_.ProviderName -like "WAS" })
                $events += $found
            } catch { }
        }

        if ($events.Count -eq 0) {
            Record-Pass "event log clean" "0 IIS errors in last 24h"
        } else {
            $latest = $events[0]
            $msg = $latest.Message.Substring(0, [math]::Min(80, $latest.Message.Length))
            Record-Fail "event log" "$($events.Count) IIS error(s) in 24h — latest: $msg"
        }
    } catch {
        if ($_.Exception.Message -match "No events were found") {
            Record-Pass "event log clean" "0 IIS errors in last 24h"
        } else {
            Record-Fail "event log" $_.Exception.Message
        }
    }
}

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

function Write-Header {
    if ($OutputFormat -eq "tap") {
        Write-Host "TAP version 13"
    } else {
        Write-Host ""
        Write-Color "Windows IIS Smoke Tests" "White"
        Write-Host "Server: $env:COMPUTERNAME"
        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  $env:COMPUTERNAME" "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

# Load WebAdministration module
try {
    Import-Module WebAdministration -ErrorAction Stop
    Write-Verbose "WebAdministration module loaded"
} catch {
    Write-Err "WebAdministration module not available. Install IIS Management Tools."
    exit 1
}

# Run all tests
Test-W3SVC
Test-WAS
Test-AppPoolStates
Test-SiteResponses
Test-SSLCerts
Test-SiteBindings
Test-DefaultDocument
Test-CustomUrl
Test-ARRHealth
Test-URLRewrite
Test-LogDirectory
Test-FREB
Test-EventLog

Write-Summary

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