tests/DeclarativeCollectorGolden.Tests.ps1

#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }
#Requires -Modules ImportExcel

<#
    Proof-preservation prerequisite for Epic AB#5638.

    The retired source-script equivalence suite was replaced by committed canonical output. This
    suite verifies the same two contracts against that output:

      * every processed row, including order, keys, values and null/array shape;
      * every worksheet column and cell under both tag states.

    This file does not dot-source Invoke-ScoutCollector, does not read a collector .ps1, and does
    not invoke a source-script mode. It remains executable after the collector tree is gone.
    Records are updated only through a reviewed, documented behavior change.
#>


$script:GoldenDefinitionRoot = Join-Path (Split-Path -Parent $PSScriptRoot) 'manifests/collectors'
$GoldenCollectors = @(
    foreach ($Folder in (Get-ChildItem -LiteralPath $script:GoldenDefinitionRoot -Directory | Sort-Object Name)) {
        foreach ($File in (Get-ChildItem -LiteralPath $Folder.FullName -Filter '*.psd1' -File | Sort-Object Name)) {
            @{ Category = $Folder.Name; Name = $File.BaseName }
        }
    }
)

BeforeAll {
    $script:RepoRoot = Split-Path -Parent $PSScriptRoot
    $script:GoldenDefinitionRoot = Join-Path $script:RepoRoot 'manifests/collectors'
    $script:GoldenRoot = Join-Path $script:RepoRoot 'tests/fixtures/collector-golden'

    . (Join-Path $script:RepoRoot 'scripts/CollectorGolden.Common.ps1')
    . (Join-Path $script:RepoRoot 'src/pipeline/Get-ScoutCollectorDefinition.ps1')
    . (Join-Path $script:RepoRoot 'src/pipeline/Invoke-ScoutDeclarativeCollector.ps1')

    # Definitions call these pure accessors from lifted expressions. They are interpreter
    # dependencies, not references to the imperative collector implementation.
    . (Join-Path $script:RepoRoot 'src/Get-AZSCSafeProperty.ps1')
    . (Join-Path $script:RepoRoot 'src/Get-AZTICollectedValue.ps1')
    . (Join-Path $script:RepoRoot 'src/Get-AZSCIdSegment.ps1')

    $script:FixtureCache = @{}
    $script:GoldenCache = @{}
    $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("scout-golden-proof-" + [guid]::NewGuid().ToString('N'))
    $null = New-Item -Path $script:TempDir -ItemType Directory -Force

    function Get-GoldenFixturePath {
        param([string]$Category)

        if ($Category -eq 'Databases') {
            return Join-Path $script:RepoRoot 'tests/fixtures/databases-collector-input.json'
        }
        return Join-Path $script:RepoRoot "tests/fixtures/collector-equivalence/$Category.json"
    }

    function Get-GoldenFixture {
        param([string]$Category)

        $Path = Get-GoldenFixturePath -Category $Category
        if (-not $script:FixtureCache.ContainsKey($Path)) {
            $script:FixtureCache[$Path] = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
        }
        return $script:FixtureCache[$Path]
    }

    function Get-CommittedGolden {
        param([string]$Category, [string]$Name)

        $Key = "$Category/$Name"
        if (-not $script:GoldenCache.ContainsKey($Key)) {
            $Path = Join-Path $script:GoldenRoot "$Category/$Name.json"
            $script:GoldenCache[$Key] = Import-ScoutGoldenOutput -Path $Path -FixturePath (
                Get-GoldenFixturePath -Category $Category
            )
        }
        return $script:GoldenCache[$Key]
    }

    function Get-GoldenVerificationContext {
        param(
            [string]$Category,
            [string]$Name,
            [string]$Task = 'Processing',
            $File,
            $SmaResources,
            [bool]$InTag = $false
        )

        $Fixture = Get-GoldenFixture -Category $Category
        $Resources = if ($Category -eq 'Databases') {
            $Fixture.resources
        }
        else {
            $CollectorFixture = $Fixture.collectors.PSObject.Properties[$Name]
            if ($null -eq $CollectorFixture) {
                throw "Fixture for category '$Category' has no collector entry named '$Name'."
            }
            $CollectorFixture.Value.resources
        }

        @{
            ScriptRoot    = $script:RepoRoot
            Subscriptions = $Fixture.subscriptions
            InTag         = $InTag
            Resources     = $Resources
            Retirements   = $Fixture.retirements
            Task          = $Task
            File          = $File
            SmaResources  = $SmaResources
            TableStyle    = 'Light20'
            Unsupported   = $Fixture.unsupported
        }
    }

    function Get-LiveDeclarativeRowSet {
        param([string]$Category, [string]$Name)

        $Definition = Get-ScoutCollectorDefinition -Path (
            Join-Path $script:RepoRoot "manifests/collectors/$Category/$Name.psd1"
        )
        return @(Invoke-ScoutDeclarativeCollector -Definition $Definition -Context (
            Get-GoldenVerificationContext -Category $Category -Name $Name
        ))
    }

    function Get-LiveDeclarativeWorksheet {
        param([string]$Category, [string]$Name, [bool]$InTag, $Rows)

        $Definition = Get-ScoutCollectorDefinition -Path (
            Join-Path $script:RepoRoot "manifests/collectors/$Category/$Name.psd1"
        )
        $Suffix = if ($InTag) { 'tag' } else { 'notag' }
        $Path = Join-Path $script:TempDir "$Category-$Name-$Suffix.xlsx"

        Invoke-ScoutDeclarativeCollector -Definition $Definition -Context (
            Get-GoldenVerificationContext -Category $Category -Name $Name -Task 'Reporting' `
                -File $Path -SmaResources $Rows -InTag $InTag
        )

        return ConvertTo-ScoutWorksheetText -Path $Path -Worksheet $Definition.Export.WorksheetName
    }
}

AfterAll {
    if ($script:TempDir -and (Test-Path -LiteralPath $script:TempDir)) {
        Remove-Item -LiteralPath $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue
    }
}

Describe 'Golden collector proof - coverage and independence' {
    It 'has exactly one golden record for every definition' {
        $Definitions = @(
            Get-ChildItem -LiteralPath $script:GoldenDefinitionRoot -Filter '*.psd1' -File -Recurse |
                ForEach-Object { "$($_.Directory.Name)/$($_.BaseName)" } |
                Sort-Object
        )
        $Golden = @(
            Get-ChildItem -LiteralPath $script:GoldenRoot -Filter '*.json' -File -Recurse |
                ForEach-Object { "$($_.Directory.Name)/$($_.BaseName)" } |
                Sort-Object
        )

        $Golden -join "`n" | Should -Be ($Definitions -join "`n")
    }

    It 'has no executable dependency on the imperative collector path' {
        $Files = @(
            Join-Path $script:RepoRoot 'tests/DeclarativeCollectorGolden.Tests.ps1'
            Join-Path $script:RepoRoot 'scripts/CollectorGolden.Common.ps1'
        )

        foreach ($File in $Files) {
            $Tokens = $null
            $Errors = $null
            $Ast = [System.Management.Automation.Language.Parser]::ParseFile($File, [ref]$Tokens, [ref]$Errors)
            @($Errors).Count | Should -Be 0

            $ImperativeCalls = @($Ast.FindAll({
                param($Node)
                $Node -is [System.Management.Automation.Language.CommandAst] -and
                $Node.GetCommandName() -eq 'Invoke-ScoutCollector'
            }, $true))
            $ImperativeCalls | Should -BeNullOrEmpty -Because "$File must verify from data, not execute the reference collector"
        }
    }

    It 'fails the comparison when a definition field is mutated in memory' {
        $Category = 'Databases'
        $Name = 'SQLSERVER'
        $Golden = Get-CommittedGolden -Category $Category -Name $Name
        $Definition = Get-ScoutCollectorDefinition -Path (
            Join-Path $script:RepoRoot "manifests/collectors/$Category/$Name.psd1"
        )
        $Definition.Fields[0].Expression = "'golden-proof-mutation'"

        $Rows = @(Invoke-ScoutDeclarativeCollector -Definition $Definition -Context (
            Get-GoldenVerificationContext -Category $Category -Name $Name
        ))
        $Comparable = @($Rows | ForEach-Object { ConvertTo-ScoutComparableRow -Row $_ })

        ($Comparable -join "`n") | Should -Not -Be (@($Golden.Rows) -join "`n")
    }

    It 'preserves legacy empty subscription and tag cells without weakening StrictMode' {
        # The AIFoundry fixture deliberately includes a resource whose subscription is absent
        # from $SUB and resources with no tags. Legacy collectors yielded blank cells in both
        # cases because missing member reads returned $null. The declarative path must express
        # that data contract explicitly, not disable StrictMode around lifted source.
        $Rows = @(Get-LiveDeclarativeRowSet -Category 'AI' -Name 'AIFoundryHubs')

        $Rows.Count | Should -BeGreaterThan 0
        @($Rows | Where-Object { $_['Subscription'] -eq $null }).Count | Should -BeGreaterThan 0
        @($Rows | Where-Object { $_['Tag Name'] -eq '' -and $_['Tag Value'] -eq '' }).Count | Should -BeGreaterThan 0
    }
}

Describe 'Golden processing proof - <Category>/<Name>' -ForEach $GoldenCollectors {
    BeforeAll {
        $script:CollectorGolden = Get-CommittedGolden -Category $Category -Name $Name
        $script:CollectorRows = @(Get-LiveDeclarativeRowSet -Category $Category -Name $Name)
        $script:ComparableRows = @(
            $script:CollectorRows | ForEach-Object { ConvertTo-ScoutComparableRow -Row $_ }
        )
    }

    It 'is exercised by the fixture' {
        $script:ComparableRows.Count | Should -BeGreaterThan 0
    }

    It 'matches every recorded row in order' {
        $script:ComparableRows.Count | Should -Be @($script:CollectorGolden.Rows).Count
        ($script:ComparableRows -join "`n") | Should -Be (@($script:CollectorGolden.Rows) -join "`n")
    }
}

Describe 'Golden workbook proof - <Category>/<Name>' -ForEach $GoldenCollectors {
    BeforeAll {
        $script:WorkbookGolden = Get-CommittedGolden -Category $Category -Name $Name
        $script:WorkbookRows = @(Get-LiveDeclarativeRowSet -Category $Category -Name $Name)
    }

    It 'matches every recorded cell with IncludeTags <InTag>' -Tag @('GoldenWorkbook', "GoldenWorkbook_$Category") -ForEach @(
        @{ InTag = $false; GoldenProperty = 'WithoutTags' }
        @{ InTag = $true; GoldenProperty = 'WithTags' }
    ) {
        $Actual = Get-LiveDeclarativeWorksheet -Category $Category -Name $Name -InTag $InTag -Rows $script:WorkbookRows
        $Expected = $script:WorkbookGolden.Workbook.$GoldenProperty

        ($Actual -split "`n")[0] | Should -Be ($Expected -split "`n")[0] -Because 'column order is part of the report contract'
        $Actual | Should -Be $Expected
    }
}