Tests/New-GitWorktree.Tests.ps1

BeforeAll {
    Import-Module "$PSScriptRoot\..\GittHelpers.psd1" -Force
}

Describe 'New-GitWorktree' {
    BeforeEach {
        Mock Get-GitRoot           { return $TestDrive } -ModuleName GittHelpers
        Mock Get-WorktreeList      { return @() }        -ModuleName GittHelpers
        Mock Get-GitAuthorSlug     { return 'snkemp' }   -ModuleName GittHelpers
        Mock Open-WorktreeSolution { }                   -ModuleName GittHelpers
        Mock Open-WorktreeTerminal { }                   -ModuleName GittHelpers
        Mock git {
            $global:LASTEXITCODE = 0
            if ($args -contains 'symbolic-ref') { return 'main' }
            if ($args -contains 'worktree' -and $args -contains '-b') {
                $bIdx = [array]::IndexOf($args, '-b')
                if ($bIdx -ge 0) { $script:BranchCreated = $args[$bIdx + 1] }
            }
        } -ModuleName GittHelpers
        Mock New-Item { } -ModuleName GittHelpers
    }

    It 'Uses feature/ prefix for -Feature parameter set' {
        { New-GitWorktree -Feature 'login' -WhatIf } | Should -Not -Throw
    }

    It 'Includes author segment in dev-path branches (feature)' {
        $script:BranchCreated = $null
        New-GitWorktree -Feature 'login' -Confirm:$false
        $script:BranchCreated | Should -Be 'feature/snkemp/login'
    }

    It 'Includes author segment in dev-path branches (bug)' {
        $script:BranchCreated = $null
        New-GitWorktree -Bug 'null-ref' -Confirm:$false
        $script:BranchCreated | Should -Be 'bug/snkemp/null-ref'
    }

    It 'Includes author segment in dev-path branches (temp)' {
        $script:BranchCreated = $null
        New-GitWorktree -Temp 'experiment' -Confirm:$false
        $script:BranchCreated | Should -Be 'temp/snkemp/experiment'
    }

    It 'Does NOT include author segment for hotfix (prod path)' {
        $script:BranchCreated = $null
        New-GitWorktree -Hotfix '2.1.1' -Confirm:$false
        $script:BranchCreated | Should -Be 'hotfix/2.1.1'
    }

    It 'Does NOT include author segment for release (prod path)' {
        $script:BranchCreated = $null
        New-GitWorktree -Release '3.0.0' -Confirm:$false
        $script:BranchCreated | Should -Be 'release/3.0.0'
    }

    It 'Suppresses author with -NoAuthor on dev paths' {
        $script:BranchCreated = $null
        New-GitWorktree -Feature 'login' -NoAuthor -Confirm:$false
        $script:BranchCreated | Should -Be 'feature/login'
    }

    It 'Builds correct branch name from -Hotfix and version (dots preserved)' {
        $slug = & (Get-Module GittHelpers) { ConvertTo-SafeBranchName '2.1.1' }
        $slug | Should -Be '2.1.1'
    }

    It 'Appends description slug to branch name' {
        $slug = & (Get-Module GittHelpers) {
            $n = ConvertTo-SafeBranchName 'login'
            $d = ConvertTo-SafeBranchName 'Add OAuth login'
            "$n-$d"
        }
        $slug | Should -Be 'login-add-oauth-login'
    }

    It 'Throws when no source branch can be found' {
        Mock git { $global:LASTEXITCODE = 1; return $null } -ModuleName GittHelpers
        { New-GitWorktree -Feature 'x' -SourceBranch 'nonexistent' } | Should -Throw
    }

    It 'Returns a result object with Path, Branch, and SourceBranch' {
        $result = New-GitWorktree -Feature 'login' -Confirm:$false
        $result.Branch       | Should -Be 'feature/snkemp/login'
        $result.SourceBranch | Should -Be 'main'
        $result.Path         | Should -Not -BeNullOrEmpty
    }
}

Describe 'New-GitWorktree parameter sets' {
    It 'All branch-type parameters are in distinct parameter sets' {
        $cmd  = Get-Command New-GitWorktree
        $sets = $cmd.ParameterSets | Select-Object -ExpandProperty Name
        $sets | Should -Contain 'Feature'
        $sets | Should -Contain 'Hotfix'
        $sets | Should -Contain 'Release'
        $sets | Should -Contain 'Bug'
        $sets | Should -Contain 'Temp'
    }
}

