Tests/Infra-RunbookEngine.Tests.ps1

BeforeAll {
    $modulePath = Split-Path -Path $PSScriptRoot -Parent
    Import-Module $modulePath -Force

    # Set up test directories
    $script:testRoot = Join-Path $env:TEMP "RunbookEngineTests_$(Get-Random)"
    $script:testExecPath = Join-Path $script:testRoot 'executions'
    $script:testLearningsPath = Join-Path $script:testRoot 'learnings.json'
    $script:testRunbooksPath = Join-Path $script:testRoot 'runbooks'
    $script:testMaintenancePath = Join-Path $script:testRoot 'maintenance-windows.json'

    New-Item -Path $script:testExecPath -ItemType Directory -Force | Out-Null
    New-Item -Path $script:testRunbooksPath -ItemType Directory -Force | Out-Null
    '[]' | Set-Content -Path $script:testLearningsPath -Encoding UTF8

    # Create a valid test runbook
    $script:validRunbook = @"
name: Test Runbook
version: "1.0"
description: A test runbook for unit testing
trigger:
  metric: test_metric
  threshold: 50
 
parameters:
  - name: ComputerName
    required: true
  - name: TestParam
    default: default_value
 
steps:
  - id: step_one
    action: script
    description: First test step
    script: |
      Write-Output "Testing on ComputerName"
    outputs:
      - test_output
 
  - id: step_decision
    action: decision
    description: Make a decision
    condition: "1 -eq 1"
    if_true: step_true_path
    if_false: step_false_path
 
  - id: step_true_path
    action: notify
    description: True path notification
    message: "Decision was true for ComputerName"
 
  - id: step_false_path
    action: escalate
    description: False path escalation
    priority: medium
    message: "Decision was false"
"@


    $script:validRunbookPath = Join-Path $script:testRunbooksPath 'test-runbook.yml'
    $script:validRunbook | Set-Content -Path $script:validRunbookPath -Encoding UTF8

    # Create invalid runbook (no name)
    $script:invalidRunbookNoName = @"
version: "1.0"
steps:
  - id: step1
    action: script
    description: test
    script: |
      Write-Output "test"
"@


    $script:invalidRunbookNoNamePath = Join-Path $script:testRunbooksPath 'invalid-no-name.yml'
    $script:invalidRunbookNoName | Set-Content -Path $script:invalidRunbookNoNamePath -Encoding UTF8

    # Create invalid runbook (no steps)
    $script:invalidRunbookNoSteps = @"
name: Bad Runbook
version: "1.0"
"@


    $script:invalidRunbookNoStepsPath = Join-Path $script:testRunbooksPath 'invalid-no-steps.yml'
    $script:invalidRunbookNoSteps | Set-Content -Path $script:invalidRunbookNoStepsPath -Encoding UTF8

    # Create invalid runbook (step missing action)
    $script:invalidRunbookNoAction = @"
name: Bad Runbook 2
steps:
  - id: step1
    description: missing action field
"@


    $script:invalidRunbookNoActionPath = Join-Path $script:testRunbooksPath 'invalid-no-action.yml'
    $script:invalidRunbookNoAction | Set-Content -Path $script:invalidRunbookNoActionPath -Encoding UTF8

    # Create a runbook with approval step
    $script:approvalRunbook = @"
name: Approval Test Runbook
version: "1.0"
description: Tests approval flow
 
parameters:
  - name: ComputerName
    required: true
 
steps:
  - id: do_stuff
    action: script
    description: Do something that needs approval
    requires_approval: true
    blast_radius: single_service
    script: |
      Write-Output "Approved action"
"@


    $script:approvalRunbookPath = Join-Path $script:testRunbooksPath 'approval-test.yml'
    $script:approvalRunbook | Set-Content -Path $script:approvalRunbookPath -Encoding UTF8

    # Create a runbook with integration step
    $script:integrationRunbook = @"
name: Integration Test Runbook
version: "1.0"
description: Tests integration steps
 
parameters:
  - name: ComputerName
    required: true
 
steps:
  - id: integrate
    action: integration
    description: Call another module
    module: FakeModule
    function: Get-FakeData
    parameters:
      ComputerName: ComputerName
    outputs:
      - fake_data
 
  - id: done
    action: notify
    description: Done
    message: "Integration complete"
"@


    $script:integrationRunbookPath = Join-Path $script:testRunbooksPath 'integration-test.yml'
    $script:integrationRunbook | Set-Content -Path $script:integrationRunbookPath -Encoding UTF8
}

AfterAll {
    if (Test-Path $script:testRoot) {
        Remove-Item -Path $script:testRoot -Recurse -Force -ErrorAction SilentlyContinue
    }
}

