tests/CodeCompass.GitHub.Tests.ps1

#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0' }

BeforeAll {
    $modulePath = Join-Path $PSScriptRoot '..' 'CodeCompass.GitHub.psd1'
    Import-Module $modulePath -Force
}

AfterAll {
    Remove-Module CodeCompass.GitHub -Force -ErrorAction SilentlyContinue
}

# Mock helper to simulate GitHub API responses
function New-MockRepoInfo {
    param(
        [string]$DefaultBranch = 'main',
        [bool]$HasIssues = $true,
        [bool]$HasWiki = $false,
        [bool]$DeleteOnMerge = $true,
        [bool]$AllowSquash = $true,
        [string]$Description = 'A test repository',
        [string[]]$Topics = @('test', 'powershell')
    )
    return @{
        default_branch         = $DefaultBranch
        has_issues             = $HasIssues
        has_wiki               = $HasWiki
        delete_branch_on_merge = $DeleteOnMerge
        allow_squash_merge     = $AllowSquash
        description            = $Description
        topics                 = $Topics
        visibility             = 'public'
        security_and_analysis  = @{
            secret_scanning                 = @{ status = 'enabled' }
            secret_scanning_push_protection = @{ status = 'enabled' }
            dependabot_security_updates     = @{ status = 'enabled' }
        }
    }
}

function New-MockProtection {
    param(
        [int]$Reviewers = 1,
        [bool]$DismissStale = $true,
        [bool]$StatusChecks = $true,
        [bool]$Strict = $true,
        [bool]$EnforceAdmins = $false,
        [bool]$AllowForcePush = $false,
        [bool]$AllowDelete = $false
    )
    return @{
        required_pull_request_reviews = @{
            required_approving_review_count = $Reviewers
            dismiss_stale_reviews           = $DismissStale
        }
        required_status_checks        = if ($StatusChecks) { @{ strict = $Strict; contexts = @('ci') } } else { $null }
        enforce_admins                = @{ enabled = $EnforceAdmins }
        allow_force_pushes            = @{ enabled = $AllowForcePush }
        allow_deletions               = @{ enabled = $AllowDelete }
    }
}

Describe 'Module loads correctly' {
    It 'Exports expected functions' {
        $commands = Get-Command -Module CodeCompass.GitHub
        $commands.Name | Should -Contain 'Test-CCGitHub'
        $commands.Name | Should -Contain 'Test-CCBranchProtection'
        $commands.Name | Should -Contain 'Test-CCSecurityConfig'
        $commands.Name | Should -Contain 'Test-CCRepoSettings'
        $commands.Name | Should -Contain 'Test-CCCollaboration'
        $commands.Name | Should -Contain 'Repair-CCGitHub'
        $commands.Name | Should -Contain 'Get-CCGitHubReport'
    }
}

Describe 'Standard configuration' {
    It 'Loads core standard' {
        InModuleScope CodeCompass.GitHub {
            $config = Get-CCStandardConfig -Standard 'core'
            $config | Should -Not -BeNullOrEmpty
            $config.branch_protection.require | Should -BeTrue
            $config.branch_protection.min_reviewers | Should -Be 1
        }
    }

    It 'Loads active standard with relaxed settings' {
        InModuleScope CodeCompass.GitHub {
            $config = Get-CCStandardConfig -Standard 'active'
            $config.branch_protection.dismiss_stale_reviews | Should -BeFalse
            $config.branch_protection.require_status_checks | Should -BeFalse
        }
    }

    It 'Loads minimal standard with most checks disabled' {
        InModuleScope CodeCompass.GitHub {
            $config = Get-CCStandardConfig -Standard 'minimal'
            $config.branch_protection.require | Should -BeFalse
            $config.security.secret_scanning | Should -BeFalse
        }
    }

    It 'Throws on unknown standard' {
        InModuleScope CodeCompass.GitHub {
            { Get-CCStandardConfig -Standard 'nonexistent' } | Should -Throw '*Unknown standard*'
        }
    }
}

Describe 'Token resolution' {
    It 'Uses explicit token parameter' {
        InModuleScope CodeCompass.GitHub {
            Resolve-CCToken -Token 'explicit-token' | Should -Be 'explicit-token'
        }
    }

    It 'Falls back to GITHUB_TOKEN env var' {
        $env:GITHUB_TOKEN = 'env-token'
        try {
            InModuleScope CodeCompass.GitHub {
                Resolve-CCToken | Should -Be 'env-token'
            }
        }
        finally {
            Remove-Item env:GITHUB_TOKEN -ErrorAction SilentlyContinue
        }
    }

    It 'Returns null when no token available' {
        $origGH = $env:GITHUB_TOKEN
        $origGHT = $env:GH_TOKEN
        Remove-Item env:GITHUB_TOKEN -ErrorAction SilentlyContinue
        Remove-Item env:GH_TOKEN -ErrorAction SilentlyContinue
        try {
            InModuleScope CodeCompass.GitHub {
                # Mock gh CLI to fail
                Mock gh { throw "not found" }
                Resolve-CCToken | Should -BeNullOrEmpty
            }
        }
        finally {
            if ($origGH) { $env:GITHUB_TOKEN = $origGH }
            if ($origGHT) { $env:GH_TOKEN = $origGHT }
        }
    }
}

