tests/Select-DFPackage.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/../Public/New-DFDirectory.ps1"
    . "$PSScriptRoot/../Private/Invoke-DFFzf.ps1"
    . "$PSScriptRoot/../Public/Invoke-DFPicker.ps1"
    . "$PSScriptRoot/../Private/DFCatalog.ps1"
    . "$PSScriptRoot/../Private/Invoke-DFSqliteQuery.ps1"
    . "$PSScriptRoot/../Private/Test-DFToolSchema.ps1"
    . "$PSScriptRoot/../Private/Import-DFToolDb.ps1"
    . "$PSScriptRoot/../Private/Get-DFCatalogInstalled.ps1"
    . "$PSScriptRoot/../Private/Get-DFCatalogLocalPackages.ps1"
    . "$PSScriptRoot/../Private/Format-DFToolInfo.ps1"
    . "$PSScriptRoot/../Public/Find-DFPackage.ps1"
    . "$PSScriptRoot/../Public/Select-DFPackage.ps1"
    . "$PSScriptRoot/../Private/Get-DFCategoryDb.ps1"

    function Initialize-LocalCaches {
        # Scoop index + one cached npm query + installed snapshot — local data only.
        Write-DFCatalogCacheFile -Path (Join-Path $Env:XDG_CACHE_HOME 'dotforge/catalogs/scoop/index.json') -Query '' -Results @(
            [pscustomobject]@{ name = 'ripgrep'; bucket = 'main'; version = '15.1.0'; description = 'regex search'; homepage = ''; license = 'MIT' }
        )
        Write-DFCatalogCacheFile -Path (Join-Path $Env:XDG_CACHE_HOME 'dotforge/catalogs/npm/queries/prettier-abc.json') -Query 'prettier' -Results @(
            [pscustomobject]@{ Source = 'npm'; PackageId = 'prettier'; Name = 'prettier'; Description = 'formatter'; LatestVersion = '3.3.0'; MatchKind = 'exact-name' }
        )
    }
}

Describe 'Get-DFCatalogLocalPackages' {
    BeforeEach {
        $script:SavedXdgCache = $Env:XDG_CACHE_HOME
        $Env:XDG_CACHE_HOME = Join-Path $TestDrive 'cache'
        Initialize-LocalCaches
        Mock Get-DFCatalogInstalled {
            @{
                Items       = @([pscustomobject]@{ Source = 'crates'; Name = 'ripgrep'; PackageId = 'ripgrep'; InstalledVersion = '14.1.1' })
                IdentityMap = @{}
            }
        }
    }
    AfterEach {
        $Env:XDG_CACHE_HOME = $script:SavedXdgCache
        Remove-Item (Join-Path $TestDrive 'cache') -Recurse -Force -ErrorAction Ignore
    }

    It 'aggregates local caches into one entry per package with merged sources' {
        $packages = @(Get-DFCatalogLocalPackages)
        $rg = $packages | Where-Object Name -eq 'ripgrep'
        $rg | Should -Not -BeNullOrEmpty
        $rg.Sources | Should -Match 'scoop'
        $rg.Sources | Should -Match 'crates'
        $rg.Description | Should -Be 'regex search'
        ($packages | Where-Object Name -eq 'prettier').Sources | Should -Match 'npm'
    }

    It 'returns nothing when no cache root is configured' {
        $Env:XDG_CACHE_HOME = $null
        @(Get-DFCatalogLocalPackages -WarningAction SilentlyContinue) | Should -BeNullOrEmpty
    }
}