Describe 'Module Structure' {
    It 'Should import the module without errors' {
        { Import-Module (Split-Path -Path $PSScriptRoot -Parent) -Force } | Should -Not -Throw
    }

    It 'Should export Invoke-Runbook' {
        Get-Command -Module 'Infra-RunbookEngine' -Name 'Invoke-Runbook' | Should -Not -BeNullOrEmpty
    }

    It 'Should export New-Runbook' {
        Get-Command -Module 'Infra-RunbookEngine' -Name 'New-Runbook' | Should -Not -BeNullOrEmpty
    }

    It 'Should export Get-RunbookStatus' {
        Get-Command -Module 'Infra-RunbookEngine' -Name 'Get-RunbookStatus' | Should -Not -BeNullOrEmpty
    }

    It 'Should export Get-CIHealthScore' {
        Get-Command -Module 'Infra-RunbookEngine' -Name 'Get-CIHealthScore' | Should -Not -BeNullOrEmpty
    }

    It 'Should export Get-ShiftHandoff' {
        Get-Command -Module 'Infra-RunbookEngine' -Name 'Get-ShiftHandoff' | Should -Not -BeNullOrEmpty
    }

    It 'Should not export private functions' {
        Get-Command -Module 'Infra-RunbookEngine' -Name 'Read-RunbookDefinition' -ErrorAction SilentlyContinue |
            Should -BeNullOrEmpty
    }

    It 'Should have exactly 5 public functions' {
        $commands = Get-Command -Module 'Infra-RunbookEngine'
        $commands.Count | Should -Be 5
    }
}

Describe 'Read-RunbookDefinition - YAML Parsing' {
    It 'Should parse a valid runbook YAML file' {
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:validRunbookPath } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result | Should -Not -BeNullOrEmpty
        $result.Name | Should -Be 'Test Runbook'
    }

    It 'Should parse version field' {
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:validRunbookPath } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Version | Should -Be '1.0'
    }

    It 'Should parse description field' {
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:validRunbookPath } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Description | Should -Be 'A test runbook for unit testing'
    }

    It 'Should parse all steps' {
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:validRunbookPath } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Steps.Count | Should -BeGreaterOrEqual 3
    }

    It 'Should parse step id and action fields' {
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:validRunbookPath } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $firstStep = $result.Steps[0]
        $firstStep.id | Should -Be 'step_one'
        $firstStep.action | Should -Be 'script'
    }

    It 'Should throw for missing file' {
        {
            InModuleScope 'Infra-RunbookEngine' {
                Read-RunbookDefinition -Path 'C:\nonexistent\fake.yml'
            }
        } | Should -Throw '*not found*'
    }

    It 'Should throw for runbook without name' {
        {
            InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:invalidRunbookNoNamePath } {
                param($Path)
                Read-RunbookDefinition -Path $Path
            }
        } | Should -Throw "*'name' field is required*"
    }

    It 'Should throw for runbook without steps' {
        {
            InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:invalidRunbookNoStepsPath } {
                param($Path)
                Read-RunbookDefinition -Path $Path
            }
        } | Should -Throw "*'steps' field is required*"
    }

    It 'Should throw for step without action field' {
        {
            InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:invalidRunbookNoActionPath } {
                param($Path)
                Read-RunbookDefinition -Path $Path
            }
        } | Should -Throw "*must have an 'action' field*"
    }

    It 'Should parse parameters list' {
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:validRunbookPath } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Parameters | Should -Not -BeNullOrEmpty
    }

    It 'Should return source path' {
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $script:validRunbookPath } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.SourcePath | Should -Be $script:validRunbookPath
    }
}

Describe 'Decision Tree Navigation' {
    It 'Should follow if_true path when condition is true' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf

        $decisionStep = $result.StepResults | Where-Object { $_.StepId -eq 'step_decision' }
        $decisionStep | Should -Not -BeNullOrEmpty
    }

    It 'Should execute the true-path step after a true decision' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf

        $trueStep = $result.StepResults | Where-Object { $_.StepId -eq 'step_true_path' }
        $trueStep | Should -Not -BeNullOrEmpty
    }

    It 'Should execute the true-path step before the false-path step in WhatIf' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf

        $trueIdx = ($result.StepResults | ForEach-Object { $_.StepId }).IndexOf('step_true_path')
        $trueIdx | Should -BeGreaterOrEqual 0

        # The true path should appear in the results (decision followed true branch)
        $decisionStep = $result.StepResults | Where-Object { $_.StepId -eq 'step_decision' }
        $decisionStep | Should -Not -BeNullOrEmpty
    }

    It 'Should record step durations' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf

        foreach ($step in $result.StepResults) {
            $step.Duration | Should -Match '\d+\.\d+s'
        }
    }
}

