vscode-qbuild/install.tests.ps1

BeforeAll {
    $script:installScript = Join-Path $PSScriptRoot 'install.ps1'
    . $script:installScript

    $script:originalEditor = $env:QBUILD_EDITOR
    $script:originalVersion = $env:QBUILD_MAP_TOOLS_VERSION
    $env:QBUILD_EDITOR = $null
    $env:QBUILD_MAP_TOOLS_VERSION = $null
}

AfterAll {
    $env:QBUILD_EDITOR = $script:originalEditor
    $env:QBUILD_MAP_TOOLS_VERSION = $script:originalVersion

    @(
        'Get-QBuildMapToolsNextLink'
        'Select-QBuildMapToolsTag'
        'Select-QBuildMapToolsAsset'
        'Get-QBuildMapToolsEditorPath'
        'Resolve-QBuildMapToolsEditor'
        'Install-QBuildMapTools'
    ) | ForEach-Object {
        Remove-Item -LiteralPath "Function:\$_" -ErrorAction SilentlyContinue
    }
}

Describe 'qbuild Map Tools install script' {
    It 'installs only when the script is executed or piped to Invoke-Expression' {
        $tokens = $null
        $errors = $null
        $ast = [System.Management.Automation.Language.Parser]::ParseFile(
            $script:installScript,
            [ref]$tokens,
            [ref]$errors
        )

        $errors | Should -BeNullOrEmpty
        $guard = $ast.FindAll({
                param($node)
                $node -is [System.Management.Automation.Language.IfStatementAst] -and
                $node.Clauses[0].Item1.Extent.Text -match 'InvocationName\s*-ne\s*''\.'''
            }, $true) | Select-Object -First 1

        $guard | Should -Not -BeNullOrEmpty
        $guard.Clauses[0].Item2.Extent.Text | Should -Match 'Install-QBuildMapTools'
    }

    It 'selects the highest qbuild Map Tools tag and ignores other refs' {
        $refs = @(
            [pscustomobject]@{ ref = 'refs/tags/v1.0.0' }
            [pscustomobject]@{ ref = 'refs/tags/qbuild-map-tools-v0.1.0' }
            [pscustomobject]@{ ref = 'refs/tags/qbuild-map-tools-v0.2.0' }
            [pscustomobject]@{ ref = 'refs/tags/qbuild-map-tools-v0.1.9' }
        )

        $selected = Select-QBuildMapToolsTag -Refs $refs

        $selected.Tag | Should -Be 'qbuild-map-tools-v0.2.0'
        $selected.Version | Should -Be ([version]'0.2.0')
    }

    It 'selects the requested version' {
        $refs = @(
            [pscustomobject]@{ ref = 'refs/tags/qbuild-map-tools-v0.1.0' }
            [pscustomobject]@{ ref = 'refs/tags/qbuild-map-tools-v0.1.1' }
        )

        $selected = Select-QBuildMapToolsTag -Refs $refs -Version '0.1.0'

        $selected.Tag | Should -Be 'qbuild-map-tools-v0.1.0'
    }

    It 'reports published versions when the requested version is missing' {
        $refs = @(
            [pscustomobject]@{ ref = 'refs/tags/qbuild-map-tools-v0.1.1' }
        )

        { Select-QBuildMapToolsTag -Refs $refs -Version '9.9.9' } |
            Should -Throw "*version '9.9.9' was not found*0.1.1*"
    }

    It 'rejects a version that is not major.minor.patch' {
        { Select-QBuildMapToolsTag -Refs @() -Version 'latest' } |
            Should -Throw "*major.minor.patch*"
    }

    It 'fails when no extension release exists' {
        { Select-QBuildMapToolsTag -Refs @([pscustomobject]@{ ref = 'refs/tags/v1.2.3' }) } |
            Should -Throw '*No qbuild Map Tools release was found*'
    }

    It 'reads the next GitHub page from a link header' {
        $link = '<https://api.github.com/repos/qbikez/ps-publishmap/git/matching-refs/tags/qbuild-map-tools-v?page=2>; rel="next", <https://api.github.com/repos/qbikez/ps-publishmap/git/matching-refs/tags/qbuild-map-tools-v?page=2>; rel="last"'

        Get-QBuildMapToolsNextLink -Link $link |
            Should -Be 'https://api.github.com/repos/qbikez/ps-publishmap/git/matching-refs/tags/qbuild-map-tools-v?page=2'
        Get-QBuildMapToolsNextLink -Link $null | Should -BeNullOrEmpty
    }

    It 'selects the VSIX asset from a GitHub release' {
        $release = [pscustomobject]@{
            tag_name = 'qbuild-map-tools-v0.1.1'
            assets   = @(
                [pscustomobject]@{ name = 'notes.txt'; browser_download_url = 'https://example.test/notes.txt' }
                [pscustomobject]@{
                    name                 = 'qbuild-map-tools-0.1.1.vsix'
                    browser_download_url = 'https://example.test/qbuild-map-tools-0.1.1.vsix'
                }
            )
        }

        $asset = Select-QBuildMapToolsAsset -Release $release

        $asset.Version | Should -Be ([version]'0.1.1')
        $asset.AssetName | Should -Be 'qbuild-map-tools-0.1.1.vsix'
        $asset.DownloadUrl | Should -Be 'https://example.test/qbuild-map-tools-0.1.1.vsix'
    }

    It 'fails when a release has no VSIX asset' {
        $release = [pscustomobject]@{
            tag_name = 'qbuild-map-tools-v0.1.1'
            assets   = @()
        }

        { Select-QBuildMapToolsAsset -Release $release } |
            Should -Throw '*does not include a VSIX*'
    }

    It 'returns editor install paths for VS Code, Cursor, and Insiders' {
        $paths = @(Get-QBuildMapToolsEditorPath -Name 'code' -LocalAppData 'C:\Local' -ProgramFiles 'C:\Program Files')
        $paths -contains 'C:\Local\Programs\Microsoft VS Code\bin\code.cmd' | Should -BeTrue
        $paths -contains 'C:\Program Files\Microsoft VS Code\bin\code.cmd' | Should -BeTrue

        $insiders = @(Get-QBuildMapToolsEditorPath -Name 'code-insiders' -LocalAppData 'C:\Local' -ProgramFiles 'C:\Program Files')
        $insiders -contains 'C:\Program Files\Microsoft VS Code Insiders\bin\code-insiders.cmd' | Should -BeTrue

        $cursor = @(Get-QBuildMapToolsEditorPath -Name 'cursor' -LocalAppData 'C:\Local' -ProgramFiles 'C:\Program Files')
        $cursor -contains 'C:\Local\Programs\cursor\resources\app\bin\cursor.cmd' | Should -BeTrue
    }

    It 'uses a PATH command and skips Windows app execution aliases' {
        $resolved = Resolve-QBuildMapToolsEditor -Editor 'code' -LocalAppData 'C:\Local' -ProgramFiles 'C:\Program Files' -GetCommand {
            param($Name)
            if ($Name -eq 'code') { 'C:\Users\me\AppData\Local\Microsoft\WindowsApps\code.exe' }
        } -TestEditorPath {
            param($Path)
            $Path -eq 'C:\Program Files\Microsoft VS Code\bin\code.cmd'
        }

        $resolved.Name | Should -Be 'code'
        $resolved.Command | Should -Be 'C:\Program Files\Microsoft VS Code\bin\code.cmd'
    }

    It 'resolves every detected editor when no editor is requested' {
        $resolved = @(Resolve-QBuildMapToolsEditor -GetCommand {
                param($Name)
                switch ($Name) {
                    'code' { 'C:\vscode\bin\code.cmd' }
                    'cursor' { 'C:\cursor\cursor.cmd' }
                }
            } -TestEditorPath { $false })

        ($resolved.Name -join ',') | Should -Be 'code,cursor'
    }

    It 'fails when the requested editor is not installed' {
        { Resolve-QBuildMapToolsEditor -Editor 'cursor' -GetCommand { $null } -TestEditorPath { $false } } |
            Should -Throw "*Editor 'cursor' was not found*"
    }

    It 'downloads the selected VSIX and installs it into each editor' {
        $script:savedUrl = $null
        $script:installed = @()
        $script:vsixPath = $null

        Install-QBuildMapTools -GetRelease {
            [pscustomobject]@{
                Version     = [version]'0.1.1'
                Tag         = 'qbuild-map-tools-v0.1.1'
                AssetName   = 'qbuild-map-tools-0.1.1.vsix'
                DownloadUrl = 'https://example.test/qbuild-map-tools-0.1.1.vsix'
            }
        } -ResolveEditor {
            @(
                [pscustomobject]@{ Name = 'code'; Command = 'C:\vscode\bin\code.cmd' }
                [pscustomobject]@{ Name = 'cursor'; Command = 'C:\cursor\cursor.cmd' }
            )
        } -SaveAsset {
            param($Url, $Destination)
            $script:savedUrl = $Url
            $script:vsixPath = $Destination
            Set-Content -LiteralPath $Destination -Value 'vsix'
        } -InstallExtension {
            param($Command, $VsixPath)
            $script:installed += $Command
            Test-Path -LiteralPath $VsixPath | Should -BeTrue
        }

        $script:savedUrl | Should -Be 'https://example.test/qbuild-map-tools-0.1.1.vsix'
        ($script:installed -join ',') | Should -Be 'C:\vscode\bin\code.cmd,C:\cursor\cursor.cmd'
        Test-Path -LiteralPath $script:vsixPath | Should -BeFalse
    }

    It 'removes the downloaded VSIX when installation fails' {
        $script:vsixPath = $null
        {
            Install-QBuildMapTools -GetRelease {
                [pscustomobject]@{
                    Version     = [version]'0.1.1'
                    Tag         = 'qbuild-map-tools-v0.1.1'
                    AssetName   = 'qbuild-map-tools-0.1.1.vsix'
                    DownloadUrl = 'https://example.test/qbuild-map-tools-0.1.1.vsix'
                }
            } -ResolveEditor {
                [pscustomobject]@{ Name = 'code'; Command = 'code.cmd' }
            } -SaveAsset {
                param($Url, $Destination)
                $script:vsixPath = $Destination
                Set-Content -LiteralPath $Destination -Value 'vsix'
            } -InstallExtension {
                throw 'install failed'
            }
        } | Should -Throw '*install failed*'

        Test-Path -LiteralPath $script:vsixPath | Should -BeFalse
    }

    It 'honors QBUILD_EDITOR and QBUILD_MAP_TOOLS_VERSION when parameters are omitted' {
        $env:QBUILD_EDITOR = 'cursor'
        $env:QBUILD_MAP_TOOLS_VERSION = '0.1.1'
        $script:seen = $null
        try {
            Install-QBuildMapTools -GetRelease {
                param($RequestedVersion)
                $script:seen = $RequestedVersion
                [pscustomobject]@{
                    Version     = [version]'0.1.1'
                    Tag         = 'qbuild-map-tools-v0.1.1'
                    AssetName   = 'qbuild-map-tools-0.1.1.vsix'
                    DownloadUrl = 'https://example.test/qbuild-map-tools-0.1.1.vsix'
                }
            } -ResolveEditor {
                param($Name)
                $script:seen = "$script:seen|$Name"
                [pscustomobject]@{ Name = $Name; Command = "$Name.cmd" }
            } -SaveAsset {
                param($Url, $Destination)
                Set-Content -LiteralPath $Destination -Value 'vsix'
            } -InstallExtension { }

            $script:seen | Should -Be '0.1.1|cursor'
        }
        finally {
            $env:QBUILD_EDITOR = $null
            $env:QBUILD_MAP_TOOLS_VERSION = $null
        }
    }
}