functions/_Get-GistMapData.Tests.ps1

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

Describe "_Get-GistMapData" {

    BeforeAll {
        # Define stubs for external module functions to avoid dependency and ensure mocking works
        function ConvertFrom-Yaml { param([switch]$Ordered) }
        function _Get-RemoteGistMap { }

        $sut = "$PSScriptRoot\_Get-GistMapData.ps1"
        . $sut

        $script:validGistMap = @{
            'group1' = @(
                @{ name = 'gist1'; source = 'https://example.com'; ref = 'main'; includePaths = @('**/*') }
            )
        }
    }

    BeforeEach {
        # Reset cache between tests to avoid cross-test interference
        $script:cachedMap = $null
        $script:cacheTimestamp = [datetime]::MinValue
    }

    Context "Golden Path" {
        BeforeAll {
            Mock Test-Path { return $true }
            Mock Get-Content { return "group1:`n - name: gist1" }
            Mock ConvertFrom-Yaml { return @{ 'group1' = @(@{ name = 'gist1'; source = 'https://example.com'; ref = 'main'; includePaths = @('**/*') }) } }
        }

        It "Should return parsed hashtable when file exists" {
            $result = _Get-GistMapData -ScriptRoot '/fake/path' -NoCache

            $result | Should -BeOfType [hashtable]
            $result.ContainsKey('group1') | Should -Be $true
            $result['group1'][0].name | Should -Be 'gist1'
        }

        It "Should call Get-Content with correct path" {
            # Ensure path comparison is cross-platform compatible, since the 'Join-Path' inside '_Get-GistMapData' will change the slashes
            $fakePathRoot = Join-Path '/fake' 'path'
            _Get-GistMapData -ScriptRoot $fakePathRoot -NoCache

            $fakeFullPath = Join-Path $fakePathRoot 'gist-map.yml'
            Should -Invoke Get-Content -ParameterFilter { $Path -eq $fakeFullPath }
        }

        It "Should pipe content to ConvertFrom-Yaml" {
            _Get-GistMapData -ScriptRoot '/fake/path' -NoCache

            Should -Invoke ConvertFrom-Yaml -Times 1
        }
    }

    Context "File Not Found" {
        BeforeAll {
            Mock Test-Path { return $false }
            Mock Get-Content { }
            Mock ConvertFrom-Yaml { }
        }

        It "Should return null when gist-map.yml doesn't exist" {
            $result = _Get-GistMapData -ScriptRoot '/nonexistent/path' -NoCache

            $result | Should -BeNullOrEmpty
        }

        It "Should not attempt to read file when it doesn't exist" {
            _Get-GistMapData -ScriptRoot '/nonexistent/path' -NoCache

            Should -Invoke Get-Content -Times 0
            Should -Invoke ConvertFrom-Yaml -Times 0
        }
    }

    Context "Schema Validation" {
        BeforeAll {
            Mock Test-Path { return $true }
            Mock Get-Content { return "yaml content" }
        }

        It "Should throw when gist entry is missing a required field" {
            Mock ConvertFrom-Yaml {
                return @{
                    'test-group' = @(
                        @{
                            name = 'broken-gist'
                            ref = 'main'
                            includePaths = @('**/*')
                            # 'source' is missing
                        }
                    )
                }
            }

            { _Get-GistMapData -ScriptRoot '/fake/path' -NoCache } | Should -Throw "*missing required field: source*"
        }

        It "Should accept valid gist entries without error" {
            Mock ConvertFrom-Yaml {
                return @{
                    'test-group' = @(
                        @{
                            name = 'valid-gist'
                            source = 'https://github.com/org/repo.git'
                            ref = 'main'
                            includePaths = @('**/*')
                        }
                    )
                }
            }

            { _Get-GistMapData -ScriptRoot '/fake/path' -NoCache } | Should -Not -Throw
        }
    }

    Context "Path Construction" {
        BeforeAll {
            $testScriptRoot = Join-Path $TestDrive 'module' 'functions'
            $expectedMapPath = Join-Path $testScriptRoot 'gist-map.yml'

            Mock Test-Path { return $true } -ParameterFilter { $Path -eq $expectedMapPath }
            Mock Test-Path { return $false }
            Mock Get-Content { return "yaml content" }
            Mock ConvertFrom-Yaml { return @{ 'group1' = @(@{ name = 'gist1'; source = 'https://example.com'; ref = 'main'; includePaths = @('**/*') }) } }
        }

        It "Should build correct path from ScriptRoot using Join-Path" {
            $testScriptRoot = Join-Path $TestDrive 'module' 'functions'
            $expectedMapPath = Join-Path $testScriptRoot 'gist-map.yml'

            _Get-GistMapData -ScriptRoot $testScriptRoot -NoCache

            Should -Invoke Test-Path -ParameterFilter { $Path -eq $expectedMapPath }
        }
    }

    Context "Caching" {
        BeforeAll {
            Mock Test-Path { return $true }
            Mock Get-Content { return "yaml content" }
            Mock ConvertFrom-Yaml {
                return @{
                    'group1' = @(
                        @{ name = 'gist1'; source = 'https://example.com'; ref = 'main'; includePaths = @('**/*') }
                    )
                }
            }
        }

        It "Should return cached data on subsequent calls within TTL" {
            _Get-GistMapData -ScriptRoot '/fake/path'
            _Get-GistMapData -ScriptRoot '/fake/path'

            Should -Invoke Get-Content -Times 1
        }

        It "Should re-read file when -NoCache is specified" {
            _Get-GistMapData -ScriptRoot '/fake/path'
            _Get-GistMapData -ScriptRoot '/fake/path' -NoCache

            Should -Invoke Get-Content -Times 2
        }
    }

    Context "Remote HTTP Golden Path" {
        BeforeAll {
            Mock Invoke-RestMethod { return "group1:`n - name: gist1" }
            Mock ConvertFrom-Yaml { return @{ 'group1' = @(@{ name = 'gist1'; source = 'https://example.com'; ref = 'main'; includePaths = @('**/*') }) } }
            Mock Test-Path { }
            Mock Get-Content { }
            Mock _Get-RemoteGistMap { }
        }

        It "Should return parsed data from HTTP fetch" {
            $result = _Get-GistMapData -GistMapUrl 'https://example.com/gist-map.yml' -NoCache

            $result | Should -BeOfType [hashtable]
            $result.ContainsKey('group1') | Should -Be $true
        }

        It "Should not attempt local file or vendir fetch on HTTP success" {
            _Get-GistMapData -GistMapUrl 'https://example.com/gist-map.yml' -ScriptRoot '/fake/path' -NoCache

            Should -Invoke Test-Path -Times 0
            Should -Invoke Get-Content -Times 0
            Should -Invoke _Get-RemoteGistMap -Times 0
        }
    }

    Context "HTTP Fails, Vendir Fallback Succeeds" {
        BeforeAll {
            Mock Invoke-RestMethod { throw "HTTP 404" }
            Mock _Get-RemoteGistMap { return "group1:`n - name: gist1" }
            Mock ConvertFrom-Yaml { return @{ 'group1' = @(@{ name = 'gist1'; source = 'https://example.com'; ref = 'main'; includePaths = @('**/*') }) } }
            Mock Test-Path { }
            Mock Get-Content { }
        }

        It "Should fall back to vendir when HTTP fails" {
            $gitSource = @{ url = 'https://github.com/endjin/endjin-gists.git'; ref = 'main'; path = 'module/gist-map.yml' }
            $result = _Get-GistMapData -GistMapUrl 'https://example.com/gist-map.yml' -GistMapGitSource $gitSource -NoCache

            $result | Should -BeOfType [hashtable]
            Should -Invoke _Get-RemoteGistMap -Times 1
        }

        It "Should not attempt local file fetch on vendir success" {
            $gitSource = @{ url = 'https://github.com/endjin/endjin-gists.git'; ref = 'main'; path = 'module/gist-map.yml' }
            _Get-GistMapData -GistMapUrl 'https://example.com/gist-map.yml' -GistMapGitSource $gitSource -ScriptRoot '/fake/path' -NoCache

            Should -Invoke Test-Path -Times 0
            Should -Invoke Get-Content -Times 0
        }
    }

    Context "Both Remote Methods Fail, Local Fallback Succeeds" {
        BeforeAll {
            Mock Invoke-RestMethod { throw "HTTP 404" }
            Mock _Get-RemoteGistMap { return $null }
            Mock Test-Path { return $true }
            Mock Get-Content { return "yaml content" }
            Mock ConvertFrom-Yaml { return @{ 'group1' = @(@{ name = 'gist1'; source = 'https://example.com'; ref = 'main'; includePaths = @('**/*') }) } }
        }

        It "Should fall back to local file when both remote methods fail" {
            $gitSource = @{ url = 'https://github.com/endjin/endjin-gists.git'; ref = 'main'; path = 'module/gist-map.yml' }
            $result = _Get-GistMapData -GistMapUrl 'https://example.com/gist-map.yml' -GistMapGitSource $gitSource -ScriptRoot '/fake/path' -NoCache

            $result | Should -BeOfType [hashtable]
            Should -Invoke Get-Content -Times 1
        }
    }

    Context "All Three Sources Fail" {
        BeforeAll {
            Mock Invoke-RestMethod { throw "HTTP 404" }
            Mock _Get-RemoteGistMap { return $null }
            Mock Test-Path { return $false }
            Mock Get-Content { }
            Mock ConvertFrom-Yaml { }
        }

        It "Should return null when all sources fail" {
            $gitSource = @{ url = 'https://github.com/endjin/endjin-gists.git'; ref = 'main'; path = 'module/gist-map.yml' }
            $result = _Get-GistMapData -GistMapUrl 'https://example.com/gist-map.yml' -GistMapGitSource $gitSource -ScriptRoot '/nonexistent/path' -NoCache

            $result | Should -BeNullOrEmpty
        }
    }
}