Describe 'Test-BlastRadius' {
    It 'Should return Low level for unknown servers when step has single_service blast radius' {
        $step = [PSCustomObject]@{ blast_radius = 'single_service'; requires_approval = $false }
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Step = $step } {
            param($Step)
            Test-BlastRadius -ComputerName 'UNKNOWNSERVER' -Action 'Test action' -RunbookStep $Step
        }
        $result.Level | Should -Be 'Low'
    }

    It 'Should return Medium level for single_server blast radius' {
        $step = [PSCustomObject]@{ blast_radius = 'single_server'; requires_approval = $true }
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Step = $step } {
            param($Step)
            Test-BlastRadius -ComputerName 'TESTSERVER' -Action 'Test action' -RunbookStep $Step
        }
        $result.Level | Should -Be 'Medium'
    }

    It 'Should set RequiresApproval to true for High blast radius' {
        $step = [PSCustomObject]@{ blast_radius = 'multi_server'; requires_approval = $true }
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Step = $step } {
            param($Step)
            Test-BlastRadius -ComputerName 'TESTSERVER' -Action 'Test action' -RunbookStep $Step
        }
        $result.RequiresApproval | Should -Be $true
    }

    It 'Should set Critical level for domain_wide blast radius' {
        $step = [PSCustomObject]@{ blast_radius = 'domain_wide'; requires_approval = $true }
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Step = $step } {
            param($Step)
            Test-BlastRadius -ComputerName 'DC01' -Action 'Domain action' -RunbookStep $Step
        }
        $result.Level | Should -Be 'Critical'
    }

    It 'Should include ComputerName in the result' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Test-BlastRadius -ComputerName 'MYSERVER' -Action 'Test'
        }
        $result.ComputerName | Should -Be 'MYSERVER'
    }

    It 'Should include AssessedAt timestamp' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Test-BlastRadius -ComputerName 'MYSERVER' -Action 'Test'
        }
        $result.AssessedAt | Should -Not -BeNullOrEmpty
    }

    It 'Should set RequiresApproval when step requires_approval is true' {
        $step = [PSCustomObject]@{ blast_radius = 'single_service'; requires_approval = $true }
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Step = $step } {
            param($Step)
            Test-BlastRadius -ComputerName 'TESTSERVER' -Action 'Test' -RunbookStep $Step
        }
        $result.RequiresApproval | Should -Be $true
    }
}

Describe 'Test-MaintenanceWindow' {
    BeforeEach {
        $script:testMWPath = Join-Path $script:testRoot 'mw-test.json'
    }

    It 'Should return false when no maintenance windows configured' {
        '[]' | Set-Content -Path $script:testMWPath -Encoding UTF8
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ConfigPath = $script:testMWPath } {
            param($ConfigPath)
            Test-MaintenanceWindow -ComputerName 'SERVER01' -ConfigPath $ConfigPath
        }
        $result.InWindow | Should -Be $false
    }

    It 'Should return true when computer is in a one-time window' {
        $now = Get-Date
        $windows = @(
            @{
                ComputerName = 'SERVER01'
                Name         = 'Test Window'
                Start        = $now.AddHours(-1).ToString('o')
                End          = $now.AddHours(1).ToString('o')
                Recurring    = $false
            }
        )
        $windows | ConvertTo-Json -Depth 5 | Set-Content -Path $script:testMWPath -Encoding UTF8

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ConfigPath = $script:testMWPath } {
            param($ConfigPath)
            Test-MaintenanceWindow -ComputerName 'SERVER01' -ConfigPath $ConfigPath
        }
        $result.InWindow | Should -Be $true
    }

    It 'Should return false when one-time window has expired' {
        $now = Get-Date
        $windows = @(
            @{
                ComputerName = 'SERVER01'
                Name         = 'Expired Window'
                Start        = $now.AddHours(-3).ToString('o')
                End          = $now.AddHours(-1).ToString('o')
                Recurring    = $false
            }
        )
        $windows | ConvertTo-Json -Depth 5 | Set-Content -Path $script:testMWPath -Encoding UTF8

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ConfigPath = $script:testMWPath } {
            param($ConfigPath)
            Test-MaintenanceWindow -ComputerName 'SERVER01' -ConfigPath $ConfigPath
        }
        $result.InWindow | Should -Be $false
    }

    It 'Should match using Pattern wildcard' {
        $now = Get-Date
        $windows = @(
            @{
                Pattern   = 'WEB*'
                Name      = 'Web Server Window'
                Start     = $now.AddHours(-1).ToString('o')
                End       = $now.AddHours(1).ToString('o')
                Recurring = $false
            }
        )
        $windows | ConvertTo-Json -Depth 5 | Set-Content -Path $script:testMWPath -Encoding UTF8

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ConfigPath = $script:testMWPath } {
            param($ConfigPath)
            Test-MaintenanceWindow -ComputerName 'WEB01' -ConfigPath $ConfigPath
        }
        $result.InWindow | Should -Be $true
    }

    It 'Should return false when config file does not exist' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Test-MaintenanceWindow -ComputerName 'SERVER01' -ConfigPath 'C:\nonexistent\fake.json'
        }
        $result.InWindow | Should -Be $false
    }

    It 'Should handle recurring window on correct day' {
        $now = Get-Date
        $today = $now.DayOfWeek.ToString()
        $windows = @(
            @{
                ComputerName = 'SERVER01'
                Name         = 'Weekly Maintenance'
                Start        = $now.AddHours(-1).ToString('HH:mm')
                End          = $now.AddHours(1).ToString('HH:mm')
                Recurring    = $true
                DayOfWeek    = $today
            }
        )
        $windows | ConvertTo-Json -Depth 5 | Set-Content -Path $script:testMWPath -Encoding UTF8

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ConfigPath = $script:testMWPath } {
            param($ConfigPath)
            Test-MaintenanceWindow -ComputerName 'SERVER01' -ConfigPath $ConfigPath
        }
        $result.InWindow | Should -Be $true
        $result.IsRecurring | Should -Be $true
    }
}

