Private/Testing/Compare-ApiResponse.ps1

function Compare-ApiResponse {
    <#
    .SYNOPSIS
        Compares function output against raw API response.
    .DESCRIPTION
        Performs a deep comparison of two objects to identify differences
        in structure and values. Useful for validating that NMM-PS functions
        correctly process API responses.
    .PARAMETER FunctionOutput
        The output from the NMM-PS function.
    .PARAMETER RawApiOutput
        The raw output from Invoke-APIRequest.
    .PARAMETER IgnoreProperties
        Properties to ignore during comparison (e.g., PSTypeName additions).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $FunctionOutput,

        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $RawApiOutput,

        [Parameter()]
        [string[]]$IgnoreProperties = @()
    )

    $differences = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Handle null cases
    if ($null -eq $FunctionOutput -and $null -eq $RawApiOutput) {
        return [PSCustomObject]@{
            AreEqual    = $true
            Differences = @()
        }
    }

    if ($null -eq $FunctionOutput -or $null -eq $RawApiOutput) {
        $differences.Add([PSCustomObject]@{
                Path           = '$root'
                Type           = 'NullMismatch'
                FunctionValue  = if ($null -eq $FunctionOutput) { '<null>' } else { '<has value>' }
                RawApiValue    = if ($null -eq $RawApiOutput) { '<null>' } else { '<has value>' }
            })
        return [PSCustomObject]@{
            AreEqual    = $false
            Differences = $differences.ToArray()
        }
    }

    # Compare arrays
    $funcArray = @($FunctionOutput)
    $rawArray = @($RawApiOutput)

    if ($funcArray.Count -ne $rawArray.Count) {
        $differences.Add([PSCustomObject]@{
                Path           = '$root'
                Type           = 'CountMismatch'
                FunctionValue  = $funcArray.Count
                RawApiValue    = $rawArray.Count
            })
    }

    # Compare first item's properties (for schema comparison)
    if ($funcArray.Count -gt 0 -and $rawArray.Count -gt 0) {
        $funcItem = $funcArray[0]
        $rawItem = $rawArray[0]

        # Get properties (excluding PSTypeName-related)
        $funcProps = @($funcItem.PSObject.Properties | Where-Object { $_.Name -notin $IgnoreProperties }).Name | Sort-Object
        $rawProps = @($rawItem.PSObject.Properties | Where-Object { $_.Name -notin $IgnoreProperties }).Name | Sort-Object

        # Find missing properties in function output
        $missingInFunc = $rawProps | Where-Object { $_ -notin $funcProps }
        foreach ($prop in $missingInFunc) {
            $differences.Add([PSCustomObject]@{
                    Path           = $prop
                    Type           = 'MissingInFunction'
                    FunctionValue  = '<missing>'
                    RawApiValue    = $rawItem.$prop
                })
        }

        # Find extra properties in function output (usually PSTypeName additions)
        $extraInFunc = $funcProps | Where-Object { $_ -notin $rawProps }
        foreach ($prop in $extraInFunc) {
            $differences.Add([PSCustomObject]@{
                    Path           = $prop
                    Type           = 'ExtraInFunction'
                    FunctionValue  = $funcItem.$prop
                    RawApiValue    = '<not present>'
                })
        }

        # Compare values of common properties
        $commonProps = $funcProps | Where-Object { $_ -in $rawProps }
        foreach ($prop in $commonProps) {
            $funcValue = $funcItem.$prop
            $rawValue = $rawItem.$prop

            # Skip complex objects for now (arrays, nested objects)
            if ($funcValue -is [array] -or $rawValue -is [array]) {
                continue
            }

            # Compare simple values
            if ($funcValue -ne $rawValue) {
                # Handle type differences (e.g., string vs int)
                if ($funcValue.ToString() -eq $rawValue.ToString()) {
                    continue
                }

                $differences.Add([PSCustomObject]@{
                        Path           = $prop
                        Type           = 'ValueMismatch'
                        FunctionValue  = $funcValue
                        RawApiValue    = $rawValue
                    })
            }
        }
    }

    return [PSCustomObject]@{
        AreEqual       = ($differences.Count -eq 0)
        Differences    = $differences.ToArray()
        FunctionCount  = $funcArray.Count
        RawApiCount    = $rawArray.Count
        FunctionProps  = if ($funcArray.Count -gt 0) { @($funcArray[0].PSObject.Properties.Name) } else { @() }
        RawApiProps    = if ($rawArray.Count -gt 0) { @($rawArray[0].PSObject.Properties.Name) } else { @() }
    }
}

function Get-PropertyDiff {
    <#
    .SYNOPSIS
        Gets a simple property difference summary between two objects.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $Object1,

        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $Object2,

        [Parameter()]
        [string[]]$IgnoreProperties = @()
    )

    $obj1Props = @()
    $obj2Props = @()

    if ($Object1) {
        $items = @($Object1)
        if ($items.Count -gt 0) {
            $obj1Props = @($items[0].PSObject.Properties | Where-Object { $_.Name -notin $IgnoreProperties }).Name
        }
    }

    if ($Object2) {
        $items = @($Object2)
        if ($items.Count -gt 0) {
            $obj2Props = @($items[0].PSObject.Properties | Where-Object { $_.Name -notin $IgnoreProperties }).Name
        }
    }

    return [PSCustomObject]@{
        OnlyInFirst  = @($obj1Props | Where-Object { $_ -notin $obj2Props })
        OnlyInSecond = @($obj2Props | Where-Object { $_ -notin $obj1Props })
        InBoth       = @($obj1Props | Where-Object { $_ -in $obj2Props })
    }
}