###############################################################################
#### windows-pki-smoke-tests.ps1 — Verify Windows PKI infrastructure      ####
#### Checks CA service, certificates, CRL, OCSP, templates,               ####
#### auto-enrollment, pending requests, AIA/CDP extensions.                ####
####                                                                       ####
#### Author:  Phil Connor                                                  ####
#### Contact: contact@mylinux.work                                         ####
#### License: MIT                                                          ####
#### Version: 1.0                                                          ####
####                                                                       ####
#### Usage:   .\windows-pki-smoke-tests.ps1                                ####
####          .\windows-pki-smoke-tests.ps1 -CAServer CA01                 ####
####          .\windows-pki-smoke-tests.ps1 -OutputFormat tap              ####
####                                                                       ####
#### See -Help for all options.                                            ####
###############################################################################

[CmdletBinding()]
param(
    [string]$CAName = "",
    [string]$CAServer = "",
    [int]$CRLThresholdHours = 24,
    [int]$PendingThreshold = 10,
    [ValidateSet("text","tap")]
    [string]$OutputFormat = "text",
    [switch]$NoColor,
    [switch]$Help
)

$ErrorActionPreference = "Continue"

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

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

Smoke-test Windows PKI infrastructure. PowerShell 5.1+.
Designed for domain-joined Windows machines with AD CS.

Parameters:
  -CAName NAME           CA common name (default: auto-detect via certutil)
  -CAServer SERVER       CA server hostname or IP (default: auto-detect via certutil)
  -CRLThresholdHours N   Warn if CRL expires within N hours (default: 24)
  -PendingThreshold N    Warn if pending requests exceed N (default: 10)
  -OutputFormat FORMAT   Output: text (default), tap
  -NoColor               Disable coloured output
  -Verbose               Show debug output
  -Help                  Show this help

Examples:
  .\windows-pki-smoke-tests.ps1
  .\windows-pki-smoke-tests.ps1 -CAServer CA01.corp.local
  .\windows-pki-smoke-tests.ps1 -CAName "corp-CA01-CA" -CAServer CA01
  .\windows-pki-smoke-tests.ps1 -CRLThresholdHours 48 -OutputFormat tap
  .\windows-pki-smoke-tests.ps1 -NoColor -Verbose
"@
    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"
    }
}

# ============================================================================
# AUTO-DETECTION
# ============================================================================

