tests/DFCatalog.Scoop.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/../Public/New-DFDirectory.ps1"
    . "$PSScriptRoot/../Private/DFCatalog.ps1"
    . "$PSScriptRoot/../Private/DFCatalog.Scoop.ps1"

    function New-FakeScoopRoot {
        param([string]$Root)
        $bucket = Join-Path $Root 'buckets/main/bucket'
        New-Item -ItemType Directory -Path $bucket -Force | Out-Null

        @{
            version     = '14.1.1'
            description = 'Recursively search directories for a regex pattern'
            homepage    = 'https://github.com/BurntSushi/ripgrep'
            license     = 'MIT'
        } | ConvertTo-Json | Set-Content (Join-Path $bucket 'ripgrep.json')

        @{
            version     = '10.2.0'
            description = 'A simple, fast alternative to find'
            homepage    = 'https://github.com/sharkdp/fd'
            license     = 'MIT'
        } | ConvertTo-Json | Set-Content (Join-Path $bucket 'fd.json')

        $git = Join-Path $Root 'buckets/main/.git'
        New-Item -ItemType Directory -Path (Join-Path $git 'refs/heads') -Force | Out-Null
        Set-Content (Join-Path $git 'HEAD') 'ref: refs/heads/master'
        Set-Content (Join-Path $git 'refs/heads/master') 'aaaa1111'

        $app = Join-Path $Root 'apps/ripgrep/current'
        New-Item -ItemType Directory -Path $app -Force | Out-Null
        @{ version = '14.1.0' } | ConvertTo-Json | Set-Content (Join-Path $app 'manifest.json')
    }
}

