Tests/Infra-LivingDoc.Tests.ps1

BeforeAll {
    $ModuleRoot = Split-Path -Parent $PSScriptRoot
    $ModuleName = 'Infra-LivingDoc'
    $ManifestPath = Join-Path $ModuleRoot "$ModuleName.psd1"

    # Remove module if already loaded
    Get-Module $ModuleName -ErrorAction SilentlyContinue | Remove-Module -Force

    # Import the module
    Import-Module $ManifestPath -Force -ErrorAction Stop
}

AfterAll {
    Get-Module 'Infra-LivingDoc' -ErrorAction SilentlyContinue | Remove-Module -Force
}

Describe 'Module Loading' {
    It 'Should import the module without errors' {
        $module = Get-Module 'Infra-LivingDoc'
        $module | Should -Not -BeNullOrEmpty
    }

    It 'Should export exactly 5 public functions' {
        $module = Get-Module 'Infra-LivingDoc'
        $module.ExportedFunctions.Count | Should -Be 5
    }

    It 'Should export Import-Documentation' {
        Get-Command 'Import-Documentation' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty
    }

    It 'Should export Extract-EnvironmentFacts' {
        Get-Command 'Extract-EnvironmentFacts' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty
    }

    It 'Should export Test-EnvironmentFacts' {
        Get-Command 'Test-EnvironmentFacts' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty
    }

    It 'Should export Get-DocumentationDrift' {
        Get-Command 'Get-DocumentationDrift' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty
    }

    It 'Should export Update-Documentation' {
        Get-Command 'Update-Documentation' -Module 'Infra-LivingDoc' | Should -Not -BeNullOrEmpty
    }

    It 'Should NOT export private functions' {
        $module = Get-Module 'Infra-LivingDoc'
        $module.ExportedFunctions.Keys | Should -Not -Contain 'Invoke-AICompletion'
        $module.ExportedFunctions.Keys | Should -Not -Contain 'Read-WordDocument'
        $module.ExportedFunctions.Keys | Should -Not -Contain 'Read-ExcelDocument'
        $module.ExportedFunctions.Keys | Should -Not -Contain 'Test-SingleFact'
        $module.ExportedFunctions.Keys | Should -Not -Contain 'New-HtmlDashboard'
    }

    It 'Should have a Templates directory' {
        $templatesPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates'
        Test-Path $templatesPath | Should -BeTrue
    }

    It 'Should have extraction-prompt.txt template' {
        $templatePath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates') 'extraction-prompt.txt'
        Test-Path $templatePath | Should -BeTrue
    }

    It 'Should have update-prompt.txt template' {
        $templatePath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Templates') 'update-prompt.txt'
        Test-Path $templatePath | Should -BeTrue
    }
}

Describe 'Module Manifest Validation' {
    BeforeAll {
        $manifestPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'Infra-LivingDoc.psd1'
        $manifest = Test-ModuleManifest -Path $manifestPath -ErrorAction Stop
    }

    It 'Should have the correct GUID' {
        $manifest.GUID | Should -Be 'b8c9d0e1-5f46-4bc2-a3d4-9f0a1b2c3d45'
    }

    It 'Should require PowerShell 5.1' {
        $manifest.PowerShellVersion | Should -Be '5.1'
    }

    It 'Should have the correct author' {
        $manifest.Author | Should -BeLike '*Larry Roberts*'
    }

    It 'Should have required tags' {
        $tags = $manifest.PrivateData.PSData.Tags
        $tags | Should -Contain 'Documentation'
        $tags | Should -Contain 'LivingDoc'
        $tags | Should -Contain 'AI'
        $tags | Should -Contain 'Drift'
        $tags | Should -Contain 'Infrastructure'
    }

    It 'Should have a ProjectUri' {
        $manifest.PrivateData.PSData.ProjectUri | Should -Not -BeNullOrEmpty
    }

    It 'Should have a LicenseUri' {
        $manifest.PrivateData.PSData.LicenseUri | Should -Not -BeNullOrEmpty
    }
}

