Public/Invoke-TaxEditorSmokeTest.ps1

# Copyright (c) 2026 Jeffrey Snover. All rights reserved.
# Licensed under the MIT License. See LICENSE file in the project root.

function Invoke-TaxEditorSmokeTest {
    <#
    .SYNOPSIS
        Runs a comprehensive remote smoke test against the deployed Taxonomy Editor.
    .DESCRIPTION
        Orchestrates Test-TaxEditorHealth, Test-TaxEditorEndpoints,
        Test-AzureHealth, and Test-GitHubHealth, then produces a summary
        report with pass/fail counts, response time stats, and per-category
        breakdowns.
    .PARAMETER BaseUrl
        The base URL of the deployed Taxonomy Editor site.
    .PARAMETER TimeoutSec
        HTTP request timeout in seconds per endpoint. Default: 15.
    .PARAMETER HealthMaxAttempts
        Cold-start tolerance for the Health-Checks phase (t/1696). Polls the
        health probe up to this many attempts, returning on the first healthy
        result, so a scale-from-zero container whose first request exceeds
        TimeoutSec is not false-red'd. Default: 5. Set to 1 for fast-fail
        (single-shot, prior behavior).
    .PARAMETER HealthRetryIntervalSec
        Seconds to sleep between health-probe attempts when HealthMaxAttempts > 1.
        Default: 10.
    .PARAMETER Detailed
        Show per-endpoint results in addition to the summary.
    .EXAMPLE
        Invoke-TaxEditorSmokeTest
    .EXAMPLE
        Invoke-TaxEditorSmokeTest -Detailed
    .EXAMPLE
        Invoke-TaxEditorSmokeTest -BaseUrl 'https://staging.example.io' | ConvertTo-Json -Depth 5
    .LINK
        Show-AITriadHelp
    .LINK
        Test-TaxEditorHealth
    .LINK
        Test-TaxEditorEndpoints
    .LINK
        Test-AnonymousDebateFlow
    .LINK
        Test-PersonaEndpoints
    .LINK
        Test-ServiceWorkerHealth
    .LINK
        Get-FreeTierStatus
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$BaseUrl = (Get-TaxEditorBaseUrl),

        [Parameter()]
        [ValidateRange(1, 120)]
        [int]$TimeoutSec = 15,

        [Parameter()]
        [ValidateRange(1, 60)]
        [int]$HealthMaxAttempts = 5,

        [Parameter()]
        [ValidateRange(1, 300)]
        [int]$HealthRetryIntervalSec = 10,

        [Parameter()]
        [switch]$Detailed
    )

    Set-StrictMode -Version Latest

    $StartTime = Get-Date
    Write-Host "Smoke testing: $BaseUrl" -ForegroundColor Cyan
    Write-Host ''

    # ── Phase 1: Health checks ───────────────────────────────────────────
    # t/1696 — tolerate a scale-from-zero cold start: retry the health probe so a
    # healthy-but-cold container (whose first request exceeds TimeoutSec) is not
    # false-red'd. Mirrors the deploy workflow's cold-start-tolerant health gate
    # (deploy-azure.yml: Test-TaxEditorHealth -MaxAttempts 42 -RetryIntervalSec 10).
    Write-Host '=== Health Checks ===' -ForegroundColor Cyan
    $Health = Test-TaxEditorHealth -BaseUrl $BaseUrl -TimeoutSec $TimeoutSec `
        -MaxAttempts $HealthMaxAttempts -RetryIntervalSec $HealthRetryIntervalSec

    foreach ($Check in $Health.Checks) {
        $Icon = if ($Check.Healthy) { '[PASS]' } else { '[FAIL]' }
        $Color = if ($Check.Healthy) { 'Green' } else { 'Red' }
        Write-Host " $Icon $($Check.Endpoint) ($($Check.Purpose)) — $($Check.Ms)ms" -ForegroundColor $Color
        if (-not $Check.Healthy) {
            Write-Host " $($Check.Detail)" -ForegroundColor DarkRed
        }
    }
    Write-Host ''

    # ── Phase 2: Endpoint smoke tests ────────────────────────────────────
    Write-Host '=== Endpoint Tests ===' -ForegroundColor Cyan
    $Endpoints = @(Test-TaxEditorEndpoints -BaseUrl $BaseUrl -TimeoutSec $TimeoutSec)

    foreach ($Ep in $Endpoints) {
        $Icon = if ($Ep.Pass) { '[PASS]' } else { '[FAIL]' }
        $Color = if ($Ep.Pass) { 'Green' } else { 'Red' }
        $Extra = if ($Ep.NodeCount) { " ($($Ep.NodeCount) nodes)" } else { '' }
        Write-Host " $Icon $($Ep.Endpoint) — $($Ep.Status) $($Ep.Ms)ms$Extra" -ForegroundColor $Color
        if (-not $Ep.Pass -and $Ep.Error) {
            Write-Host " $($Ep.Error)" -ForegroundColor DarkRed
        }
    }
    Write-Host ''

    # ── Phase 3: Azure infrastructure ───────────────────────────────────
    Write-Host '=== Azure Infrastructure ===' -ForegroundColor Cyan
    $Azure = Test-AzureHealth -BaseUrl $BaseUrl -TimeoutSec $TimeoutSec

    foreach ($Check in $Azure.Checks) {
        $Icon = if ($Check.Pass) { '[PASS]' } else { '[FAIL]' }
        $Color = if ($Check.Pass) { 'Green' } else { 'Red' }
        Write-Host " $Icon $($Check.Check) — $($Check.Detail)" -ForegroundColor $Color
    }
    Write-Host ''

    # ── Phase 4: GitHub services ─────────────────────────────────────────
    Write-Host '=== GitHub Services ===' -ForegroundColor Cyan
    $GitHub = Test-GitHubHealth -TimeoutSec $TimeoutSec

    foreach ($Check in $GitHub.Checks) {
        $Icon = if ($Check.Pass) { '[PASS]' } else { '[FAIL]' }
        $Color = if ($Check.Pass) { 'Green' } else { 'Red' }
        Write-Host " $Icon $($Check.Check) — $($Check.Detail)" -ForegroundColor $Color
    }
    Write-Host ''

    # ── Summary ──────────────────────────────────────────────────────────
    $AllResults = $Endpoints
    $Passed = @($AllResults | Where-Object { $_.Pass }).Count
    $Failed = @($AllResults | Where-Object { -not $_.Pass }).Count
    $Total = $AllResults.Count

    $ResponseTimes = @($AllResults | Where-Object { $_.Ms -gt 0 } |
        Measure-Object -Property Ms -Average -Maximum -Minimum)

    $ByCategory = @{}
    foreach ($R in $AllResults) {
        if (-not $ByCategory.ContainsKey($R.Category)) {
            $ByCategory[$R.Category] = @{ Pass = 0; Fail = 0 }
        }
        if ($R.Pass) { $ByCategory[$R.Category].Pass++ }
        else { $ByCategory[$R.Category].Fail++ }
    }
    $CategorySummary = foreach ($Cat in ($ByCategory.Keys | Sort-Object)) {
        [PSCustomObject]@{
            Category = $Cat
            Pass     = $ByCategory[$Cat].Pass
            Fail     = $ByCategory[$Cat].Fail
        }
    }

    $Duration = (Get-Date) - $StartTime
    $OverallPass = $Health.Healthy -and $Failed -eq 0 -and $Azure.Healthy -and $GitHub.Healthy

    Write-Host '=== Summary ===' -ForegroundColor Cyan
    $SummaryColor = if ($OverallPass) { 'Green' } else { 'Red' }
    Write-Host " Overall: $(if ($OverallPass) { 'PASS' } else { 'FAIL' })" -ForegroundColor $SummaryColor
    Write-Host " Health: $(if ($Health.Healthy) { 'Healthy' } else { 'Unhealthy' })"
    Write-Host " Azure: $(if ($Azure.Healthy) { 'Healthy' } else { 'Unhealthy' })"
    Write-Host " GitHub: $(if ($GitHub.Healthy) { 'Healthy' } else { 'Unhealthy' })"
    Write-Host " Endpoints: $Passed/$Total passed"
    if ($ResponseTimes.Count -gt 0) {
        Write-Host " Response: avg=$([math]::Round($ResponseTimes.Average, 0))ms min=$($ResponseTimes.Minimum)ms max=$($ResponseTimes.Maximum)ms"
    }
    Write-Host " Duration: $([math]::Round($Duration.TotalSeconds, 1))s"
    Write-Host ''

    if ($Detailed) {
        Write-Host '=== Per-Category Breakdown ===' -ForegroundColor Cyan
        $CategorySummary | Format-Table -AutoSize | Out-String | Write-Host
    }

    $FailedEndpoints = @($AllResults | Where-Object { -not $_.Pass })

    [PSCustomObject]@{
        BaseUrl         = $BaseUrl
        OverallPass     = $OverallPass
        HealthOk        = $Health.Healthy
        AzureOk         = $Azure.Healthy
        GitHubOk        = $GitHub.Healthy
        EndpointsPassed = $Passed
        EndpointsFailed = $Failed
        EndpointsTotal  = $Total
        AvgResponseMs   = if ($ResponseTimes.Count -gt 0) { [math]::Round($ResponseTimes.Average, 0) } else { 0 }
        MaxResponseMs   = if ($ResponseTimes.Count -gt 0) { $ResponseTimes.Maximum } else { 0 }
        MinResponseMs   = if ($ResponseTimes.Count -gt 0) { $ResponseTimes.Minimum } else { 0 }
        DurationSec     = [math]::Round($Duration.TotalSeconds, 1)
        Categories      = @($CategorySummary)
        FailedEndpoints = $FailedEndpoints
        Timestamp       = (Get-Date).ToString('o')
    }
}