tests/Report.SectionIndex.Tests.ps1

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

BeforeAll {
    $script:RepoRoot = Split-Path $PSScriptRoot -Parent
    . (Join-Path $script:RepoRoot 'src/pipeline/Get-ScoutCollectorDefinition.ps1')
    . (Join-Path $script:RepoRoot 'src/report/Get-ScoutReportSectionIndex.ps1')

    $script:FixtureRoot = Join-Path ([System.IO.Path]::GetTempPath()) (
        'AZSC_ReportSectionIndex_' + [guid]::NewGuid().ToString('N')
    )
    New-Item -ItemType Directory -Path $script:FixtureRoot -Force | Out-Null

    function Write-SectionIndexDefinition {
        param(
            [Parameter(Mandatory)] [string]$Category,
            [Parameter(Mandatory)] [string]$Name,
            [Parameter(Mandatory)] [string]$WorksheetName,
            [string[]]$ResourceTypes = @('microsoft.example/widgets')
        )

        $Directory = Join-Path $script:FixtureRoot $Category
        New-Item -ItemType Directory -Path $Directory -Force | Out-Null
        $QuotedTypes = @($ResourceTypes | ForEach-Object { " '$($_ -replace "'", "''")'" }) -join "`n"
        $DefinitionText = @"
@{
    ResourceTypes = @(
$QuotedTypes
    )
    RowLoopVariable = '1'
    Fields = @(
        @{ Name = 'Name'; Expression = '`$1.name' }
    )
    Export = @{
        WorksheetName = '$($WorksheetName -replace "'", "''")'
        Columns = @('Name')
    }
}
"@

        Set-Content -LiteralPath (Join-Path $Directory "$Name.psd1") -Value $DefinitionText -Encoding utf8
    }

    # Deliberately create these out of lexical order. Filesystem creation order must not affect
    # the index returned to report renderers.
    Write-SectionIndexDefinition -Category 'Zulu' -Name 'Second' -WorksheetName 'Zulu Second'
    Write-SectionIndexDefinition -Category 'Alpha' -Name 'ZuluName' -WorksheetName 'Alpha Zulu'
    Write-SectionIndexDefinition -Category 'Alpha' -Name 'AlphaName' -WorksheetName 'Alpha First' `
        -ResourceTypes @('AZSC/Example/Synthetic', 'microsoft.example/widgets')
    Set-Content -LiteralPath (Join-Path $script:FixtureRoot 'Alpha/README.txt') -Value 'not a definition'
}

AfterAll {
    if (Test-Path -LiteralPath $script:FixtureRoot) {
        Remove-Item -LiteralPath $script:FixtureRoot -Recurse -Force
    }
}

Describe 'Get-ScoutReportSectionIndex' {
    BeforeAll {
        $script:Index = @(Get-ScoutReportSectionIndex -DefinitionRoot $script:FixtureRoot)
    }

    It 'fails loudly when the definition root is missing' {
        $Missing = Join-Path $script:FixtureRoot 'missing'
        { Get-ScoutReportSectionIndex -DefinitionRoot $Missing } |
            Should -Throw '*Collector definition root not found*'
    }

    It 'returns only definition files in deterministic category/name order' {
        @($script:Index).Count | Should -Be 3
        @($script:Index | ForEach-Object { "$($_.Category)/$($_.Name)" }) |
            Should -Be @('Alpha/AlphaName', 'Alpha/ZuluName', 'Zulu/Second')
        @($script:Index.Order) | Should -Be @(0, 1, 2)
    }

    It 'provides the cache and worksheet fields report renderers need' {
        $Entry = $script:Index[0]
        $Entry.SectionName | Should -Be 'Alpha'
        $Entry.CacheFileName | Should -Be 'Alpha.json'
        $Entry.CacheKey | Should -Be 'AlphaName'
        $Entry.WorksheetName | Should -Be 'Alpha First'
        $Entry.DefinitionPath | Should -BeLike '*AlphaName.psd1'
    }

    It 'keeps all declared types but excludes synthetic types from the Azure type view' {
        @($script:Index[0].ResourceTypes) |
            Should -Be @('AZSC/Example/Synthetic', 'microsoft.example/widgets')
        @($script:Index[0].AzureResourceTypes) |
            Should -Be @('microsoft.example/widgets')
    }

    It 'filters categories without changing deterministic order' {
        $Filtered = @(
            Get-ScoutReportSectionIndex -DefinitionRoot $script:FixtureRoot -Category 'Zulu'
        )
        @($Filtered).Count | Should -Be 1
        $Filtered[0].Category | Should -Be 'Zulu'
        $Filtered[0].Order | Should -Be 0
    }

    It 'treats null, an empty category list, and All as no filter' {
        @(Get-ScoutReportSectionIndex -DefinitionRoot $script:FixtureRoot -Category $null).Count |
            Should -Be 3
        @(Get-ScoutReportSectionIndex -DefinitionRoot $script:FixtureRoot -Category @()).Count |
            Should -Be 3
        @(Get-ScoutReportSectionIndex -DefinitionRoot $script:FixtureRoot -Category 'All').Count |
            Should -Be 3
    }

    It 'indexes the shipped manifests without consulting the legacy collector tree' {
        $ShippedRoot = Join-Path $script:RepoRoot 'manifests/collectors'
        $Shipped = @(Get-ScoutReportSectionIndex -DefinitionRoot $ShippedRoot)
        $DefinitionCount = @(
            Get-ChildItem -LiteralPath $ShippedRoot -Recurse -Filter '*.psd1' -File
        ).Count

        @($Shipped).Count | Should -Be $DefinitionCount
        @($Shipped | Select-Object -ExpandProperty CacheKey -Unique).Count |
            Should -Be $DefinitionCount

        $IndexPath = Join-Path $script:RepoRoot 'src/report/Get-ScoutReportSectionIndex.ps1'
        $Tokens = $null
        $ParseErrors = $null
        $Ast = [System.Management.Automation.Language.Parser]::ParseFile(
            $IndexPath,
            [ref]$Tokens,
            [ref]$ParseErrors
        )
        @($ParseErrors).Count | Should -Be 0
        $LegacyPathLiterals = @(
            $Ast.FindAll(
                {
                    param($Node)
                    $Node -is [System.Management.Automation.Language.StringConstantExpressionAst] -and
                    $Node.Value -match 'InventoryModules|Modules[/\\]Public'
                },
                $true
            )
        )
        @($LegacyPathLiterals).Count | Should -Be 0
    }
}