Describe 'Parameter Validation' {
    Context 'Import-Documentation' {
        It 'Should have mandatory Path parameter' {
            $cmd = Get-Command 'Import-Documentation'
            $param = $cmd.Parameters['Path']
            $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | Should -Not -BeNullOrEmpty
        }

        It 'Should have ValidateSet on FileType parameter' {
            $cmd = Get-Command 'Import-Documentation'
            $param = $cmd.Parameters['FileType']
            $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet | Should -Not -BeNullOrEmpty
            $validateSet.ValidValues | Should -Contain 'All'
            $validateSet.ValidValues | Should -Contain 'Word'
            $validateSet.ValidValues | Should -Contain 'Excel'
            $validateSet.ValidValues | Should -Contain 'Markdown'
            $validateSet.ValidValues | Should -Contain 'Text'
            $validateSet.ValidValues | Should -Contain 'CSV'
        }
    }

    Context 'Extract-EnvironmentFacts' {
        It 'Should have mandatory DocumentText parameter' {
            $cmd = Get-Command 'Extract-EnvironmentFacts'
            $param = $cmd.Parameters['DocumentText']
            $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | Should -Not -BeNullOrEmpty
        }

        It 'Should have ValidateSet on Provider parameter' {
            $cmd = Get-Command 'Extract-EnvironmentFacts'
            $param = $cmd.Parameters['Provider']
            $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet | Should -Not -BeNullOrEmpty
            $validateSet.ValidValues | Should -Contain 'Anthropic'
            $validateSet.ValidValues | Should -Contain 'OpenAI'
            $validateSet.ValidValues | Should -Contain 'Ollama'
            $validateSet.ValidValues | Should -Contain 'Custom'
        }
    }

    Context 'Update-Documentation' {
        It 'Should have ValidateSet on Format parameter' {
            $cmd = Get-Command 'Update-Documentation'
            $param = $cmd.Parameters['Format']
            $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet | Should -Not -BeNullOrEmpty
            $validateSet.ValidValues | Should -Contain 'Markdown'
            $validateSet.ValidValues | Should -Contain 'HTML'
        }
    }

    Context 'Get-DocumentationDrift' {
        It 'Should have mandatory DocumentPath parameter' {
            $cmd = Get-Command 'Get-DocumentationDrift'
            $param = $cmd.Parameters['DocumentPath']
            $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | Should -Not -BeNullOrEmpty
        }

        It 'Should have ValidateSet on Provider parameter' {
            $cmd = Get-Command 'Get-DocumentationDrift'
            $param = $cmd.Parameters['Provider']
            $validateSet = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet | Should -Not -BeNullOrEmpty
        }
    }
}

Describe 'Import-Documentation' {
    BeforeAll {
        $testDir = Join-Path $TestDrive 'docs'
        New-Item -ItemType Directory -Path $testDir -Force | Out-Null
    }

    Context 'Markdown file extraction' {
        BeforeAll {
            $mdContent = @"
# Test Document
## Servers
- SRV01 (10.0.0.1) - Web Server
- SRV02 (10.0.0.2) - Database Server
"@

            $mdPath = Join-Path $testDir 'test.md'
            $mdContent | Out-File -FilePath $mdPath -Encoding UTF8
        }

        It 'Should extract text from .md files' {
            $result = Import-Documentation -Path $mdPath
            $result | Should -Not -BeNullOrEmpty
            $result.Content | Should -BeLike '*Test Document*'
            $result.Content | Should -BeLike '*SRV01*'
        }

        It 'Should return correct metadata' {
            $result = Import-Documentation -Path $mdPath
            $result.FileName | Should -Be 'test.md'
            $result.FileType | Should -Be 'Markdown'
            $result.CharacterCount | Should -BeGreaterThan 0
            $result.ExtractedDate | Should -Not -BeNullOrEmpty
        }
    }

    Context 'Text file extraction' {
        BeforeAll {
            $txtContent = "Server: APP01`nIP: 192.168.1.100`nRole: Application Server"
            $txtPath = Join-Path $testDir 'test.txt'
            $txtContent | Out-File -FilePath $txtPath -Encoding UTF8
        }

        It 'Should extract text from .txt files' {
            $result = Import-Documentation -Path $txtPath
            $result | Should -Not -BeNullOrEmpty
            $result.Content | Should -BeLike '*APP01*'
            $result.FileType | Should -Be 'Text'
        }
    }

    Context 'Directory scanning' {
        BeforeAll {
            'File A content' | Out-File -FilePath (Join-Path $testDir 'a.md') -Encoding UTF8
            'File B content' | Out-File -FilePath (Join-Path $testDir 'b.txt') -Encoding UTF8
        }

        It 'Should import multiple files from a directory' {
            $results = Import-Documentation -Path $testDir
            $results.Count | Should -BeGreaterOrEqual 2
        }

        It 'Should filter by FileType' {
            $results = Import-Documentation -Path $testDir -FileType Markdown
            $results | ForEach-Object { $_.FileType | Should -Be 'Markdown' }
        }
    }

    Context 'Missing file handling' {
        It 'Should warn on nonexistent path' {
            $result = Import-Documentation -Path (Join-Path $testDir 'nonexistent.md') -WarningAction SilentlyContinue -WarningVariable warnings
            $warnings.Count | Should -BeGreaterThan 0
        }
    }
}