Describe 'DFCatalog.Scoop' {
    BeforeEach {
        $script:SavedXdgCache = $Env:XDG_CACHE_HOME
        $Env:XDG_CACHE_HOME = Join-Path $TestDrive 'cache'
        $script:ScoopRoot = Join-Path $TestDrive 'scoop'
        New-FakeScoopRoot -Root $script:ScoopRoot
    }
    AfterEach {
        $Env:XDG_CACHE_HOME = $script:SavedXdgCache
        Remove-Item (Join-Path $TestDrive 'scoop'), (Join-Path $TestDrive 'cache') -Recurse -Force -ErrorAction Ignore
    }

    Context 'Update-DFCatalogScoopIndex' {
        It 'builds an index file from bucket JSONs' {
            Update-DFCatalogScoopIndex -ScoopRoot $script:ScoopRoot
            $index = Join-Path $Env:XDG_CACHE_HOME 'dotforge/catalogs/scoop/index.json'
            Test-Path $index | Should -BeTrue
            $data = (Get-Content $index -Raw | ConvertFrom-Json).results
            @($data).Count | Should -Be 2
            @($data).name | Should -Contain 'ripgrep'
            @($data).name | Should -Contain 'fd'
        }

        It 'records a fingerprint key file' {
            Update-DFCatalogScoopIndex -ScoopRoot $script:ScoopRoot
            $key = Join-Path $Env:XDG_CACHE_HOME 'dotforge/catalogs/scoop/index.key'
            (Get-Content $key -Raw).Trim() | Should -Match 'aaaa1111'
        }
    }

    Context 'Build-DFCatalogScoopIndexData -IncludeRaw' {
        It 'omits the raw manifest by default so the runtime index shape is unchanged' {
            $entry = @(Build-DFCatalogScoopIndexData -ScoopRoot $script:ScoopRoot | Where-Object name -eq 'ripgrep')[0]
            $entry.PSObject.Properties.Name | Should -Not -Contain 'raw'
        }

        It 'attaches the verbatim manifest json as raw when -IncludeRaw is set' {
            # A manifest whose real repo is only in checkver, not homepage --
            # exactly the signal Phase B needs and Phase A discarded.
            @{
                version  = '1.0'
                homepage = 'https://vanity.example/tool'
                license  = 'MIT'
                checkver = @{ github = 'https://github.com/acme/tool' }
            } | ConvertTo-Json | Set-Content (Join-Path $script:ScoopRoot 'buckets/main/bucket/tool.json')

            $entry = @(Build-DFCatalogScoopIndexData -ScoopRoot $script:ScoopRoot -IncludeRaw | Where-Object name -eq 'tool')[0]

            $entry.raw | Should -Not -BeNullOrEmpty
            ($entry.raw | ConvertFrom-Json).checkver.github | Should -Be 'https://github.com/acme/tool'
        }
    }

    Context 'Search-DFCatalogScoop' {
        It 'returns an exact-name match as DotForge.ToolSourceInfo' {
            $r = @(Search-DFCatalogScoop -Query 'ripgrep' -ScoopRoot $script:ScoopRoot)
            $r.Count | Should -Be 1
            $r[0].PSObject.TypeNames[0] | Should -Be 'DotForge.ToolSourceInfo'
            $r[0].Source | Should -Be 'scoop'
            $r[0].PackageId | Should -Be 'main/ripgrep'
            $r[0].LatestVersion | Should -Be '14.1.1'
            $r[0].Homepage | Should -Be 'https://github.com/BurntSushi/ripgrep'
            $r[0].License | Should -Be 'MIT'
            $r[0].MatchKind | Should -Be 'exact-name'
        }

        It 'matches keywords against name and description' {
            $r = @(Search-DFCatalogScoop -Query 'regex pattern' -ScoopRoot $script:ScoopRoot)
            $r.Count | Should -Be 1
            $r[0].Name | Should -Be 'ripgrep'
            $r[0].MatchKind | Should -Be 'keyword'
        }

        It 'returns nothing for no match' {
            @(Search-DFCatalogScoop -Query 'zzz-nonexistent' -ScoopRoot $script:ScoopRoot) |
                Should -BeNullOrEmpty
        }

        It 'rebuilds the index when the bucket git HEAD changes' {
            $null = Search-DFCatalogScoop -Query 'ripgrep' -ScoopRoot $script:ScoopRoot

            # New tool lands in the bucket; commit hash moves.
            @{ version = '1.0'; description = 'new tool' } | ConvertTo-Json |
                Set-Content (Join-Path $script:ScoopRoot 'buckets/main/bucket/newtool.json')
            Set-Content (Join-Path $script:ScoopRoot 'buckets/main/.git/refs/heads/master') 'bbbb2222'

            $r = @(Search-DFCatalogScoop -Query 'newtool' -ScoopRoot $script:ScoopRoot)
            $r.Count | Should -Be 1
        }

        It 'serves the cached index when the fingerprint is unchanged' {
            $null = Search-DFCatalogScoop -Query 'ripgrep' -ScoopRoot $script:ScoopRoot

            # Bucket file appears WITHOUT a commit-hash change (e.g. mid-git-operation):
            # the cached index must still be used.
            @{ version = '1.0'; description = 'sneaky' } | ConvertTo-Json |
                Set-Content (Join-Path $script:ScoopRoot 'buckets/main/bucket/sneaky.json')

            @(Search-DFCatalogScoop -Query 'sneaky' -ScoopRoot $script:ScoopRoot) |
                Should -BeNullOrEmpty
        }

        It 'works without XDG_CACHE_HOME by building in memory' {
            $Env:XDG_CACHE_HOME = $null
            $r = @(Search-DFCatalogScoop -Query 'ripgrep' -ScoopRoot $script:ScoopRoot -WarningAction SilentlyContinue)
            $r.Count | Should -Be 1
        }
    }

    Context 'Get-DFCatalogScoopInstalled' {
        It 'reads installed apps and versions from the apps dir' {
            $r = @(Get-DFCatalogScoopInstalled -ScoopRoot $script:ScoopRoot)
            $r.Count | Should -Be 1
            $r[0].Name | Should -Be 'ripgrep'
            $r[0].InstalledVersion | Should -Be '14.1.0'
            $r[0].Source | Should -Be 'scoop'
        }

        It 'returns nothing when the apps dir is missing' {
            @(Get-DFCatalogScoopInstalled -ScoopRoot (Join-Path $TestDrive 'no-such')) |
                Should -BeNullOrEmpty
        }
    }

    Context 'provider registration' {
        It 'registers scoop as a snapshot provider' {
            $script:DFCatalogProviders['scoop'] | Should -Not -BeNullOrEmpty
            $script:DFCatalogProviders['scoop'].Kind | Should -Be 'snapshot'
            $script:DFCatalogProviders['scoop'].Name | Should -Be 'scoop'
        }
    }
}

Describe 'Get-DFCatalogScoopRoot' {
    It 'prefers $Env:SCOOP when set' {
        $saved = $Env:SCOOP
        $Env:SCOOP = 'C:\custom\scoop'
        try { Get-DFCatalogScoopRoot | Should -Be 'C:\custom\scoop' }
        finally { $Env:SCOOP = $saved }
    }
}