Private/Utils/Test-LinkedFieldConfiguration.ps1

function Test-LinkedFieldConfiguration {
    <#
    .SYNOPSIS
        Validates linked field configuration to ensure no circular references or invalid links.
 
    .DESCRIPTION
        This function validates that all linked fields in a configuration:
        - Do not create circular references
        - Do not create chained links
        - Only link to existing fields
        - Only link upward or to siblings (not downward to children)
        - Array items only link within the same array item
 
    .PARAMETER Config
        The configuration hashtable to validate.
 
    .PARAMETER Path
        The current path in the configuration (used for recursion).
 
    .EXAMPLE
        Test-LinkedFieldConfiguration -Config $configuration
 
        Validates all linked fields in the configuration.
 
    .OUTPUTS
        None - throws an exception if validation fails.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Config,

        [Parameter(Mandatory = $false)]
        [string]$Path = ""
    )

    $linkedFields = @{}  # Track all linked fields: path -> target path
    $allFields = @{}     # Track all fields and their depths for validation

    function Collect-Fields {
        param(
            [hashtable]$Cfg,
            [string]$CurrentPath = "",
            [int]$Depth = 0,
            [bool]$InArrayItem = $false
        )

        foreach ($key in $Cfg.Keys) {
            $fieldConfig = $Cfg[$key]
            $fieldPath = if ($CurrentPath) { "$CurrentPath.$key" } else { $key }

            if ($fieldConfig -is [hashtable]) {
                if ($fieldConfig.ContainsKey('Action') -and $fieldConfig.Action -eq 'Link') {
                    if ($fieldConfig.ContainsKey('LinkTo')) {
                        $linkedFields[$fieldPath] = @{
                            Target = $fieldConfig.LinkTo
                            InArrayItem = $InArrayItem
                            ParentPath = $CurrentPath
                        }
                        $allFields[$fieldPath] = $Depth
                        Write-Verbose "Test-LinkedFieldConfiguration: Found linked field '$fieldPath' (depth=$Depth, inArray=$InArrayItem) -> '$($fieldConfig.LinkTo)'"
                    }
                }
                elseif ($fieldConfig.ContainsKey('Type')) {
                    # Regular field
                    $allFields[$fieldPath] = $Depth

                    if ($fieldConfig.Type -eq 'array' -and $fieldConfig.ContainsKey('ItemStructure')) {
                        # Array with item structure - recurse with increased depth
                        # Mark that we're inside an array item structure
                        Collect-Fields -Cfg $fieldConfig.ItemStructure -CurrentPath $fieldPath -Depth ($Depth + 1) -InArrayItem $true
                    }
                }
                else {
                    # Nested object - recurse with increased depth
                    Collect-Fields -Cfg $fieldConfig -CurrentPath $fieldPath -Depth ($Depth + 1) -InArrayItem $InArrayItem
                }
            }
        }
    }

    # Collect all fields
    Collect-Fields -Cfg $Config

    # Validate linked fields
    foreach ($sourceField in $linkedFields.Keys) {
        $linkInfo = $linkedFields[$sourceField]
        $targetField = $linkInfo.Target
        $inArrayItem = $linkInfo.InArrayItem
        $parentPath = $linkInfo.ParentPath

        # For array items, resolve the target field relative to the array item structure
        $resolvedTarget = $targetField
        if ($inArrayItem) {
            # Get the array field name (first segment of parentPath)
            $arrayFieldName = ($parentPath -split '\.')[0]

            # Check if target refers to a field outside the array (at root or in other structures)
            if ($allFields.ContainsKey($targetField)) {
                # Target exists at root level - this is linking outside the array
                throw "Invalid link: Field '$sourceField' in array item cannot link to '$targetField' outside the array. Array items can only link to fields within the same item structure."
            }

            # If target doesn't start with the array field name, it's relative to the current context
            if (-not $targetField.StartsWith($arrayFieldName)) {
                # For nested fields, try resolving as sibling first (from parent's parent)
                # e.g., for 'items.metadata.labels.X' linking to 'name', check 'items.metadata.name'
                if ($parentPath) {
                    $parentSegments = $parentPath -split '\.'
                    Write-Verbose "Test-LinkedFieldConfiguration: DEBUG - parentPath='$parentPath', parentSegments=$($parentSegments -join ',')"
                    if ($parentSegments.Count -gt 1) {
                        # Try sibling resolution (parent's parent + target)
                        $siblingPath = ($parentSegments[0..($parentSegments.Count - 2)] -join '.') + ".$targetField"
                        Write-Verbose "Test-LinkedFieldConfiguration: DEBUG - Testing sibling path '$siblingPath'"
                        if ($allFields.ContainsKey($siblingPath)) {
                            $resolvedTarget = $siblingPath
                            Write-Verbose "Test-LinkedFieldConfiguration: Resolved '$targetField' to '$resolvedTarget' (nested sibling) for field '$sourceField'"
                        }
                        else {
                            # Not a sibling, try array item root
                            $resolvedTarget = "$arrayFieldName.$targetField"
                            Write-Verbose "Test-LinkedFieldConfiguration: Resolved '$targetField' to '$resolvedTarget' (array item root) for field '$sourceField'"
                        }
                    }
                    else {
                        # Direct child of array item, resolve from array root
                        $resolvedTarget = "$arrayFieldName.$targetField"
                        Write-Verbose "Test-LinkedFieldConfiguration: Resolved '$targetField' to '$resolvedTarget' (array item root) for field '$sourceField'"
                    }
                }
                else {
                    # No parent path, use array field name
                    $resolvedTarget = "$arrayFieldName.$targetField"
                    Write-Verbose "Test-LinkedFieldConfiguration: Resolved '$targetField' to '$resolvedTarget' (array item root) for field '$sourceField'"
                }
            }

            # Verify the resolved target exists
            if (-not $allFields.ContainsKey($resolvedTarget)) {
                throw "Invalid link: Field '$sourceField' links to '$targetField', but resolved path '$resolvedTarget' does not exist in the configuration."
            }
        }

        # Check if target is also a linked field (prevent chaining/circular)
        if ($linkedFields.ContainsKey($resolvedTarget)) {
            $targetLinkInfo = $linkedFields[$resolvedTarget]
            throw "Circular or chained link detected: Field '$sourceField' links to '$targetField', but '$targetField' also links to '$($targetLinkInfo.Target)'. Linked fields cannot point to other linked fields."
        }

        # Check that target exists (only for non-array items, array items already checked)
        if (-not $inArrayItem -and -not $allFields.ContainsKey($resolvedTarget)) {
            throw "Invalid link: Field '$sourceField' links to '$targetField', but '$targetField' does not exist in the configuration."
        }

        # For array items, validation is complete (already checked scope and existence above)
        if ($inArrayItem) {
            Write-Verbose "Test-LinkedFieldConfiguration: Validated '$sourceField' -> '$resolvedTarget' (array item internal link)"
            continue
        }

        # Check that link is not downward (parent cannot link to child)
        # Source field can only link to fields at same level or ancestor levels
        $sourceParts = $sourceField -split '\.'
        $targetParts = $resolvedTarget -split '\.'

        # If target path is longer than source, it might be a child
        if ($targetParts.Count -gt $sourceParts.Count) {
            # Check if target is a descendant of source
            $isDescendant = $true
            for ($i = 0; $i -lt $sourceParts.Count; $i++) {
                if ($sourceParts[$i] -ne $targetParts[$i]) {
                    $isDescendant = $false
                    break
                }
            }
            if ($isDescendant) {
                throw "Invalid link: Field '$sourceField' cannot link downward to nested field '$resolvedTarget'. Links can only reference fields at the same level or ancestor levels."
            }
        }

        # Check that link is within scope (same parent path or ancestor)
        # For fields at the same depth, they should share the same parent path
        $sourceDepth = $allFields[$sourceField]
        $targetDepth = $allFields[$resolvedTarget]

        if ($sourceDepth -eq $targetDepth) {
            # Same depth - must have same parent
            $sourceParent = if ($sourceParts.Count -gt 1) { ($sourceParts[0..($sourceParts.Count - 2)] -join '.') } else { '' }
            $targetParent = if ($targetParts.Count -gt 1) { ($targetParts[0..($targetParts.Count - 2)] -join '.') } else { '' }

            if ($sourceParent -ne $targetParent) {
                throw "Invalid link: Field '$sourceField' cannot link to '$resolvedTarget' because they are at the same depth but in different scopes. Links must be within the same object or to ancestor fields."
            }
        }
        elseif ($sourceDepth -lt $targetDepth) {
            # Source is higher (less depth) trying to link to lower (more depth) - not allowed
            throw "Invalid link: Field '$sourceField' at depth $sourceDepth cannot link to '$resolvedTarget' at depth $targetDepth. Parent fields cannot link to nested child fields."
        }
        # else: sourceDepth > targetDepth is OK - child linking to ancestor

        Write-Verbose "Test-LinkedFieldConfiguration: Validated '$sourceField' -> '$resolvedTarget' (scope and direction valid)"
    }

    Write-Verbose "Test-LinkedFieldConfiguration: All linked fields validated successfully"
}