Tests/SharpDown.Tests.ps1

#### <h1 style="color: #DCA657;">🧪 SharpDown.Tests</h1>
####
#### > Pester unit tests for `ConvertTo-SharpDown`. Ported from the legacy Test-SharpDown.ps1 smoke harness.
####
#### The temp-path helpers live in BeforeAll because Pester 5 only exposes functions defined there
#### to the It blocks at run time. Top-level function definitions run during discovery and vanish.
BeforeAll {
    Import-Module (Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'Sharpdown.psd1') -Force

    function New-TempLeafPath {
        param([string]$Extension = '')
        Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString('N') + $Extension)
    }

    function New-TempWorkspace {
        $workspaceRoot = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString('N'))
        New-Item -ItemType Directory -Path $workspaceRoot | Out-Null
        return $workspaceRoot
    }
}
AfterAll {
    Remove-Module Sharpdown -Force -ErrorAction SilentlyContinue
}

#### ---
#### <h2 style="color: #DCA657;">CSharp file mode</h2>
####
#### Single-file conversion of C# sources.
####
#### <b style="color: #D2A8FF;">Cases</b>
####
Describe 'CSharp file mode' {
    #### - Strips the `////` marker and fences the declaration lines.
    ####
    It 'Strips //// markers and fences declaration lines' {
        $sourceFile = New-TempLeafPath -Extension '.cs'
        $outputFile = New-TempLeafPath -Extension '.md'
        try {
            $sample = @(
                '//// # WookiePuddin',
                '//// > A sweet, fictional concoction.',
                '////',
                'public class WookiePuddin',
                '{',
                ' public string Flavor { get; init; }',
                '}'
            ) -join [Environment]::NewLine

            Set-Content -LiteralPath $sourceFile -Value $sample -Encoding utf8NoBOM
            $result = ConvertTo-SharpDown -Language CSharp -Path $sourceFile -OutPath $outputFile

            Test-Path -LiteralPath $outputFile | Should -BeTrue
            $rendered = Get-Content -LiteralPath $outputFile -Raw
            $rendered | Should -Match '# WookiePuddin'
            $rendered | Should -Match '> A sweet, fictional concoction\.'
            $rendered | Should -Match '(?ms)```csharp\s*\r?\npublic class WookiePuddin\s*\r?\n```'
            $rendered | Should -Match '(?ms)```csharp\s*\r?\npublic string Flavor[^\n]*\r?\n```'
            $result.Lines | Should -BeGreaterThan 0
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile -Force -ErrorAction SilentlyContinue
        }
    }

    #### - Drops lines that are neither markers nor declarations.
    ####
    It 'Skips lines that are neither markers nor declarations' {
        $sourceFile = New-TempLeafPath -Extension '.cs'
        $outputFile = New-TempLeafPath -Extension '.md'
        try {
            $sample = @(
                '//// # Visible',
                'public class Visible { }',
                '// regular comment, should be dropped',
                'var ignored = 42;',
                'private string hiddenField;'
            ) -join [Environment]::NewLine

            Set-Content -LiteralPath $sourceFile -Value $sample -Encoding utf8NoBOM
            ConvertTo-SharpDown -Language CSharp -Path $sourceFile -OutPath $outputFile | Out-Null

            $rendered = Get-Content -LiteralPath $outputFile -Raw
            $rendered | Should -Match 'public class Visible'
            $rendered | Should -Not -Match 'regular comment'
            $rendered | Should -Not -Match 'var ignored'
            $rendered | Should -Not -Match 'hiddenField'
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile -Force -ErrorAction SilentlyContinue
        }
    }

    #### - Throws when the source path is not a file.
    ####
    It 'Throws when File path is not a file' {
        $missing = New-TempLeafPath -Extension '.cs'
        $output  = New-TempLeafPath -Extension '.md'
        { ConvertTo-SharpDown -Language CSharp -Path $missing -OutPath $output } | Should -Throw
    }

    #### - Throws when `-OutPath` points at an existing directory.
    ####
    It 'Throws when File OutPath points to an existing directory' {
        $sourceFile = New-TempLeafPath -Extension '.cs'
        $outputDir  = New-TempWorkspace
        try {
            Set-Content -LiteralPath $sourceFile -Value '//// # Anything' -Encoding utf8NoBOM
            { ConvertTo-SharpDown -Language CSharp -Path $sourceFile -OutPath $outputDir } | Should -Throw
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputDir  -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    #### - Warns when the source holds no SharpDown content.
    ####
    It 'Warns when source has no SharpDown content' {
        $sourceFile = New-TempLeafPath -Extension '.cs'
        $outputFile = New-TempLeafPath -Extension '.md'
        try {
            Set-Content -LiteralPath $sourceFile -Value '// nothing to see here' -Encoding utf8NoBOM
            $warnings = @()
            ConvertTo-SharpDown -Language CSharp -Path $sourceFile -OutPath $outputFile -WarningVariable warnings -WarningAction SilentlyContinue 3> $null 6> $null
            $warnings.Count | Should -BeGreaterThan 0
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile -Force -ErrorAction SilentlyContinue
        }
    }
}

#### ---
#### <h2 style="color: #DCA657;">Language configs</h2>
####
#### Per-language markers, fences, and the `-API` switch.
####
#### <b style="color: #D2A8FF;">Cases</b>
####
Describe 'Language configs' {
    #### - SQL: the `----` marker fences `CREATE` statements.
    ####
    It 'Sql marker ---- fences CREATE statements' {
        $sourceFile = New-TempLeafPath -Extension '.sql'
        $outputFile = New-TempLeafPath -Extension '.md'
        try {
            $sample = @(
                '---- # GetThot',
                '---- > Returns a thot by id.',
                'CREATE OR REPLACE FUNCTION get_thot(p_id BIGINT)'
            ) -join [Environment]::NewLine

            Set-Content -LiteralPath $sourceFile -Value $sample -Encoding utf8NoBOM
            ConvertTo-SharpDown -Language Sql -Path $sourceFile -OutPath $outputFile | Out-Null

            $rendered = Get-Content -LiteralPath $outputFile -Raw
            $rendered | Should -Match '# GetThot'
            $rendered | Should -Match '(?ms)```sql\s*\r?\nCREATE OR REPLACE FUNCTION[^\n]*\r?\n```'
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile -Force -ErrorAction SilentlyContinue
        }
    }

    #### - PowerShell: the `####` marker fences function declarations.
    ####
    It 'PowerShell marker #### fences function declarations' {
        $sourceFile = New-TempLeafPath -Extension '.ps1'
        $outputFile = New-TempLeafPath -Extension '.md'
        try {
            $sample = @(
                '#### # New-Whatever',
                '#### > Does a thing.',
                'function New-Whatever {',
                ' param([string]$Name)',
                '}'
            ) -join [Environment]::NewLine

            Set-Content -LiteralPath $sourceFile -Value $sample -Encoding utf8NoBOM
            ConvertTo-SharpDown -Language PowerShell -Path $sourceFile -OutPath $outputFile | Out-Null

            $rendered = Get-Content -LiteralPath $outputFile -Raw
            $rendered | Should -Match '# New-Whatever'
            $rendered | Should -Match '(?ms)```powershell\s*\r?\nfunction New-Whatever[^\n]*\r?\n```'
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile -Force -ErrorAction SilentlyContinue
        }
    }

    #### - JavaScript: the `////` marker fences `export class` declarations.
    ####
    It 'JavaScript marker //// fences export class declarations' {
        $sourceFile = New-TempLeafPath -Extension '.js'
        $outputFile = New-TempLeafPath -Extension '.md'
        try {
            $sample = @(
                '//// # SxThing',
                '//// > A Lit element.',
                'export class SxThing extends LitElement {',
                '}'
            ) -join [Environment]::NewLine

            Set-Content -LiteralPath $sourceFile -Value $sample -Encoding utf8NoBOM
            ConvertTo-SharpDown -Language JavaScript -Path $sourceFile -OutPath $outputFile | Out-Null

            $rendered = Get-Content -LiteralPath $outputFile -Raw
            $rendered | Should -Match '# SxThing'
            $rendered | Should -Match '(?ms)```javascript\s*\r?\nexport class SxThing[^\n]*\r?\n```'
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile -Force -ErrorAction SilentlyContinue
        }
    }

    #### - `-API` suppresses every auto-generated declaration fence.
    ####
    It '-API suppresses all auto-generated declaration fences' {
        $sourceFile = New-TempLeafPath -Extension '.ts'
        $outputFile = New-TempLeafPath -Extension '.md'
        try {
            $sample = @(
                '//// # MyEndpoint',
                '//// `GET /things`',
                'import { thing } from "thing";',
                'type ThingShape = { id: string };',
                'function ThingHelper(): void { }'
            ) -join [Environment]::NewLine

            Set-Content -LiteralPath $sourceFile -Value $sample -Encoding utf8NoBOM
            ConvertTo-SharpDown -Language JavaScript -Path $sourceFile -OutPath $outputFile -API | Out-Null

            $rendered = Get-Content -LiteralPath $outputFile -Raw
            $rendered | Should -Match '# MyEndpoint'
            $rendered | Should -Match '`GET /things`'
            $rendered | Should -Not -Match '```javascript'
            $rendered | Should -Not -Match 'import \{ thing'
            $rendered | Should -Not -Match 'type ThingShape'
            $rendered | Should -Not -Match 'function ThingHelper'
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile -Force -ErrorAction SilentlyContinue
        }
    }

    #### - JavaScript keeps top-level function and type decls, drops interior locals.
    ####
    It 'JavaScript drops interior const/let locals but keeps top-level function and type declarations' {
        $sourceFile = New-TempLeafPath -Extension '.ts'
        $outputFile = New-TempLeafPath -Extension '.md'
        try {
            $sample = @(
                '//// # ThingInput',
                'type ThingInput = { name: string };',
                '',
                '//// # MakeThing',
                'async function MakeThing(req: Request): Promise<ThingInput> {',
                ' const url = "/x";',
                ' let count = 0;',
                ' const body = await req.json();',
                ' return body;',
                '}',
                '',
                '//// # Plain',
                'function Plain(): void {',
                ' const inner = 1;',
                '}'
            ) -join [Environment]::NewLine

            Set-Content -LiteralPath $sourceFile -Value $sample -Encoding utf8NoBOM
            ConvertTo-SharpDown -Language JavaScript -Path $sourceFile -OutPath $outputFile | Out-Null

            $rendered = Get-Content -LiteralPath $outputFile -Raw
            $rendered | Should -Match '# ThingInput'
            $rendered | Should -Match '(?ms)```javascript\s*\r?\ntype ThingInput[^\n]*\r?\n```'
            $rendered | Should -Match '(?ms)```javascript\s*\r?\nasync function MakeThing[^\n]*\r?\n```'
            $rendered | Should -Match '(?ms)```javascript\s*\r?\nfunction Plain[^\n]*\r?\n```'
            $rendered | Should -Not -Match 'const url'
            $rendered | Should -Not -Match 'let count'
            $rendered | Should -Not -Match 'const body'
            $rendered | Should -Not -Match 'const inner'
        }
        finally {
            Remove-Item -LiteralPath $sourceFile -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile -Force -ErrorAction SilentlyContinue
        }
    }
}

#### ---
#### <h2 style="color: #DCA657;">Directory mode</h2>
####
#### Recursive tree walking, mirroring, and pipeline input.
####
#### <b style="color: #D2A8FF;">Cases</b>
####
Describe 'Directory mode' {
    #### - Throws when the `-Recurse` path is not a directory.
    ####
    It 'Throws when -Recurse path is not a directory' {
        $missing = New-TempLeafPath
        $output  = New-TempLeafPath
        { ConvertTo-SharpDown -Language CSharp -Path $missing -OutPath $output -Recurse } | Should -Throw
    }

    #### - Throws when `-OutPath` points at an existing file.
    ####
    It 'Throws when -Recurse OutPath points to an existing file' {
        $workspaceRoot = New-TempWorkspace
        $outputFile    = New-TempLeafPath -Extension '.md'
        try {
            Set-Content -LiteralPath $outputFile -Value 'occupied' -Encoding utf8NoBOM
            { ConvertTo-SharpDown -Language CSharp -Path $workspaceRoot -OutPath $outputFile -Recurse } | Should -Throw
        }
        finally {
            Remove-Item -LiteralPath $workspaceRoot -Recurse -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputFile    -Force -ErrorAction SilentlyContinue
        }
    }

    #### - Without `-Recurse`, a directory path throws as not-a-file.
    ####
    It 'Without -Recurse, a directory path throws as not-a-file' {
        $workspaceRoot = New-TempWorkspace
        $output        = New-TempLeafPath -Extension '.md'
        try {
            { ConvertTo-SharpDown -Language CSharp -Path $workspaceRoot -OutPath $output } | Should -Throw
        }
        finally {
            Remove-Item -LiteralPath $workspaceRoot -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    #### - Mirrors the tree, stripping `src/` and project-name legs.
    ####
    It 'Mirrors tree, strips src/ and project name legs' {
        $workspaceRoot = New-TempWorkspace
        $outputRoot    = New-TempWorkspace
        try {
            $projectName = 'WookieProject'
            $projectRoot = Join-Path $workspaceRoot $projectName
            $serviceDir  = Join-Path $projectRoot 'src' |
                           Join-Path -ChildPath $projectName |
                           Join-Path -ChildPath 'Service'
            New-Item -ItemType Directory -Path $serviceDir -Force | Out-Null

            $wookieSourcePath = Join-Path $serviceDir 'WookieService.cs'
            $wookieSample = @(
                '//// # WookieService',
                'public class WookieService { }'
            ) -join [Environment]::NewLine
            Set-Content -LiteralPath $wookieSourcePath -Value $wookieSample -Encoding utf8NoBOM

            $excludedDir = Join-Path $projectRoot 'bin'
            New-Item -ItemType Directory -Path $excludedDir -Force | Out-Null
            Set-Content -LiteralPath (Join-Path $excludedDir 'Skip.cs') -Value '//// # Skipped' -Encoding utf8NoBOM

            ConvertTo-SharpDown -Language CSharp -Path $projectRoot -OutPath $outputRoot -Recurse 6> $null

            $expectedOutput = Join-Path $outputRoot 'Service' |
                              Join-Path -ChildPath 'WookieService.md'
            Test-Path -LiteralPath $expectedOutput | Should -BeTrue

            $rendered = Get-Content -LiteralPath $expectedOutput -Raw
            $rendered | Should -Match '# WookieService'

            $skippedOutput = Get-ChildItem -Path $outputRoot -Recurse -Filter 'Skip.md' -File -ErrorAction SilentlyContinue
            $skippedOutput | Should -BeNullOrEmpty
        }
        finally {
            Remove-Item -LiteralPath $workspaceRoot -Recurse -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputRoot    -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    #### - Accepts a piped `DirectoryInfo` as `-Path` under `-Recurse`.
    ####
    It 'Accepts a piped DirectoryInfo as -Path under -Recurse' {
        $workspaceRoot = New-TempWorkspace
        $outputRoot    = New-TempWorkspace
        try {
            $projectName = 'PipedWookie'
            $projectRoot = Join-Path $workspaceRoot $projectName
            New-Item -ItemType Directory -Path $projectRoot -Force | Out-Null
            $sourceFile = Join-Path $projectRoot 'PipedWookie.cs'
            Set-Content -LiteralPath $sourceFile -Value (@(
                '//// # PipedWookie',
                'public class PipedWookie { }'
            ) -join [Environment]::NewLine) -Encoding utf8NoBOM

            Get-Item -LiteralPath $projectRoot | ConvertTo-SharpDown -Language CSharp -OutPath $outputRoot -Recurse 6> $null

            $expectedOutput = Join-Path $outputRoot 'PipedWookie.md'
            Test-Path -LiteralPath $expectedOutput | Should -BeTrue
            $rendered = Get-Content -LiteralPath $expectedOutput -Raw
            $rendered | Should -Match '# PipedWookie'
        }
        finally {
            Remove-Item -LiteralPath $workspaceRoot -Recurse -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputRoot    -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    #### - Processes multiple piped directories in one invocation.
    ####
    It 'Processes multiple directories piped into a single invocation' {
        $workspaceRoot = New-TempWorkspace
        $outputRoot    = New-TempWorkspace
        try {
            foreach ($name in 'AlphaProject', 'BetaProject') {
                $projectRoot = Join-Path $workspaceRoot $name
                New-Item -ItemType Directory -Path $projectRoot -Force | Out-Null
                Set-Content -LiteralPath (Join-Path $projectRoot "$name.cs") -Value (@(
                    "//// # $name",
                    "public class $name { }"
                ) -join [Environment]::NewLine) -Encoding utf8NoBOM
            }

            Get-ChildItem -Path $workspaceRoot -Directory |
                ConvertTo-SharpDown -Language CSharp -OutPath $outputRoot -Recurse 6> $null | Out-Null

            Test-Path -LiteralPath (Join-Path $outputRoot 'AlphaProject.md') | Should -BeTrue
            Test-Path -LiteralPath (Join-Path $outputRoot 'BetaProject.md')  | Should -BeTrue
        }
        finally {
            Remove-Item -LiteralPath $workspaceRoot -Recurse -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputRoot    -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    #### - Warns when no files match the language extensions.
    ####
    It 'Warns when directory has no matching extensions' {
        $workspaceRoot = New-TempWorkspace
        $outputRoot    = New-TempWorkspace
        try {
            Set-Content -LiteralPath (Join-Path $workspaceRoot 'notes.txt') -Value 'no code here' -Encoding utf8NoBOM
            $warnings = @()
            ConvertTo-SharpDown -Language CSharp -Path $workspaceRoot -OutPath $outputRoot -Recurse -WarningVariable warnings -WarningAction SilentlyContinue 3> $null 6> $null
            $warnings.Count | Should -BeGreaterThan 0
        }
        finally {
            Remove-Item -LiteralPath $workspaceRoot -Recurse -Force -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $outputRoot    -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}