functions/Get-EndjinGist.Tests.ps1

# <copyright file="Get-EndjinGist.Tests.ps1" company="Endjin Limited">
# Copyright (c) Endjin Limited. All rights reserved.
# </copyright>

Describe "Get-EndjinGist" {
    
    BeforeAll {
        # Define stubs for external module functions to avoid dependency and ensure mocking works
        function ConvertTo-Yaml { param($Data, $OutFile, [switch]$Force) }
        function ConvertFrom-Yaml { param([switch]$Ordered) }
        function _Get-VendirPath { }
        function _Install-Vendir { }

        $sut = "$PSScriptRoot\Get-EndjinGist.ps1"
        . $sut

        # Mock Data
        $mockGistMap = @{
            'test-group' = @(
                @{
                    name = 'test-gist'
                    source = 'https://github.com/org/repo.git'
                    ref = 'main'
                    includePaths = @('path/to/include')
                }
            )
        }

        # Create a real temp file so ValidateScript({Test-Path $_ -PathType Leaf}) passes
        $dummyGistMapPath = Join-Path $TestDrive 'gist-map.yml'
        Set-Content -Path $dummyGistMapPath -Value 'placeholder'
    }

    Context "Prerequisites" {
        It "Should call _Get-VendirPath to resolve vendir" {
            Mock _Get-VendirPath { return 'vendir' }
            Mock Get-Content { return "yaml content" }
            Mock ConvertFrom-Yaml { return @{ 'test-group' = @(@{ name = 'test-gist'; source = 'https://github.com/org/repo.git'; ref = 'main'; includePaths = @('path/to/include') }) } }
            Mock ConvertTo-Yaml { }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }

            Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath

            Should -Invoke _Get-VendirPath -Times 1
        }

        It "Should propagate exceptions from _Get-VendirPath" {
            Mock _Get-VendirPath { throw "Failed to download vendir from https://example.com: error" }

            { Get-EndjinGist -Group 'any' -Name 'any' -GistMapPath $dummyGistMapPath } | Should -Throw "*Failed to download vendir*"
        }
    }

    Context "Input Validation" {
        BeforeAll {
            Mock _Get-VendirPath { return 'vendir' }
            Mock Get-Content { return "yaml content" }
            Mock ConvertFrom-Yaml { return $mockGistMap }
        }

        It "Should throw if Group is unknown" {
            { Get-EndjinGist -Group 'unknown-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } | Should -Throw "Unknown gist group: unknown-group"
        }

        It "Should throw if Name is unknown in Group" {
            { Get-EndjinGist -Group 'test-group' -Name 'unknown-gist' -GistMapPath $dummyGistMapPath } | Should -Throw "Unknown gist 'unknown-gist' in group 'test-group'"
        }

        It "Should throw when GistMapPath does not exist" {
            { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath '/nonexistent/path/gist-map.yml' } | Should -Throw "*Cannot validate argument on parameter 'GistMapPath'*"
        }
    }

    Context "Execution" {
        BeforeAll {
            Mock _Get-VendirPath { return 'vendir' }
            Mock Get-Content { return "yaml content" }
            Mock ConvertFrom-Yaml { return $mockGistMap }
            Mock ConvertTo-Yaml { }
            Mock Remove-Item { }
            Mock Write-Host { }
        }

        It "Should invoke vendir with generated config" {
            Mock Invoke-Command { 
                $global:LASTEXITCODE = 0
                return "Success" 
            }

            Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath

            Assert-MockCalled ConvertTo-Yaml -Times 1
            Assert-MockCalled Invoke-Command -Times 1
        }

        It "Should handle vendir failure" {
             Mock Invoke-Command {
                # Simulate failure
                $global:LASTEXITCODE = 1
                return "Error message"
            }

            { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } | Should -Throw "Operation failed with exit code 1.`nError message"
        }

        It "Should throw access denied error with VSCode guidance when vendir output contains 'Access is denied'" {
            Mock Invoke-Command {
                $global:LASTEXITCODE = 1
                return "Syncing directory '.endjin/test-group/test-gist'... Access is denied"
            }

            { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } | Should -Throw "vendir sync failed with an access denied error*"
        }
    }

    Context "Vendir Config Generation" {
        BeforeAll {
            Mock _Get-VendirPath { return 'vendir' }
            Mock Get-Content { return "yaml content" }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }
            Mock Write-Host { }
        }

        It "Should set newRootPath in vendir config when gist has sourceDirectory" {
            $mockGistMapWithSourceDir = @{
                'test-group' = @(
                    @{
                        name = 'test-gist'
                        source = 'https://github.com/org/repo.git'
                        ref = 'main'
                        includePaths = @('path/to/include')
                        sourceDirectory = 'src/subdir'
                    }
                )
            }
            Mock ConvertFrom-Yaml { return $mockGistMapWithSourceDir }

            $capturedData = $null
            Mock ConvertTo-Yaml { $script:capturedData = $Data }

            Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath

            $script:capturedData.directories[0].contents[0].newRootPath | Should -Be 'src/subdir'
        }

        It "Should not set newRootPath when gist lacks sourceDirectory" {
            Mock ConvertFrom-Yaml { return $mockGistMap }

            $capturedData = $null
            Mock ConvertTo-Yaml { $script:capturedData = $Data }

            Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath

            $script:capturedData.directories[0].contents[0].Keys | Should -Not -Contain 'newRootPath'
        }
    }

    Context "MovePaths Processing" {
        BeforeAll {
            $mockGistMapWithMovePaths = @{
                'test-group' = @(
                    @{
                        name = 'test-gist'
                        source = 'https://github.com/org/repo.git'
                        ref = 'main'
                        includePaths = @('content/**/*')
                        movePaths = @(
                            @{
                                from = 'content/**/*'
                                to = '${repoRoot}/target/'
                            }
                        )
                    }
                )
            }

            Mock _Get-VendirPath { return 'vendir' }
            Mock Get-Content { return "yaml content" }
            Mock ConvertTo-Yaml { }
            Mock Write-Host { }
        }

        It "Should expand repoRoot variable in destination path" {
            Mock ConvertFrom-Yaml { return $mockGistMapWithMovePaths }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }

            # Mock git to return a specific repo root
            Mock git { return '/mock/repo/root' } -ParameterFilter { $args[0] -eq 'rev-parse' }

            # Mock Get-ChildItem to return no files (avoiding actual file operations)
            Mock Get-ChildItem { return @() }

            Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath

            # Verify the function executed without errors (variable expansion happened internally)
            Should -Invoke Invoke-Command -Times 1
        }

        It "Should not process moves if movePaths is not defined" {
            $mockGistMapWithoutMovePaths = @{
                'test-group' = @(
                    @{
                        name = 'test-gist'
                        source = 'https://github.com/org/repo.git'
                        ref = 'main'
                        includePaths = @('content/**/*')
                    }
                )
            }

            Mock ConvertFrom-Yaml { return $mockGistMapWithoutMovePaths }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }
            Mock Move-Item { }

            Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath

            # Move-Item should never be called when movePaths is not defined
            Should -Invoke Move-Item -Times 0
        }

        It "Should move files and preserve directory structure" {
            Mock ConvertFrom-Yaml { return $mockGistMapWithMovePaths }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }
            Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' }

            # Setup source files in TestDrive
            $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist/content'
            $subDir = Join-Path $sourceDir 'subfolder'
            New-Item -ItemType Directory -Path $subDir -Force | Out-Null
            Set-Content -Path (Join-Path $sourceDir 'file1.txt') -Value 'content1'
            Set-Content -Path (Join-Path $subDir 'file2.txt') -Value 'content2'

            # Run in TestDrive context
            Push-Location $TestDrive
            try {
                Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath
            }
            finally {
                Pop-Location
            }

            # Verify files were moved to target with preserved structure
            $targetFile1 = Join-Path $TestDrive 'target/file1.txt'
            $targetFile2 = Join-Path $TestDrive 'target/subfolder/file2.txt'

            Test-Path $targetFile1 | Should -Be $true
            Test-Path $targetFile2 | Should -Be $true
            # Use [System.IO.File] to bypass the Get-Content mock
            [System.IO.File]::ReadAllText($targetFile1).Trim() | Should -Be 'content1'
            [System.IO.File]::ReadAllText($targetFile2).Trim() | Should -Be 'content2'
        }

        It "Should clean up empty source directories after move" {
            Mock ConvertFrom-Yaml { return $mockGistMapWithMovePaths }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { } -ParameterFilter { $Path -like '*vendir*' -or $Path -like '*.yml' }
            Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' }

            # Setup source files in TestDrive
            $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist/content'
            New-Item -ItemType Directory -Path $sourceDir -Force | Out-Null
            Set-Content -Path (Join-Path $sourceDir 'file.txt') -Value 'content'

            Push-Location $TestDrive
            try {
                Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath
            }
            finally {
                Pop-Location
            }

            # Source directory should be cleaned up (empty after move)
            Test-Path $sourceDir | Should -Be $false
        }

        It "Should move a single file when movePaths source is a file" {
            $mockGistMapWithFileMovePath = @{
                'test-group' = @(
                    @{
                        name = 'test-gist'
                        source = 'https://github.com/org/repo.git'
                        ref = 'main'
                        includePaths = @('somefile.txt')
                        movePaths = @(
                            @{
                                from = 'somefile.txt'
                                to = '${repoRoot}/target/renamed.txt'
                            }
                        )
                    }
                )
            }

            Mock ConvertFrom-Yaml { return $mockGistMapWithFileMovePath }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }
            Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' }

            # Setup a single source file in TestDrive
            $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist'
            New-Item -ItemType Directory -Path $sourceDir -Force | Out-Null
            Set-Content -Path (Join-Path $sourceDir 'somefile.txt') -Value 'file-content'

            Push-Location $TestDrive
            try {
                Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath
            }
            finally {
                Pop-Location
            }

            # Verify the file was moved to the explicit destination
            $targetFile = Join-Path $TestDrive 'target/renamed.txt'
            Test-Path $targetFile | Should -Be $true
            [System.IO.File]::ReadAllText($targetFile).Trim() | Should -Be 'file-content'

            # Verify the source file was removed
            Test-Path (Join-Path $sourceDir 'somefile.txt') | Should -Be $false
        }

        It "Should append filename to destination when destination ends with a path separator" {
            $mockGistMapWithTrailingSlash = @{
                'test-group' = @(
                    @{
                        name = 'test-gist'
                        source = 'https://github.com/org/repo.git'
                        ref = 'main'
                        includePaths = @('somefile.txt')
                        movePaths = @(
                            @{
                                from = 'somefile.txt'
                                to = '${repoRoot}/target/'
                            }
                        )
                    }
                )
            }

            Mock ConvertFrom-Yaml { return $mockGistMapWithTrailingSlash }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }
            Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' }

            # Setup a single source file in TestDrive
            $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist'
            New-Item -ItemType Directory -Path $sourceDir -Force | Out-Null
            Set-Content -Path (Join-Path $sourceDir 'somefile.txt') -Value 'trailing-slash-content'

            Push-Location $TestDrive
            try {
                Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath
            }
            finally {
                Pop-Location
            }

            # Verify the filename was appended to the trailing-slash destination
            $targetFile = Join-Path $TestDrive 'target/somefile.txt'
            Test-Path $targetFile | Should -Be $true
            [System.IO.File]::ReadAllText($targetFile).Trim() | Should -Be 'trailing-slash-content'

            # Verify the source file was removed
            Test-Path (Join-Path $sourceDir 'somefile.txt') | Should -Be $false
        }

        It "Should skip gracefully when movePaths source path does not exist" {
            $mockGistMapWithMissingSource = @{
                'test-group' = @(
                    @{
                        name = 'test-gist'
                        source = 'https://github.com/org/repo.git'
                        ref = 'main'
                        includePaths = @('nonexistent/**/*')
                        movePaths = @(
                            @{
                                from = 'nonexistent/**/*'
                                to = '${repoRoot}/target/'
                            }
                        )
                    }
                )
            }

            Mock ConvertFrom-Yaml { return $mockGistMapWithMissingSource }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }
            Mock Move-Item { }
            Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' }

            # Ensure the source path does NOT exist in TestDrive
            Push-Location $TestDrive
            try {
                { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } | Should -Not -Throw
            }
            finally {
                Pop-Location
            }

            Should -Invoke Move-Item -Times 0
        }

        It "Should fall back to PWD when git rev-parse returns nothing" {
            Mock ConvertFrom-Yaml { return $mockGistMapWithMovePaths }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Remove-Item { }
            # Mock git to return $null (simulating no git repo)
            Mock git { return $null } -ParameterFilter { $args[0] -eq 'rev-parse' }

            # Setup source file in TestDrive
            $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist/content'
            New-Item -ItemType Directory -Path $sourceDir -Force | Out-Null
            Set-Content -Path (Join-Path $sourceDir 'file1.txt') -Value 'fallback-content'

            Push-Location $TestDrive
            try {
                Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath
            }
            finally {
                Pop-Location
            }

            # PWD was TestDrive, so ${repoRoot} should have resolved to TestDrive
            $targetFile = Join-Path $TestDrive 'target/file1.txt'
            Test-Path $targetFile | Should -Be $true
            [System.IO.File]::ReadAllText($targetFile).Trim() | Should -Be 'fallback-content'
        }
    }

    Context "Cleanup Behavior" {
        BeforeAll {
            Mock _Get-VendirPath { return 'vendir' }
            Mock Get-Content { return "yaml content" }
            Mock ConvertFrom-Yaml { return $mockGistMap }
            Mock ConvertTo-Yaml { }
            Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" }
            Mock Write-Host { }
        }

        It "Should remove temp files when not running with -Verbose" {
            Mock Remove-Item { }

            Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath

            Should -Invoke Remove-Item -Times 2
        }

        It "Should preserve temp files when running with -Verbose" {
            Mock Remove-Item { }

            Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath -Verbose

            Should -Invoke Remove-Item -Times 0
        }
    }
}