tests/Invoke-CpmfUipsPack.Tests.ps1

#Requires -Version 7
#Requires -Modules Pester

BeforeAll {
    Import-Module (Join-Path $PSScriptRoot '../CpmfUipsPack.psd1') -Force
}

Describe 'Invoke-PackAndStage' {

    BeforeEach {
        $script:tmpRoot     = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsPackTest-$(New-Guid)"
        $script:tmpFeed     = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsFeed-$(New-Guid)"
        $script:projectJson = Join-Path $script:tmpRoot 'project.json'
        $null = New-Item -ItemType Directory -Path $script:tmpRoot -Force
        Set-Content $script:projectJson '{"projectVersion": "1.0.0", "name": "TestProject"}'
    }

    AfterEach {
        Remove-Item $script:tmpRoot -Recurse -Force -ErrorAction SilentlyContinue
        Remove-Item $script:tmpFeed -Recurse -Force -ErrorAction SilentlyContinue
    }

    Context 'Successful pack' {
        It 'bumps version, returns staged nupkg path, prunes to 3 files' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                $outDir = Join-Path (Split-Path $pj) '.pack-output'
                $null = New-Item -ItemType Directory -Path $outDir -Force

                # Pre-populate 3 old nupkgs
                1..3 | ForEach-Object {
                    $f = Join-Path $outDir "Old.$_.nupkg"
                    New-Item -ItemType File -Path $f -Force | Out-Null
                    (Get-Item $f).LastWriteTime = (Get-Date).AddDays(-$_)
                }

                Mock Invoke-UipcliPack {
                    param($UipcliExe, $PackArgs)
                    $outDir = $PackArgs[$PackArgs.IndexOf('-o') + 1]
                    New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.1.0.nupkg') -Force | Out-Null
                    return 0
                }

                $result = Invoke-PackAndStage -ProjectJson $pj -FeedPath $feed -UipcliArgs @() -UipcliExe 'fake.exe'

                $result           | Should -BeLike '*.nupkg'
                Test-Path $result | Should -Be $true
                (Get-Content $pj -Raw) | Should -Match '"projectVersion":\s*"1\.1\.0"'
                (Get-ChildItem $outDir -Filter '*.nupkg').Count | Should -Be 3
            }
        }
    }

    Context 'Pack failure and version rollback' {
        It 'restores projectVersion when uipcli exits non-zero' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                Mock Invoke-UipcliPack { return 1 }

                { Invoke-PackAndStage -ProjectJson $pj -FeedPath $feed -UipcliArgs @() -UipcliExe 'fake.exe' } |
                    Should -Throw '*uipcli pack failed*'

                (Get-Content $pj -Raw) | Should -Match '"projectVersion":\s*"1\.0\.0"'
            }
        }
    }

    Context '-NoBump' {
        It 'does not change projectVersion' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                Mock Invoke-UipcliPack {
                    param($UipcliExe, $PackArgs)
                    $outDir = $PackArgs[$PackArgs.IndexOf('-o') + 1]
                    New-Item -ItemType File -Path (Join-Path $outDir 'TestProject.1.0.0.nupkg') -Force | Out-Null
                    return 0
                }

                Invoke-PackAndStage -ProjectJson $pj -FeedPath $feed -UipcliArgs @() -NoBump -UipcliExe 'fake.exe' | Out-Null
                (Get-Content $pj -Raw) | Should -Match '"projectVersion":\s*"1\.0\.0"'
            }
        }
    }
}

