tests/DFCatalog.Winget.Tests.ps1

BeforeDiscovery {
    # -Skip: expressions evaluate during discovery, before BeforeAll runs.
    $script:HasSqlite = Test-Path "$Env:SystemRoot\System32\winsqlite3.dll"
}

BeforeAll {
    . "$PSScriptRoot/../Public/New-DFDirectory.ps1"
    . "$PSScriptRoot/../Private/DFCatalog.ps1"
    . "$PSScriptRoot/../Private/Start-DFCatalogRefreshJob.ps1"
    . "$PSScriptRoot/../Private/Invoke-DFSqliteQuery.ps1"
    . "$PSScriptRoot/../Private/DFCatalog.Winget.ps1"

    $script:Fixture = Join-Path $PSScriptRoot 'fixtures/winget-index-v2.db'

    function New-FakeMsix {
        param([string]$Path)
        New-Item -ItemType Directory (Split-Path $Path) -Force | Out-Null
        if (Test-Path $Path) { Remove-Item $Path -Force }   # CreateFromDirectory cannot overwrite
        $staging = Join-Path ([System.IO.Path]::GetTempPath()) "df-msix-$(New-Guid)"
        New-Item -ItemType Directory (Join-Path $staging 'Public') -Force | Out-Null
        Copy-Item $script:Fixture (Join-Path $staging 'Public/index.db')
        Add-Type -AssemblyName System.IO.Compression.FileSystem
        [System.IO.Compression.ZipFile]::CreateFromDirectory($staging, $Path)
        Remove-Item $staging -Recurse -Force
    }
}

Describe 'DFCatalog.Winget' {
    BeforeEach {
        $script:SavedXdgCache = $Env:XDG_CACHE_HOME
        $Env:XDG_CACHE_HOME = Join-Path $TestDrive 'cache'
    }
    AfterEach {
        $Env:XDG_CACHE_HOME = $script:SavedXdgCache
        Remove-Item (Join-Path $TestDrive 'cache') -Recurse -Force -ErrorAction Ignore
    }

    Context 'index extraction' {
        It 'extracts Public/index.db from the msix and records metadata' -Skip:(-not $script:HasSqlite) {
            $msix = Join-Path $TestDrive 'winget/source.msix'
            New-FakeMsix -Path $msix
            Update-DFCatalogWingetIndex -MsixPath $msix

            $indexDb = Join-Path $Env:XDG_CACHE_HOME 'dotforge/catalogs/winget/index.db'
            Test-Path $indexDb | Should -BeTrue
            $meta = Get-Content (Join-Path $Env:XDG_CACHE_HOME 'dotforge/catalogs/winget/index.meta.json') -Raw | ConvertFrom-Json
            $meta.length | Should -Be (Get-Item $msix).Length
        }

        It 'skips re-extraction when the msix is unchanged' -Skip:(-not $script:HasSqlite) {
            $msix = Join-Path $TestDrive 'winget/source.msix'
            New-FakeMsix -Path $msix
            Update-DFCatalogWingetIndex -MsixPath $msix
            $indexDb = Join-Path $Env:XDG_CACHE_HOME 'dotforge/catalogs/winget/index.db'
            $stamp = (Get-Item $indexDb).LastWriteTimeUtc
            Start-Sleep -Milliseconds 50
            Update-DFCatalogWingetIndex -MsixPath $msix
            (Get-Item $indexDb).LastWriteTimeUtc | Should -Be $stamp
        }
    }

    Context 'Search-DFCatalogWinget (direct SQLite)' {
        It 'finds packages by exact id' -Skip:(-not $script:HasSqlite) {
            $r = @(Search-DFCatalogWinget -Query 'BurntSushi.ripgrep.MSVC' -IndexPath $script:Fixture)
            $r.Count | Should -Be 1
            $r[0].PSObject.TypeNames[0] | Should -Be 'DotForge.ToolSourceInfo'
            $r[0].Source | Should -Be 'winget'
            $r[0].PackageId | Should -Be 'BurntSushi.ripgrep.MSVC'
            $r[0].LatestVersion | Should -Be '15.1.0'
            $r[0].MatchKind | Should -Be 'exact-id'
        }

        It 'finds packages by moniker (command name)' -Skip:(-not $script:HasSqlite) {
            $r = @(Search-DFCatalogWinget -Query 'rg' -IndexPath $script:Fixture)
            $r.Count | Should -Be 1
            $r[0].PackageId | Should -Be 'BurntSushi.ripgrep.MSVC'
            $r[0].MatchKind | Should -Be 'exact-name'
        }

        It 'finds packages by keyword across id and name' -Skip:(-not $script:HasSqlite) {
            $r = @(Search-DFCatalogWinget -Query 'ripgrep' -IndexPath $script:Fixture)
            $r.Count | Should -Be 1
            $r[0].MatchKind | Should -Be 'keyword'
        }

        It 'returns nothing for no match' -Skip:(-not $script:HasSqlite) {
            @(Search-DFCatalogWinget -Query 'zzz-nonexistent' -IndexPath $script:Fixture) |
                Should -BeNullOrEmpty
        }
    }

    Context 'CLI fallback' {
        It 'parses winget.exe column output when SQLite is unavailable' {
            Mock Invoke-DFSqliteQuery { $null }
            Mock Invoke-DFCatalogWingetCli {
                @(
                    ' -',
                    ' \',
                    'Name Id Version',
                    '-----------------------------------------------',
                    'RipGrep MSVC BurntSushi.ripgrep.MSVC 15.1.0',
                    'fd sharkdp.fd 10.2.0'
                )
            }
            $r = @(Search-DFCatalogWinget -Query 'ripgrep' -Fresh)
            $r.Count | Should -Be 2
            $r[0].Name | Should -Be 'RipGrep MSVC'
            $r[0].PackageId | Should -Be 'BurntSushi.ripgrep.MSVC'
            $r[0].LatestVersion | Should -Be '15.1.0'
            $r[1].PackageId | Should -Be 'sharkdp.fd'
        }
    }

    Context 'Get-DFCatalogWingetInstalled' {
        It 'maps the V1 installed.db join to installed items' {
            Mock Invoke-DFSqliteQuery {
                [pscustomobject]@{ pkg_id = 'BurntSushi.ripgrep.MSVC'; pkg_name = 'RipGrep MSVC'; pkg_version = '15.1.0' }
            }
            Mock Get-DFCatalogWingetInstalledDbPath { Join-Path $TestDrive 'installed.db' }
            $r = @(Get-DFCatalogWingetInstalled)
            $r.Count | Should -Be 1
            $r[0].Source | Should -Be 'winget'
            $r[0].PackageId | Should -Be 'BurntSushi.ripgrep.MSVC'
            $r[0].InstalledVersion | Should -Be '15.1.0'
        }

        It 'returns nothing when no installed.db exists' {
            Mock Get-DFCatalogWingetInstalledDbPath { $null }
            @(Get-DFCatalogWingetInstalled) | Should -BeNullOrEmpty
        }
    }

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