BeforeAll {
    $script:DevRoot = Join-Path $PSScriptRoot '..' 'dev'
    $script:Launcher = Join-Path $script:DevRoot 'launch-dev-mcp.ps1'
    Import-Module (Join-Path $script:DevRoot 'KompasMcpDev.psm1') -Force

    function New-TempDir {
        $d = Join-Path ([System.IO.Path]::GetTempPath()) ('kompasdev_' + [guid]::NewGuid().ToString('N'))
        New-Item -ItemType Directory -Path $d -Force | Out-Null
        return $d
    }

    # Фейковая «сборка»: cmd.exe под именем kompas-mcp.exe. Даёт настоящий запускаемый
    # процесс, поведением которого можно управлять аргументами (/c echo …, /c exit N).
    function New-FakeBuild {
        $dir = New-TempDir
        Copy-Item -LiteralPath $env:ComSpec -Destination (Join-Path $dir 'kompas-mcp.exe')
        Set-Content -LiteralPath (Join-Path $dir 'Kompas.Mcp.Core.dll') -Value 'stub' -Encoding ascii
        return $dir
    }

    function Invoke-DevLauncher {
        param([string[]] $LauncherArgs = @())

        $stdOut = [System.IO.Path]::GetTempFileName()
        $stdErr = [System.IO.Path]::GetTempFileName()
        # powershell (5.1), а не pwsh: именно так лаунчер запускается из .mcp.json.
        $psArgs = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $script:Launcher) + $LauncherArgs

        $proc = Start-Process -FilePath 'powershell' -ArgumentList $psArgs -NoNewWindow -Wait -PassThru `
            -RedirectStandardOutput $stdOut -RedirectStandardError $stdErr

        return [pscustomobject]@{
            ExitCode   = $proc.ExitCode
            StdOutPath = $stdOut
            StdErrPath = $stdErr
        }
    }

    function Remove-LauncherResult {
        param($Result)
        Remove-Item -LiteralPath $Result.StdOutPath, $Result.StdErrPath -ErrorAction SilentlyContinue
    }
}

Describe 'Get-KompasMcpBuildFingerprint' {
    It 'стабилен для неизменного каталога' {
        $dir = New-FakeBuild
        try {
            Get-KompasMcpBuildFingerprint -Directory $dir |
                Should -Be (Get-KompasMcpBuildFingerprint -Directory $dir)
        } finally { Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue }
    }

    It 'меняется, когда файл пересобран' {
        $dir = New-FakeBuild
        try {
            $before = Get-KompasMcpBuildFingerprint -Directory $dir
            Set-Content -LiteralPath (Join-Path $dir 'Kompas.Mcp.Core.dll') -Value 'stub v2' -Encoding ascii
            Get-KompasMcpBuildFingerprint -Directory $dir | Should -Not -Be $before
        } finally { Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue }
    }

    It 'меняется, когда в сборке появился новый файл' {
        $dir = New-FakeBuild
        try {
            $before = Get-KompasMcpBuildFingerprint -Directory $dir
            Set-Content -LiteralPath (Join-Path $dir 'Extra.dll') -Value 'x' -Encoding ascii
            Get-KompasMcpBuildFingerprint -Directory $dir | Should -Not -Be $before
        } finally { Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue }
    }

    It 'совпадает между Windows PowerShell 5.1 и pwsh 7' {
        # Лаунчер считает отпечаток под 5.1 (так его запускает .mcp.json), а
        # rebuild-mcp.ps1 — под 7. Разойдись они, имя каталога теневой копии не
        # совпало бы с тем, что сообщает сборка. Ровно это и происходило, пока
        # строки сортировались через Sort-Object: 5.1 (NLS) и 7 (ICU) упорядочивают
        # пунктуацию по-разному.
        $dir = New-FakeBuild
        try {
            $module = Join-Path $script:DevRoot 'KompasMcpDev.psm1'
            $fp51 = & powershell -NoProfile -NonInteractive -Command `
                "Import-Module '$module' -Force; Get-KompasMcpBuildFingerprint -Directory '$dir'"
            $fp51 | Should -Be (Get-KompasMcpBuildFingerprint -Directory $dir)
        } finally { Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue }
    }

    It 'падает с внятным сообщением на несуществующем каталоге' {
        { Get-KompasMcpBuildFingerprint -Directory 'C:\нет\такого\каталога' } |
            Should -Throw -ExpectedMessage '*нет каталога сборки*'
    }

    It 'работает под 5.1, когда в каталоге ровно один файл' {
        # Скалярный (не массивом) результат Get-ChildItem под Set-StrictMode в 5.1 —
        # обращение к .Count даёт PropertyNotFoundException. В pwsh 7 не воспроизводится.
        $dir = New-TempDir
        try {
            Copy-Item -LiteralPath $env:ComSpec -Destination (Join-Path $dir 'kompas-mcp.exe')
            $module = Join-Path $script:DevRoot 'KompasMcpDev.psm1'
            $out = & powershell -NoProfile -NonInteractive -Command `
                "Import-Module '$module' -Force; Get-KompasMcpBuildFingerprint -Directory '$dir'" 2>&1
            ($out | Out-String) | Should -Not -Match 'PropertyNotFound'
            ($out | Out-String).Trim() | Should -Match '^[0-9a-f]{16}$'
        } finally { Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue }
    }
}

Describe 'Copy-KompasMcpBuild' {
    # Имена тестов Pester разворачивает как интерполируемые строки, поэтому в них
    # не должно быть $-переменных и угловых «плейсхолдеров» вида <root>: под
    # Set-StrictMode (его включает run-ps-tests.ps1) такое имя валит тест ещё до тела.
    It 'раскладывает сборку в каталог с именем-отпечатком и помечает маркером' {
        $src = New-FakeBuild
        $root = New-TempDir
        try {
            $fp = Get-KompasMcpBuildFingerprint -Directory $src
            $target = Copy-KompasMcpBuild -SourceDirectory $src -InstallRoot $root -Fingerprint $fp

            (Split-Path -Leaf $target) | Should -Be $fp
            Test-Path -LiteralPath (Join-Path $target 'kompas-mcp.exe') | Should -BeTrue
            Test-Path -LiteralPath (Join-Path $target 'Kompas.Mcp.Core.dll') | Should -BeTrue
            Test-KompasMcpShadow -Directory $target -Fingerprint $fp | Should -BeTrue
        } finally {
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'повторный вызов не копирует заново' {
        $src = New-FakeBuild
        $root = New-TempDir
        try {
            $fp = Get-KompasMcpBuildFingerprint -Directory $src
            $target = Copy-KompasMcpBuild -SourceDirectory $src -InstallRoot $root -Fingerprint $fp
            $marker = Join-Path $target '.kompas-mcp-dev.json'
            $stampBefore = (Get-Item -LiteralPath $marker).LastWriteTimeUtc

            Copy-KompasMcpBuild -SourceDirectory $src -InstallRoot $root -Fingerprint $fp | Should -Be $target
            (Get-Item -LiteralPath $marker).LastWriteTimeUtc | Should -Be $stampBefore
        } finally {
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'не оставляет мусор в .tmp' {
        $src = New-FakeBuild
        $root = New-TempDir
        try {
            $fp = Get-KompasMcpBuildFingerprint -Directory $src
            Copy-KompasMcpBuild -SourceDirectory $src -InstallRoot $root -Fingerprint $fp | Out-Null

            $tmp = Join-Path $root '.tmp'
            if (Test-Path -LiteralPath $tmp) {
                # @(...) — пустой каталог даёт $null, и .Count под StrictMode падает.
                @(Get-ChildItem -LiteralPath $tmp -Force).Count | Should -Be 0
            }
        } finally {
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'падает с внятным сообщением, когда в каталоге сборки нет kompas-mcp.exe' {
        $src = New-TempDir
        $root = New-TempDir
        try {
            Set-Content -LiteralPath (Join-Path $src 'readme.txt') -Value 'x' -Encoding ascii
            { Copy-KompasMcpBuild -SourceDirectory $src -InstallRoot $root -Fingerprint 'deadbeefdeadbeef' } |
                Should -Throw -ExpectedMessage '*нет kompas-mcp.exe*'
        } finally {
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

Describe 'Test-KompasMcpShadow' {
    It 'отвергает копию с чужим отпечатком' {
        $src = New-FakeBuild
        $root = New-TempDir
        try {
            $fp = Get-KompasMcpBuildFingerprint -Directory $src
            $target = Copy-KompasMcpBuild -SourceDirectory $src -InstallRoot $root -Fingerprint $fp
            Test-KompasMcpShadow -Directory $target -Fingerprint 'aaaaaaaaaaaaaaaa' | Should -BeFalse
        } finally {
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'отвергает копию без маркера (оборванное копирование)' {
        $src = New-FakeBuild
        $root = New-TempDir
        try {
            $fp = Get-KompasMcpBuildFingerprint -Directory $src
            $target = Copy-KompasMcpBuild -SourceDirectory $src -InstallRoot $root -Fingerprint $fp
            Remove-Item -LiteralPath (Join-Path $target '.kompas-mcp-dev.json') -Force
            Test-KompasMcpShadow -Directory $target -Fingerprint $fp | Should -BeFalse
        } finally {
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'отвергает копию с повреждённым маркером' {
        $src = New-FakeBuild
        $root = New-TempDir
        try {
            $fp = Get-KompasMcpBuildFingerprint -Directory $src
            $target = Copy-KompasMcpBuild -SourceDirectory $src -InstallRoot $root -Fingerprint $fp
            Set-Content -LiteralPath (Join-Path $target '.kompas-mcp-dev.json') -Value '{ это не json' -Encoding utf8
            Test-KompasMcpShadow -Directory $target -Fingerprint $fp | Should -BeFalse
        } finally {
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

Describe 'Remove-KompasMcpStaleShadow' {
    It 'убирает старую копию, но не текущую и не .tmp' {
        $root = New-TempDir
        try {
            $old = Join-Path $root 'aaaaaaaaaaaaaaaa'
            $keep = Join-Path $root 'bbbbbbbbbbbbbbbb'
            $tmp = Join-Path $root '.tmp'
            foreach ($d in @($old, $keep, $tmp)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }

            $past = (Get-Date).AddDays(-30)
            (Get-Item -LiteralPath $old).LastWriteTime = $past
            (Get-Item -LiteralPath $tmp).LastWriteTime = $past

            Remove-KompasMcpStaleShadow -Root $root -Keep 'bbbbbbbbbbbbbbbb' -MaxAge ([timespan]::FromDays(3))

            Test-Path -LiteralPath $old | Should -BeFalse
            Test-Path -LiteralPath $keep | Should -BeTrue
            Test-Path -LiteralPath $tmp | Should -BeTrue
        } finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
    }

    It 'молча переживает отсутствующий каталог' {
        { Remove-KompasMcpStaleShadow -Root 'C:\нет\такого\каталога' } | Should -Not -Throw
    }
}

Describe 'launch-dev-mcp.ps1' {
    It 'запускает сервер из теневой копии, а не из каталога сборки' {
        $src = New-FakeBuild
        $root = New-TempDir
        $env:KOMPAS_MCP_DEV_BUILD_DIR = $src
        $env:KOMPAS_MCP_DEV_ROOT = $root
        try {
            # cmd /c echo %~dp0 не годится (это путь скрипта, а не exe), поэтому просто
            # убеждаемся: теневая копия создалась и сервер запустился именно после неё.
            $result = Invoke-DevLauncher -LauncherArgs @('/c', 'echo', 'PONG')
            try {
                $result.ExitCode | Should -Be 0
                (Get-Content -LiteralPath $result.StdOutPath -Raw) | Should -Be "PONG`r`n"

                $fp = Get-KompasMcpBuildFingerprint -Directory $src
                Test-Path -LiteralPath (Join-Path $root "$fp\kompas-mcp.exe") | Should -BeTrue
            } finally { Remove-LauncherResult $result }
        } finally {
            Remove-Item Env:\KOMPAS_MCP_DEV_BUILD_DIR, Env:\KOMPAS_MCP_DEV_ROOT -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'оставляет каталог сборки незалоченным: его можно удалить, пока сервер работает' {
        $src = New-FakeBuild
        $root = New-TempDir
        $env:KOMPAS_MCP_DEV_BUILD_DIR = $src
        $env:KOMPAS_MCP_DEV_ROOT = $root
        try {
            $result = Invoke-DevLauncher -LauncherArgs @('/c', 'exit', '0')
            Remove-LauncherResult $result

            # Ровно то, ради чего затеян теневой запуск: после старта сервера каталог
            # сборки свободен — MSBuild сможет перезаписать его, не дожидаясь выхода сессии.
            { Remove-Item -LiteralPath $src -Recurse -Force } | Should -Not -Throw
        } finally {
            Remove-Item Env:\KOMPAS_MCP_DEV_BUILD_DIR, Env:\KOMPAS_MCP_DEV_ROOT -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'пробрасывает код возврата сервера' {
        $src = New-FakeBuild
        $root = New-TempDir
        $env:KOMPAS_MCP_DEV_BUILD_DIR = $src
        $env:KOMPAS_MCP_DEV_ROOT = $root
        try {
            $result = Invoke-DevLauncher -LauncherArgs @('/c', 'exit', '42')
            try { $result.ExitCode | Should -Be 42 } finally { Remove-LauncherResult $result }
        } finally {
            Remove-Item Env:\KOMPAS_MCP_DEV_BUILD_DIR, Env:\KOMPAS_MCP_DEV_ROOT -ErrorAction SilentlyContinue
            Remove-Item -LiteralPath $src, $root -Recurse -Force -ErrorAction SilentlyContinue
        }
    }

    It 'уважает KOMPAS_MCP_EXE в обход теневой копии' {
        $env:KOMPAS_MCP_EXE = $env:ComSpec
        try {
            $result = Invoke-DevLauncher -LauncherArgs @('/c', 'echo', 'PONG')
            try {
                (Get-Content -LiteralPath $result.StdOutPath -Raw) | Should -Be "PONG`r`n"
            } finally { Remove-LauncherResult $result }
        } finally { Remove-Item Env:\KOMPAS_MCP_EXE -ErrorAction SilentlyContinue }
    }

    It 'падает с кодом 1 и подсказкой про dotnet build, когда сборки нет' {
        $env:KOMPAS_MCP_DEV_BUILD_DIR = 'C:\нет\такого\bin'
        try {
            $result = Invoke-DevLauncher
            try {
                $result.ExitCode | Should -Be 1
                $stderrText = Get-Content -LiteralPath $result.StdErrPath -Raw
                $stderrText | Should -Match 'kompas-mcp dev-launcher'
                $stderrText | Should -Match 'dotnet build'
            } finally { Remove-LauncherResult $result }
        } finally { Remove-Item Env:\KOMPAS_MCP_DEV_BUILD_DIR -ErrorAction SilentlyContinue }
    }

    It 'на пути отказа не пишет в stdout ни одного байта' {
        $env:KOMPAS_MCP_DEV_BUILD_DIR = 'C:\нет\такого\bin'
        try {
            $result = Invoke-DevLauncher
            try {
                $result.ExitCode | Should -Be 1
                # Один лишний байт в stdout ломает JSON-RPC на всю MCP-сессию —
                # самый критичный инвариант лаунчера, проверяем по длине файла.
                (Get-Item -LiteralPath $result.StdOutPath).Length | Should -Be 0
            } finally { Remove-LauncherResult $result }
        } finally { Remove-Item Env:\KOMPAS_MCP_DEV_BUILD_DIR -ErrorAction SilentlyContinue }
    }
}