Describe 'Get-CIHealthScore - Health Score Calculation' {
    It 'Should return a score between 0 and 100' {
        $result = Get-CIHealthScore -ComputerName 'TESTPC'
        $result.Score | Should -BeGreaterOrEqual 0
        $result.Score | Should -BeLessOrEqual 100
    }

    It 'Should return a valid grade (A-F)' {
        $result = Get-CIHealthScore -ComputerName 'TESTPC'
        $result.Grade | Should -BeIn @('A', 'B', 'C', 'D', 'F')
    }

    It 'Should include ComputerName in result' {
        $result = Get-CIHealthScore -ComputerName 'TESTPC'
        $result.ComputerName | Should -Be 'TESTPC'
    }

    It 'Should return A grade for score 90-100' {
        $result = Get-CIHealthScore -ComputerName 'TESTPC'
        if ($result.Score -ge 90) {
            $result.Grade | Should -Be 'A'
        }
        elseif ($result.Score -ge 80) {
            $result.Grade | Should -Be 'B'
        }
        # Just verify the grading logic works
        $result.Grade | Should -Not -BeNullOrEmpty
    }

    It 'Should include Trend field' {
        $result = Get-CIHealthScore -ComputerName 'TESTPC'
        $result.Trend | Should -BeIn @('Improving', 'Declining', 'Stable')
    }

    It 'Should include DaysBack parameter in result' {
        $result = Get-CIHealthScore -ComputerName 'TESTPC' -DaysBack 7
        $result.DaysBack | Should -Be 7
    }

    It 'Should include AssessedAt timestamp' {
        $result = Get-CIHealthScore -ComputerName 'TESTPC'
        $result.AssessedAt | Should -Not -BeNullOrEmpty
    }

    It 'Should handle multiple computers' {
        $results = Get-CIHealthScore -ComputerName 'TESTPC1', 'TESTPC2'
        @($results).Count | Should -Be 2
    }

    It 'Should subtract points for failed runbook executions' {
        # Create a fake failed execution
        $fakeExec = @{
            ExecutionId = [guid]::NewGuid().ToString()
            RunbookName = 'test'
            ComputerName = 'FAILPC'
            Status = 'Failed'
            StartTime = (Get-Date).ToString('o')
        }
        $fakeExecPath = Join-Path $script:testExecPath "$($fakeExec.ExecutionId).json"
        $fakeExec | ConvertTo-Json -Depth 5 | Set-Content -Path $fakeExecPath -Encoding UTF8

        $result = Get-CIHealthScore -ComputerName 'FAILPC' -IncludeRunbookHistory
        # The score should be less than perfect if failure is detected
        # (depends on execution path finding the test directory)
        $result.Score | Should -BeLessOrEqual 100
    }
}

Describe 'Update-RunbookLearning - Learning Loop' {
    It 'Should record a successful step' {
        $testLearnPath = Join-Path $script:testRoot 'test-learnings.json'
        '[]' | Set-Content -Path $testLearnPath -Encoding UTF8

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ LearnPath = $testLearnPath } {
            param($LearnPath)
            Update-RunbookLearning -RunbookName 'test-rb' -StepId 'step1' -Action 'script' `
                -Succeeded $true -ComputerName 'SVR01' -LearningsPath $LearnPath
        }
        $result.Successes | Should -Be 1
        $result.TotalRuns | Should -Be 1
        $result.SuccessRate | Should -Be 100.0
    }

    It 'Should record a failed step' {
        $testLearnPath = Join-Path $script:testRoot 'test-learnings2.json'
        '[]' | Set-Content -Path $testLearnPath -Encoding UTF8

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ LearnPath = $testLearnPath } {
            param($LearnPath)
            Update-RunbookLearning -RunbookName 'test-rb' -StepId 'step1' -Action 'script' `
                -Succeeded $false -ComputerName 'SVR01' -Context @{ Error = 'Test error' } -LearningsPath $LearnPath
        }
        $result.Failures | Should -Be 1
        $result.SuccessRate | Should -Be 0.0
    }

    It 'Should accumulate runs on existing entries' {
        $testLearnPath = Join-Path $script:testRoot 'test-learnings3.json'
        '[]' | Set-Content -Path $testLearnPath -Encoding UTF8

        InModuleScope 'Infra-RunbookEngine' -Parameters @{ LearnPath = $testLearnPath } {
            param($LearnPath)
            Update-RunbookLearning -RunbookName 'test-rb' -StepId 'step1' -Action 'script' `
                -Succeeded $true -ComputerName 'SVR01' -LearningsPath $LearnPath
        }

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ LearnPath = $testLearnPath } {
            param($LearnPath)
            Update-RunbookLearning -RunbookName 'test-rb' -StepId 'step1' -Action 'script' `
                -Succeeded $false -ComputerName 'SVR01' -LearningsPath $LearnPath
        }

        $result.TotalRuns | Should -Be 2
        $result.Successes | Should -Be 1
        $result.Failures | Should -Be 1
        $result.SuccessRate | Should -Be 50.0
    }

    It 'Should track failure patterns' {
        $testLearnPath = Join-Path $script:testRoot 'test-learnings4.json'
        '[]' | Set-Content -Path $testLearnPath -Encoding UTF8

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ LearnPath = $testLearnPath } {
            param($LearnPath)
            Update-RunbookLearning -RunbookName 'test-rb' -StepId 'step1' -Action 'script' `
                -Succeeded $false -ComputerName 'SVR01' -Context @{ Error = 'Connection refused' } -LearningsPath $LearnPath
        }

        $result.FailurePatterns | Should -Not -BeNullOrEmpty
        @($result.FailurePatterns).Count | Should -BeGreaterOrEqual 1
    }

    It 'Should persist learnings to JSON file' {
        $testLearnPath = Join-Path $script:testRoot 'test-learnings5.json'
        '[]' | Set-Content -Path $testLearnPath -Encoding UTF8

        InModuleScope 'Infra-RunbookEngine' -Parameters @{ LearnPath = $testLearnPath } {
            param($LearnPath)
            Update-RunbookLearning -RunbookName 'persist-test' -StepId 'step1' -Action 'script' `
                -Succeeded $true -ComputerName 'SVR01' -LearningsPath $LearnPath
        }

        $content = Get-Content -Path $testLearnPath -Raw | ConvertFrom-Json
        @($content).Count | Should -BeGreaterOrEqual 1
        @($content)[0].RunbookName | Should -Be 'persist-test'
    }
}

