Tests/Unit/ErrorActionPreference.Tests.ps1

BeforeDiscovery {
    $psuAppRoot = Join-Path $PSScriptRoot '..' '..'
    $repoRoot = Resolve-Path (Join-Path $psuAppRoot '..')

    # Directories to scan for function files
    $searchPaths = @(
        (Join-Path $psuAppRoot 'Public')
        (Join-Path $psuAppRoot 'Private')
        (Join-Path $repoRoot 'Devolutions.CIEM.Admin' 'Public')
        (Join-Path $repoRoot 'Devolutions.CIEM.Admin' 'Private')
    )

    # Add all modules/**/Public and modules/**/Private directories.
    $modulesRoot = Join-Path $psuAppRoot 'modules'
    if (Test-Path $modulesRoot) {
        Get-ChildItem -Path $modulesRoot -Directory -Recurse |
            Where-Object {
                $_.Name -eq 'Public' -or $_.Name -eq 'Private'
            } |
            ForEach-Object { $searchPaths += $_.FullName }
    }

    # Exempt files
    $exemptFileNames = @(
        'RegisterCIEMArgumentCompleters.ps1'
    )

    # Collect all .ps1 files from the search paths
    $ps1Files = foreach ($dir in $searchPaths) {
        if (Test-Path $dir) {
            Get-ChildItem -Path $dir -Filter '*.ps1' -File
        }
    }

    $resolvedRoot = $repoRoot.Path

    function Get-FirstFunctionStatement {
        param(
            [Parameter(Mandatory)]
            [System.Management.Automation.Language.FunctionDefinitionAst]$Function
        )

        foreach ($blockName in 'BeginBlock', 'ProcessBlock', 'EndBlock') {
            $block = $Function.Body.$blockName
            if ($block -and $block.Statements.Count -gt 0) {
                return @($block.Statements)[0]
            }
        }

        $null
    }

    # Parse each file with the AST and find functions where the first executable
    # statement is not $ErrorActionPreference = 'Stop'.
    $violations = @(foreach ($file in $ps1Files) {
        # Skip exempt files
        if ($file.Name -in $exemptFileNames) { continue }

        $tokens = $null
        $errors = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseFile(
            $file.FullName, [ref]$tokens, [ref]$errors
        )

        # Find all function definitions in the file
        $functions = $ast.FindAll(
            { param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] },
            $true
        )

        foreach ($func in $functions) {
            $firstStatement = Get-FirstFunctionStatement -Function $func

            $hasEapFirst = $firstStatement -is [System.Management.Automation.Language.AssignmentStatementAst] -and
                $firstStatement.Left.ToString() -eq '$ErrorActionPreference' -and
                $firstStatement.Right.ToString() -eq "'Stop'"

            if (-not $hasEapFirst) {
                @{
                    FilePath     = $file.FullName -replace [regex]::Escape($resolvedRoot + [IO.Path]::DirectorySeparatorChar), ''
                    FunctionName = $func.Name
                    FirstStatement = if ($firstStatement) { $firstStatement.Extent.Text } else { '<none>' }
                }
            }
        }
    })

    # Store counts as discovery-time data for -ForEach on the Describe block
    $fileCount = ($ps1Files | Measure-Object).Count
    $violationCount = $violations.Count
}

Describe 'ErrorActionPreference Enforcement' {

    It "Scanned function files from Public/ and Private/ directories" {
        # Re-derive file count at runtime to validate discovery worked
        $psuAppRoot = Join-Path $PSScriptRoot '..' '..'
        $repoRoot = Resolve-Path (Join-Path $psuAppRoot '..')
        $searchPaths = @(
            (Join-Path $psuAppRoot 'Public')
            (Join-Path $psuAppRoot 'Private')
            (Join-Path $repoRoot 'Devolutions.CIEM.Admin' 'Public')
            (Join-Path $repoRoot 'Devolutions.CIEM.Admin' 'Private')
        )
        $modulesRoot = Join-Path $psuAppRoot 'modules'
        if (Test-Path $modulesRoot) {
            Get-ChildItem -Path $modulesRoot -Directory -Recurse |
                Where-Object {
                    $_.Name -eq 'Public' -or $_.Name -eq 'Private'
                } |
                ForEach-Object { $searchPaths += $_.FullName }
        }
        $count = 0
        foreach ($dir in $searchPaths) {
            if (Test-Path $dir) {
                $count += (Get-ChildItem -Path $dir -Filter '*.ps1' -File | Measure-Object).Count
            }
        }
        $count | Should -BeGreaterThan 0
    }

    Context 'Every function must set $ErrorActionPreference = ''Stop'' first' {

        It '<FunctionName> in <FilePath> should set $ErrorActionPreference first' -ForEach $violations {
            # Each violation generates a dedicated failing test with function name and file path.
            $false | Should -BeTrue -Because "function '$($_.FunctionName)' in '$($_.FilePath)' does not start with `$ErrorActionPreference = 'Stop'; first statement is '$($_.FirstStatement)'"
        }
    }
}