Private/Model/Import-PpaSnapshot.ps1

# Import-PpaSnapshot.ps1 - the snapshot loader (Wave 4 spec 3.4/3.5).
# Engine-agnostic code: the PS 7 gate lives at the DELTA ENTRY POINT (spec section 1),
# not here, so the round-trip pinned test can exercise the loader under 5.1 too.
# Validates: schemaVersion present, major supported, required top-level fields; then
# coerces every declared array field (schema manifest) so single-element and empty
# arrays behave identically after ConvertFrom-Json. Fails with actionable messages.
# ASCII-only source (Windows PowerShell 5.1).

Set-StrictMode -Off

function ConvertTo-PpaArray {
    # Declared-array coercion: null -> @(); scalar -> one-element array; array as-is.
    param($Value)
    if ($null -eq $Value) { return , @() }
    return , @($Value)
}

function Import-PpaSnapshot {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$Path
    )

    if (-not (Test-Path -LiteralPath $Path)) {
        throw "Snapshot file not found: '$Path'."
    }
    $text = [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
    # PS 7 ConvertFrom-Json auto-converts full ISO-8601 strings (capturedAt,
    # denylisted When* artifacts) to DateTime objects, silently breaking the
    # leaves-are-strings contract on load. -DateKind String (7.5+) keeps them
    # verbatim; Windows PowerShell 5.1 never converts, so no parameter needed.
    $parseArgs = @{}
    if ((Get-Command ConvertFrom-Json).Parameters.ContainsKey('DateKind')) { $parseArgs['DateKind'] = 'String' }
    try { $snap = ConvertFrom-Json -InputObject $text @parseArgs }
    catch { throw "Snapshot file is not valid JSON: '$Path'. $($_.Exception.Message)" }

    if ($null -eq $snap -or $snap.PSObject.Properties.Name -notcontains 'schemaVersion' -or $null -eq $snap.schemaVersion) {
        throw "File '$Path' has no schemaVersion - is this a PurviewPostureAnalyzer snapshot? Snapshots are written as PPA-Snapshot_*.json alongside the HTML report."
    }

    $schema   = Get-PpaSnapshotSchema
    $ourMajor = [int]$schema.schemaVersion.major
    $ourMinor = [int]$schema.schemaVersion.minor
    $major    = [int]$snap.schemaVersion.major
    $minor    = [int]$snap.schemaVersion.minor
    if ($major -ne $ourMajor) {
        throw ("Snapshot '{0}' is schema v{1}, this tool compares schema v{2}. Re-run the newer tool against the tenant to produce a comparable snapshot." -f (Split-Path -Leaf $Path), $major, $ourMajor)
    }
    if ($minor -gt $ourMinor) {
        # Tolerant read (ruled 3.5): unknown fields ignored, one summary warning.
        Write-Warning ("Snapshot '{0}' is schema v{1}.{2} - a newer minor than this tool (v{3}.{4}); unknown fields are ignored." -f (Split-Path -Leaf $Path), $major, $minor, $ourMajor, $ourMinor)
    }

    $required = @('snapshotId', 'capturedAt', 'sectionsRun', 'collectorOutcomes', 'objects', 'findings')
    $missing  = @($required | Where-Object { $snap.PSObject.Properties.Name -notcontains $_ })
    if ($missing.Count -gt 0) {
        throw ("Snapshot '{0}' is missing required field(s): {1}. It may be truncated or not a PPA snapshot." -f (Split-Path -Leaf $Path), ($missing -join ', '))
    }

    # ---- declared-array coercion ----
    $snap.sectionsRun = ConvertTo-PpaArray $snap.sectionsRun
    $snap.findings    = ConvertTo-PpaArray $snap.findings
    foreach ($typeProp in @($snap.objects.PSObject.Properties)) {
        $typeName = $typeProp.Name
        $coerced  = ConvertTo-PpaArray $typeProp.Value
        $snap.objects.$typeName = $coerced
        $def = $null
        if ($schema.types.PSObject.Properties.Name -contains $typeName) { $def = $schema.types.$typeName }
        if ($null -eq $def) { continue }   # unknown type from a newer minor: tolerated
        $arrays = @($def.arrays | ForEach-Object { [string]$_ })
        if ($arrays.Count -eq 0) { continue }
        foreach ($item in @($snap.objects.$typeName)) {
            foreach ($arrName in $arrays) {
                if ($item.PSObject.Properties.Name -contains $arrName) {
                    $item.$arrName = ConvertTo-PpaArray $item.$arrName
                }
            }
        }
    }

    return $snap
}