Describe 'Invoke-CpmfUipsPack' {

    BeforeEach {
        $script:tmpRoot     = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsPackTest-$(New-Guid)"
        $script:tmpFeed     = Join-Path ([System.IO.Path]::GetTempPath()) "CpmfUipsFeed-$(New-Guid)"
        $script:projectJson = Join-Path $script:tmpRoot 'project.json'
        $null = New-Item -ItemType Directory -Path $script:tmpRoot -Force
        Set-Content $script:projectJson '{"projectVersion": "1.0.0", "name": "TestProject"}'
    }

    AfterEach {
        Remove-Item $script:tmpRoot -Recurse -Force -ErrorAction SilentlyContinue
        Remove-Item $script:tmpFeed -Recurse -Force -ErrorAction SilentlyContinue
    }

    Context 'Orchestration' {
        It 'calls Install-CpmfUipsPackCommandLineTool unless -SkipInstall' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                Mock Install-CpmfUipsPackCommandLineTool { }
                Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } }
                Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock); & $ScriptBlock }
                Mock Invoke-PackAndStage { 'C:\feed\fake.nupkg' }

                Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed
                Should -Invoke Install-CpmfUipsPackCommandLineTool -Times 1

                Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -SkipInstall
                Should -Invoke Install-CpmfUipsPackCommandLineTool -Times 1  # still only 1 total
            }
        }

        It 'returns the result from Invoke-PackAndStage' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                Mock Install-CpmfUipsPackCommandLineTool { }
                Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } }
                Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock); & $ScriptBlock }
                Mock Invoke-PackAndStage { 'C:\feed\Test.1.1.0.nupkg' }

                $result = Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -SkipInstall
                $result | Should -Be 'C:\feed\Test.1.1.0.nupkg'
            }
        }
    }

    Context '-UseWorktree' {
        It 'does not modify the original project.json' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                Mock Install-CpmfUipsPackCommandLineTool { }
                Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } }
                Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock); & $ScriptBlock }
                Mock Get-GitWorktreePath { Join-Path ([System.IO.Path]::GetTempPath()) "wt-test-$(New-Guid)" }
                Mock Invoke-GitWorktree { param($RepoRoot, $WorktreePath, $ScriptBlock); & $ScriptBlock $WorktreePath }
                Mock Invoke-PackAndStage { 'C:\feed\fake.nupkg' }
                Mock -CommandName 'git' -MockWith { 'C:\repos\MyProject'; $global:LASTEXITCODE = 0 }

                $originalContent = Get-Content $pj -Raw
                Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -UseWorktree -SkipInstall | Out-Null
                Get-Content $pj -Raw | Should -Be $originalContent
            }
        }

        It 'always removes the worktree (success and failure)' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                Mock Install-CpmfUipsPackCommandLineTool { }
                Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } }
                Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock); & $ScriptBlock }
                Mock Get-GitWorktreePath { Join-Path ([System.IO.Path]::GetTempPath()) "wt-test-$(New-Guid)" }
                $script:removed = $false
                Mock Invoke-GitWorktree {
                    param($RepoRoot, $WorktreePath, $ScriptBlock)
                    try { & $ScriptBlock $WorktreePath } finally { $script:removed = $true }
                }
                Mock Invoke-PackAndStage { }
                Mock -CommandName 'git' -MockWith { 'C:\repos\MyProject'; $global:LASTEXITCODE = 0 }

                Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -UseWorktree -SkipInstall | Out-Null
                $script:removed | Should -Be $true
            }
        }
    }

    Context 'Prerequisite check' {
        It 'throws when git is absent and -UseWorktree is requested' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                Mock Install-CpmfUipsPackCommandLineTool { }
                Mock Test-CpmfUipsPackPrerequisites { throw 'git executable not found on PATH.' }

                { Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -UseWorktree -SkipInstall } |
                    Should -Throw '*git executable not found*'
            }
        }
    }

    Context '-ConfigFile' {
        It 'applies FeedPath from config when not supplied explicitly' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                $cfgPath = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.psd1'
                Set-Content $cfgPath "@{ FeedPath = '$feed' }"

                Mock Install-CpmfUipsPackCommandLineTool { }
                Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } }
                Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock); & $ScriptBlock }
                $script:capturedFeed = $null
                Mock Invoke-PackAndStage {
                    param($ProjectJson, $FeedPath, $UipcliArgs, $NoBump, $UipcliExe)
                    $script:capturedFeed = $FeedPath
                    'C:\feed\fake.nupkg'
                }

                Invoke-CpmfUipsPack -ProjectJson $pj -ConfigFile $cfgPath -SkipInstall | Out-Null
                $script:capturedFeed | Should -Be $feed

                Remove-Item $cfgPath -Force -ErrorAction SilentlyContinue
            }
        }

        It 'explicit parameter overrides config value' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                $cfgPath = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.psd1'
                Set-Content $cfgPath "@{ FeedPath = 'C:\should-not-be-used' }"

                Mock Install-CpmfUipsPackCommandLineTool { }
                Mock Get-CpmfUipsToolPaths { @{ UipcliExe = 'fake.exe'; DotnetDir = 'C:\fake' } }
                Mock Invoke-WithFileLock { param($LockFile, $ScriptBlock); & $ScriptBlock }
                $script:capturedFeed = $null
                Mock Invoke-PackAndStage {
                    param($ProjectJson, $FeedPath, $UipcliArgs, $NoBump, $UipcliExe)
                    $script:capturedFeed = $FeedPath
                    'C:\feed\fake.nupkg'
                }

                Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -ConfigFile $cfgPath -SkipInstall | Out-Null
                $script:capturedFeed | Should -Be $feed

                Remove-Item $cfgPath -Force -ErrorAction SilentlyContinue
            }
        }

        It 'throws when ConfigFile path does not exist' {
            InModuleScope CpmfUipsPack -Parameters @{ pj = $script:projectJson; feed = $script:tmpFeed } {
                param($pj, $feed)
                { Invoke-CpmfUipsPack -ProjectJson $pj -FeedPath $feed -ConfigFile 'C:\no-such-file.psd1' -SkipInstall } |
                    Should -Throw '*not found*'
            }
        }
    }
}