Private/TestCaseManagement/ConvertTo-TcmTestCaseInput.ps1

function ConvertTo-TcmTestCaseInput {
    <#
        .SYNOPSIS
            Converts various input types to standardized TcmTestCaseInput objects for test case operations.
        .DESCRIPTION
            Accepts test case IDs, file paths, directories, or existing test case objects from the pipeline or parameters.
            Normalizes all input types into standardized TcmTestCaseInput objects with ID-to-file-path caching for efficient lookups.
            Used as the input normalization layer for sync operations and test case retrieval.
        .PARAMETER InputObject
            The input object(s) from the pipeline or parameter. Accepts:
            - Test case ID (string) - e.g., "TC001" or "123"
            - File path (string) - relative or absolute path to YAML file
            - Directory path (string) - recursively finds all YAML files
            - Test case object (PSCustomObject/hashtable) - from Get-TcmTestCase or Get-TcmTestCaseFromFile
            - TcmTestCaseInput object - pass-through
        .OUTPUTS
            [PSCustomObject[]] Array of TcmTestCaseInput objects with PSTypeName 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput'
        .EXAMPLE
            'TestCases/area/TC001.yaml' | ConvertTo-TcmTestCaseInput

            Converts a file path to a TcmTestCaseInput object.
        .EXAMPLE
            '123' | ConvertTo-TcmTestCaseInput -TestCasesRoot 'C:\TestCases'

            Converts a numeric ID to a TcmTestCaseInput object, checking for local files first.
        .EXAMPLE
            ConvertTo-TcmTestCaseInput -InputObject 'TestCases' -TestCasesRoot 'C:\TestCases'

            Converts a directory path to multiple TcmTestCaseInput objects for all YAML files.
        .EXAMPLE
            Get-TcmTestCase -Id 'TC001' | ConvertTo-TcmTestCaseInput

            Converts an existing test case object back to TcmTestCaseInput format.
    #>

    [OutputType('PSTypeNames.AzureDevOpsApi.TcmTestCaseInput')]
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
        $InputObject,

        [string] $TestCasesRoot
    )

    begin {
        $items = @()
        # Cache for ID to file path mappings to avoid rescanning
        $script:idToFilePathCache = $null

        # Helper function to build ID to file path cache
        function Get-IdToFilePathCache {
            param([string] $RootPath)

            if ($null -eq $script:idToFilePathCache) {
                Write-Debug "Building ID to file path cache for root: $RootPath"
                $script:idToFilePathCache = @{}

                # Get all YAML files
                $yamlFiles = Get-ChildItem -Path $RootPath -Filter "*.yaml" -Recurse -File

                foreach ($file in $yamlFiles) {
                    # First try: Extract ID from filename pattern (fast)
                    $fileName = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
                    if ($fileName -match '^([^-]+)-') {
                        $extractedId = $matches[1]
                        if (-not $script:idToFilePathCache.ContainsKey($extractedId)) {
                            $script:idToFilePathCache[$extractedId] = $file.FullName
                            Write-Debug "Cached ID '$extractedId' -> '$($file.FullName)' from filename"
                        }
                    }

                    # Second try: If ID is not numeric, also check file contents for ID
                    if ($extractedId -and $extractedId -notmatch '^\d+$') {
                        try {
                            $content = Get-TcmTestCaseFromFile -FilePath $file.FullName -ErrorAction SilentlyContinue
                            if ($content.testCase.id -and -not $script:idToFilePathCache.ContainsKey($content.testCase.id)) {
                                $script:idToFilePathCache[$content.testCase.id] = $file.FullName
                                Write-Debug "Cached ID '$($content.testCase.id)' -> '$($file.FullName)' from file content"
                            }
                        } catch {
                            # Skip files that can't be parsed
                            continue
                        }
                    }
                }

                Write-Debug "Cache built with $($script:idToFilePathCache.Count) entries"
            }

            return $script:idToFilePathCache
        }
    }

    process {

        # If no input, get all test cases from root
        if ($null -eq $InputObject) {
            $InputObject = Get-Item -Path $TestCasesRoot
        }

        $item = $InputObject

        # If object is already a TcmTestCaseInput, pass through
        if ($item -and $item.PSTypeNames `
                -and $item.PSTypeNames -contains 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput') {
            Write-Debug "Input is already a TcmTestCaseInput object, passing through."
            $items += $item
            return
        }

        # Handle TestCase objects from Get-TcmTestCaseFromFile
        if ($item -and $item.PSTypeNames `
                -and $item.PSTypeNames -contains $global:PSTypeNames.AzureDevOpsApi.TcmTestCaseLocalData) {
            Write-Debug "Input is a TcmTestCaseLocalData object, wrapping in TcmTestCaseInput."
            $wrappedObject = [PSCustomObject] @{
                PSTypeName = 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput'
                FilePath   = $item.FilePath
                SyncStatus = $null
                LocalData  = $item
                RemoteData = $null
                Id         = $item.testCase.id
            }
            $items += $wrappedObject
            return
        }

        # Handle hashtable/PSCustomObject from Get-TcmTestCase (has testCase.id property)
        if ($item -is [hashtable] -or $item -is [PSCustomObject]) {
            if ($item.testCase -and $item.testCase.id) {
                Write-Debug "Input is a hashtable/PSCustomObject with testCase.id, wrapping in TcmTestCaseInput."
                $wrappedObject = [PSCustomObject] @{
                    PSTypeName = 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput'
                    FilePath   = $item.FilePath
                    SyncStatus = $null
                    LocalData  = $item
                    RemoteData = $null
                    Id         = $item.testCase.id
                }
                $items += $wrappedObject
                return
            }
            # Handle PSCustomObject with Id property (from pipeline input)
            elseif ($item.Id) {
                Write-Debug "Input is a PSCustomObject with Id, wrapping in TcmTestCaseInput."
                $wrappedObject = [PSCustomObject] @{
                    PSTypeName = 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput'
                    FilePath   = $null
                    SyncStatus = $null
                    LocalData  = $null
                    RemoteData = $null
                    Id         = $item.Id
                }
                $items += $wrappedObject
                return
            }
        }

        # Handle string input (file path or ID)
        $resolvedPath = $item
        if ($TestCasesRoot -and -not [System.IO.Path]::IsPathRooted($item)) {
            # If relative path and TestCasesRoot provided, resolve against root
            $resolvedPath = Join-Path -Path $TestCasesRoot -ChildPath $item
        }

        # Check if path looks like a file path but doesn't exist - treat as invalid
        if ($resolvedPath -match '\.yaml$|\.yml$' -and -not (Test-Path $resolvedPath -PathType Leaf)) {
            Write-Error "File path does not exist: $resolvedPath"
            return
        }

        if ((Test-Path $resolvedPath -PathType Leaf)) {
            try {
                Write-Debug "Input is a file path, wrapping in TcmTestCaseInput."
                $wrappedObject = [PSCustomObject] @{
                    PSTypeName = 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput'
                    FilePath   = $resolvedPath
                    SyncStatus = $null
                    LocalData  = $null
                    RemoteData = $null
                    Id         = $null
                }
                $items += $wrappedObject
            } catch {
                Write-Error "Failed to read test case file: $resolvedPath. $_"
            }
            return
        }

        if ((Test-Path $resolvedPath -PathType Container)) {
            try {
                Write-Debug "Input is a directory, getting all YAML files within."
                $yamlFiles = Get-ChildItem `
                    -Path $resolvedPath `
                    -Include "*.yaml" `
                    -Exclude $config.ExcludePatterns `
                    -Recurse `
                    -File `
                | Where-Object { [System.IO.Path]::GetFileName($_.FullName) -notmatch '^\.' }  # Exclude dot-files

                foreach ($file in $yamlFiles) {
                    $wrappedObject = [PSCustomObject] @{
                        PSTypeName = 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput'
                        FilePath   = $file.FullName
                        SyncStatus = $null
                        LocalData  = $null
                        RemoteData = $null
                        Id         = $null
                    }
                    $items += $wrappedObject
                }
            } catch {
                Write-Warning "Failed to read test cases from directory: $resolvedPath. $_"
            }
            return
        }

        # Treat as ID (any string that doesn't match a file/directory path)
        Write-Debug "Input '$item' treated as test case ID"

        # Check local files first for any ID (including numeric ones)
        Write-Debug "Checking local files for ID '$item'..."
        $cache = Get-IdToFilePathCache -RootPath $TestCasesRoot
        $matchedFilePath = $cache[[string]$item]

        if ($matchedFilePath) {
            Write-Debug "Found local file for ID '$item': $matchedFilePath"
            $wrappedObject = [PSCustomObject] @{
                PSTypeName = 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput'
                FilePath   = $matchedFilePath
                SyncStatus = $null
                LocalData  = $null
                RemoteData = $null
                Id         = $item
            }
            $items += $wrappedObject
        } else {
            Write-Debug "No local file found for ID '$item', will use for remote loading"
            $wrappedObject = [PSCustomObject] @{
                PSTypeName = 'PSTypeNames.AzureDevOpsApi.TcmTestCaseInput'
                FilePath   = $null
                SyncStatus = $null
                LocalData  = $null
                RemoteData = $null
                Id         = $item
            }
            $items += $wrappedObject
        }
    }
    end {
        Write-Debug "Finished processing input, total items: $($items.Count)"
        # Only return objects with FilePath or Id
        # Use Write-Output -NoEnumerate to preserve array for single items (PS5 compatibility)
        $result = @(
            $items `
            | Where-Object { $null -ne $_ } `
            | Where-Object { ($null -ne $_.Id) -or ($null -ne $_.FilePath) }
        )
        Write-Output -NoEnumerate $result
    }
}