Functions/AssessmentCompleteness.ps1

# Private helpers: collection status never enters the assessment target stream.
function Set-AzDevOpsCollectionStatus {
    param (
        [ValidateSet('Empty', 'Partial', 'Unavailable', 'Failed')]
        [string]$Status,
        [string]$ReasonCode
    )

    if ($null -eq $script:collectionStatus) { return }
    $priority = @{ Completed = 0; Empty = 1; Partial = 2; Unavailable = 3; Failed = 4 }
    if ($Status -eq 'Empty' -and $priority[$script:collectionStatus.Status] -gt $priority.Empty) { return }
    if ($priority[$Status] -gt $priority[$script:collectionStatus.Status]) {
        $script:collectionStatus.Status = $Status
    }
    if ($ReasonCode -and $ReasonCode -notin $script:collectionStatus.ReasonCodes) {
        $script:collectionStatus.ReasonCodes += $ReasonCode
    }
}

function Set-AzDevOpsMissingCollectionData {
    param ($Data, [string[]]$RequiredProperties, [string[]]$AllowNullProperties, [switch]$Partial)

    if ($null -eq $script:collectionStatus) { return }
    if ($null -eq $Data) {
        $status = if ($Partial) { 'Partial' } else { 'Unavailable' }
        Set-AzDevOpsCollectionStatus -Status $status -ReasonCode MissingRequiredData
        return
    }
    if (-not $RequiredProperties) { return }
    $properties = if ($Data -is [System.Collections.IDictionary]) { @($Data.Keys) } else { @($Data.PSObject.Properties.Name) }
    $missing = @()
    foreach ($property in $RequiredProperties) {
        if ($property -notin $properties -or ($null -eq $Data.$property -and $property -notin $AllowNullProperties)) {
            $missing += $property
        }
    }
    if ($missing.Count) {
        $status = if (-not $Partial -and $missing.Count -eq $RequiredProperties.Count) { 'Unavailable' } else { 'Partial' }
        Set-AzDevOpsCollectionStatus -Status $status -ReasonCode MissingRequiredData
    }
}

function Get-AzDevOpsCollectionSummary {
    param ([object[]]$Records)

    $incomplete = @($Records | Where-Object { $_.Status -notin 'Completed', 'Empty' })
    if ($incomplete.Count -eq 0) { return 'Complete' }
    if (@($Records | Where-Object { $_.Status -in 'Completed', 'Empty', 'Partial' }).Count -eq 0) { return 'Unavailable' }
    return 'Partial'
}

function Assert-AzDevOpsCompletenessReportPath {
    param ([string]$OutputPath, [string]$CompletenessReportPath)

    if (-not $CompletenessReportPath) { return }
    $outputDirectory = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath).TrimEnd([char[]]@('/', '\'))
    $reportPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($CompletenessReportPath)
    $comparison = if ([System.IO.Path]::DirectorySeparatorChar -eq '\') { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
    if ($reportPath.Equals($outputDirectory, $comparison) -or $reportPath.StartsWith($outputDirectory + [System.IO.Path]::DirectorySeparatorChar, $comparison)) {
        throw 'CompletenessReportPath must be outside the assessment export directory.'
    }
}

function Complete-AzDevOpsAssessmentExport {
    param ($Report, [string]$CompletenessReportPath, [switch]$Strict)

    $records = @()
    if ($Report.Contains('OrganizationCollection')) {
        $Report.OrganizationCollection.Status = Get-AzDevOpsCollectionSummary -Records $Report.OrganizationCollection.Collectors
        $records += $Report.OrganizationCollection.Collectors
    }
    foreach ($project in $Report.Projects) {
        $project.Status = Get-AzDevOpsCollectionSummary -Records $project.Collectors
        $records += $project.Collectors
    }
    if ($Report.Contains('ProjectDiscovery') -and ($Report.Projects.Count -eq 0 -or $Report.ProjectDiscovery.Status -notin 'Completed', 'Empty')) {
        $records += $Report.ProjectDiscovery
    }
    $Report.Status = Get-AzDevOpsCollectionSummary -Records $records
    if ($CompletenessReportPath) {
        $Report | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $CompletenessReportPath -Encoding utf8 -ErrorAction Stop
    }
    if ($Report.Status -ne 'Complete') {
        $commands = @($records | Where-Object { $_.Status -notin 'Completed', 'Empty' } | ForEach-Object { $_.Command } | Select-Object -Unique)
        $message = "Assessment collection is incomplete: $($commands -join ', ')."
        if ($Strict) { throw $message }
        Write-Warning $message
    }
}