lib/rules/releases/patch_release_required/patch_release_required.Tests.ps1

#############################################################################
# Tests for Rule: patch_release_required
#############################################################################

BeforeAll {
    . "$PSScriptRoot/../../../StateModel.ps1"
    . "$PSScriptRoot/../../../ValidationRules.ps1"
    . "$PSScriptRoot/../../../RemediationActions.ps1"
    . "$PSScriptRoot/patch_release_required.ps1"
}

Describe "patch_release_required" {
    Context "Condition - AppliesWhen check-releases" {
        It "should return results when check-releases is 'error'" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state.Releases = @()
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 1
        }
        
        It "should return results when check-releases is 'warning'" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state.Releases = @()
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'warning' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 1
        }
        
        It "should return empty when check-releases is 'none'" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state.Releases = @()
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'none' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 0
        }
    }
    
    Context "Condition - Finding missing releases" {
        It "should return patch tag without release" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state.Releases = @()  # No releases
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 1
            $result[0].Version | Should -Be "v1.0.0"
        }
        
        It "should not return patch tag with release" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $releaseData = [PSCustomObject]@{
                tag_name = "v1.0.0"
                id = 123
                draft = $false
                prerelease = $false
                html_url = "https://github.com/repo/releases/tag/v1.0.0"
                target_commitish = "abc123"
                immutable = $false
            }
            $state.Releases = @([ReleaseInfo]::new($releaseData))
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 0
        }
        
        It "should not return patch tag with draft release" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $releaseData = [PSCustomObject]@{
                tag_name = "v1.0.0"
                id = 123
                draft = $true  # Draft release
                prerelease = $false
                html_url = "https://github.com/repo/releases/tag/v1.0.0"
                target_commitish = "abc123"
                immutable = $false
            }
            $state.Releases = @([ReleaseInfo]::new($releaseData))
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 0  # Should skip because draft release exists
        }
        
        It "should skip ignored versions" {
            $state = [RepositoryState]::new()
            $ignored = [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $ignored.IsIgnored = $true
            $state.Tags += $ignored
            $state.Releases = @()
            $state.IgnoreVersions = @("v1.0.0")
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 0
        }
        
        It "should return expected patch when v1 exists but v1.0.0 doesn't" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1", "refs/tags/v1", "abc123", "tag")
            $state.Releases = @()  # No releases
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 1
            $result[0].Version | Should -Be "v1.0.0"
            $result[0].SHA | Should -Be "abc123"  # Should use floating version's SHA
        }
        
        It "should return expected patch when v1.0 exists but v1.0.0 doesn't" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1.0", "refs/tags/v1.0", "def456", "tag")
            $state.Releases = @()  # No releases
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 1
            $result[0].Version | Should -Be "v1.0.0"
            $result[0].SHA | Should -Be "def456"
        }
        
        It "should skip when draft release exists for expected patch" {
            $state = [RepositoryState]::new()
            $state.Tags += [VersionRef]::new("v1", "refs/tags/v1", "abc123", "tag")
            $releaseData = [PSCustomObject]@{
                tag_name = "v1.0.0"
                id = 123
                draft = $true
                prerelease = $false
                html_url = "https://github.com/repo/releases/tag/v1.0.0"
                target_commitish = "abc123"
                immutable = $false
            }
            $state.Releases = @([ReleaseInfo]::new($releaseData))
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            $result.Count | Should -Be 0  # Should skip because draft exists
        }
        
        It "should not return duplicate results when both vX and vX.Y exist for same patch" {
            $state = [RepositoryState]::new()
            # Both v1 and v1.0 exist pointing to same commit
            $state.Tags += [VersionRef]::new("v1", "refs/tags/v1", "abc123", "tag")
            $state.Tags += [VersionRef]::new("v1.0", "refs/tags/v1.0", "abc123", "tag")
            $state.Releases = @()  # No releases
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            # Should only return ONE entry for v1.0.0, not duplicates
            $result.Count | Should -Be 1
            $result[0].Version | Should -Be "v1.0.0"
        }
        
        It "should not return duplicate results when vX.Y.Z tag and vX floating both exist" {
            $state = [RepositoryState]::new()
            # v1.0.0 patch tag exists without release
            $state.Tags += [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            # v1 floating tag also exists
            $state.Tags += [VersionRef]::new("v1", "refs/tags/v1", "abc123", "tag")
            $state.Releases = @()  # No releases
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            # Should only return ONE entry for v1.0.0 (from existing tag), not from synthetic
            $result.Count | Should -Be 1
            $result[0].Version | Should -Be "v1.0.0"
        }
        
        It "should not return duplicate results when all v0, v0.0, v0.0.0 exist without releases" {
            $state = [RepositoryState]::new()
            # Simulate the actual scenario from the bug report
            $state.Tags += [VersionRef]::new("v0", "refs/tags/v0", "sha0", "tag")
            $state.Tags += [VersionRef]::new("v0.0", "refs/tags/v0.0", "sha0", "tag")
            $state.Tags += [VersionRef]::new("v0.0.0", "refs/tags/v0.0.0", "sha0", "tag")
            $state.Releases = @()  # No releases
            $state.IgnoreVersions = @()
            
            $config = @{ 'check-releases' = 'error' }
            $result = & $Rule_PatchReleaseRequired.Condition $state $config
            
            # Should only return ONE entry for v0.0.0
            $result.Count | Should -Be 1
            $result[0].Version | Should -Be "v0.0.0"
        }
    }
    
    Context "CreateIssue" {
        It "should create issue with error severity" {
            $versionRef = [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state = [RepositoryState]::new()
            $config = @{ 
                'check-releases' = 'error'
                'check-release-immutability' = 'none'
            }
            
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            $issue.Type | Should -Be "missing_release"
            $issue.Severity | Should -Be "error"
            $issue.Message | Should -BeLike "*v1.0.0*"
            $issue.RemediationAction | Should -Not -BeNullOrEmpty
            $issue.RemediationAction.GetType().Name | Should -Be "CreateReleaseAction"
        }
        
        It "should create issue with warning severity" {
            $versionRef = [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state = [RepositoryState]::new()
            $config = @{ 
                'check-releases' = 'warning'
                'check-release-immutability' = 'none'
            }
            
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            $issue.Severity | Should -Be "warning"
        }
        
        It "should configure CreateReleaseAction to auto-publish when immutability check is error" {
            $versionRef = [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state = [RepositoryState]::new()
            $config = @{ 
                'check-releases' = 'error'
                'check-release-immutability' = 'error'
            }
            
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            $issue.RemediationAction.AutoPublish | Should -Be $true
        }
        
        It "should configure CreateReleaseAction to NOT auto-publish when immutability check is none" {
            $versionRef = [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state = [RepositoryState]::new()
            $config = @{ 
                'check-releases' = 'error'
                'check-release-immutability' = 'none'
            }
            
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            $issue.RemediationAction.AutoPublish | Should -Be $false
        }
    }
    
    Context "CreateIssue - MakeLatest integration with Test-ShouldBeLatestRelease" {
        It "should set MakeLatest=false when higher version release exists" {
            $versionRef = [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state = [RepositoryState]::new()
            
            # v2.0.0 already exists and is latest
            $existingRelease = [PSCustomObject]@{
                tag_name = "v2.0.0"
                id = 200
                draft = $false
                prerelease = $false
                html_url = "https://github.com/repo/releases/tag/v2.0.0"
                target_commitish = "def456"
                immutable = $false
            }
            $releaseInfo = [ReleaseInfo]::new($existingRelease)
            $releaseInfo.IsLatest = $true
            $state.Releases = @($releaseInfo)
            
            $config = @{ 'check-releases' = 'error' }
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            # MakeLatest should be false to prevent overwriting v2.0.0 as latest
            $issue.RemediationAction.MakeLatest | Should -Be $false
        }
        
        It "should NOT set MakeLatest (let GitHub decide) when creating highest version" {
            $versionRef = [VersionRef]::new("v2.0.0", "refs/tags/v2.0.0", "def456", "tag")
            $state = [RepositoryState]::new()
            
            # v1.0.0 already exists
            $existingRelease = [PSCustomObject]@{
                tag_name = "v1.0.0"
                id = 100
                draft = $false
                prerelease = $false
                html_url = "https://github.com/repo/releases/tag/v1.0.0"
                target_commitish = "abc123"
                immutable = $false
            }
            $releaseInfo = [ReleaseInfo]::new($existingRelease)
            $releaseInfo.IsLatest = $true
            $state.Releases = @($releaseInfo)
            
            $config = @{ 'check-releases' = 'error' }
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            # MakeLatest should be null (not explicitly set) to let GitHub make it latest
            $issue.RemediationAction.MakeLatest | Should -BeNullOrEmpty
        }
        
        It "should NOT set MakeLatest when no releases exist (first release)" {
            $versionRef = [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state = [RepositoryState]::new()
            $state.Releases = @()  # No existing releases
            
            $config = @{ 'check-releases' = 'error' }
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            # MakeLatest should be null to let GitHub make it latest
            $issue.RemediationAction.MakeLatest | Should -BeNullOrEmpty
        }
        
        It "should set MakeLatest=false when creating backport release" {
            $versionRef = [VersionRef]::new("v1.5.1", "refs/tags/v1.5.1", "backport123", "tag")
            $state = [RepositoryState]::new()
            
            # v2.5.0 is the current latest
            $existingRelease = [PSCustomObject]@{
                tag_name = "v2.5.0"
                id = 250
                draft = $false
                prerelease = $false
                html_url = "https://github.com/repo/releases/tag/v2.5.0"
                target_commitish = "sha250"
                immutable = $false
            }
            $releaseInfo = [ReleaseInfo]::new($existingRelease)
            $releaseInfo.IsLatest = $true
            $state.Releases = @($releaseInfo)
            
            $config = @{ 'check-releases' = 'error' }
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            # MakeLatest should be false to prevent overwriting v2.5.0
            $issue.RemediationAction.MakeLatest | Should -Be $false
        }
        
        It "should NOT set MakeLatest when only prerelease exists at higher version" {
            $versionRef = [VersionRef]::new("v1.0.0", "refs/tags/v1.0.0", "abc123", "tag")
            $state = [RepositoryState]::new()
            
            # v2.0.0 exists but is a prerelease (so v1.0.0 should be latest)
            $existingRelease = [PSCustomObject]@{
                tag_name = "v2.0.0"
                id = 200
                draft = $false
                prerelease = $true  # This is a prerelease
                html_url = "https://github.com/repo/releases/tag/v2.0.0"
                target_commitish = "def456"
                immutable = $false
            }
            $state.Releases = @([ReleaseInfo]::new($existingRelease))
            
            $config = @{ 'check-releases' = 'error' }
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            # MakeLatest should be null - v1.0.0 IS the highest non-prerelease
            $issue.RemediationAction.MakeLatest | Should -BeNullOrEmpty
        }
        
        It "should handle multiple existing releases correctly" {
            $versionRef = [VersionRef]::new("v1.2.0", "refs/tags/v1.2.0", "sha120", "tag")
            $state = [RepositoryState]::new()
            
            # Multiple releases exist: v1.0.0, v1.5.0, v2.0.0
            $releases = @()
            foreach ($v in @("v1.0.0", "v1.5.0", "v2.0.0")) {
                $release = [PSCustomObject]@{
                    tag_name = $v
                    id = [int]($v -replace '\D', '')
                    draft = $false
                    prerelease = $false
                    html_url = "https://github.com/repo/releases/tag/$v"
                    target_commitish = "sha$v"
                    immutable = $false
                }
                $ri = [ReleaseInfo]::new($release)
                $ri.IsLatest = ($v -eq "v2.0.0")
                $releases += $ri
            }
            $state.Releases = $releases
            
            $config = @{ 'check-releases' = 'error' }
            $issue = & $Rule_PatchReleaseRequired.CreateIssue $versionRef $state $config
            
            # Should be false - v2.0.0 is still highest
            $issue.RemediationAction.MakeLatest | Should -Be $false
        }
    }
}