internal/functions/Compare-PIMPolicy.ps1

function ConvertTo-BooleanString {
    <#
    .SYNOPSIS
    Normalizes various boolean representations to consistent string format.
 
    .DESCRIPTION
    Converts different boolean representations (true, "true", "True", 1, "false", false, 0, etc.)
    to a consistent lowercase string format for comparison purposes.
 
    .PARAMETER Value
    The value to normalize.
 
    .OUTPUTS
    String. Returns "true" or "false" as normalized string.
    #>

    param([object]$Value)

    if ($null -eq $Value -or $Value -eq '' -or $Value -eq 'None') {
        return 'false'
    }

    # Handle boolean types
    if ($Value -is [bool]) {
        return $Value.ToString().ToLower()
    }

    # Handle string representations
    $stringValue = "$Value".ToLower().Trim()
    if ($stringValue -in @('true', '1', 'yes', 'on', 'enabled')) {
        return 'true'
    }
    if ($stringValue -in @('false', '0', 'no', 'off', 'disabled')) {
        return 'false'
    }

    # Default fallback
    return 'false'
}

function Compare-PIMPolicy {
    <#
    .SYNOPSIS
    Compares expected PIM policy settings against live policy configuration.
 
    .DESCRIPTION
    Performs field-by-field comparison of PIM policy settings, applying business
    rules validation and handling requirement normalization. Returns structured
    comparison results.
 
    .PARAMETER Type
    The type of policy being compared (EntraRole, AzureRole, Group).
 
    .PARAMETER Name
    The name of the role or policy being compared.
 
    .PARAMETER Expected
    The expected policy configuration object.
 
    .PARAMETER Live
    The live policy configuration from the system.
 
    .PARAMETER ExtraId
    Optional additional identifier (scope, group ID, etc.).
 
    .PARAMETER ApproverCountExpected
    Expected number of approvers when approval is required.
 
    .PARAMETER Results
    Reference to the results array to append to.
 
    .PARAMETER DriftCount
    Reference to the drift counter to increment.
 
    .OUTPUTS
    None. Updates the provided Results array and DriftCount reference.
 
    .EXAMPLE
    Compare-PIMPolicy -Type "EntraRole" -Name "Global Administrator" -Expected $expected -Live $live -Results ([ref]$results) -DriftCount ([ref]$driftCount)
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Type,
        [Parameter(Mandatory)][string]$Name,
        [Parameter()][object]$Expected,
        [Parameter()][object]$Live,
        [Parameter()][string]$ExtraId = $null,
        [Parameter()][int]$ApproverCountExpected = $null,
        [Parameter(Mandatory)][ref]$Results,
        [Parameter(Mandatory)][ref]$DriftCount
    )

    # Fields to compare
    $fields = @(
        'ActivationDuration',
        'ActivationRequirement',
        'ApprovalRequired',
        'MaximumEligibilityDuration',
        'AllowPermanentEligibility',
        'MaximumActiveAssignmentDuration',
        'AllowPermanentActiveAssignment'
    )

    # Mapping from config names to live policy property names
    $liveNameMap = @{
        'ActivationRequirement' = 'EnablementRules'
        'MaximumEligibilityDuration' = 'MaximumEligibleAssignmentDuration'
        'AllowPermanentEligibility' = 'AllowPermanentEligibleAssignment'
    }

    $differences = @()

    foreach ($field in $fields) {
        if ($Expected.PSObject.Properties[$field]) {
            $expectedValue = $Expected.$field
            $liveProperty = $field

            # Map to live property name if needed
            if ($liveNameMap.ContainsKey($field)) {
                $liveProperty = $liveNameMap[$field]
            }

            $liveValue = $null
            if ($Live -and $Live.PSObject -and $Live.PSObject.Properties[$liveProperty]) {
                $liveValue = $Live.$liveProperty
            }

            # Handle array values
            if ($expectedValue -is [System.Collections.IEnumerable] -and -not ($expectedValue -is [string])) {
                $expectedValue = ($expectedValue | ForEach-Object { "$_" }) -join ','
            }
            if ($liveValue -is [System.Collections.IEnumerable] -and -not ($liveValue -is [string])) {
                $liveValue = ($liveValue | ForEach-Object { "$_" }) -join ','
            }

            # Special handling for activation requirements
            if ($field -eq 'ActivationRequirement' -or $field -eq 'ActiveAssignmentRequirement') {
                $expectedNormalized = Convert-RequirementValue -Value $expectedValue
                $liveNormalized = Convert-RequirementValue -Value $liveValue

                # Apply business rules validation
                $policyForBusinessRules = [PSCustomObject]@{}
                $Expected.PSObject.Properties | ForEach-Object {
                    $policyForBusinessRules | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value
                }

                $businessRuleResult = Test-PIMPolicyBusinessRules -PolicySettings $policyForBusinessRules -CurrentPolicy $Live -ApplyAdjustments
                $hasBusinessRuleAdjustment = $businessRuleResult.HasChanges

                if ($hasBusinessRuleAdjustment) {
                    $adjustedExpected = $businessRuleResult.AdjustedSettings.$field
                    $adjustedExpectedNormalized = Convert-RequirementValue -Value $adjustedExpected

                    if ($adjustedExpectedNormalized -eq $liveNormalized) {
                        # This is expected behavior due to business rules, not drift
                        if ($businessRuleResult.Conflicts -and $businessRuleResult.Conflicts.Count -gt 0) {
                            Write-Verbose "$Name - Business rule applied: $($businessRuleResult.Conflicts[0]) (expected behavior, not drift)"
                        }
                        continue  # Skip adding to differences
                    } else {
                        # Still drift even after business rule adjustments
                        $expectedNormalized = $adjustedExpectedNormalized
                        $expectedValue = $adjustedExpected
                    }
                }

                if ($expectedNormalized -ne $liveNormalized) {
                    $displayExpected = if ($null -eq $expectedValue -or $expectedValue -eq '' -or $expectedValue -eq 'None') { 'None' } else { $expectedValue }
                    $displayLive = if ($null -eq $liveValue -or $liveValue -eq '' -or $liveValue -eq 'None') { 'None' } else { $liveValue }
                    $driftMessage = "{0}: expected='{1}' actual='{2}'" -f $field, $displayExpected, $displayLive

                    # Add explanatory notes for business rule conflicts
                    if ($hasBusinessRuleAdjustment) {
                        $driftMessage += " (Note: Expected value adjusted for Authentication Context business rules)"
                    }

                    $differences += $driftMessage
                }
            }
            else {
                # Standard field comparison with boolean normalization
                $normalizedExpected = $expectedValue
                $normalizedLive = $liveValue

                # Handle boolean fields that might have different representations
                if ($field -in @('AllowPermanentEligibility', 'AllowPermanentActiveAssignment', 'ApprovalRequired')) {
                    $normalizedExpected = ConvertTo-BooleanString -Value $expectedValue
                    $normalizedLive = ConvertTo-BooleanString -Value $liveValue
                }

                if ($normalizedExpected -ne $normalizedLive) {
                    $differences += ("{0}: expected='{1}' actual='{2}'" -f $field, $expectedValue, $liveValue)
                }
            }
        }
    }

    # Check approver count if approval is required
    if ($null -ne $ApproverCountExpected -and $Expected.PSObject.Properties['ApprovalRequired'] -and $Expected.ApprovalRequired) {
        $liveApproverCount = $null

        # Try different property names for approver count
        foreach ($approverProperty in @('Approvers', 'Approver', 'Approval', 'approval', 'ApproverCount')) {
            if ($Live.PSObject -and $Live.PSObject.Properties[$approverProperty]) {
                $approverValue = $Live.$approverProperty
                if ($approverValue -is [System.Collections.IEnumerable] -and -not ($approverValue -is [string])) {
                    $liveApproverCount = @($approverValue).Count
                }
                elseif ($approverValue -match '^[0-9]+$') {
                    $liveApproverCount = [int]$approverValue
                }
                if ($null -ne $liveApproverCount) { break }
            }
        }

        if ($null -ne $liveApproverCount -and $liveApproverCount -ne $ApproverCountExpected) {
            $differences += "ApproversCount: expected=$ApproverCountExpected actual=$liveApproverCount"
        }
    }

    # Determine status and update counters
    if ($differences.Count -gt 0) {
        $DriftCount.Value++
        $status = 'Drift'
    } else {
        $status = 'Match'
    }

    # Add protected role indicator to the name display
    $displayName = $Name
    if (Test-IsProtectedRole -RoleName $Name -Type $Type) {
        $displayName = "$Name [⚠️ PROTECTED]"
    }

    # Add result to the results array
    $Results.Value += [pscustomobject]@{
        Type = $Type
        Name = $displayName
        Target = $ExtraId
        Status = $status
        Differences = ($differences -join '; ')
    }
}