modules/shared/ReportVerification.ps1

#Requires -Version 7.4
<#
.SYNOPSIS
    Per-tier post-render verification stubs for the report architecture.
.DESCRIPTION
    Phase 0 (#435) lands the verification entry-point contract. Each cmdlet
    accepts a -ReportRoot path plus an optional -Manifest object and returns
    a uniform PSCustomObject (Tier, Success, Status, Errors, Warnings,
    Checks, DurationMs, Timestamp). Real verification bodies (HtmlAgilityPack
    DOM checks, sqlite-wasm decode, Pode /api/health probe) ship with the
    matching tier PRs. The Phase 0 stubs return Status='ready' for PureJson
    when canonical artefacts exist and Status='placeholder' for richer tiers
    so the orchestrator can detect a missing renderer and fall back rather
    than silently shipping a blank report.
.NOTES
    All disk-bound output (warnings, error details) is routed through
    Remove-Credentials when that helper is available.
#>

[CmdletBinding()]
param ()

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$script:ReportVerificationTiers = @('PureJson', 'EmbeddedSqlite', 'SidecarSqlite', 'PodeViewer')

function New-ReportVerificationResult {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Tier,
        [Parameter(Mandatory)][bool] $Success,
        [Parameter(Mandatory)][string] $Status,
        [string[]] $Errors = @(),
        [string[]] $Warnings = @(),
        [object[]] $Checks = @(),
        [double] $DurationMs = 0.0
    )

    if ($Tier -notin $script:ReportVerificationTiers) {
        throw "Tier '$Tier' is not a recognised report architecture tier. Expected one of: $($script:ReportVerificationTiers -join ', ')."
    }

    $sanitize = {
        param([string] $s)
        if (Get-Command Remove-Credentials -ErrorAction SilentlyContinue) {
            return (Remove-Credentials $s)
        }
        return $s
    }

    return [pscustomobject]@{
        Tier       = $Tier
        Success    = [bool]$Success
        Status     = $Status
        Errors     = @($Errors | ForEach-Object { & $sanitize ([string]$_) })
        Warnings   = @($Warnings | ForEach-Object { & $sanitize ([string]$_) })
        Checks     = @($Checks)
        DurationMs = [double]$DurationMs
        Timestamp  = (Get-Date).ToUniversalTime().ToString('o')
    }
}

function Test-ReportRootArgument {
    [CmdletBinding()]
    param([string] $ReportRoot)

    $errors = [System.Collections.Generic.List[string]]::new()
    if ([string]::IsNullOrWhiteSpace($ReportRoot)) {
        $errors.Add('ReportRoot is missing or empty.')
    } elseif (-not (Test-Path -LiteralPath $ReportRoot)) {
        $errors.Add("ReportRoot '$ReportRoot' does not exist on disk.")
    }
    return ,$errors.ToArray()
}

function Test-PureJsonOutput {
    <#
    .SYNOPSIS
        Verify the PureJson tier emitted a parseable HTML shell + sidecar JSON.
    .DESCRIPTION
        Phase 0 stub: confirms the report root exists and contains at least one
        of the canonical PureJson outputs (results.json, entities.json,
        report-manifest.json, or report.html). Real DOM validation via
        HtmlAgilityPack lands with Track F.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ReportRoot,
        [object] $Manifest
    )

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $argErrors = Test-ReportRootArgument -ReportRoot $ReportRoot
    if ($argErrors.Count -gt 0) {
        $sw.Stop()
        return New-ReportVerificationResult -Tier 'PureJson' -Success $false -Status 'invalid-arguments' -Errors $argErrors -DurationMs $sw.Elapsed.TotalMilliseconds
    }

    $expected = @('results.json','entities.json','report-manifest.json','report.html')
    $present = @()
    foreach ($name in $expected) {
        if (Test-Path -LiteralPath (Join-Path $ReportRoot $name)) {
            $present += $name
        }
    }
    $sw.Stop()

    if ($present.Count -eq 0) {
        return New-ReportVerificationResult -Tier 'PureJson' -Success $false -Status 'missing-outputs' -Errors @("None of the canonical PureJson outputs were found under '$ReportRoot'.") -Checks @([pscustomobject]@{ Name='outputs'; Expected=$expected; Found=$present }) -DurationMs $sw.Elapsed.TotalMilliseconds
    }

    return New-ReportVerificationResult -Tier 'PureJson' -Success $true -Status 'ready' -Checks @([pscustomobject]@{ Name='outputs'; Expected=$expected; Found=$present }) -DurationMs $sw.Elapsed.TotalMilliseconds
}