Describe 'Extract-EnvironmentFacts' {
    BeforeAll {
        $testFactsPath = Join-Path $TestDrive 'test-facts.json'
    }

    Context 'AI response parsing' {
        It 'Should parse AI response into fact objects' {
            # Mock the AI completion to return a known JSON response
            Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Invoke-AICompletion' -MockWith {
                return @'
{
  "facts": [
    {
      "source_text": "SRV01 (10.0.0.1) - Web Server",
      "category": "server",
      "claims": [
        {
          "claim_type": "server_exists",
          "subject": "SRV01",
          "expected_value": "exists",
          "verification_method": "ad_computer"
        },
        {
          "claim_type": "server_ip",
          "subject": "SRV01",
          "expected_value": "10.0.0.1",
          "verification_method": "dns_resolve"
        }
      ],
      "confidence": 0.95
    }
  ]
}
'@

            }

            $result = Extract-EnvironmentFacts -DocumentText 'SRV01 (10.0.0.1) - Web Server' -SourceDocument 'test.md' -Provider Anthropic -ApiKey 'fake-key' -FactsPath $testFactsPath
            $result | Should -Not -BeNullOrEmpty
            $result[0].category | Should -Be 'server'
            $result[0].claims.Count | Should -Be 2
            $result[0].claims[0].claim_type | Should -Be 'server_exists'
            $result[0].claims[0].subject | Should -Be 'SRV01'
            $result[0].claims[1].expected_value | Should -Be '10.0.0.1'
        }

        It 'Should save facts to JSON file' {
            Test-Path $testFactsPath | Should -BeTrue
            $savedData = Get-Content -Path $testFactsPath -Raw | ConvertFrom-Json
            $savedData.facts | Should -Not -BeNullOrEmpty
            $savedData.metadata | Should -Not -BeNullOrEmpty
        }
    }

    Context 'Fact deduplication' {
        It 'Should not create duplicate facts when re-extracting same document' {
            Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Invoke-AICompletion' -MockWith {
                return @'
{
  "facts": [
    {
      "source_text": "SRV01 (10.0.0.1) - Web Server",
      "category": "server",
      "claims": [
        {
          "claim_type": "server_exists",
          "subject": "SRV01",
          "expected_value": "exists",
          "verification_method": "ad_computer"
        }
      ],
      "confidence": 0.95
    }
  ]
}
'@

            }

            $dedupPath = Join-Path $TestDrive 'dedup-facts.json'

            # First extraction
            Extract-EnvironmentFacts -DocumentText 'SRV01 (10.0.0.1)' -SourceDocument 'test.md' -Provider Anthropic -ApiKey 'fake-key' -FactsPath $dedupPath | Out-Null
            $firstCount = (Get-Content $dedupPath -Raw | ConvertFrom-Json).facts.Count

            # Second extraction with same data
            Extract-EnvironmentFacts -DocumentText 'SRV01 (10.0.0.1)' -SourceDocument 'test.md' -Provider Anthropic -ApiKey 'fake-key' -FactsPath $dedupPath | Out-Null
            $secondCount = (Get-Content $dedupPath -Raw | ConvertFrom-Json).facts.Count

            $secondCount | Should -Be $firstCount
        }
    }

    Context 'AI response with code blocks' {
        It 'Should handle JSON wrapped in markdown code blocks' {
            Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Invoke-AICompletion' -MockWith {
                return @'
Here are the extracted facts:
 
```json
{
  "facts": [
    {
      "source_text": "DB01 runs PostgreSQL",
      "category": "software",
      "claims": [
        {
          "claim_type": "software_version",
          "subject": "DB01",
          "expected_value": "PostgreSQL",
          "verification_method": "cim_service"
        }
      ],
      "confidence": 0.85
    }
  ]
}
```
'@

            }

            $codeBlockPath = Join-Path $TestDrive 'codeblock-facts.json'
            $result = Extract-EnvironmentFacts -DocumentText 'DB01 runs PostgreSQL' -SourceDocument 'test.md' -Provider Anthropic -ApiKey 'fake-key' -FactsPath $codeBlockPath
            $result | Should -Not -BeNullOrEmpty
            $result[0].claims[0].subject | Should -Be 'DB01'
        }
    }
}