function Resolve-CAServer {
    if ($CAServer) {
        Write-Verbose "Using specified CA server: $CAServer"
        return $CAServer
    }
    if (-not (Test-CommandExists "certutil")) {
        Write-Err "certutil not available. Use -CAServer parameter."
        exit 1
    }
    try {
        $output = certutil -config - -ping 2>&1 | Out-String
        if ($output -match "server\s*[`"``]([^`"``]+)[`"``]") {
            Write-Verbose "Auto-detected CA server: $($Matches[1].Trim())"
            return $Matches[1].Trim()
        }
    } catch {}
    try {
        $output = certutil 2>&1 | Out-String
        if ($output -match "Config:\s*`"?([^\\`"]+)\\") {
            Write-Verbose "Auto-detected CA server from certutil: $($Matches[1].Trim())"
            return $Matches[1].Trim()
        }
    } catch {}
    Write-Err "Cannot auto-detect CA server. Use -CAServer parameter."
    exit 1
}

function Resolve-CAName {
    if ($CAName) {
        Write-Verbose "Using specified CA name: $CAName"
        return $CAName
    }
    if (-not (Test-CommandExists "certutil")) {
        Write-Err "certutil not available. Use -CAName parameter."
        exit 1
    }
    try {
        $output = certutil 2>&1 | Out-String
        if ($output -match "Name:\s*[`"``]?([^`"``\r\n]+)[`"``]?") {
            Write-Verbose "Auto-detected CA name: $($Matches[1].Trim())"
            return $Matches[1].Trim()
        }
    } catch {}
    try {
        $output = certutil -config - -ping 2>&1 | Out-String
        if ($output -match "CA\s*[`"``]([^`"``]+)[`"``]") {
            Write-Verbose "Auto-detected CA name from config: $($Matches[1].Trim())"
            return $Matches[1].Trim()
        }
    } catch {}
    Write-Err "Cannot auto-detect CA name. Use -CAName parameter."
    exit 1
}

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

# -- 1. CA Service -----------------------------------------------------------

function Test-CAService {
    Write-Section "CA Service"

    try {
        $svc = Get-Service -Name "CertSvc" -ComputerName $script:CAServerName -ErrorAction Stop
        if ($svc.Status -eq "Running") {
            Record-Pass "CA service status" "CertSvc running on $($script:CAServerName)"
        } else {
            Record-Fail "CA service status" "CertSvc is $($svc.Status) on $($script:CAServerName)"
        }
    } catch {
        try {
            $svc = Get-Service -Name "CertSvc" -ErrorAction Stop
            if ($svc.Status -eq "Running") {
                Record-Pass "CA service status" "CertSvc running (local)"
            } else {
                Record-Fail "CA service status" "CertSvc is $($svc.Status)"
            }
        } catch {
            Record-Fail "CA service status" "cannot query CertSvc - $($_.Exception.Message)"
        }
    }
}

# -- 2. CA Certificate Validity ---------------------------------------------

function Test-CACertificate {
    Write-Section "CA Certificate"
    if (-not (Test-CommandExists "certutil")) {
        Record-Skip "CA certificate validity" "certutil not available"
        return
    }

    try {
        $configString = "$($script:CAServerName)\$($script:CACommonName)"
        $output = certutil -config $configString -ca.cert 2>&1 | Out-String

        if (-not ($output -match "NotAfter:\s*(.+)")) {
            $output = certutil -ca.cert 2>&1 | Out-String
        }

        if ($output -match "NotAfter:\s*(.+)") {
            $notAfter = [DateTime]::Parse($Matches[1].Trim())
            $daysLeft = ($notAfter - (Get-Date)).Days
            $expiryStr = $notAfter.ToString("yyyy-MM-dd")

            if ($daysLeft -lt 0) {
                Record-Fail "CA certificate validity" "CA certificate expired on $expiryStr"
            } elseif ($daysLeft -le 30) {
                Record-Fail "CA certificate validity" "CA certificate expires in $daysLeft days ($expiryStr)"
            } else {
                Record-Pass "CA certificate validity" "expires $expiryStr ($daysLeft days remaining)"
            }
        } else {
            Record-Fail "CA certificate validity" "could not determine CA certificate expiry"
        }
    } catch {
        Record-Fail "CA certificate validity" "certutil error - $($_.Exception.Message)"
    }
}

# -- 3. CRL Publication ------------------------------------------------------

function Test-CRLPublication {
    Write-Section "CRL"
    if (-not (Test-CommandExists "certutil")) {
        Record-Skip "CRL publication" "certutil not available"
        return
    }

    try {
        $configString = "$($script:CAServerName)\$($script:CACommonName)"
        $output = certutil -config $configString -crl 2>&1 | Out-String

        if ($output -match "CRL published" -or $output -match "command completed successfully") {
            $crlOutput = certutil -config $configString -getcrl 2>&1 | Out-String
            if (-not ($crlOutput -match "Next Update:\s*(.+)")) {
                $crlOutput = certutil -getcrl 2>&1 | Out-String
            }

            if ($crlOutput -match "Next Update:\s*(.+)") {
                $nextUpdate = [DateTime]::Parse($Matches[1].Trim())
                $hoursLeft = ($nextUpdate - (Get-Date)).TotalHours
                $hoursStr = [math]::Floor($hoursLeft)

                if ($hoursLeft -lt 0) {
                    Record-Fail "CRL publication" "CRL has expired (next update was $($nextUpdate.ToString('yyyy-MM-dd HH:mm')))"
                } elseif ($hoursLeft -le $CRLThresholdHours) {
                    Record-Fail "CRL publication" "CRL expires in ${hoursStr}h (threshold: ${CRLThresholdHours}h)"
                } else {
                    Record-Pass "CRL publication" "next update in ${hoursStr}h"
                }
            } else {
                Record-Pass "CRL publication" "CRL published successfully"
            }
        } elseif ($output -match "error|fail") {
            Record-Fail "CRL publication" "certutil -crl reported an error"
        } else {
            Record-Fail "CRL publication" "could not verify CRL publication"
        }
    } catch {
        Record-Fail "CRL publication" "certutil error - $($_.Exception.Message)"
    }
}

# -- 4. OCSP Responder ------------------------------------------------------

function Test-OCSPResponder {
    Write-Section "OCSP"
    if (-not (Test-CommandExists "certutil")) {
        Record-Skip "OCSP responder" "certutil not available"
        return
    }

    try {
        $configString = "$($script:CAServerName)\$($script:CACommonName)"
        $output = certutil -config $configString -ca.cert 2>&1 | Out-String

        if (-not ($output -match "URL=(\S*ocsp\S*)")) {
            $output = certutil -ca.cert 2>&1 | Out-String
        }

        if ($output -match "URL=(\S*ocsp\S*)") {
            $ocspUrl = $Matches[1].Trim()
            Write-Verbose "Testing OCSP URL: $ocspUrl"

            try {
                $response = Invoke-WebRequest -Uri $ocspUrl -Method GET -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
                if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) {
                    Record-Pass "OCSP responder" $ocspUrl
                } else {
                    Record-Fail "OCSP responder" "$ocspUrl - HTTP $($response.StatusCode)"
                }
            } catch {
                $statusCode = if ($_.Exception.Response) { [int]$_.Exception.Response.StatusCode } else { $null }
                if ($statusCode -and $statusCode -lt 500) {
                    Record-Pass "OCSP responder" "$ocspUrl (HTTP $statusCode)"
                } else {
                    Record-Fail "OCSP responder" "$ocspUrl - $($_.Exception.Message)"
                }
            }
        } else {
            Record-Skip "OCSP responder" "OCSP URL not configured in CA certificate"
        }
    } catch {
        Record-Fail "OCSP responder" "error - $($_.Exception.Message)"
    }
}

# -- 5. Certificate Templates -----------------------------------------------

function Test-CertificateTemplates {
    Write-Section "Templates"
    if (-not (Test-CommandExists "certutil")) {
        Record-Skip "Certificate templates" "certutil not available"
        return
    }

    try {
        $configString = "$($script:CAServerName)\$($script:CACommonName)"
        $output = certutil -config $configString -catemplates 2>&1 | Out-String

        if ($output -match "error|fail" -and $output -notmatch "command completed") {
            $output = certutil -catemplates 2>&1 | Out-String
        }

        if ($output -match "command completed successfully" -or $output -match ":") {
            $templateLines = ($output -split "`n") | Where-Object {
                $_ -match ":" -and
                $_ -notmatch "command completed" -and
                $_ -notmatch "CertUtil" -and
                $_ -notmatch "^\s*$"
            }
            $templateCount = ($templateLines | Measure-Object).Count

            if ($templateCount -gt 0) {
                Record-Pass "Certificate templates" "$templateCount template(s) published"
            } else {
                Record-Fail "Certificate templates" "no templates found"
            }
        } else {
            Record-Fail "Certificate templates" "certutil -catemplates failed"
        }
    } catch {
        Record-Fail "Certificate templates" "certutil error - $($_.Exception.Message)"
    }
}