Describe 'Find-CorrelatedIssues - Correlation Detection' {
    BeforeEach {
        $script:testCorrelationPath = Join-Path $script:testRoot 'correlation-execs'
        if (Test-Path $script:testCorrelationPath) {
            Remove-Item $script:testCorrelationPath -Recurse -Force
        }
        New-Item -Path $script:testCorrelationPath -ItemType Directory -Force | Out-Null
    }

    It 'Should return no correlations when no executions exist' {
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ExecPath = $script:testCorrelationPath } {
            param($ExecPath)
            Find-CorrelatedIssues -ComputerName 'SVR01' -Symptom 'test' -ExecutionsPath $ExecPath
        }
        $result.CorrelationsFound | Should -Be 0
    }

    It 'Should detect DNS + replication correlation pattern' {
        $execs = @(
            @{
                ExecutionId = [guid]::NewGuid().ToString()
                RunbookName = 'dns-resolution'
                ComputerName = 'DC01'
                Status = 'Failed'
                StartTime = (Get-Date).AddMinutes(-10).ToString('o')
            }
            @{
                ExecutionId = [guid]::NewGuid().ToString()
                RunbookName = 'replication-failure'
                ComputerName = 'DC01'
                Status = 'Failed'
                StartTime = (Get-Date).AddMinutes(-5).ToString('o')
            }
        )

        foreach ($exec in $execs) {
            $path = Join-Path $script:testCorrelationPath "$($exec.ExecutionId).json"
            $exec | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8
        }

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ExecPath = $script:testCorrelationPath } {
            param($ExecPath)
            Find-CorrelatedIssues -ComputerName 'DC01' -Symptom 'DNS' -ExecutionsPath $ExecPath
        }
        $result.CorrelationsFound | Should -BeGreaterOrEqual 1
        $result.PossibleRootCause | Should -Not -BeNullOrEmpty
    }

    It 'Should return no correlation for unrelated runbooks' {
        $exec = @{
            ExecutionId = [guid]::NewGuid().ToString()
            RunbookName = 'disk-space'
            ComputerName = 'FILE01'
            Status = 'Completed'
            StartTime = (Get-Date).AddMinutes(-10).ToString('o')
        }
        $path = Join-Path $script:testCorrelationPath "$($exec.ExecutionId).json"
        $exec | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ExecPath = $script:testCorrelationPath } {
            param($ExecPath)
            Find-CorrelatedIssues -ComputerName 'WEB01' -Symptom 'disk' -ExecutionsPath $ExecPath
        }
        $result.CorrelationsFound | Should -Be 0
    }

    It 'Should detect multiple failures pattern' {
        $execs = @(
            @{ ExecutionId = [guid]::NewGuid().ToString(); RunbookName = 'test1'; ComputerName = 'SVR01'; Status = 'Failed'; StartTime = (Get-Date).AddMinutes(-10).ToString('o') }
            @{ ExecutionId = [guid]::NewGuid().ToString(); RunbookName = 'test2'; ComputerName = 'SVR01'; Status = 'Failed'; StartTime = (Get-Date).AddMinutes(-8).ToString('o') }
            @{ ExecutionId = [guid]::NewGuid().ToString(); RunbookName = 'test3'; ComputerName = 'SVR01'; Status = 'Failed'; StartTime = (Get-Date).AddMinutes(-5).ToString('o') }
        )

        foreach ($exec in $execs) {
            $path = Join-Path $script:testCorrelationPath "$($exec.ExecutionId).json"
            $exec | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8
        }

        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ ExecPath = $script:testCorrelationPath } {
            param($ExecPath)
            Find-CorrelatedIssues -ComputerName 'SVR01' -Symptom 'multiple' -ExecutionsPath $ExecPath
        }
        $result.CorrelationsFound | Should -BeGreaterOrEqual 1
    }
}