Describe 'Check result objects' {
    It 'Creates properly structured result' {
        InModuleScope CodeCompass.GitHub {
            $result = New-CCCheckResult -CheckId 'GH-TEST-001' -Category 'Test' -Item 'thing' `
                -Status 'Pass' -Severity 'Info' -Message 'All good'
            $result.CheckId | Should -Be 'GH-TEST-001'
            $result.Category | Should -Be 'Test'
            $result.Status | Should -Be 'Pass'
            $result.FixAvailable | Should -BeFalse
        }
    }

    It 'Includes fix metadata when available' {
        InModuleScope CodeCompass.GitHub {
            $result = New-CCCheckResult -CheckId 'GH-TEST-002' -Category 'Test' -Item 'thing' `
                -Status 'Fail' -Severity 'Error' -Message 'Broken' `
                -FixAvailable $true -FixId 'FixSomething' -Current 'bad' -Expected 'good'
            $result.FixAvailable | Should -BeTrue
            $result.FixId | Should -Be 'FixSomething'
            $result.Current | Should -Be 'bad'
            $result.Expected | Should -Be 'good'
        }
    }
}

Describe 'YAML config parser' {
    It 'Parses simple key-value pairs' {
        InModuleScope CodeCompass.GitHub {
            $yaml = @"
github:
  branch_protection:
    min_reviewers: 2
    require: true
  settings:
    wiki_disabled: false
"@

            $result = ConvertFrom-CCYaml -Content $yaml
            $result.github.branch_protection.min_reviewers | Should -Be 2
            $result.github.branch_protection.require | Should -BeTrue
            $result.github.settings.wiki_disabled | Should -BeFalse
        }
    }

    It 'Ignores comments and blank lines' {
        InModuleScope CodeCompass.GitHub {
            $yaml = @"
# This is a comment
key: value

# Another comment
nested:
  sub: 42
"@

            $result = ConvertFrom-CCYaml -Content $yaml
            $result.key | Should -Be 'value'
            $result.nested.sub | Should -Be 42
        }
    }
}

Describe 'Test-CCBranchProtection (unit - with mocked API)' {
    It 'Throws when no token is available' {
        InModuleScope CodeCompass.GitHub {
            # Directly test that Resolve-CCToken returns null when no sources available
            Mock gh { $global:LASTEXITCODE = 1; return $null }
            $origGH = $env:GITHUB_TOKEN; $origGHT = $env:GH_TOKEN
            Remove-Item env:GITHUB_TOKEN -ErrorAction SilentlyContinue
            Remove-Item env:GH_TOKEN -ErrorAction SilentlyContinue
            try {
                $result = Resolve-CCToken
                $result | Should -BeNullOrEmpty
            }
            finally {
                if ($origGH) { $env:GITHUB_TOKEN = $origGH }
                if ($origGHT) { $env:GH_TOKEN = $origGHT }
            }
        }
    }

    It 'Requires -Repository or git remote' {
        { Test-CCBranchProtection -Token 'fake-token' -Repository '' } | Should -Throw
    }
}

Describe 'Test-CCRepoSettings (unit - with mocked API)' {
    It 'Requires -Repository or git remote' {
        { Test-CCRepoSettings -Token 'fake-token' -Repository '' } | Should -Throw
    }
}

Describe 'Config merge' {
    It 'Override values replace base values' {
        InModuleScope CodeCompass.GitHub {
            $base = @{ branch_protection = @{ min_reviewers = 1; require = $true } }
            $override = @{ branch_protection = @{ min_reviewers = 2 } }
            $result = Merge-CCConfig -Base $base -Override $override
            $result.branch_protection.min_reviewers | Should -Be 2
            $result.branch_protection.require | Should -BeTrue
        }
    }

    It 'Override adds new keys' {
        InModuleScope CodeCompass.GitHub {
            $base = @{ settings = @{ issues = $true } }
            $override = @{ settings = @{ wiki = $false }; extra = 'new' }
            $result = Merge-CCConfig -Base $base -Override $override
            $result.settings.issues | Should -BeTrue
            $result.settings.wiki | Should -BeFalse
            $result.extra | Should -Be 'new'
        }
    }
}