Describe 'Test-EnvironmentFacts' {
    Context 'Claim verification with mocked cmdlets' {
        BeforeAll {
            $verifyFactsPath = Join-Path $TestDrive 'verify-facts.json'
            $factsDb = @{
                metadata = @{
                    created          = (Get-Date).ToString('o')
                    last_verified    = $null
                    source_documents = @('test.md')
                    total_facts      = 2
                    verified         = 0
                    drift_detected   = 0
                    unverifiable     = 0
                }
                facts = @(
                    @{
                        id              = 'fact-test-001'
                        source_document = 'test.md'
                        source_text     = 'SRV01 resolves to 10.0.0.1'
                        category        = 'server'
                        claims          = @(
                            @{
                                claim_type          = 'server_ip'
                                subject             = 'SRV01'
                                expected_value      = '10.0.0.1'
                                verification_method = 'dns_resolve'
                                actual_value        = $null
                                status              = 'pending'
                                last_checked        = $null
                            }
                        )
                        confidence      = 0.95
                        last_verified   = $null
                        overall_status  = 'pending'
                    },
                    @{
                        id              = 'fact-test-002'
                        source_document = 'test.md'
                        source_text     = 'SRV02 resolves to 10.0.0.2'
                        category        = 'server'
                        claims          = @(
                            @{
                                claim_type          = 'server_ip'
                                subject             = 'SRV02'
                                expected_value      = '10.0.0.2'
                                verification_method = 'dns_resolve'
                                actual_value        = $null
                                status              = 'pending'
                                last_checked        = $null
                            }
                        )
                        confidence      = 0.90
                        last_verified   = $null
                        overall_status  = 'pending'
                    }
                )
            }
            $factsDb | ConvertTo-Json -Depth 10 | Out-File -FilePath $verifyFactsPath -Encoding UTF8
        }

        It 'Should mark matching DNS as verified' {
            Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Resolve-DnsName' -MockWith {
                return @([PSCustomObject]@{ QueryType = 'A'; IPAddress = '10.0.0.1'; Name = 'SRV01' })
            } -ParameterFilter { $Name -eq 'SRV01' }

            Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Resolve-DnsName' -MockWith {
                return @([PSCustomObject]@{ QueryType = 'A'; IPAddress = '10.0.0.99'; Name = 'SRV02' })
            } -ParameterFilter { $Name -eq 'SRV02' }

            $result = Test-EnvironmentFacts -FactsPath $verifyFactsPath
            $result.Verified | Should -Be 1
            $result.Drift | Should -Be 1
        }

        It 'Should update the facts.json file in place' {
            $updated = Get-Content -Path $verifyFactsPath -Raw | ConvertFrom-Json
            $updated.metadata.last_verified | Should -Not -BeNullOrEmpty
        }
    }
}