# -- 6. Auto-Enrollment ------------------------------------------------------

function Test-AutoEnrollment {
    Write-Section "Auto-Enrollment"
    try {
        $regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Cryptography\AutoEnrollment"

        if (-not (Test-Path $regPath)) {
            Record-Skip "Auto-enrollment configured" "registry key not found (GPO may not be applied)"
            return
        }

        $flags = (Get-ItemProperty -Path $regPath -Name "AEPolicy" -ErrorAction SilentlyContinue).AEPolicy

        if ($null -eq $flags) {
            Record-Skip "Auto-enrollment configured" "AEPolicy value not set"
            return
        }

        # AEPolicy flags: 1 = enroll, 2 = update, 4 = renew expired
        if ($flags -band 1) {
            $features = @()
            if ($flags -band 1) { $features += "enroll" }
            if ($flags -band 2) { $features += "update" }
            if ($flags -band 4) { $features += "renew expired" }
            $featureStr = $features -join ", "
            Record-Pass "Auto-enrollment configured" "enabled ($featureStr)"
        } else {
            Record-Fail "Auto-enrollment configured" "AEPolicy is $flags (enrollment not enabled)"
        }
    } catch {
        Record-Fail "Auto-enrollment configured" "registry error - $($_.Exception.Message)"
    }
}

# -- 7. Pending Requests ----------------------------------------------------

function Test-PendingRequests {
    Write-Section "Pending Requests"
    if (-not (Test-CommandExists "certutil")) {
        Record-Skip "Pending requests" "certutil not available"
        return
    }

    try {
        $configString = "$($script:CAServerName)\$($script:CACommonName)"
        $output = certutil -config $configString -view -restrict "disposition=9" -out "requestid" 2>&1 | Out-String

        $pendingCount = 0
        if ($output -match "(\d+) Row\(s\)") {
            $pendingCount = [int]$Matches[1]
        } elseif ($output -match "0 Rows") {
            $pendingCount = 0
        }

        if ($pendingCount -gt $PendingThreshold) {
            Record-Fail "Pending requests" "$pendingCount pending (threshold: $PendingThreshold)"
        } else {
            Record-Pass "Pending requests" "$pendingCount pending (threshold: $PendingThreshold)"
        }
    } catch {
        Record-Fail "Pending requests" "certutil error - $($_.Exception.Message)"
    }
}

# -- 8. AIA Extension -------------------------------------------------------