Describe 'Select-DFPackage' {
    BeforeEach {
        $script:SavedXdgCache = $Env:XDG_CACHE_HOME
        $Env:XDG_CACHE_HOME = Join-Path $TestDrive 'cache'
        Initialize-LocalCaches
        Mock Get-DFCatalogInstalled { @{ Items = @(); IdentityMap = @{} } }
        function Find-DFPackage {
            [CmdletBinding()]
            param(
                [string[]]$Query,
                [string]$Category,
                [string]$WorksWith,
                [string[]]$Source,
                [switch]$AsObject,
                [switch]$Readme,
                [switch]$GitInfo
            )
        }   # stub for mocking
    }
    AfterEach {
        $Env:XDG_CACHE_HOME = $script:SavedXdgCache
        Remove-Item (Join-Path $TestDrive 'cache') -Recurse -Force -ErrorAction Ignore
    }

    It 'shows the info card for the picked package' {
        Mock Invoke-DFFzf { "ripgrep`tscoop,crates`tregex search" }
        Mock Find-DFPackage { }
        Select-DFPackage
        Should -Invoke Find-DFPackage -Times 1 -Exactly -ParameterFilter { $Query -contains 'ripgrep' }
    }

    It 'does nothing when the picker is cancelled' {
        Mock Invoke-DFFzf { $null }
        Mock Find-DFPackage { }
        Select-DFPackage
        Should -Invoke Find-DFPackage -Times 0 -Exactly
    }

    It 'warns when no local catalog data exists yet' {
        Remove-Item (Join-Path $TestDrive 'cache') -Recurse -Force
        Mock Invoke-DFFzf { $null }
        $warnings = @()
        Select-DFPackage -WarningVariable warnings -WarningAction SilentlyContinue 3>$null
        $warnings | Should -Not -BeNullOrEmpty
    }

    It 'is aliased to ftrifle' {
        Get-Alias ftrifle -ErrorAction Ignore | Should -Not -BeNullOrEmpty
    }

    Context 'query mode' {
        BeforeEach {
            Mock Find-DFPackage {
                $src = New-DFToolSourceInfo -Source scoop -PackageId 'extras/zed' -Name zed `
                    -Description 'editor' -LatestVersion '1.0' -MatchKind exact-name
                New-DFToolInfo -Name zed -Description 'editor' -Sources @($src) -MatchKind exact-name
            } -ParameterFilter { $AsObject }
        }

        It 'searches, pre-renders preview files, and passes a preview command' {
            Mock Invoke-DFFzf {
                $script:CapturedArgs = $FzfArgs
                $script:CapturedItems = $InputItems
                $script:PreviewFileExistedAtPickTime = Test-Path (($InputItems[0] -split "`t")[0])
                $null
            }
            Select-DFPackage zed
            $script:CapturedArgs -join ' ' | Should -Match '--preview'
            $fields = $script:CapturedItems[0] -split "`t"
            $fields[0] | Should -Match 'dotforge-preview.*\.txt$'  # pre-rendered card file path
            $fields[1] | Should -Be 'scoop:extras/zed' # qualified id
            $fields[2] | Should -Be 'zed'
            $script:PreviewFileExistedAtPickTime | Should -BeTrue
        }

        It 'cleans the preview temp dir after the picker exits' {
            $script:Dir = $null
            Mock Invoke-DFFzf {
                $script:Dir = Split-Path (($InputItems[0] -split "`t")[0]) -Parent
                $null
            }
            Select-DFPackage zed
            Test-Path $script:Dir | Should -BeFalse
        }

        It 'invokes Find-DFPackage with the qualified id and passthrough switches on selection' {
            Mock Invoke-DFFzf { $InputItems[0] }   # simulate picking the first row
            Mock Find-DFPackage {
                if ($AsObject) {
                    $src = New-DFToolSourceInfo -Source scoop -PackageId 'extras/zed' -Name zed `
                        -Description 'editor' -LatestVersion '1.0' -MatchKind exact-name
                    New-DFToolInfo -Name zed -Description 'editor' -Sources @($src) -MatchKind exact-name
                } else {
                    'card'
                }
            }
            Select-DFPackage zed -Readme -GitInfo
            Should -Invoke Find-DFPackage -ParameterFilter {
                (-not $AsObject) -and ($Query -contains 'scoop:extras/zed') -and $Readme -and $GitInfo
            }
        }

        It 'warns when the query has no matches' {
            Mock Find-DFPackage { @() } -ParameterFilter { $AsObject }
            $w = $null
            Select-DFPackage zzz -WarningVariable w -WarningAction SilentlyContinue
            "$w" | Should -Match 'no matches'
        }
    }

    Context '-Categories browse mode' {
        BeforeEach {
            $script:FakeDb = [pscustomobject]@{
                Raw = [pscustomobject]@{
                    taxonomy = [pscustomobject]@{ function = @('search', 'editor'); worksWith = @('text') }
                }
                FacetIndex = @{
                    'function:search' = @('ripgrep')
                    'function:editor' = @('micro')
                    'worksWith:text'  = @('ripgrep', 'micro')
                }
            }
            Mock Get-DFCategoryDb { $script:FakeDb }
        }

        It '-Categories lists function and worksWith values with counts, divided' {
            Mock Invoke-DFFzf {
                $script:CapturedItems = $InputItems
                $null   # simulate cancel — just inspect what was offered
            }
            Select-DFPackage -Categories
            $joined = $script:CapturedItems -join "`n"
            $joined | Should -Match 'search'
            $joined | Should -Match 'editor'
            $joined | Should -Match 'text'
            $joined | Should -Match '— browse by works-with —'
        }

        It 'recurses into -Category when a function value is picked' {
            Mock Invoke-DFFzf {
                ($InputItems | Where-Object { $_ -like 'search*' }) | Select-Object -First 1
            }
            Mock Find-DFPackage { @() } -ParameterFilter { $Category -eq 'search' -and $AsObject }
            Select-DFPackage -Categories
            Should -Invoke Find-DFPackage -ParameterFilter { $Category -eq 'search' }
        }

        It 'recurses into -WorksWith when a worksWith value is picked' {
            Mock Invoke-DFFzf {
                ($InputItems | Where-Object { $_ -like 'text*' }) | Select-Object -First 1
            }
            Mock Find-DFPackage { @() } -ParameterFilter { $WorksWith -eq 'text' -and $AsObject }
            Select-DFPackage -Categories
            Should -Invoke Find-DFPackage -ParameterFilter { $WorksWith -eq 'text' }
        }

        It 'does nothing when the divider row is selected' {
            Mock Invoke-DFFzf { '— browse by works-with —' }
            Mock Find-DFPackage { }
            Select-DFPackage -Categories
            Should -Invoke Find-DFPackage -Times 0 -Exactly
        }

        It '-Category <value> searches, previews, and selects exactly like query mode' {
            Mock Find-DFPackage {
                $src = New-DFToolSourceInfo -Source scoop -PackageId 'main/ripgrep' -Name ripgrep `
                    -Description 'search tool' -LatestVersion '14.1.0' -MatchKind exact-name
                New-DFToolInfo -Name ripgrep -Description 'search tool' -Sources @($src) -MatchKind exact-name
            } -ParameterFilter { $Category -eq 'search' -and $AsObject }
            Mock Invoke-DFFzf { $InputItems[0] }
            Mock Find-DFPackage { 'card' } -ParameterFilter { -not $AsObject }
            Select-DFPackage -Category search
            Should -Invoke Find-DFPackage -ParameterFilter { (-not $AsObject) -and ($Query -contains 'scoop:main/ripgrep') }
        }

        It 'warns when -Category <value> has no matches' {
            Mock Find-DFPackage { @() } -ParameterFilter { $Category -eq 'search' -and $AsObject }
            $w = $null
            Select-DFPackage -Category search -WarningVariable w -WarningAction SilentlyContinue
            "$w" | Should -Match "category 'search'"
        }

        It 'warns when the category database is unavailable' {
            Mock Get-DFCategoryDb { [pscustomobject]@{ Raw = $null; FacetIndex = @{} } }
            $w = $null
            Select-DFPackage -Categories -WarningVariable w -WarningAction SilentlyContinue
            "$w" | Should -Match 'unavailable'
        }
    }
}