Private/Test-NSPEntraHardMatchPair.ps1

function Test-NSPEntraHardMatchPair {
    <#
    .SYNOPSIS
        Judges one selected pair against live cloud state. Reads nothing itself.
    .DESCRIPTION
        Outcomes: Ready, AlreadySet (the cloud user holds the desired value and is waiting for sync),
        Linked (it holds it and is synced), or Blocked, with reasons. Warnings never block.
 
        A privileged account has no override here. Entra refuses that match itself, and Microsoft's
        documented route is to remove the role or eligibility first, which this module does not do.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]$Pair,
        [Parameter(Mandatory)]$Queue,
        [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$QueueBlock,
        [Parameter(Mandatory)]$TenantState,
        [AllowNull()]$CloudUser,
        [AllowEmptyCollection()][string[]]$OtherAnchorHolder = @(),
        [ValidateSet('Set', 'Clear', 'Unknown')][string]$ObjectIdentifier = 'Unknown'
    )

    $blocks = [System.Collections.Generic.List[string]]::new()
    $warnings = [System.Collections.Generic.List[string]]::new()
    foreach ($reason in @($QueueBlock) + @($TenantState.Blocks)) { if ($reason) { $blocks.Add($reason) } }
    foreach ($reason in @($TenantState.Warnings)) { if ($reason) { $warnings.Add($reason) } }

    $id = [string]$Pair.cloudObjectId
    $desired = [string]$Pair.desiredImmutableId
    $current = $null
    $upn = [string]$Pair.cloudUpnAtReview
    $outcome = $null

    if ($null -eq $CloudUser) {
        $blocks.Add('The cloud user no longer exists, or is soft-deleted.')
    } else {
        $current = $CloudUser['onPremisesImmutableId']
        $upn = [string]$CloudUser['userPrincipalName']
        $synced = $CloudUser['onPremisesSyncEnabled']
        if ($desired -and [string]$current -ceq $desired) {
            $outcome = if ($synced -eq $true) { 'Linked' } else { 'AlreadySet' }
        } else {
            # False is not the same as null: false means it was synced once.
            if ($null -ne $synced) { $blocks.Add('The cloud user is, or has been, synced. Graph must not change it.') }
            if ($current) { $blocks.Add('The cloud user already has a different immutable ID.') }
            if ([string]$CloudUser['userType'] -ne 'Member') { $blocks.Add('The cloud user is a guest.') }
        }
    }

    if (-not $outcome) {
        if ($Pair.revalidation.state -ne 'Current') {
            $blocks.Add('The pair is not Current in the last revalidation.')
        } elseif (-not $Pair.decidedAtUtc -or (ConvertTo-NSPEntraPairingUtcTime $Pair.revalidation.checkedAtUtc) -lt (ConvertTo-NSPEntraPairingUtcTime $Pair.decidedAtUtc)) {
            $blocks.Add('The pair was decided after the last revalidation.')
        }

        if (-not $desired) {
            $blocks.Add('The pair has no desired immutable ID.')
        } else {
            $bytes = $null
            try { $bytes = [Convert]::FromBase64String($desired) } catch { $bytes = $null }
            if ($null -eq $bytes -or $bytes.Length -ne 16) { $blocks.Add('The desired immutable ID is not 16 bytes of Base64.') }
            if ($TenantState.DeletedAnchors.ContainsKey($desired)) { $blocks.Add("Soft-deleted user $($TenantState.DeletedAnchors[$desired]) holds this anchor.") }
        }

        if ($Pair.adObjectGuid -in @($Queue.linkedAdObjectGuids)) { $blocks.Add('The AD user is already linked to a cloud account.') }
        $others = @($Queue.pairs | Where-Object {
                $_.decision -eq 'selected' -and $_.cloudObjectId -ne $id -and ($_.adObjectGuid -eq $Pair.adObjectGuid -or ($desired -and $_.desiredImmutableId -ceq $desired))
            })
        if ($others.Count) { $blocks.Add("One AD user or anchor is selected for several cloud users, including $($others[0].cloudObjectId).") }
        $holders = @($OtherAnchorHolder | Where-Object { $_ -and $_ -ne $id })
        if ($holders.Count) { $blocks.Add("Cloud user $($holders[0]) already holds this anchor.") }

        if ($TenantState.RoleHolders.ContainsKey($id)) { $blocks.Add('The cloud user holds a directory role. Entra refuses this match; the role has to come off first.') }
        if ($TenantState.EligibleHolders.ContainsKey($id)) { $blocks.Add('The cloud user is eligible for a directory role. Entra refuses this match; the eligibility has to come off first.') }

        if ($ObjectIdentifier -eq 'Set') { $blocks.Add('The cloud user already has onPremisesObjectIdentifier set, so Entra refuses this match until it is cleared.') }
        if ($ObjectIdentifier -eq 'Unknown' -and $null -ne $CloudUser) { $warnings.Add('onPremisesObjectIdentifier could not be read. If it is set, Entra refuses the match at sync.') }

        if ($Pair.adEnabledAtReview -eq $false) { $warnings.Add('The AD user is disabled. After sync, the cloud account is too.') }
        if ($Pair.adUserPrincipalNameAtReview -and [string]$Pair.adUserPrincipalNameAtReview -ne $upn) {
            $warnings.Add("The cloud UPN becomes AD's $($Pair.adUserPrincipalNameAtReview) at sync.")
        }
        $outcome = if ($blocks.Count) { 'Blocked' } else { 'Ready' }
    }

    [pscustomobject]@{
        PSTypeName = 'NSP.M365.UserPairing.HardMatchCheck'
        CloudObjectId = $id
        CloudUserPrincipalName = $upn
        AdObjectGuid = [string]$Pair.adObjectGuid
        AdDistinguishedName = [string]$Pair.adDistinguishedNameAtReview
        CurrentImmutableId = $current
        DesiredImmutableId = $Pair.desiredImmutableId
        Outcome = $outcome
        Blocks = @($(if ($outcome -eq 'Blocked') { $blocks }))
        Warnings = @($warnings)
    }
}