Describe 'New-GitWorktree -NoSolution behavior' {
    BeforeEach {
        Mock Get-GitRoot           { return $TestDrive } -ModuleName GittHelpers
        Mock Get-GitAuthorSlug     { return 'snkemp' }   -ModuleName GittHelpers
        Mock git {
            $global:LASTEXITCODE = 0
            if ($args -contains 'symbolic-ref') { return 'main' }
        } -ModuleName GittHelpers
        Mock New-Item              { } -ModuleName GittHelpers
        Mock Open-WorktreeSolution { } -ModuleName GittHelpers
        Mock Open-WorktreeTerminal { } -ModuleName GittHelpers
    }

    It 'Calls Open-WorktreeSolution when -NoSolution is not set' {
        New-GitWorktree -Feature 'login' -Confirm:$false
        Should -Invoke Open-WorktreeSolution -ModuleName GittHelpers -Times 1
    }

    It 'Does not call Open-WorktreeSolution when -NoSolution is set' {
        New-GitWorktree -Feature 'login' -NoSolution -Confirm:$false
        Should -Invoke Open-WorktreeSolution -ModuleName GittHelpers -Times 0
    }
}

Describe 'New-GitWorktree -OpenTerminal behavior' {
    BeforeEach {
        Mock Get-GitRoot           { return $TestDrive } -ModuleName GittHelpers
        Mock Get-GitAuthorSlug     { return 'snkemp' }   -ModuleName GittHelpers
        Mock git {
            $global:LASTEXITCODE = 0
            if ($args -contains 'symbolic-ref') { return 'main' }
        } -ModuleName GittHelpers
        Mock New-Item              { } -ModuleName GittHelpers
        Mock Open-WorktreeSolution { } -ModuleName GittHelpers
        Mock Open-WorktreeTerminal { } -ModuleName GittHelpers
    }

    It 'Does not call Open-WorktreeTerminal when -OpenTerminal is not set' {
        New-GitWorktree -Feature 'login' -Confirm:$false
        Should -Invoke Open-WorktreeTerminal -ModuleName GittHelpers -Times 0
    }

    It 'Calls Open-WorktreeTerminal when -OpenTerminal is set' {
        New-GitWorktree -Feature 'login' -OpenTerminal -Confirm:$false
        Should -Invoke Open-WorktreeTerminal -ModuleName GittHelpers -Times 1
    }
}

