tests/Collect.CostInventory.Tests.ps1

#Requires -Version 7.0
#Requires -Modules Pester

<#
    Pester tests for src/collect/Get-ScoutCostInventory.ps1 (AB#5639/5647, Epic AB#5638).
 
    Invoke-AzCostManagementQuery is mocked (or deliberately left undefined, to simulate
    Az.CostManagement not being installed) throughout -- no live Azure connection is made.
#>


BeforeAll {
    $root = Split-Path $PSScriptRoot -Parent
    . "$root/src/collect/Get-ScoutCostInventory.ps1"

    $script:subs = @(
        [pscustomobject]@{ id = 'sub-1'; name = 'sub-one' }
        [pscustomobject]@{ id = 'sub-2'; name = 'sub-two' }
    )
}

Describe 'Get-ScoutCostInventory -- Az.CostManagement not installed' {
    It 'never throws, warns once with the install command, and returns @() CostData for every subscription' {
        # No Invoke-AzCostManagementQuery function defined at all -- Get-Command must not find it.
        { Get-ScoutCostInventory -Subscriptions $script:subs -WarningAction SilentlyContinue } | Should -Not -Throw
        $result = @(Get-ScoutCostInventory -Subscriptions $script:subs -WarningVariable warnings -WarningAction SilentlyContinue)
        $result.Count | Should -Be 2
        @($result[0].CostData).Count | Should -Be 0
        @($result[1].CostData).Count | Should -Be 0
        ($warnings -join "`n") | Should -Match 'Az.CostManagement'
        ($warnings -join "`n") | Should -Match 'Install-Module'
    }
}

Describe 'Get-ScoutCostInventory -- happy path' {
    It 'returns CostData rows per subscription when Az.CostManagement succeeds' {
        function Invoke-AzCostManagementQuery {
            param([Parameter(ValueFromRemainingArguments)] $Rest)
            return @([pscustomobject]@{ PreTaxCost = 42.5 })
        }
        $result = @(Get-ScoutCostInventory -Subscriptions $script:subs)
        $result.Count | Should -Be 2
        $result[0].SubscriptionId | Should -Be 'sub-1'
        $result[0].CostData[0].PreTaxCost | Should -Be 42.5
    }
}

Describe 'Get-ScoutCostInventory -- per-subscription resilience (AB#5636 regression guard)' {
    It 'degrades ONLY the failing subscription to empty CostData and keeps collecting the rest -- never throws' {
        function Invoke-AzCostManagementQuery {
            param([string] $Scope, [Parameter(ValueFromRemainingArguments)] $Rest)
            if ($Scope -match 'sub-1') { throw 'Cost Management API transient error' }
            return @([pscustomobject]@{ PreTaxCost = 10 })
        }
        { Get-ScoutCostInventory -Subscriptions $script:subs -WarningAction SilentlyContinue } | Should -Not -Throw
        $result = @(Get-ScoutCostInventory -Subscriptions $script:subs -WarningVariable warnings -WarningAction SilentlyContinue)
        $result.Count | Should -Be 2
        @(($result | Where-Object SubscriptionId -eq 'sub-1').CostData).Count | Should -Be 0
        (($result | Where-Object SubscriptionId -eq 'sub-2').CostData)[0].PreTaxCost | Should -Be 10
        ($warnings -join "`n") | Should -Match "sub-one"
    }
}

Describe 'Get-ScoutCostInventory -- date range selection' {
    It 'queries the full previous calendar year when -Days is >= 365' {
        $script:capturedFrom = $null
        $script:capturedTo = $null
        function Invoke-AzCostManagementQuery {
            param([datetime] $TimePeriodFrom, [datetime] $TimePeriodTo, [Parameter(ValueFromRemainingArguments)] $Rest)
            $script:capturedFrom = $TimePeriodFrom
            $script:capturedTo = $TimePeriodTo
            return @()
        }
        Get-ScoutCostInventory -Subscriptions @($script:subs[0]) -Days 365 | Out-Null
        $script:capturedFrom.Month | Should -Be 1
        $script:capturedFrom.Day | Should -Be 1
        $script:capturedTo.Month | Should -Be 12
        $script:capturedTo.Day | Should -Be 31
        $script:capturedFrom.Year | Should -Be $script:capturedTo.Year
    }
}