Describe 'Invoke-Runbook - WhatIf Mode' {
    It 'Should not execute scripts in WhatIf mode' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf

        $result.WhatIf | Should -Be $true
    }

    It 'Should set WhatIf flag on the result' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf

        $result.WhatIf | Should -Be $true
    }

    It 'Should include step results even in WhatIf mode' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf

        $result.StepResults | Should -Not -BeNullOrEmpty
        @($result.StepResults).Count | Should -BeGreaterOrEqual 1
    }

    It 'Should not create an execution log file in WhatIf mode' {
        $beforeFiles = @(Get-ChildItem -Path (Join-Path $env:USERPROFILE '.runbookengine\executions') -Filter '*.json' -ErrorAction SilentlyContinue)
        Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf
        $afterFiles = @(Get-ChildItem -Path (Join-Path $env:USERPROFILE '.runbookengine\executions') -Filter '*.json' -ErrorAction SilentlyContinue)

        $afterFiles.Count | Should -Be $beforeFiles.Count
    }
}

Describe 'Invoke-Runbook - Parameter Validation' {
    It 'Should require RunbookName parameter' {
        { Invoke-Runbook -ComputerName 'TESTPC' } | Should -Throw
    }

    It 'Should throw for non-existent runbook' {
        { Invoke-Runbook -RunbookName 'nonexistent-runbook-xyz' -ComputerName 'TESTPC' } | Should -Throw '*not found*'
    }

    It 'Should accept a file path as RunbookName' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf
        $result | Should -Not -BeNullOrEmpty
    }

    It 'Should return result with ExecutionId' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf
        $result.ExecutionId | Should -Not -BeNullOrEmpty
    }

    It 'Should return result with RunbookName' {
        $result = Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -WhatIf
        $result.RunbookName | Should -Be 'Test Runbook'
    }

    It 'Should validate ApprovalMethod enum' {
        { Invoke-Runbook -RunbookName $script:validRunbookPath -ComputerName 'TESTPC' -ApprovalMethod 'InvalidMethod' } |
            Should -Throw
    }
}

Describe 'Test-FixVerification' {
    It 'Should verify a passing check' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Test-FixVerification -VerificationScript { $true } -WaitSeconds 0 -MaxRetries 1
        }
        $result.Verified | Should -Be $true
        $result.Attempts | Should -Be 1
    }

    It 'Should fail verification for a failing check' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Test-FixVerification -VerificationScript { $false } -WaitSeconds 0 -MaxRetries 1
        }
        $result.Verified | Should -Be $false
    }

    It 'Should retry up to MaxRetries' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Test-FixVerification -VerificationScript { $false } -WaitSeconds 0 -MaxRetries 3
        }
        $result.Attempts | Should -Be 3
    }

    It 'Should include ComputerName in result' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Test-FixVerification -VerificationScript { $true } -WaitSeconds 0 -ComputerName 'SVR01'
        }
        $result.ComputerName | Should -Be 'SVR01'
    }

    It 'Should capture errors from verification script' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Test-FixVerification -VerificationScript { throw "Verification failed" } -WaitSeconds 0 -MaxRetries 1
        }
        $result.Verified | Should -Be $false
        $result.LastError | Should -Not -BeNullOrEmpty
    }
}

Describe 'Get-ShiftHandoff' {
    It 'Should return a handoff report object' {
        $result = Get-ShiftHandoff -HoursBack 1
        $result | Should -Not -BeNullOrEmpty
    }

    It 'Should include HoursBack in the result' {
        $result = Get-ShiftHandoff -HoursBack 4
        $result.HoursBack | Should -Be 4
    }

    It 'Should include GeneratedAt timestamp' {
        $result = Get-ShiftHandoff -HoursBack 1
        $result.GeneratedAt | Should -Not -BeNullOrEmpty
    }

    It 'Should include execution counts' {
        $result = Get-ShiftHandoff -HoursBack 1
        $result.TotalExecutions | Should -BeGreaterOrEqual 0
        $result.Completed | Should -BeGreaterOrEqual 0
        $result.Failed | Should -BeGreaterOrEqual 0
    }

    It 'Should include NextShiftNotes array' {
        $result = Get-ShiftHandoff -HoursBack 1
        $result.NextShiftNotes | Should -Not -BeNullOrEmpty
    }

    It 'Should generate HTML report when OutputPath is specified' {
        $htmlPath = Join-Path $script:testRoot 'handoff-test.html'
        $result = Get-ShiftHandoff -HoursBack 1 -OutputPath $htmlPath
        Test-Path $htmlPath | Should -Be $true
    }
}