function Test-EmbeddedSqliteOutput {
    <#
    .SYNOPSIS
        Verify the EmbeddedSqlite tier (base64-inline sqlite-wasm).
    .DESCRIPTION
        Phase 0 stub. Real implementation (Track viewer): decode the inline
        WASM blob, open the embedded DB, assert FTS5 index + entity tables.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ReportRoot,
        [object] $Manifest
    )

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $argErrors = Test-ReportRootArgument -ReportRoot $ReportRoot
    $sw.Stop()
    if ($argErrors.Count -gt 0) {
        return New-ReportVerificationResult -Tier 'EmbeddedSqlite' -Success $false -Status 'invalid-arguments' -Errors $argErrors -DurationMs $sw.Elapsed.TotalMilliseconds
    }
    return New-ReportVerificationResult -Tier 'EmbeddedSqlite' -Success $false -Status 'placeholder' -Warnings @('EmbeddedSqlite verification stub: real WASM-decode body lands with the viewer PR.') -DurationMs $sw.Elapsed.TotalMilliseconds
}

function Test-SidecarSqliteOutput {
    <#
    .SYNOPSIS
        Verify the SidecarSqlite tier (separate .sqlite file beside HTML).
    .DESCRIPTION
        Phase 0 stub. Real implementation: open the sidecar DB, assert schema
        version, indexes, FTS5 virtual tables, foreign-key constraints.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ReportRoot,
        [object] $Manifest
    )

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $argErrors = Test-ReportRootArgument -ReportRoot $ReportRoot
    $sw.Stop()
    if ($argErrors.Count -gt 0) {
        return New-ReportVerificationResult -Tier 'SidecarSqlite' -Success $false -Status 'invalid-arguments' -Errors $argErrors -DurationMs $sw.Elapsed.TotalMilliseconds
    }
    return New-ReportVerificationResult -Tier 'SidecarSqlite' -Success $false -Status 'placeholder' -Warnings @('SidecarSqlite verification stub: schema + index assertions land with the sidecar PR.') -DurationMs $sw.Elapsed.TotalMilliseconds
}

function Test-PodeViewerOutput {
    <#
    .SYNOPSIS
        Verify the PodeViewer tier (local Pode server boots and serves /api/health).
    .DESCRIPTION
        Phase 0 stub. Real implementation: launch viewer in dry-start mode,
        poll http://127.0.0.1:<port>/api/health for HTTP 200 within 30s,
        then shut it down.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $ReportRoot,
        [object] $Manifest
    )

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $argErrors = Test-ReportRootArgument -ReportRoot $ReportRoot
    $sw.Stop()
    if ($argErrors.Count -gt 0) {
        return New-ReportVerificationResult -Tier 'PodeViewer' -Success $false -Status 'invalid-arguments' -Errors $argErrors -DurationMs $sw.Elapsed.TotalMilliseconds
    }
    return New-ReportVerificationResult -Tier 'PodeViewer' -Success $false -Status 'placeholder' -Warnings @('PodeViewer verification stub: /api/health probe lands with the viewer PR.') -DurationMs $sw.Elapsed.TotalMilliseconds
}

function Invoke-ReportVerification {
    <#
    .SYNOPSIS
        Dispatch verification to the per-tier stub for the selected architecture.
    .DESCRIPTION
        Convenience wrapper used by the orchestrator after a report is rendered.
        Looks up the matching Test-<Tier>Output cmdlet and forwards the call.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Tier,
        [Parameter(Mandatory)][string] $ReportRoot,
        [object] $Manifest
    )

    if ($Tier -notin $script:ReportVerificationTiers) {
        throw "Tier '$Tier' is not a recognised report architecture tier. Expected one of: $($script:ReportVerificationTiers -join ', ')."
    }

    $cmdName = "Test-${Tier}Output"
    $cmd = Get-Command -Name $cmdName -ErrorAction SilentlyContinue
    if (-not $cmd) {
        return New-ReportVerificationResult -Tier $Tier -Success $false -Status 'missing-verifier' -Errors @("Verifier '$cmdName' is not available in the current session.")
    }
    return & $cmd -ReportRoot $ReportRoot -Manifest $Manifest
}

function Get-ReportVerificationTiers {
    [CmdletBinding()]
    param ()
    return ,$script:ReportVerificationTiers
}