Tests/Unit/Invoke-SPCDashboardReport.Tests.ps1

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

BeforeAll {
    $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
    $publicDir  = Join-Path $moduleRoot 'Public'
    $privateDir = Join-Path $moduleRoot 'Private'

    . (Join-Path $privateDir 'Test-SPCConnection.ps1')
    . (Join-Path $privateDir 'LicenseManager.ps1')
    . (Join-Path $privateDir 'New-SPCDashboardHtmlInternal.ps1')
    . (Join-Path $publicDir 'Scan\Get-SPCOrphanedUser.ps1')
    . (Join-Path $publicDir 'Scan\Get-SPCGuestAccess.ps1')
    . (Join-Path $publicDir 'Scan\Get-SPCPrivilegedUser.ps1')
    . (Join-Path $publicDir 'Scan\Get-SPCOverPermissionedUser.ps1')

    if (Test-Path (Join-Path $publicDir 'Report\Invoke-SPCDashboardReport.ps1')) {
        . (Join-Path $publicDir 'Report\Invoke-SPCDashboardReport.ps1')
    } elseif (Test-Path (Join-Path $publicDir 'Report\Invoke-SPCDashboardReportV1.ps1')) {
        . (Join-Path $publicDir 'Report\Invoke-SPCDashboardReportV1.ps1')
        # Alias shim for test execution if file is still named V1
        if (-not (Get-Command 'Invoke-SPCDashboardReport' -ErrorAction SilentlyContinue)) {
            Set-Alias -Name 'Invoke-SPCDashboardReport' -Value 'Invoke-SPCDashboardReportV1'
        }
    }

    # Standardized mock module context schema
    $script:FakeContext = [PSCustomObject]@{
        TenantName             = 'contoso'
        TenantId               = 'tid-12345'
        IsConnected            = $true
        AuthMethod             = 'Interactive'
        ConnectedAt            = (Get-Date).ToUniversalTime()
        GraphTokenRefreshedAt  = (Get-Date).ToUniversalTime()
        PnPContext             = [PSCustomObject]@{ Url = 'https://contoso-admin.sharepoint.com' }
        GraphAccessToken       = 'mock_access_token'
        _ClientId              = 'mock_client_id'
        _CertificateThumbprint = $null
        _CertificatePath       = $null
        _CertificatePassword   = $null
        _ClientSecret          = $null
    }
}

Describe 'Invoke-SPCDashboardReport' {

    BeforeEach {
        $script:SPCContext = $script:FakeContext

        Mock Test-SPCConnection {}
        Mock Assert-SPCProLicense {}
        Mock Get-SPCOrphanedUser {
            return @(
                [PSCustomObject]@{ DisplayName = 'User1'; UPN = 'u1@contoso.com'; RiskLevel = 'HIGH'; SiteTitle = 'HR'; SiteUrl = 'https://contoso/sites/hr' }
            )
        }
        Mock Get-SPCGuestAccess {
            return @(
                [PSCustomObject]@{ DisplayName = 'Guest1'; UPN = 'g1#EXT#@contoso.com'; RiskLevel = 'HIGH'; SiteTitle = 'HR'; SiteUrl = 'https://contoso/sites/hr' }
            )
        }
        Mock Get-SPCPrivilegedUser { return @() }
        Mock Get-SPCOverPermissionedUser { return @() }
        Mock Invoke-RestMethod { return [PSCustomObject]@{ '@odata.count' = 50 } }
    }

    Context 'AC-ARCH-02: License Gating' {
        It 'AC-ARCH-02: Throws ERR-LIC-001 when Pro license check fails' {
            Mock Assert-SPCProLicense {
                throw 'ERR-LIC-001: Feature DashboardReport requires an active Pro or Consultant license.'
            }

            $outputPath = Join-Path $TestDrive 'Dashboard_LicenseFail.html'
            { Invoke-SPCDashboardReport -OutputPath $outputPath } | Should -Throw '*ERR-LIC-001*'
        }
    }

    Context 'AC-ARCH-01 / AC-TEST-03: Execution & Output Contract' {
        It 'AC-ARCH-01 / AC-ARCH-02: Generates HTML file and returns output path or FileInfo' {
            $outputPath = Join-Path $TestDrive 'Dashboard_Success.html'
            
            $result = Invoke-SPCDashboardReport -OutputPath $outputPath
            
            Test-Path $outputPath | Should -BeTrue
            $content = Get-Content -Path $outputPath -Raw
            $content | Should -Match 'Overall Permission Health Score'
            $content | Should -Match 'User1'
            $content | Should -Match 'g1#EXT#@contoso.com'
        }

        It 'AC-TEST-03: Invokes all underlying scan cmdlets once during generation' {
            $outputPath = Join-Path $TestDrive 'Dashboard_Scans.html'
            
            $null = Invoke-SPCDashboardReport -OutputPath $outputPath

            Should -Invoke Get-SPCOrphanedUser -Times 1 -Exactly
            Should -Invoke Get-SPCGuestAccess -Times 1 -Exactly
            Should -Invoke Get-SPCPrivilegedUser -Times 1 -Exactly
            Should -Invoke Get-SPCOverPermissionedUser -Times 1 -Exactly
        }
    }

    Context 'Security & Stream Isolation' {
        It 'AC-SEC-02: Does not leak bearer tokens or credentials into Verbose stream' {
            $outputPath = Join-Path $TestDrive 'Dashboard_Sec.html'
            $script:verboseLog = @()
            Mock Write-Verbose {
                param($Message)
                $script:verboseLog += $Message
            }

            $null = Invoke-SPCDashboardReport -OutputPath $outputPath -Verbose

            $leak = $script:verboseLog | Where-Object { $_ -match 'password|secret|token|bearer|ey[A-Za-z0-9_-]+' }
            $leak | Should -BeNullOrEmpty
        }
    }
}