Describe 'Test-SingleFact' {
    BeforeAll {
        # Access private function via module scope
        $testSingleFact = & (Get-Module 'Infra-LivingDoc') { Get-Command 'Test-SingleFact' }
    }

    Context 'dns_resolve verification' {
        It 'Should return verified when IP matches' {
            Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Resolve-DnsName' -MockWith {
                return @([PSCustomObject]@{ QueryType = 'A'; IPAddress = '10.0.0.1'; Name = 'SRV01' })
            }

            $claim = [PSCustomObject]@{
                claim_type          = 'server_ip'
                subject             = 'SRV01'
                expected_value      = '10.0.0.1'
                verification_method = 'dns_resolve'
                actual_value        = $null
                status              = 'pending'
                last_checked        = $null
            }

            $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim
            $result.status | Should -Be 'verified'
            $result.actual_value | Should -BeLike '*10.0.0.1*'
        }

        It 'Should return drift when IP does not match' {
            Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Resolve-DnsName' -MockWith {
                return @([PSCustomObject]@{ QueryType = 'A'; IPAddress = '10.0.0.99'; Name = 'SRV01' })
            }

            $claim = [PSCustomObject]@{
                claim_type          = 'server_ip'
                subject             = 'SRV01'
                expected_value      = '10.0.0.1'
                verification_method = 'dns_resolve'
                actual_value        = $null
                status              = 'pending'
                last_checked        = $null
            }

            $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim
            $result.status | Should -Be 'drift'
        }
    }

    Context 'unverifiable method' {
        It 'Should return unverifiable status' {
            $claim = [PSCustomObject]@{
                claim_type          = 'other'
                subject             = 'something'
                expected_value      = 'some value'
                verification_method = 'unverifiable'
                actual_value        = $null
                status              = 'pending'
                last_checked        = $null
            }

            $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim
            $result.status | Should -Be 'unverifiable'
        }
    }

    Context 'network_test verification' {
        It 'Should return verified when ping succeeds' {
            Mock -ModuleName 'Infra-LivingDoc' -CommandName 'Test-NetConnection' -MockWith {
                return [PSCustomObject]@{
                    PingSucceeded  = $true
                    RemoteAddress  = '10.0.0.1'
                    TcpTestSucceeded = $null
                }
            }

            $claim = [PSCustomObject]@{
                claim_type          = 'other'
                subject             = '10.0.0.1'
                expected_value      = 'reachable'
                verification_method = 'network_test'
                actual_value        = $null
                status              = 'pending'
                last_checked        = $null
            }

            $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim
            $result.status | Should -Be 'verified'
        }
    }

    Context 'Unknown verification method' {
        It 'Should return unverifiable for unknown methods' {
            $claim = [PSCustomObject]@{
                claim_type          = 'other'
                subject             = 'something'
                expected_value      = 'some value'
                verification_method = 'totally_unknown_method'
                actual_value        = $null
                status              = 'pending'
                last_checked        = $null
            }

            $result = & (Get-Module 'Infra-LivingDoc') { param($c) Test-SingleFact -Claim $c } $claim
            $result.status | Should -Be 'unverifiable'
        }
    }
}

Describe 'Sample Files Validation' {
    It 'Should have valid sample-facts.json' {
        $sampleFactsPath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples') 'sample-facts.json'
        Test-Path $sampleFactsPath | Should -BeTrue
        $facts = Get-Content -Path $sampleFactsPath -Raw | ConvertFrom-Json
        $facts.metadata | Should -Not -BeNullOrEmpty
        $facts.facts | Should -Not -BeNullOrEmpty
        $facts.facts.Count | Should -BeGreaterThan 0
    }

    It 'Should have sample-input.md' {
        $sampleInputPath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples') 'sample-input.md'
        Test-Path $sampleInputPath | Should -BeTrue
    }

    It 'Should have sample-drift-report.html' {
        $sampleReportPath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples') 'sample-drift-report.html'
        Test-Path $sampleReportPath | Should -BeTrue
    }

    It 'Should have sample-updated-doc.md' {
        $sampleUpdatedPath = Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Samples') 'sample-updated-doc.md'
        Test-Path $sampleUpdatedPath | Should -BeTrue
    }
}