import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path

import pytest


def _repo_root() -> Path:
    return Path(__file__).resolve().parents[2]


def _write_executable(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")
    path.chmod(0o755)


def _braced_body(text: str, declaration: str) -> str:
    start = text.index(declaration)
    brace_start = text.index("{", start)
    depth = 0
    for index, char in enumerate(text[brace_start:], start=brace_start):
        if char == "{":
            depth += 1
        elif char == "}":
            depth -= 1
            if depth == 0:
                return text[brace_start + 1 : index]
    raise AssertionError(f"Unclosed function body for {declaration}")


def _posix_command(name: str) -> str:
    help_output = (
        '    echo "  --extension, -e <path>  Load an extension"\n'
        '    echo "  --models <patterns>     Scope models"'
        if name == "pi"
        else "    :"
    )
    return f"""#!/bin/sh
echo "{name}:$*" >> "$CALL_LOG"
if [ "$FAIL_STEP" = "{name}-verify" ]; then
    exit 31
fi
if [ "${{1:-}}" = "--version" ]; then
    echo "{name} 1.0.0"
fi
if [ "${{1:-}}" = "--help" ]; then
{help_output}
fi
"""


def _posix_npm_command() -> str:
    return """#!/bin/sh
echo "npm:$*" >> "$CALL_LOG"
if [ "${1:-}" = "prefix" ] && [ "${2:-}" = "-g" ]; then
    printf '%s\n' "$FAKE_NPM_PREFIX"
    exit 0
fi
if [ "${1:-}" = "config" ] && [ "${2:-}" = "get" ] && [ "${3:-}" = "prefix" ]; then
    printf '%s\n' "$FAKE_NPM_PREFIX"
    exit 0
fi
exit 71
"""


def _posix_uv_command(version: str) -> str:
    return f"""#!/bin/sh
echo "uv:$*" >> "$CALL_LOG"
if [ "${{1:-}}" = "--version" ]; then
    if [ "$FAIL_STEP" = "uv-verify" ]; then
        exit 32
    fi
    echo "uv {version}"
    exit 0
fi
if [ "${{1:-}}" = "tool" ] && [ "${{2:-}}" = "install" ]; then
    if [ "$FAIL_STEP" = "fcc-install" ]; then
        exit 33
    fi
    mkdir -p "$FAKE_TOOL_BIN"
    cp "$FAKE_FIXTURES/fcc-command.sh" "$FAKE_TOOL_BIN/fcc-server"
    cp "$FAKE_FIXTURES/fcc-command.sh" "$FAKE_TOOL_BIN/fcc-claude"
    cp "$FAKE_FIXTURES/fcc-command.sh" "$FAKE_TOOL_BIN/fcc-pi"
    if [ "$FAIL_STEP" != "fcc-missing" ]; then
        cp "$FAKE_FIXTURES/fcc-command.sh" "$FAKE_TOOL_BIN/fcc-codex"
    fi
    chmod +x "$FAKE_TOOL_BIN"/fcc-*
    exit 0
fi
if [ "${{1:-}}" = "tool" ] && [ "${{2:-}}" = "update-shell" ]; then
    if [ "$FAIL_STEP" = "path-update" ]; then
        exit 34
    fi
    exit 0
fi
if [ "${{1:-}}" = "tool" ] && [ "${{2:-}}" = "dir" ] && [ "${{3:-}}" = "--bin" ]; then
    printf '%s\n' "$FAKE_TOOL_BIN"
    exit 0
fi
exit 35
"""


@dataclass
class PosixHarness:
    root: Path
    bin_dir: Path
    fixtures: Path
    tool_bin: Path
    log: Path
    env: dict[str, str]

    def add_client(self, name: str) -> None:
        _write_executable(self.bin_dir / name, _posix_command(name))

    def add_unrelated_pi(self) -> None:
        _write_executable(self.bin_dir / "pi", _posix_command("unrelated-pi"))

    def add_npm_prefix(self, prefix: Path) -> None:
        prefix.mkdir(parents=True)
        self.env["FAKE_NPM_PREFIX"] = str(prefix)
        _write_executable(self.bin_dir / "npm", _posix_npm_command())

    def add_uv(self, version: str) -> None:
        _write_executable(self.bin_dir / "uv", _posix_uv_command(version))

    def run(self, *args: str, fail_step: str = "") -> subprocess.CompletedProcess[str]:
        env = self.env | {"FAIL_STEP": fail_step}
        return subprocess.run(
            ["/bin/sh", str(_repo_root() / "scripts" / "install.sh"), *args],
            check=False,
            capture_output=True,
            text=True,
            env=env,
        )

    def calls(self) -> list[str]:
        if not self.log.exists():
            return []
        return self.log.read_text(encoding="utf-8").splitlines()


@pytest.fixture
def posix_harness(tmp_path: Path) -> PosixHarness:
    if os.name == "nt":
        pytest.skip("POSIX installer scenarios run on POSIX hosts")

    bin_dir = tmp_path / "bin"
    fixtures = tmp_path / "fixtures"
    tool_bin = tmp_path / "tool-bin"
    home = tmp_path / "home"
    log = tmp_path / "calls.log"
    for path in (bin_dir, fixtures, tool_bin, home):
        path.mkdir(parents=True)

    _write_executable(
        bin_dir / "curl",
        """#!/bin/sh
url=""
output=""
while [ "$#" -gt 0 ]; do
    case "$1" in
        -o)
            shift
            output=$1
            ;;
        http*)
            url=$1
            ;;
    esac
    shift
done
echo "download:$url" >> "$CALL_LOG"
case "$url:$FAIL_STEP" in
    *claude.ai*:claude-download|*chatgpt.com*:codex-download|*pi.dev*:pi-download|*astral.sh*:uv-download)
        exit 41
        ;;
esac
case "$url" in
    *claude.ai*) source="$FAKE_FIXTURES/claude-installer.sh" ;;
    *chatgpt.com*) source="$FAKE_FIXTURES/codex-installer.sh" ;;
    *pi.dev*) source="$FAKE_FIXTURES/pi-installer.sh" ;;
    *astral.sh*) source="$FAKE_FIXTURES/uv-installer.sh" ;;
    *) exit 42 ;;
esac
cp "$source" "$output"
""",
    )
    _write_executable(
        fixtures / "claude-installer.sh",
        """#!/bin/sh
echo "claude-install" >> "$CALL_LOG"
[ "$FAIL_STEP" = "claude-install" ] && exit 21
mkdir -p "$HOME/.local/bin"
cp "$FAKE_FIXTURES/claude-command.sh" "$HOME/.local/bin/claude"
chmod +x "$HOME/.local/bin/claude"
""",
    )
    _write_executable(
        fixtures / "codex-installer.sh",
        """#!/bin/sh
echo "codex-install:$CODEX_NON_INTERACTIVE" >> "$CALL_LOG"
[ "$FAIL_STEP" = "codex-install" ] && exit 22
mkdir -p "$HOME/.local/bin"
cp "$FAKE_FIXTURES/codex-command.sh" "$HOME/.local/bin/codex"
chmod +x "$HOME/.local/bin/codex"
""",
    )
    _write_executable(
        fixtures / "pi-installer.sh",
        """#!/bin/sh
echo "pi-install" >> "$CALL_LOG"
[ "$FAIL_STEP" = "pi-install" ] && exit 24
if [ -n "${FAKE_NPM_PREFIX:-}" ]; then
    pi_bin="$FAKE_NPM_PREFIX/bin"
else
    pi_bin="$HOME/.local/bin"
fi
mkdir -p "$pi_bin"
cp "$FAKE_FIXTURES/pi-command.sh" "$pi_bin/pi"
chmod +x "$pi_bin/pi"
""",
    )
    _write_executable(
        fixtures / "uv-installer.sh",
        """#!/bin/sh
echo "uv-install" >> "$CALL_LOG"
[ "$FAIL_STEP" = "uv-install" ] && exit 23
mkdir -p "$HOME/.local/bin"
cp "$FAKE_FIXTURES/uv-command.sh" "$HOME/.local/bin/uv"
chmod +x "$HOME/.local/bin/uv"
""",
    )
    _write_executable(fixtures / "claude-command.sh", _posix_command("claude"))
    _write_executable(fixtures / "codex-command.sh", _posix_command("codex"))
    _write_executable(fixtures / "pi-command.sh", _posix_command("pi"))
    _write_executable(fixtures / "uv-command.sh", _posix_uv_command("0.11.28"))
    _write_executable(
        fixtures / "fcc-command.sh",
        """#!/bin/sh
name=${0##*/}
echo "$name:$*" >> "$CALL_LOG"
if [ "$FAIL_STEP" = "fcc-verify" ]; then
    exit 36
fi
if [ "$name" = "fcc-server" ] && [ "${1:-}" = "--version" ]; then
    echo "free-claude-code 3.5.18"
fi
""",
    )

    env = os.environ.copy()
    env.update(
        {
            "PATH": f"{bin_dir}:/usr/bin:/bin",
            "HOME": str(home),
            "CALL_LOG": str(log),
            "FAKE_FIXTURES": str(fixtures),
            "FAKE_TOOL_BIN": str(tool_bin),
            "FAIL_STEP": "",
        }
    )
    env.pop("XDG_BIN_HOME", None)
    return PosixHarness(tmp_path, bin_dir, fixtures, tool_bin, log, env)


def test_install_sh_fresh_install_is_verified(posix_harness: PosixHarness) -> None:
    result = posix_harness.run()

    assert result.returncode == 0, result.stderr
    assert "Free Claude Code is installed and verified." in result.stdout
    calls = posix_harness.calls()
    assert calls.index("claude-install") < calls.index("claude:--version")
    assert calls.index("codex-install:1") < calls.index("codex:--version")
    assert calls.index("pi-install") < calls.index("pi:--version")
    assert calls.index("uv-install") < calls.index("uv:--version")
    assert any(
        call.startswith(
            "uv:tool install --force --refresh-package free-claude-code "
            "--python 3.14.0 free-claude-code @ "
            "https://github.com/Alishahryar1/free-claude-code/archive/refs/heads/main.zip"
        )
        for call in calls
    )
    assert not any(call.startswith("git:") for call in calls)
    assert calls[-3:] == [
        "uv:tool update-shell",
        "uv:tool dir --bin",
        "fcc-server:--version",
    ]


def test_install_sh_preserves_valid_existing_tools(
    posix_harness: PosixHarness,
) -> None:
    posix_harness.add_client("claude")
    posix_harness.add_client("codex")
    posix_harness.add_client("pi")
    posix_harness.add_uv("0.11.7")

    result = posix_harness.run()

    assert result.returncode == 0, result.stderr
    assert not any(call.startswith("download:") for call in posix_harness.calls())
    assert "leaving it unchanged" in result.stdout


def test_install_sh_replaces_unrelated_pi_command(
    posix_harness: PosixHarness,
) -> None:
    posix_harness.add_client("claude")
    posix_harness.add_client("codex")
    posix_harness.add_unrelated_pi()
    posix_harness.add_uv("0.11.7")

    result = posix_harness.run()

    assert result.returncode == 0, result.stderr
    assert "is not Pi Coding Agent; installing Pi" in result.stdout
    assert "pi-install" in posix_harness.calls()


def test_install_sh_discovers_custom_pi_npm_prefix(
    posix_harness: PosixHarness,
) -> None:
    posix_harness.add_client("claude")
    posix_harness.add_client("codex")
    posix_harness.add_npm_prefix(posix_harness.root / "custom-npm")
    posix_harness.add_uv("0.11.7")

    result = posix_harness.run()

    assert result.returncode == 0, result.stderr
    calls = posix_harness.calls()
    assert "npm:prefix -g" in calls
    assert "pi:--help" in calls
    assert "pi:--version" in calls


def test_install_sh_replaces_obsolete_uv(posix_harness: PosixHarness) -> None:
    posix_harness.add_client("claude")
    posix_harness.add_client("codex")
    posix_harness.add_client("pi")
    posix_harness.add_uv("0.5.9")

    result = posix_harness.run()

    assert result.returncode == 0, result.stderr
    assert "uv 0.5.9 is below 0.11.0" in result.stdout
    assert "uv-install" in posix_harness.calls()


@pytest.mark.parametrize(
    "failure",
    [
        "claude-download",
        "claude-install",
        "claude-verify",
        "codex-download",
        "codex-install",
        "codex-verify",
        "pi-download",
        "pi-install",
        "pi-verify",
        "uv-download",
        "uv-install",
        "uv-verify",
        "fcc-install",
        "path-update",
        "fcc-missing",
        "fcc-verify",
    ],
)
def test_install_sh_stops_without_success_on_each_failure(
    posix_harness: PosixHarness,
    failure: str,
) -> None:
    result = posix_harness.run(fail_step=failure)

    assert result.returncode != 0
    assert "Free Claude Code is installed and verified." not in result.stdout
    forbidden = {
        "claude-download": "claude-install",
        "claude-install": "claude:--version",
        "claude-verify": "chatgpt.com",
        "codex-download": "codex-install",
        "codex-install": "codex:--version",
        "codex-verify": "pi.dev",
        "pi-download": "pi-install",
        "pi-install": "pi:--version",
        "pi-verify": "astral.sh",
        "uv-download": "uv-install",
        "uv-install": "uv:--version",
        "uv-verify": "uv:tool install",
        "fcc-install": "uv:tool update-shell",
        "path-update": "uv:tool dir --bin",
        "fcc-missing": "fcc-server:--version",
    }.get(failure)
    if forbidden is not None:
        assert not any(forbidden in call for call in posix_harness.calls())


def test_install_sh_dry_run_never_executes_commands(
    posix_harness: PosixHarness,
) -> None:
    result = posix_harness.run("--dry-run")

    assert result.returncode == 0, result.stderr
    assert posix_harness.calls() == []
    assert "Dry run complete. No changes were made." in result.stdout
    assert "Free Claude Code is installed and verified." not in result.stdout


def test_install_sh_rejects_broken_existing_client_without_replacing_it(
    posix_harness: PosixHarness,
) -> None:
    posix_harness.add_client("claude")

    result = posix_harness.run(fail_step="claude-verify")

    assert result.returncode != 0
    assert not any(call.startswith("download:") for call in posix_harness.calls())


def test_install_sh_rejects_unparseable_existing_uv(
    posix_harness: PosixHarness,
) -> None:
    posix_harness.add_client("claude")
    posix_harness.add_client("codex")
    posix_harness.add_client("pi")
    posix_harness.add_uv("not-a-version")

    result = posix_harness.run()

    assert result.returncode != 0
    assert not any("astral.sh" in call for call in posix_harness.calls())


def test_install_sh_voice_flags_only_change_fcc_spec(
    posix_harness: PosixHarness,
) -> None:
    result = posix_harness.run("--voice-all", "--torch-backend", "cu130")

    assert result.returncode == 0, result.stderr
    assert any(
        "--torch-backend cu130 free-claude-code[voice,voice_local] @ "
        "https://github.com/Alishahryar1/free-claude-code/archive/refs/heads/main.zip"
        in call
        for call in posix_harness.calls()
    )


def test_install_sh_rejects_invalid_options_before_mutation(
    posix_harness: PosixHarness,
) -> None:
    result = posix_harness.run("--torch-backend", "cu130")

    assert result.returncode != 0
    assert posix_harness.calls() == []


def _powershells() -> tuple[str, ...]:
    candidates = (shutil.which("pwsh"), shutil.which("powershell"))
    return tuple(dict.fromkeys(path for path in candidates if path is not None))


def _batch_client(name: str) -> str:
    help_output = (
        "echo   --extension, -e ^<path^>  Load an extension\n"
        "echo   --models ^<patterns^>     Scope models"
        if name == "pi"
        else "rem no product help"
    )
    return f"""@echo off
echo {name}:%*>>"%CALL_LOG%"
if "%FAIL_STEP%"=="{name}-verify" exit /b 51
if "%1"=="--version" echo {name} 1.0.0
if "%1"=="--help" (
{help_output}
)
exit /b 0
"""


def _batch_npm() -> str:
    return r"""@echo off
echo npm:%*>>"%CALL_LOG%"
if "%1"=="prefix" if "%2"=="-g" echo %FAKE_NPM_PREFIX%& exit /b 0
if "%1"=="config" if "%2"=="get" if "%3"=="prefix" echo %FAKE_NPM_PREFIX%& exit /b 0
exit /b 71
"""


def _batch_uv(version: str) -> str:
    return rf"""@echo off
echo uv:%*>>"%CALL_LOG%"
if "%1"=="--version" goto version
if "%1"=="tool" if "%2"=="install" goto install
if "%1"=="tool" if "%2"=="update-shell" goto update_shell
if "%1"=="tool" if "%2"=="dir" if "%3"=="--bin" goto tool_bin
exit /b 59
:version
if "%FAIL_STEP%"=="uv-verify" exit /b 52
echo uv {version}
exit /b 0
:install
if "%FAIL_STEP%"=="fcc-install" exit /b 53
if not exist "%FAKE_TOOL_BIN%" mkdir "%FAKE_TOOL_BIN%"
copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%FAKE_TOOL_BIN%\fcc-server.cmd" >nul
copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%FAKE_TOOL_BIN%\fcc-claude.cmd" >nul
copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%FAKE_TOOL_BIN%\fcc-pi.cmd" >nul
if not "%FAIL_STEP%"=="fcc-missing" copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%FAKE_TOOL_BIN%\fcc-codex.cmd" >nul
exit /b 0
:update_shell
if "%FAIL_STEP%"=="path-update" exit /b 54
exit /b 0
:tool_bin
echo %FAKE_TOOL_BIN%
exit /b 0
"""


@dataclass
class PowerShellHarness:
    root: Path
    bin_dir: Path
    fixtures: Path
    tool_bin: Path
    log: Path
    env: dict[str, str]
    powershell: str
    wrapper: Path

    def add_client(self, name: str) -> None:
        _write_executable(self.bin_dir / f"{name}.cmd", _batch_client(name))

    def add_unrelated_pi(self) -> None:
        _write_executable(self.bin_dir / "pi.cmd", _batch_client("unrelated-pi"))

    def add_npm_prefix(self, prefix: Path) -> None:
        prefix.mkdir(parents=True)
        self.env["FAKE_NPM_PREFIX"] = str(prefix)
        _write_executable(self.bin_dir / "npm.cmd", _batch_npm())

    def add_uv(self, version: str) -> None:
        _write_executable(self.bin_dir / "uv.cmd", _batch_uv(version))

    def run(self, *args: str, fail_step: str = "") -> subprocess.CompletedProcess[str]:
        env = self.env | {"FAIL_STEP": fail_step}
        return subprocess.run(
            [
                self.powershell,
                "-NoProfile",
                "-ExecutionPolicy",
                "Bypass",
                "-File",
                str(self.wrapper),
                *args,
            ],
            check=False,
            capture_output=True,
            text=True,
            env=env,
        )

    def calls(self) -> list[str]:
        if not self.log.exists():
            return []
        return self.log.read_text(encoding="utf-8").splitlines()


@pytest.fixture(
    params=_powershells() or (None,),
    ids=lambda path: Path(path).name if path is not None else "unavailable",
)
def powershell_harness(
    tmp_path: Path,
    request: pytest.FixtureRequest,
) -> PowerShellHarness:
    powershell = request.param
    if powershell is None or os.name != "nt":
        pytest.skip("PowerShell installer scenarios run on Windows hosts")

    bin_dir = tmp_path / "bin"
    fixtures = tmp_path / "fixtures"
    tool_bin = tmp_path / "tool-bin"
    home = tmp_path / "home"
    local_app_data = tmp_path / "local-app-data"
    app_data = tmp_path / "app-data"
    log = tmp_path / "calls.log"
    for path in (bin_dir, fixtures, tool_bin, home, local_app_data, app_data):
        path.mkdir(parents=True)

    (fixtures / "claude-command.cmd").write_text(
        _batch_client("claude"), encoding="utf-8"
    )
    (fixtures / "codex-command.cmd").write_text(
        _batch_client("codex"), encoding="utf-8"
    )
    (fixtures / "pi-command.cmd").write_text(_batch_client("pi"), encoding="utf-8")
    (fixtures / "uv-command.cmd").write_text(_batch_uv("0.11.28"), encoding="utf-8")
    (fixtures / "fcc-command.cmd").write_text(
        """@echo off
for %%I in ("%~f0") do set "FCC_NAME=%%~nI"
echo %FCC_NAME%:%*>>"%CALL_LOG%"
if "%FAIL_STEP%"=="fcc-verify" exit /b 55
if "%FCC_NAME%"=="fcc-server" if "%1"=="--version" echo free-claude-code 3.5.18
exit /b 0
""",
        encoding="utf-8",
    )
    (fixtures / "claude-installer.ps1").write_text(
        r"""if ($env:FAIL_STEP -eq "claude-install") { exit 61 }
$bin = Join-Path $env:USERPROFILE ".local\bin"
New-Item -ItemType Directory -Force -Path $bin | Out-Null
Copy-Item (Join-Path $env:FAKE_FIXTURES "claude-command.cmd") (Join-Path $bin "claude.cmd") -Force
Add-Content -LiteralPath $env:CALL_LOG -Value "claude-install"
""",
        encoding="utf-8",
    )
    (fixtures / "codex-installer.ps1").write_text(
        r"""if ($env:FAIL_STEP -eq "codex-install") { exit 62 }
$bin = Join-Path $env:LOCALAPPDATA "Programs\OpenAI\Codex\bin"
New-Item -ItemType Directory -Force -Path $bin | Out-Null
Copy-Item (Join-Path $env:FAKE_FIXTURES "codex-command.cmd") (Join-Path $bin "codex.cmd") -Force
Add-Content -LiteralPath $env:CALL_LOG -Value "codex-install:$env:CODEX_NON_INTERACTIVE"
""",
        encoding="utf-8",
    )
    (fixtures / "pi-installer.ps1").write_text(
        r"""if ($env:FAIL_STEP -eq "pi-install") { exit 64 }
$bin = if ($env:FAKE_NPM_PREFIX) { $env:FAKE_NPM_PREFIX } else { Join-Path $env:APPDATA "npm" }
New-Item -ItemType Directory -Force -Path $bin | Out-Null
Copy-Item (Join-Path $env:FAKE_FIXTURES "pi-command.cmd") (Join-Path $bin "pi.cmd") -Force
Add-Content -LiteralPath $env:CALL_LOG -Value "pi-install"
""",
        encoding="utf-8",
    )
    (fixtures / "uv-installer.ps1").write_text(
        r"""if ($env:FAIL_STEP -eq "uv-install") { exit 63 }
$bin = Join-Path $env:USERPROFILE ".local\bin"
New-Item -ItemType Directory -Force -Path $bin | Out-Null
Copy-Item (Join-Path $env:FAKE_FIXTURES "uv-command.cmd") (Join-Path $bin "uv.cmd") -Force
Add-Content -LiteralPath $env:CALL_LOG -Value "uv-install"
""",
        encoding="utf-8",
    )

    wrapper = tmp_path / "run-installer.ps1"
    wrapper.write_text(
        """Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Invoke-RestMethod {
    [CmdletBinding()]
    param([string] $Uri, [string] $OutFile)

    Add-Content -LiteralPath $env:CALL_LOG -Value "download:$Uri"
    if (
        ($env:FAIL_STEP -eq "claude-download" -and $Uri.Contains("claude.ai")) -or
        ($env:FAIL_STEP -eq "codex-download" -and $Uri.Contains("chatgpt.com")) -or
        ($env:FAIL_STEP -eq "pi-download" -and $Uri.Contains("pi.dev")) -or
        ($env:FAIL_STEP -eq "uv-download" -and $Uri.Contains("astral.sh"))
    ) {
        throw "simulated download failure"
    }
    if ($Uri.Contains("claude.ai")) {
        $source = Join-Path $env:FAKE_FIXTURES "claude-installer.ps1"
    }
    elseif ($Uri.Contains("chatgpt.com")) {
        $source = Join-Path $env:FAKE_FIXTURES "codex-installer.ps1"
    }
    elseif ($Uri.Contains("pi.dev")) {
        $source = Join-Path $env:FAKE_FIXTURES "pi-installer.ps1"
    }
    elseif ($Uri.Contains("astral.sh")) {
        $source = Join-Path $env:FAKE_FIXTURES "uv-installer.ps1"
    }
    else {
        throw "unexpected installer URL: $Uri"
    }
    Copy-Item -LiteralPath $source -Destination $OutFile -Force
}
$installer = [scriptblock]::Create([IO.File]::ReadAllText($env:FCC_INSTALLER))
& $installer @args
""",
        encoding="utf-8",
    )

    system_root = os.environ["SYSTEMROOT"]
    env = os.environ.copy()
    env.update(
        {
            "PATH": os.pathsep.join(
                [str(bin_dir), str(Path(system_root) / "System32"), system_root]
            ),
            "PATHEXT": ".COM;.EXE;.BAT;.CMD",
            "USERPROFILE": str(home),
            "LOCALAPPDATA": str(local_app_data),
            "APPDATA": str(app_data),
            "CALL_LOG": str(log),
            "FAKE_FIXTURES": str(fixtures),
            "FAKE_TOOL_BIN": str(tool_bin),
            "FCC_INSTALLER": str(_repo_root() / "scripts" / "install.ps1"),
            "FAIL_STEP": "",
        }
    )
    return PowerShellHarness(
        tmp_path, bin_dir, fixtures, tool_bin, log, env, powershell, wrapper
    )


def test_install_ps1_fresh_install_is_verified(
    powershell_harness: PowerShellHarness,
) -> None:
    result = powershell_harness.run()

    assert result.returncode == 0, result.stderr
    assert "Free Claude Code is installed and verified." in result.stdout
    calls = powershell_harness.calls()
    assert calls.index("claude-install") < calls.index("claude:--version")
    assert calls.index("codex-install:1") < calls.index("codex:--version")
    assert calls.index("pi-install") < calls.index("pi:--version")
    assert calls.index("uv-install") < calls.index("uv:--version")
    assert any(
        call.startswith(
            "uv:tool install --force --refresh-package free-claude-code "
            '--python 3.14.0 "free-claude-code @ '
            'https://github.com/Alishahryar1/free-claude-code/archive/refs/heads/main.zip"'
        )
        for call in calls
    )
    assert not any(call.startswith("git:") for call in calls)
    assert calls[-3:] == [
        "uv:tool update-shell",
        "uv:tool dir --bin",
        "fcc-server:--version",
    ]


def test_install_ps1_preserves_valid_existing_tools(
    powershell_harness: PowerShellHarness,
) -> None:
    powershell_harness.add_client("claude")
    powershell_harness.add_client("codex")
    powershell_harness.add_client("pi")
    powershell_harness.add_uv("0.11.7")

    result = powershell_harness.run()

    assert result.returncode == 0, result.stderr
    assert not any(call.startswith("download:") for call in powershell_harness.calls())
    assert "leaving it unchanged" in result.stdout


def test_install_ps1_replaces_unrelated_pi_command(
    powershell_harness: PowerShellHarness,
) -> None:
    powershell_harness.add_client("claude")
    powershell_harness.add_client("codex")
    powershell_harness.add_unrelated_pi()
    powershell_harness.add_uv("0.11.7")

    result = powershell_harness.run()

    assert result.returncode == 0, result.stderr
    assert "is not Pi Coding Agent; installing Pi" in result.stdout
    assert "pi-install" in powershell_harness.calls()


def test_install_ps1_discovers_custom_pi_npm_prefix(
    powershell_harness: PowerShellHarness,
) -> None:
    powershell_harness.add_client("claude")
    powershell_harness.add_client("codex")
    powershell_harness.add_npm_prefix(powershell_harness.root / "custom-npm")
    powershell_harness.add_uv("0.11.7")

    result = powershell_harness.run()

    assert result.returncode == 0, result.stderr
    calls = powershell_harness.calls()
    assert "npm:prefix -g" in calls
    assert "pi:--help" in calls
    assert "pi:--version" in calls


def test_install_ps1_replaces_obsolete_uv(
    powershell_harness: PowerShellHarness,
) -> None:
    powershell_harness.add_client("claude")
    powershell_harness.add_client("codex")
    powershell_harness.add_client("pi")
    powershell_harness.add_uv("0.5.9")

    result = powershell_harness.run()

    assert result.returncode == 0, result.stderr
    assert "uv 0.5.9 is below 0.11.0" in result.stdout
    assert "uv-install" in powershell_harness.calls()


@pytest.mark.parametrize(
    "failure",
    [
        "claude-download",
        "claude-install",
        "claude-verify",
        "codex-download",
        "codex-install",
        "codex-verify",
        "pi-download",
        "pi-install",
        "pi-verify",
        "uv-download",
        "uv-install",
        "uv-verify",
        "fcc-install",
        "path-update",
        "fcc-missing",
        "fcc-verify",
    ],
)
def test_install_ps1_stops_without_success_on_each_failure(
    powershell_harness: PowerShellHarness,
    failure: str,
) -> None:
    result = powershell_harness.run(fail_step=failure)

    assert result.returncode != 0
    assert "Free Claude Code is installed and verified." not in result.stdout
    forbidden = {
        "claude-download": "claude-install",
        "claude-install": "claude:--version",
        "claude-verify": "chatgpt.com",
        "codex-download": "codex-install",
        "codex-install": "codex:--version",
        "codex-verify": "pi.dev",
        "pi-download": "pi-install",
        "pi-install": "pi:--version",
        "pi-verify": "astral.sh",
        "uv-download": "uv-install",
        "uv-install": "uv:--version",
        "uv-verify": "uv:tool install",
        "fcc-install": "uv:tool update-shell",
        "path-update": "uv:tool dir --bin",
        "fcc-missing": "fcc-server:--version",
    }.get(failure)
    if forbidden is not None:
        assert not any(forbidden in call for call in powershell_harness.calls())


def test_install_ps1_dry_run_never_executes_commands(
    powershell_harness: PowerShellHarness,
) -> None:
    result = subprocess.run(
        [
            powershell_harness.powershell,
            "-NoProfile",
            "-ExecutionPolicy",
            "Bypass",
            "-File",
            str(_repo_root() / "scripts" / "install.ps1"),
            "-DryRun",
        ],
        check=False,
        capture_output=True,
        text=True,
        env=powershell_harness.env,
    )

    assert result.returncode == 0, result.stderr
    assert powershell_harness.calls() == []
    assert "Dry run complete. No changes were made." in result.stdout
    assert "Free Claude Code is installed and verified." not in result.stdout


def test_install_ps1_rejects_broken_existing_client_without_replacing_it(
    powershell_harness: PowerShellHarness,
) -> None:
    powershell_harness.add_client("claude")

    result = powershell_harness.run(fail_step="claude-verify")

    assert result.returncode != 0
    assert not any(call.startswith("download:") for call in powershell_harness.calls())


def test_install_ps1_rejects_unparseable_existing_uv(
    powershell_harness: PowerShellHarness,
) -> None:
    powershell_harness.add_client("claude")
    powershell_harness.add_client("codex")
    powershell_harness.add_client("pi")
    powershell_harness.add_uv("not-a-version")

    result = powershell_harness.run()

    assert result.returncode != 0
    assert not any("astral.sh" in call for call in powershell_harness.calls())


def test_install_ps1_voice_flags_only_change_fcc_spec(
    powershell_harness: PowerShellHarness,
) -> None:
    result = powershell_harness.run("-VoiceAll", "-TorchBackend", "cu130")

    assert result.returncode == 0, result.stderr
    assert any(
        '--torch-backend cu130 "free-claude-code[voice,voice_local] @ '
        'https://github.com/Alishahryar1/free-claude-code/archive/refs/heads/main.zip"'
        in call
        for call in powershell_harness.calls()
    )


def test_installers_use_native_clients_and_single_python_selection() -> None:
    shell = (_repo_root() / "scripts" / "install.sh").read_text(encoding="utf-8")
    powershell = (_repo_root() / "scripts" / "install.ps1").read_text(encoding="utf-8")

    for text in (shell, powershell):
        assert "@anthropic-ai/claude-code" not in text
        assert "@openai/codex" not in text
        assert "@earendil-works/pi-coding-agent" not in text
        assert "git+" not in text
        assert "git --version" not in text
        assert (
            "https://github.com/Alishahryar1/free-claude-code/archive/refs/heads/main.zip"
            in text
        )
        assert "python install" not in text
        assert "--refresh-package" in text
        assert "tool update-shell" in text
        assert "--python" in text

    assert "https://pi.dev/install.sh" in shell
    assert "https://pi.dev/install.ps1" in powershell


def test_readme_install_section_has_no_manual_git_prerequisite() -> None:
    readme = (_repo_root() / "README.md").read_text(encoding="utf-8")
    install_section = readme.split("### 1. Install Or Update", 1)[1].split(
        "### 2. Start The Server", 1
    )[0]

    assert "Install Git" not in install_section
    assert "official native installers" not in install_section


@pytest.mark.parametrize("powershell", _powershells())
def test_install_ps1_falls_back_when_pshome_executable_is_unavailable(
    tmp_path: Path,
    powershell: str,
) -> None:
    text = (_repo_root() / "scripts" / "install.ps1").read_text(encoding="utf-8")
    body = _braced_body(text, "function Get-PowerShellExecutable")
    fallback = tmp_path / "fallback" / "powershell.exe"
    script = tmp_path / "test-powershell-resolution.ps1"
    script.write_text(
        f"""Set-StrictMode -Version Latest
function Get-ApplicationCommand {{
    param([string] $Name)
    return [pscustomobject] @{{ Source = {str(fallback)!r} }}
}}
function Get-PowerShellExecutable {{
{body}
}}
$resolved = Get-PowerShellExecutable -PowerShellHome {str(tmp_path / "missing")!r}
if ($resolved -ne {str(fallback)!r}) {{
    throw "Unexpected fallback: $resolved"
}}
""",
        encoding="utf-8",
    )

    result = subprocess.run(
        [powershell, "-NoProfile", "-File", str(script)],
        check=False,
        capture_output=True,
        text=True,
    )

    assert result.returncode == 0, result.stderr
