Private/ConvertFrom-CaWhatIfResponse.ps1

function ConvertFrom-CaWhatIfResponse {
    <#
    .SYNOPSIS
        Normalises the several shapes a What If response arrives in into one array of policy
        results

    .DESCRIPTION
        The same evaluation comes back looking different depending on who fetched it.
        Invoke-MgGraphRequest -OutputType Json hands back a string, -OutputType PSObject hands
        back an envelope with a value property, Maester's Test-MtConditionalAccessWhatIf hands
        back the collection already unwrapped, and a fixture on disk is whatever was saved.
        Every one of those is the same whatIfAnalysisResult collection underneath.

        Normalising here rather than in the public function is what lets the folding logic be
        written against a single shape, and what lets the tests feed it a file without
        pretending to be Graph.

        A hashtable is treated as a single result rather than as a collection, because Graph
        deserialises to hashtables under -OutputType Hashtable and a lone policy result is a
        legitimate thing to fold.

    .PARAMETER InputObject
        The response in any of its forms: a JSON string, an envelope with a value property, an
        array of results, or a single result.

    .OUTPUTS
        Object[] of policy result objects. An empty array if the response held none.

    .EXAMPLE
        $results = ConvertFrom-CaWhatIfResponse -InputObject (Get-Content .\whatif.json -Raw)

    .NOTES
        Author: Jeffrey Stuhr
        Version: 0.1.0
        Last Updated: 2026-08-17
    #>


    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param(
        [Parameter(Mandatory)]
        [AllowNull()]
        [object]$InputObject
    )

    if ($null -eq $InputObject) {
        return @()
    }

    $candidate = $InputObject

    # A JSON string, from -OutputType Json or from a file read raw
    if ($candidate -is [string]) {
        if ([string]::IsNullOrWhiteSpace($candidate)) {
            return @()
        }
        try {
            $candidate = $candidate | ConvertFrom-Json -ErrorAction Stop
        } catch {
            throw "The input is a string but not valid JSON: $($_.Exception.Message)"
        }
    }

    # The OData envelope, from -OutputType PSObject or Hashtable
    if ($candidate -isnot [System.Collections.IEnumerable] -or $candidate -is [hashtable]) {
        $valueProperty = $null
        if ($candidate -is [hashtable]) {
            if ($candidate.ContainsKey('value')) { $valueProperty = $candidate['value'] }
        } elseif ($candidate.PSObject.Properties['value']) {
            $valueProperty = $candidate.PSObject.Properties['value'].Value
        }

        if ($null -ne $valueProperty) {
            return @($valueProperty)
        }
    }

    # An array that is itself the envelope, which happens when a caller splats one in
    if ($candidate -is [System.Collections.IEnumerable] -and $candidate -isnot [string]) {
        $items = @($candidate)
        if ($items.Count -eq 1 -and $null -ne $items[0]) {
            $single = $items[0]
            $hasValue = $false
            if ($single -is [hashtable]) {
                $hasValue = $single.ContainsKey('value')
            } elseif ($single.PSObject -and $single.PSObject.Properties['value']) {
                $hasValue = $true
            }
            if ($hasValue) {
                return ConvertFrom-CaWhatIfResponse -InputObject $single
            }
        }
        return $items
    }

    return @($candidate)
}