Public/Snapshot/Compare-TBSnapshot.ps1

function Compare-TBSnapshot {
    <#
    .SYNOPSIS
        Compares two tenant configuration snapshots.
    .DESCRIPTION
        Downloads the content of two snapshots and compares resource properties
        between them. Returns an array of diff objects indicating what changed,
        was added, or was removed between the reference and difference snapshots.
    .PARAMETER ReferenceSnapshotId
        The ID of the reference (baseline) snapshot.
    .PARAMETER DifferenceSnapshotId
        The ID of the difference (comparison) snapshot.
    .PARAMETER OutputPath
        Optional file path to export the comparison as JSON.
    .EXAMPLE
        Compare-TBSnapshot -ReferenceSnapshotId 'aaa...' -DifferenceSnapshotId 'bbb...'
    .EXAMPLE
        Compare-TBSnapshot -ReferenceSnapshotId 'aaa...' -DifferenceSnapshotId 'bbb...' -OutputPath './diff.json'
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ReferenceSnapshotId,

        [Parameter(Mandatory = $true)]
        [string]$DifferenceSnapshotId,

        [Parameter()]
        [string]$OutputPath
    )

    # Fetch both snapshots
    $refSnapshot = Get-TBSnapshot -SnapshotId $ReferenceSnapshotId
    $diffSnapshot = Get-TBSnapshot -SnapshotId $DifferenceSnapshotId

    foreach ($snap in @($refSnapshot, $diffSnapshot)) {
        if ($snap.Status -ne 'succeeded' -and $snap.Status -ne 'partiallySuccessful') {
            Write-TBLog -Message ('Snapshot {0} status is {1}. Only succeeded or partiallySuccessful snapshots can be compared.' -f $snap.Id, $snap.Status) -Level 'Warning'
            return
        }
        if (-not $snap.ResourceLocation) {
            Write-TBLog -Message ('Snapshot {0} has no ResourceLocation.' -f $snap.Id) -Level 'Warning'
            return
        }
    }

    Write-TBLog -Message ('Downloading reference snapshot content: {0}' -f $ReferenceSnapshotId)
    $refContent = Invoke-TBGraphRequest -Uri $refSnapshot.ResourceLocation -Method 'GET'

    Write-TBLog -Message ('Downloading difference snapshot content: {0}' -f $DifferenceSnapshotId)
    $diffContent = Invoke-TBGraphRequest -Uri $diffSnapshot.ResourceLocation -Method 'GET'

    # Build lookup tables keyed by resourceType+displayName
    $refLookup = @{}
    $diffLookup = @{}

    foreach ($item in @($refContent)) {
        $key = Get-TBSnapshotResourceKey -Resource $item
        $refLookup[$key] = $item
    }

    foreach ($item in @($diffContent)) {
        $key = Get-TBSnapshotResourceKey -Resource $item
        $diffLookup[$key] = $item
    }

    $diffs = [System.Collections.ArrayList]::new()

    # Find changed and removed
    foreach ($key in $refLookup.Keys) {
        $refItem = $refLookup[$key]
        $refProps = Get-TBCompareResourceProperties -Resource $refItem

        if ($diffLookup.ContainsKey($key)) {
            $diffItem = $diffLookup[$key]
            $diffProps = Get-TBCompareResourceProperties -Resource $diffItem

            # Compare properties
            $allPropNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
            foreach ($p in $refProps.Keys) { $null = $allPropNames.Add($p) }
            foreach ($p in $diffProps.Keys) { $null = $allPropNames.Add($p) }

            foreach ($propName in $allPropNames) {
                $refVal = if ($refProps.ContainsKey($propName)) { $refProps[$propName] } else { $null }
                $diffVal = if ($diffProps.ContainsKey($propName)) { $diffProps[$propName] } else { $null }

                $refStr = ConvertTo-TBCompareString -Value $refVal
                $diffStr = ConvertTo-TBCompareString -Value $diffVal

                if ($refStr -ne $diffStr) {
                    $diffType = if ($null -eq $refVal) { 'Added' }
                                elseif ($null -eq $diffVal) { 'Removed' }
                                else { 'Changed' }

                    $parts = $key -split '\|', 2
                    $null = $diffs.Add([PSCustomObject]@{
                        ResourceType     = $parts[0]
                        ResourceName     = if ($parts.Count -gt 1) { $parts[1] } else { '' }
                        Property         = $propName
                        ReferenceValue   = $refStr
                        DifferenceValue  = $diffStr
                        DiffType         = $diffType
                    })
                }
            }
        }
        else {
            # Resource removed in difference
            $parts = $key -split '\|', 2
            $null = $diffs.Add([PSCustomObject]@{
                ResourceType     = $parts[0]
                ResourceName     = if ($parts.Count -gt 1) { $parts[1] } else { '' }
                Property         = '(entire resource)'
                ReferenceValue   = 'present'
                DifferenceValue  = 'absent'
                DiffType         = 'Removed'
            })
        }
    }

    # Find added resources
    foreach ($key in $diffLookup.Keys) {
        if (-not $refLookup.ContainsKey($key)) {
            $parts = $key -split '\|', 2
            $null = $diffs.Add([PSCustomObject]@{
                ResourceType     = $parts[0]
                ResourceName     = if ($parts.Count -gt 1) { $parts[1] } else { '' }
                Property         = '(entire resource)'
                ReferenceValue   = 'absent'
                DifferenceValue  = 'present'
                DiffType         = 'Added'
            })
        }
    }

    if ($OutputPath) {
        $parentDir = Split-Path -Path $OutputPath -Parent
        if ($parentDir -and -not (Test-Path -Path $parentDir)) {
            $null = New-Item -Path $parentDir -ItemType Directory -Force
        }

        $exportData = [PSCustomObject]@{
            ComparedAt           = (Get-Date).ToString('o')
            ReferenceSnapshotId  = $ReferenceSnapshotId
            DifferenceSnapshotId = $DifferenceSnapshotId
            DiffCount            = $diffs.Count
            Diffs                = @($diffs)
        }
        $exportData | ConvertTo-Json -Depth 20 | Out-File -FilePath $OutputPath -Encoding utf8 -Force
        Write-TBLog -Message ('Comparison exported to: {0}' -f $OutputPath)
    }

    return @($diffs)
}