tests/DFPackageUniverse.Scoop.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/../build/Private/DFPackageUniverse.Scoop.ps1"
}

Describe 'DFPackageUniverse.Scoop' {
    Context 'ConvertTo-DFPackageUniverseScoopRow' {
        It 'maps a manifest entry to a raw_packages row' {
            $entry = [pscustomobject]@{
                name        = 'ripgrep'
                bucket      = 'main'
                version     = '14.1.1'
                description = 'Recursively search directories for a regex pattern'
                homepage    = 'https://github.com/BurntSushi/ripgrep'
                license     = 'MIT'
            }

            $row = ConvertTo-DFPackageUniverseScoopRow -Entry $entry

            $row.source | Should -Be 'scoop'
            $row.package_id | Should -Be 'main/ripgrep'
            $row.name | Should -Be 'ripgrep'
            $row.version | Should -Be '14.1.1'
            $row.description | Should -Be 'Recursively search directories for a regex pattern'
            $row.homepage | Should -Be 'https://github.com/BurntSushi/ripgrep'
            $row.license | Should -Be 'MIT'
            $row.publisher | Should -BeNullOrEmpty
            $row.tags | Should -BeNullOrEmpty
            $row.extra | Should -BeNullOrEmpty
            $row.fetched_at | Should -Not -BeNullOrEmpty
        }

        It 'captures the full manifest json (raw) into extra, preserving checkver/autoupdate' {
            $manifest = '{"version":"14.1.1","homepage":"https://vanity.example/ripgrep","license":"MIT","checkver":{"github":"https://github.com/BurntSushi/ripgrep"},"autoupdate":{"url":"https://github.com/BurntSushi/ripgrep/releases/download/v$version/x.zip"},"bin":"rg.exe"}'
            $entry = [pscustomobject]@{
                name = 'ripgrep'; bucket = 'main'; version = '14.1.1'
                description = 'd'; homepage = 'https://vanity.example/ripgrep'; license = 'MIT'
                raw = $manifest
            }

            $extra = (ConvertTo-DFPackageUniverseScoopRow -Entry $entry).extra | ConvertFrom-Json

            $extra.checkver.github | Should -Be 'https://github.com/BurntSushi/ripgrep'
            $extra.autoupdate.url  | Should -Match 'github\.com/BurntSushi/ripgrep'
            $extra.bin             | Should -Be 'rg.exe'
        }

        It 'sets extra to $null and does not throw under strict when the entry carries no raw manifest' {
            Set-StrictMode -Version Latest
            $entry = [pscustomobject]@{ name = 'fd'; bucket = 'main'; version = '10'; description = 'd'; homepage = 'h'; license = 'MIT' }
            { ConvertTo-DFPackageUniverseScoopRow -Entry $entry } | Should -Not -Throw
            (ConvertTo-DFPackageUniverseScoopRow -Entry $entry).extra | Should -BeNullOrEmpty
        }
    }

    Context 'Get-DFPackageUniverseScoopRows' {
        BeforeEach {
            $script:logged = @()
            $script:updateRan = $false
        }

        It 'invokes the ScoopUpdate seam before acquiring rows (refresh buckets first)' {
            $update = { $script:updateRan = $true }
            $fetch = { param($ScoopRoot) @([pscustomobject]@{ name = 'a'; bucket = 'main'; version = '1'; description = 'd'; homepage = 'h'; license = 'MIT' }) }
            $log = { param($Level, $PackageId, $Message) $script:logged += , @($Level, $PackageId, $Message) }

            $rows = @(Get-DFPackageUniverseScoopRows -ScoopRoot 'C:\fake\scoop' -ScoopUpdate $update -FetchItems $fetch -Log $log)

            $script:updateRan | Should -BeTrue
            $rows.Count | Should -Be 1
        }

        It 'logs a warning and still acquires rows when ScoopUpdate fails (catalog-level isolation)' {
            $update = { throw 'scoop not installed' }
            $fetch = { param($ScoopRoot) @([pscustomobject]@{ name = 'a'; bucket = 'main'; version = '1'; description = 'd'; homepage = 'h'; license = 'MIT' }) }
            $log = { param($Level, $PackageId, $Message) $script:logged += , @($Level, $PackageId, $Message) }

            $rows = @(Get-DFPackageUniverseScoopRows -ScoopRoot 'C:\fake\scoop' -ScoopUpdate $update -FetchItems $fetch -Log $log)

            $rows.Count | Should -Be 1
            @($script:logged | Where-Object { $_[0] -eq 'warning' }).Count | Should -BeGreaterThan 0
        }

        It 'does not require a ScoopUpdate seam (existing callers unaffected)' {
            $fetch = { param($ScoopRoot) @() }
            $log = { param($Level, $PackageId, $Message) $script:logged += , @($Level, $PackageId, $Message) }
            { Get-DFPackageUniverseScoopRows -ScoopRoot 'C:\fake\scoop' -FetchItems $fetch -Log $log } | Should -Not -Throw
        }

        It 'converts every fetched item to a row' {
            $items = @(
                [pscustomobject]@{ name = 'ripgrep'; bucket = 'main'; version = '14.1.1'; description = 'd1'; homepage = 'h1'; license = 'MIT' }
                [pscustomobject]@{ name = 'fd'; bucket = 'main'; version = '10.2.0'; description = 'd2'; homepage = 'h2'; license = 'MIT' }
            )
            $fetchItems = { param($ScoopRoot) $items }.GetNewClosure()
            $log = { param($Level, $PackageId, $Message) $script:logged += , @($Level, $PackageId, $Message) }

            $rows = @(Get-DFPackageUniverseScoopRows -ScoopRoot 'C:\fake\scoop' -FetchItems $fetchItems -Log $log)

            $rows.Count | Should -Be 2
            $rows.package_id | Should -Contain 'main/ripgrep'
            $rows.package_id | Should -Contain 'main/fd'
            $script:logged.Count | Should -Be 0
        }

        It 'logs an error and returns no rows when FetchItems returns nothing' {
            $fetchItems = { param($ScoopRoot) @() }
            $log = { param($Level, $PackageId, $Message) $script:logged += , @($Level, $PackageId, $Message) }

            $rows = @(Get-DFPackageUniverseScoopRows -ScoopRoot 'C:\fake\scoop' -FetchItems $fetchItems -Log $log)

            $rows.Count | Should -Be 0
            $script:logged.Count | Should -Be 1
            $script:logged[0][0] | Should -Be 'error'
        }

        It 'logs a warning and skips an entry that fails conversion, without aborting the rest' {
            Mock ConvertTo-DFPackageUniverseScoopRow {
                param($Entry)
                if ($Entry.name -eq 'broken') { throw 'boom' }
                [pscustomobject]@{
                    source = 'scoop'; package_id = "$($Entry.bucket)/$($Entry.name)"
                    name = $Entry.name; version = $Entry.version; description = $Entry.description
                    homepage = $Entry.homepage; license = $Entry.license
                    publisher = $null; tags = $null; extra = $null; fetched_at = [datetime]::UtcNow.ToString('o')
                }
            }
            $badEntry = [pscustomobject]@{ bucket = 'main'; name = 'broken' }
            $goodEntry = [pscustomobject]@{ name = 'fd'; bucket = 'main'; version = '10.2.0'; description = 'd'; homepage = 'h'; license = 'MIT' }
            $fetchItems = { param($ScoopRoot) @($badEntry, $goodEntry) }.GetNewClosure()
            $log = { param($Level, $PackageId, $Message) $script:logged += , @($Level, $PackageId, $Message) }

            $rows = @(Get-DFPackageUniverseScoopRows -ScoopRoot 'C:\fake\scoop' -FetchItems $fetchItems -Log $log)

            $rows.Count | Should -Be 1
            $rows[0].package_id | Should -Be 'main/fd'
            $script:logged.Count | Should -Be 1
            $script:logged[0][0] | Should -Be 'warning'
            $script:logged[0][1] | Should -Be 'main/broken'
        }
    }
}