Describe 'Open-WorktreeSolution' {
    BeforeEach {
        Mock Start-Process { } -ModuleName GittHelpers
        Mock Read-Host     { return '0' } -ModuleName GittHelpers
    }

    It 'Does nothing when no solution files are found' {
        Mock Get-ChildItem { return @() } -ModuleName GittHelpers `
            -ParameterFilter { $Include -contains '*.sln' }

        & (Get-Module GittHelpers) { Open-WorktreeSolution -WorktreePath 'C:\fake' }

        Should -Invoke Start-Process -ModuleName GittHelpers -Times 0
    }

    It 'Opens single solution without prompting' {
        $sln = [PSCustomObject]@{ FullName = 'C:\fake\App.sln'; Name = 'App.sln' }
        Mock Get-ChildItem { return @($sln) } -ModuleName GittHelpers `
            -ParameterFilter { $Include -contains '*.sln' }

        & (Get-Module GittHelpers) { Open-WorktreeSolution -WorktreePath 'C:\fake' }

        Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 `
            -ParameterFilter { $FilePath -eq 'C:\fake\App.sln' }
        Should -Invoke Read-Host -ModuleName GittHelpers -Times 0
    }

    It 'Prompts user and opens selected file when multiple solutions found' {
        $slns = @(
            [PSCustomObject]@{ FullName = 'C:\fake\App.sln';   Name = 'App.sln'   },
            [PSCustomObject]@{ FullName = 'C:\fake\Tests.sln'; Name = 'Tests.sln' }
        )
        Mock Get-ChildItem { return $slns } -ModuleName GittHelpers `
            -ParameterFilter { $Include -contains '*.sln' }
        Mock Read-Host { return '1' } -ModuleName GittHelpers

        & (Get-Module GittHelpers) { Open-WorktreeSolution -WorktreePath 'C:\fake' }

        Should -Invoke Read-Host     -ModuleName GittHelpers -Times 1
        Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 `
            -ParameterFilter { $FilePath -eq 'C:\fake\Tests.sln' }
    }

    It 'Re-prompts on invalid input until a valid selection is made' {
        $slns = @(
            [PSCustomObject]@{ FullName = 'C:\fake\App.sln';   Name = 'App.sln'   },
            [PSCustomObject]@{ FullName = 'C:\fake\Tests.sln'; Name = 'Tests.sln' }
        )
        Mock Get-ChildItem { return $slns } -ModuleName GittHelpers `
            -ParameterFilter { $Include -contains '*.sln' }
        $script:ReadCount = 0
        Mock Read-Host {
            $script:ReadCount++
            if ($script:ReadCount -eq 1) { return 'nope' } else { return '0' }
        } -ModuleName GittHelpers

        & (Get-Module GittHelpers) { Open-WorktreeSolution -WorktreePath 'C:\fake' }

        Should -Invoke Read-Host     -ModuleName GittHelpers -Times 2
        Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 `
            -ParameterFilter { $FilePath -eq 'C:\fake\App.sln' }
    }
}

Describe 'Open-WorktreeTerminal' {
    BeforeEach {
        Mock Start-Process { } -ModuleName GittHelpers
    }

    It 'Uses Windows Terminal when wt is available' {
        Mock Get-Command { return [PSCustomObject]@{ Name = 'wt' } } -ModuleName GittHelpers `
            -ParameterFilter { $Name -eq 'wt' }

        & (Get-Module GittHelpers) { Open-WorktreeTerminal -WorktreePath 'C:\fake' }

        Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 `
            -ParameterFilter { $FilePath -eq 'wt' }
    }

    It 'Falls back to pwsh when wt is not available' {
        Mock Get-Command { return $null } -ModuleName GittHelpers `
            -ParameterFilter { $Name -eq 'wt' }

        & (Get-Module GittHelpers) { Open-WorktreeTerminal -WorktreePath 'C:\fake' }

        Should -Invoke Start-Process -ModuleName GittHelpers -Times 1 `
            -ParameterFilter { $FilePath -eq 'pwsh' }
    }
}

Describe 'Get-GitAuthorSlug' {
    AfterEach {
        $env:GIT_USER_SHORTNAME = $null
    }

    It 'Prefers $env:GIT_USER_SHORTNAME over everything else' {
        $env:GIT_USER_SHORTNAME = 'myshortname'
        Mock git { $global:LASTEXITCODE = 0; return 'Other Name' } -ModuleName GittHelpers

        $result = & (Get-Module GittHelpers) { Get-GitAuthorSlug }
        $result | Should -Be 'myshortname'
    }

    It 'Slugifies $env:GIT_USER_SHORTNAME' {
        $env:GIT_USER_SHORTNAME = 'Spencer Kemp'
        Mock git { $global:LASTEXITCODE = 0 } -ModuleName GittHelpers

        $result = & (Get-Module GittHelpers) { Get-GitAuthorSlug }
        $result | Should -Be 'spencer-kemp'
    }

    It 'Falls back to git user.name when GIT_USER_SHORTNAME is not set' {
        $env:GIT_USER_SHORTNAME = $null
        Mock git {
            $global:LASTEXITCODE = 0
            if ($args -contains 'user.name') { return 'snkemp' }
        } -ModuleName GittHelpers

        $result = & (Get-Module GittHelpers) { Get-GitAuthorSlug }
        $result | Should -Be 'snkemp'
    }

    It 'Falls back to $env:USERNAME when both other sources are empty' {
        $env:GIT_USER_SHORTNAME = $null
        Mock git { $global:LASTEXITCODE = 0; return $null } -ModuleName GittHelpers

        $result = & (Get-Module GittHelpers) { Get-GitAuthorSlug }
        $expected = $env:USERNAME.ToLowerInvariant() -replace '[^\w\-\.]+', '-'
        $result | Should -Be $expected
    }
}