Describe 'New-HtmlDashboard - HTML Report Generation' {
    It 'Should generate an execution report HTML file' {
        $data = [PSCustomObject]@{
            RunbookName = 'test-runbook'
            ComputerName = 'SVR01'
            Status = 'Completed'
            Duration = '5.2s'
            StepResults = @(
                [PSCustomObject]@{ StepId = 'step1'; Action = 'script'; Description = 'Test step'; Status = 'Success'; Output = 'OK'; Duration = '1.0s' }
            )
            ApprovalLog = @()
            VerificationResults = @()
        }

        $htmlPath = Join-Path $script:testRoot 'exec-report.html'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Data = $data; Path = $htmlPath } {
            param($Data, $Path)
            New-HtmlDashboard -ReportType 'ExecutionReport' -Data $Data -OutputPath $Path
        }

        Test-Path $htmlPath | Should -Be $true
    }

    It 'Should include accent color #f97316 in the HTML' {
        $data = [PSCustomObject]@{
            RunbookName = 'test'; ComputerName = 'SVR01'; Status = 'Completed'; Duration = '1s'
            StepResults = @(); ApprovalLog = @(); VerificationResults = @()
        }
        $htmlPath = Join-Path $script:testRoot 'color-test.html'

        InModuleScope 'Infra-RunbookEngine' -Parameters @{ Data = $data; Path = $htmlPath } {
            param($Data, $Path)
            New-HtmlDashboard -ReportType 'ExecutionReport' -Data $Data -OutputPath $Path
        }

        $content = Get-Content -Path $htmlPath -Raw
        $content | Should -Match '#f97316'
    }

    It 'Should generate health score HTML' {
        $data = [PSCustomObject]@{
            ComputerName = 'SVR01'; Score = 85; Grade = 'B'; Trend = 'Improving'
            Factors = @([PSCustomObject]@{ Name = 'Test'; Impact = -5; Details = 'Detail' })
            Recommendations = @('Do something')
        }

        $htmlPath = Join-Path $script:testRoot 'health-report.html'
        InModuleScope 'Infra-RunbookEngine' -Parameters @{ Data = $data; Path = $htmlPath } {
            param($Data, $Path)
            New-HtmlDashboard -ReportType 'HealthScore' -Data $Data -OutputPath $Path
        }

        Test-Path $htmlPath | Should -Be $true
        $content = Get-Content -Path $htmlPath -Raw
        $content | Should -Match 'Health'
    }

    It 'Should use only inline CSS (no external stylesheet references)' {
        $data = [PSCustomObject]@{
            ComputerName = 'SVR01'; Score = 90; Grade = 'A'; Trend = 'Stable'
            Factors = @(); Recommendations = @()
        }
        $htmlPath = Join-Path $script:testRoot 'inline-css-test.html'

        InModuleScope 'Infra-RunbookEngine' -Parameters @{ Data = $data; Path = $htmlPath } {
            param($Data, $Path)
            New-HtmlDashboard -ReportType 'HealthScore' -Data $Data -OutputPath $Path
        }

        $content = Get-Content -Path $htmlPath -Raw
        $content | Should -Not -Match 'rel="stylesheet"'
        $content | Should -Match '<style>'
    }
}

Describe 'New-Runbook - From Template' {
    It 'Should create a runbook from a built-in template' {
        $outputDir = Join-Path $script:testRoot 'from-template'
        New-Item -Path $outputDir -ItemType Directory -Force | Out-Null

        $result = New-Runbook -Name 'my-cpu-runbook' -FromTemplate 'high-cpu' -OutputPath $outputDir
        $result | Should -Not -BeNullOrEmpty
        Test-Path $result | Should -Be $true
    }

    It 'Should throw for non-existent template' {
        $outputDir = Join-Path $script:testRoot 'from-bad-template'
        New-Item -Path $outputDir -ItemType Directory -Force | Out-Null

        { New-Runbook -Name 'test' -FromTemplate 'nonexistent-template' -OutputPath $outputDir } |
            Should -Throw '*not found*'
    }

    It 'Should create a runbook from disk-space template' {
        $outputDir = Join-Path $script:testRoot 'blank-runbook'
        New-Item -Path $outputDir -ItemType Directory -Force | Out-Null

        $result = New-Runbook -Name 'my-disk-runbook' -FromTemplate 'disk-space' -OutputPath $outputDir
        Test-Path $result | Should -Be $true
        $content = Get-Content -Path $result -Raw
        $content | Should -Match 'name: my-disk-runbook'
    }
}

Describe 'Get-RunbookStatus' {
    BeforeAll {
        # Create test execution files
        $script:testStatusExec = @{
            ExecutionId = 'test-status-001'
            RunbookName = 'status-test'
            ComputerName = 'STATUSSVR'
            Status = 'Completed'
            StartTime = (Get-Date).ToString('o')
            EndTime = (Get-Date).AddSeconds(30).ToString('o')
            Duration = '30.0s'
            StepResults = @(
                @{ StepId = 'step1'; Action = 'script'; Description = 'Test'; Status = 'Success'; Duration = '5.0s' }
            )
        }
        $execPath = Join-Path $env:USERPROFILE '.runbookengine\executions'
        if (-not (Test-Path $execPath)) { New-Item -Path $execPath -ItemType Directory -Force | Out-Null }
        $script:testStatusExec | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $execPath 'test-status-001.json') -Encoding UTF8
    }

    It 'Should return execution by ID' {
        $result = Get-RunbookStatus -ExecutionId 'test-status-001'
        $result | Should -Not -BeNullOrEmpty
    }

    It 'Should return empty array when no executions exist for a filter' {
        $result = Get-RunbookStatus -ComputerName 'NONEXISTENT999' -Status 'Failed'
        @($result).Count | Should -Be 0
    }

    It 'Should support -Last parameter' {
        $result = Get-RunbookStatus -Last 5
        @($result).Count | Should -BeLessOrEqual 5
    }

    It 'Should support -Since parameter' {
        $result = Get-RunbookStatus -Since (Get-Date).AddDays(-1)
        # Should return results including our test execution
        $result | Should -Not -BeNullOrEmpty
    }

    AfterAll {
        $cleanupPath = Join-Path $env:USERPROFILE '.runbookengine\executions\test-status-001.json'
        if (Test-Path $cleanupPath) { Remove-Item $cleanupPath -Force -ErrorAction SilentlyContinue }
    }
}