function Test-AIAExtension {
    Write-Section "Extensions"
    if (-not (Test-CommandExists "certutil")) {
        Record-Skip "AIA extension configured" "certutil not available"
        return
    }

    try {
        $configString = "$($script:CAServerName)\$($script:CACommonName)"
        $output = certutil -config $configString -ca.cert 2>&1 | Out-String
        if (-not ($output -match "Authority Information Access")) {
            $output = certutil -ca.cert 2>&1 | Out-String
        }

        if ($output -match "Authority Information Access") {
            $urlCount = ([regex]::Matches($output, "URL=\S+")).Count
            if ($urlCount -gt 0) {
                Record-Pass "AIA extension configured" "$urlCount URL(s) configured"
            } else {
                Record-Pass "AIA extension configured" "AIA extension present"
            }
        } else {
            Record-Fail "AIA extension configured" "AIA extension not found in CA certificate"
        }
    } catch {
        Record-Fail "AIA extension configured" "certutil error - $($_.Exception.Message)"
    }
}

# -- 9. CDP Extension -------------------------------------------------------

function Test-CDPExtension {
    if (-not (Test-CommandExists "certutil")) {
        Record-Skip "CDP extension configured" "certutil not available"
        return
    }

    try {
        $configString = "$($script:CAServerName)\$($script:CACommonName)"
        $output = certutil -config $configString -ca.cert 2>&1 | Out-String
        if (-not ($output -match "CRL Distribution Point")) {
            $output = certutil -ca.cert 2>&1 | Out-String
        }

        if ($output -match "CRL Distribution Point") {
            $urlCount = ([regex]::Matches($output, "URL=\S+")).Count
            if ($urlCount -gt 0) {
                Record-Pass "CDP extension configured" "$urlCount URL(s) configured"
            } else {
                Record-Pass "CDP extension configured" "CDP extension present"
            }
        } else {
            Record-Fail "CDP extension configured" "CDP extension not found in CA certificate"
        }
    } catch {
        Record-Fail "CDP extension configured" "certutil error - $($_.Exception.Message)"
    }
}

# -- 10. Enterprise CA Registration -----------------------------------------

function Test-EnterpriseCARegistration {
    Write-Section "AD Registration"
    if (-not (Test-CommandExists "certutil")) {
        Record-Skip "Enterprise CA registration" "certutil not available"
        return
    }

    try {
        $output = certutil -dcsinfo 2>&1 | Out-String
        if ($output -match $script:CACommonName -or $output -match "Enrollment Services") {
            Record-Pass "Enterprise CA registration" "CA registered in AD"
            return
        }
    } catch { Write-Verbose "certutil -dcsinfo failed: $($_.Exception.Message)" }

    try {
        $output = certutil -config "$($script:CAServerName)\$($script:CACommonName)" -ping 2>&1 | Out-String
        if ($output -match "interface is alive" -or $output -match "command completed successfully") {
            Record-Pass "Enterprise CA registration" "CA reachable and responsive"
            return
        }
    } catch { Write-Verbose "certutil -ping failed: $($_.Exception.Message)" }

    try {
        $rootDSE = [ADSI]"LDAP://RootDSE"
        $configNC = $rootDSE.configurationNamingContext
        $enrollDN = "LDAP://CN=Enrollment Services,CN=Public Key Services,CN=Services,$configNC"
        $enrollContainer = [ADSI]$enrollDN

        $found = $false
        foreach ($child in $enrollContainer.Children) {
            if ($child.cn -eq $script:CACommonName -or $child.cn -match $script:CACommonName) {
                $found = $true
                break
            }
        }

        if ($found) {
            Record-Pass "Enterprise CA registration" "CA found in AD Enrollment Services"
        } else {
            Record-Fail "Enterprise CA registration" "CA not found in AD Enrollment Services"
        }
    } catch {
        Record-Fail "Enterprise CA registration" "cannot verify AD registration - $($_.Exception.Message)"
    }
}

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

function Write-Header {
    if ($OutputFormat -eq "tap") {
        Write-Host "TAP version 13"
    } else {
        Write-Host ""
        Write-Color "Windows PKI Smoke Tests" "White"
        Write-Host "CA Server: $($script:CAServerName)"
        Write-Host "CA Name:   $($script:CACommonName)"
        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  $($script:CAServerName)\$($script:CACommonName)" "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
# ============================================================================

# Resolve targets
$script:CAServerName = Resolve-CAServer
$script:CACommonName = Resolve-CAName

Write-Header

# Run all tests
Test-CAService
Test-CACertificate
Test-CRLPublication
Test-OCSPResponder
Test-CertificateTemplates
Test-AutoEnrollment
Test-PendingRequests
Test-AIAExtension
Test-CDPExtension
Test-EnterpriseCARegistration

Write-Summary

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