Public/TestCaseManagement/Sync-TcmTestCaseFromRemote.ps1

function Sync-TcmTestCaseFromRemote {
    <#
        .SYNOPSIS
            Pulls test case(s) from Azure DevOps to local YAML files.

        .DESCRIPTION
            When called with -Id (numeric), treats Id as an Azure DevOps Work Item ID.
            The corresponding local YAML file will be updated or created. When -Id is omitted, the
            cmdlet pulls all test cases that have remote changes by scanning local YAML files.

        .PARAMETER Id
            The Azure DevOps Work Item ID to pull (numeric). If omitted, pulls all
            test cases that need updating.

        .PARAMETER OutputPath
            Relative path where to create the test case file (used when pulling a
            Work Item as a new test case file).

        .PARAMETER TestCasesRoot
            Root directory for test cases. If not specified, uses the default TestCases directory.

        .PARAMETER Force
            Force pull even if there are local changes (overwrite local).

        .EXAMPLE
            # Pull a single work item and create/update the local YAML file
            Sync-TcmTestCaseFromRemote -Id 12345

        .EXAMPLE
            # Pull all test cases with remote changes
            Sync-TcmTestCaseFromRemote
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        # ID of already existing Azure DevOps Work Item;
        [Alias("WorkItemId")]
        [string] $Id,

        # Optional output path (used when pulling a Work Item as a new test case file)
        [string] $OutputPath,

        [string] $TestCasesRoot,

        [switch] $Force
    )

    begin {
        # Get configuration
        $config = Get-TcmTestCaseConfig -TestCasesRoot $TestCasesRoot

        # Get credentials and project info
        $collectionUri = $config.azureDevOps.collectionUri
        $project = $config.azureDevOps.project

        if (-not $collectionUri -or -not $project) {
            throw "Azure DevOps collectionUri and project must be configured in config.yaml"
        }

        $processedCount = 0
        $hasErrors = $false
    }

    process {

        try {
            # Id must be a numeric Work Item ID when provided
            if ($Id) {
                if (-not ($Id -match '^[0-9]+$')) {
                    throw "Id must be a numeric Azure DevOps Work Item ID"
                }

                $workItemId = [int]$Id
                Write-Verbose "Pulling work item $workItemId from Azure DevOps..."

                $workItem = Get-WorkItem -WorkItem $workItemId -CollectionUri $collectionUri -Project $project

                if ($workItem.fields.'System.WorkItemType' -ne 'Test Case') {
                    throw "Work item $workItemId is not a Test Case"
                }

                # Check if the work item already has a local file
                $existingFilePath = $null
                $yamlFiles = Get-ChildItem -Path $config.TestCasesRoot -Filter "*.yaml" -Recurse -File
                foreach ($file in $yamlFiles) {
                    try {
                        $fileData = Get-TcmTestCaseFromFile -FilePath $file.FullName -IncludeMetadata
                        if ($fileData.testCase.id -eq $workItemId) {
                            $existingFilePath = $file.FullName
                            break
                        }
                    } catch {
                        # Skip files that can't be parsed
                        continue
                    }
                }

                # Convert work item to test case data
                $testCaseData = ConvertFrom-TcmWorkItemToTestCase -WorkItem $workItem

                if ($existingFilePath) {
                    # Update existing local file
                    $existingData = Get-TcmTestCaseFromFile -FilePath $existingFilePath -IncludeMetadata

                    $existingData.testCase = $testCaseData
                    $existingData.testCase.title = $workItem.fields.'System.Title'
                    $existingData.testCase.id = $workItem.id
                    $existingData.history.lastModifiedAt = $workItem.fields.'System.ChangedDate'
                    $existingData.history.lastModifiedBy = $workItem.fields.'System.ChangedBy'.displayName

                    if ($PSCmdlet.ShouldProcess($existingFilePath, "Update test case file")) {
                        Save-TcmTestCaseYaml -FilePath $existingFilePath -Data $existingData -TestCasesRoot $config.TestCasesRoot

                        Write-Host "Updated test case from work item $workItemId`: $existingFilePath" -ForegroundColor Green
                        $processedCount++
                    }
                } else {
                    # Create new test case file
                    if (-not $OutputPath) {
                        $sanitizedTitle = $workItem.fields.'System.Title' -replace '[^\w\s-]', '' -replace '\s+', '-'
                        $fileName = "$workItemId-$sanitizedTitle.yaml".ToLower()
                        # Use just the filename - let Save-TcmTestCaseYaml handle folder structure
                        $OutputPath = $fileName
                    }

                    $fullOutputPath = Join-Path $config.TestCasesRoot $OutputPath

                    $fullTestCase = [ordered]@{
                        testCase = $testCaseData
                    }

                    $fullTestCase.testCase.title = $workItem.fields.'System.Title'
                    $fullTestCase.testCase.id = $workItemId

                    if ($PSCmdlet.ShouldProcess($fullOutputPath, "Create test case file")) {
                        $actualFilePath = Save-TcmTestCaseYaml -FilePath $fullOutputPath -Data $fullTestCase -TestCasesRoot $config.TestCasesRoot

                        Write-Host "Pulled work item $workItemId to: $actualFilePath" -ForegroundColor Green
                        $processedCount++
                    }
                }
            } else {
                # Pull all test cases with remote changes
                Write-Verbose "Pulling all test cases with remote changes..."

                # Scan all YAML files and check for remote changes
                $yamlFiles = Get-ChildItem -Path $config.TestCasesRoot -Filter "*.yaml" -Recurse -File

                foreach ($file in $yamlFiles) {
                    try {
                        $fileData = Get-TcmTestCaseFromFile -FilePath $file.FullName -IncludeMetadata
                        if ($fileData.testCase.id -and ($fileData.testCase.id -match '^\d+$')) {
                            $syncStatus = Get-TcmTestCaseSyncStatus -Id $fileData.testCase.id -Config $config -TestCaseData $fileData

                            if ($syncStatus -eq "remote-changes" -or ($Force -and $syncStatus -eq "conflict")) {
                                Sync-TcmTestCaseFromRemote -Id $fileData.testCase.id -TestCasesRoot $config.TestCasesRoot -Force:$Force
                                $processedCount++
                            }
                        }
                    } catch {
                        Write-Warning "Failed to process file $($file.FullName): $($_.Exception.Message)"
                        continue
                    }
                }

                if ($processedCount -eq 0) {
                    Write-Host "No test cases need to be pulled." -ForegroundColor Green
                }
            }
        } catch {
            $hasErrors = $true
            Write-Error "Failed to pull test case: $($_.Exception.Message)"
            throw
        }

    }

    end {
        if ($processedCount -gt 0 -and -not $hasErrors) {
            Write-Host "Pulled $processedCount test case(s) successfully." -ForegroundColor Green
        }
    }
}