Describe 'YAML Templates' {
    BeforeAll {
        $script:templateDir = Join-Path (Split-Path -Path $PSScriptRoot -Parent) 'Templates'
    }

    It 'Should have high-cpu.yml template' {
        Test-Path (Join-Path $script:templateDir 'high-cpu.yml') | Should -Be $true
    }

    It 'Should parse high-cpu.yml without errors' {
        $path = Join-Path $script:templateDir 'high-cpu.yml'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $path } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Name | Should -Not -BeNullOrEmpty
        $result.Steps | Should -Not -BeNullOrEmpty
    }

    It 'Should have disk-space.yml template' {
        Test-Path (Join-Path $script:templateDir 'disk-space.yml') | Should -Be $true
    }

    It 'Should parse disk-space.yml without errors' {
        $path = Join-Path $script:templateDir 'disk-space.yml'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $path } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Name | Should -Not -BeNullOrEmpty
        $result.Steps | Should -Not -BeNullOrEmpty
    }

    It 'Should have service-recovery.yml template' {
        Test-Path (Join-Path $script:templateDir 'service-recovery.yml') | Should -Be $true
    }

    It 'Should parse service-recovery.yml without errors' {
        $path = Join-Path $script:templateDir 'service-recovery.yml'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $path } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Name | Should -Not -BeNullOrEmpty
        $result.Steps | Should -Not -BeNullOrEmpty
    }

    It 'Should have certificate-expiry.yml template' {
        Test-Path (Join-Path $script:templateDir 'certificate-expiry.yml') | Should -Be $true
    }

    It 'Should parse certificate-expiry.yml without errors' {
        $path = Join-Path $script:templateDir 'certificate-expiry.yml'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $path } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Name | Should -Not -BeNullOrEmpty
        $result.Steps | Should -Not -BeNullOrEmpty
    }

    It 'Should have dns-resolution.yml template' {
        Test-Path (Join-Path $script:templateDir 'dns-resolution.yml') | Should -Be $true
    }

    It 'Should parse dns-resolution.yml without errors' {
        $path = Join-Path $script:templateDir 'dns-resolution.yml'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $path } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Name | Should -Not -BeNullOrEmpty
        $result.Steps | Should -Not -BeNullOrEmpty
    }

    It 'Should have replication-failure.yml template' {
        Test-Path (Join-Path $script:templateDir 'replication-failure.yml') | Should -Be $true
    }

    It 'Should parse replication-failure.yml without errors' {
        $path = Join-Path $script:templateDir 'replication-failure.yml'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $path } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Name | Should -Not -BeNullOrEmpty
        $result.Steps | Should -Not -BeNullOrEmpty
    }

    It 'Should have backup-failure.yml template' {
        Test-Path (Join-Path $script:templateDir 'backup-failure.yml') | Should -Be $true
    }

    It 'Should parse backup-failure.yml without errors' {
        $path = Join-Path $script:templateDir 'backup-failure.yml'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $path } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Name | Should -Not -BeNullOrEmpty
        $result.Steps | Should -Not -BeNullOrEmpty
    }

    It 'Should have memory-pressure.yml template' {
        Test-Path (Join-Path $script:templateDir 'memory-pressure.yml') | Should -Be $true
    }

    It 'Should parse memory-pressure.yml without errors' {
        $path = Join-Path $script:templateDir 'memory-pressure.yml'
        $result = InModuleScope 'Infra-RunbookEngine' -Parameters @{ Path = $path } {
            param($Path)
            Read-RunbookDefinition -Path $Path
        }
        $result.Name | Should -Not -BeNullOrEmpty
        $result.Steps | Should -Not -BeNullOrEmpty
    }
}

Describe 'Send-ApprovalRequest - Console Mode' {
    It 'Should create a valid approval result object structure' {
        # We cannot truly test Read-Host in automated tests, but we can verify the function exists
        # and test the output structure by mocking
        $result = InModuleScope 'Infra-RunbookEngine' {
            Mock Read-Host { return 'YES' }
            Send-ApprovalRequest -Method Console -RunbookName 'test' -StepDescription 'test step' -ComputerName 'SVR01'
        }
        $result.RunbookName | Should -Be 'test'
        $result.Method | Should -Be 'Console'
        $result.Approved | Should -Be $true
    }

    It 'Should deny when response is not YES' {
        $result = InModuleScope 'Infra-RunbookEngine' {
            Mock Read-Host { return 'NO' }
            Send-ApprovalRequest -Method Console -RunbookName 'test' -StepDescription 'test step' -ComputerName 'SVR01'
        }
        $result.Approved | Should -Be $false
    }
}