"""Functional E2E tests for installer/install.sh.

Each test runs the real bash script via subprocess in a sandboxed environment
with fake HOME, SHELL, PATH, and mock external commands.
"""

from __future__ import annotations

import hashlib
import stat
import subprocess
import tarfile
from pathlib import Path
from typing import Any

from tests.installer.conftest import INSTALL_SH, ScriptResult, write_fake_cmd

UNAME_LINUX = 'case "$1" in -s) echo Linux;; -m) echo x86_64;; *) echo Linux;; esac'


def _make_uname(fake_bin: Path, script: str = UNAME_LINUX) -> None:
    write_fake_cmd(fake_bin, "uname", script)


def _setup_binary_download(
    fake_bin: Path,
    tmp_path: Path,
    *,
    platform: str = "linux-amd64",
    bad_checksum: bool = False,
) -> Path:
    """Create tarball + fake curl + fail pipx/uv. Returns the download dir."""
    _make_uname(fake_bin)
    write_fake_cmd(fake_bin, "pipx", "exit 1")
    write_fake_cmd(fake_bin, "uv", "exit 1")

    tarball_dir = tmp_path / "tarsrc"
    tarball_dir.mkdir(exist_ok=True)
    ilum_bin = tarball_dir / "ilum"
    ilum_bin.write_text("#!/bin/bash\necho 'ilum 0.3.0'\n")
    ilum_bin.chmod(ilum_bin.stat().st_mode | stat.S_IEXEC)

    tarball_name = f"ilum-{platform}.tar.gz"
    download_dir = tmp_path / "dl"
    download_dir.mkdir(exist_ok=True)
    tarball_path = download_dir / tarball_name
    with tarfile.open(tarball_path, "w:gz") as tar:
        tar.add(str(ilum_bin), arcname="ilum")

    sha = hashlib.sha256(tarball_path.read_bytes()).hexdigest()
    if bad_checksum:
        sha = "0" * 64
    (download_dir / "SHA256SUMS").write_text(f"{sha}  {tarball_name}\n")

    write_fake_cmd(
        fake_bin,
        "curl",
        f"""
url="${{@: -1}}"
filename=$(basename "$url")
if [ -f "{download_dir}/$filename" ]; then
    cat "{download_dir}/$filename"
else
    exit 1
fi
""",
    )
    return download_dir


# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------


class TestArgumentParsing:
    """Tests for CLI argument parsing."""

    def test_help_flag_shows_usage(self, run_installer: Any) -> None:
        result: ScriptResult = run_installer("--help")
        assert result.returncode == 0
        assert "Usage:" in result.stdout

    def test_short_help_flag(self, run_installer: Any) -> None:
        result: ScriptResult = run_installer("-h")
        assert result.returncode == 0
        assert "Usage:" in result.stdout

    def test_version_flag_sets_version(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        result: ScriptResult = run_installer(
            "--version", "1.2.3", env_override={"ILUM_VERSION": ""}
        )
        assert "1.2.3" in result.output

    def test_version_flag_without_value_fails(self, run_installer: Any) -> None:
        result: ScriptResult = run_installer("--version")
        assert result.returncode != 0

    def test_unknown_arg_fails_with_message(self, run_installer: Any) -> None:
        """Regression test for bug #1: used to call undefined 'error' instead of 'err'."""
        result: ScriptResult = run_installer("--bogus-flag")
        assert result.returncode != 0
        assert "Unknown argument: --bogus-flag" in result.output


# ---------------------------------------------------------------------------
# Platform detection
# ---------------------------------------------------------------------------


class TestPlatformDetection:
    """Tests for detect_platform()."""

    def test_linux_x86_64(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        result: ScriptResult = run_installer()
        assert "linux-amd64" in result.output

    def test_darwin_arm64(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(
            fake_bin,
            'case "$1" in -s) echo Darwin;; -m) echo arm64;; *) echo Darwin;; esac',
        )
        result: ScriptResult = run_installer()
        assert "darwin-arm64" in result.output

    def test_linux_aarch64_maps_to_arm64(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(
            fake_bin,
            'case "$1" in -s) echo Linux;; -m) echo aarch64;; *) echo Linux;; esac',
        )
        result: ScriptResult = run_installer()
        assert "linux-arm64" in result.output

    def test_unsupported_arch_dies(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(
            fake_bin,
            'case "$1" in -s) echo Linux;; -m) echo s390x;; *) echo Linux;; esac',
        )
        result: ScriptResult = run_installer()
        assert result.returncode != 0
        assert "Unsupported architecture: s390x" in result.output

    def test_unsupported_os_dies(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(
            fake_bin,
            'case "$1" in -s) echo FreeBSD;; -m) echo x86_64;; *) echo FreeBSD;; esac',
        )
        result: ScriptResult = run_installer()
        assert result.returncode != 0
        assert "Unsupported operating system: freebsd" in result.output


# ---------------------------------------------------------------------------
# Version resolution
# ---------------------------------------------------------------------------


class TestVersionResolution:
    """Tests for determine_version()."""

    def test_flag_takes_priority(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        result: ScriptResult = run_installer(
            "--version", "9.9.9", env_override={"ILUM_VERSION": "1.0.0"}
        )
        assert "9.9.9" in result.output
        assert "from --version flag" in result.output

    def test_env_var_used_when_no_flag(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        result: ScriptResult = run_installer(env_override={"ILUM_VERSION": "2.5.0"})
        assert "2.5.0" in result.output
        assert "pinned version" in result.output

    def test_github_api_parsed(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(
            fake_bin,
            "curl",
            'echo \'{"tag_name": "cli-v4.2.0", "name": "cli-v4.2.0"}\'',
        )
        result: ScriptResult = run_installer(env_override={"ILUM_VERSION": ""})
        assert "4.2.0" in result.output

    def test_github_api_failure_dies(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(fake_bin, "curl", "exit 1")
        write_fake_cmd(fake_bin, "wget", "exit 1")
        result: ScriptResult = run_installer(env_override={"ILUM_VERSION": ""})
        assert result.returncode != 0


# ---------------------------------------------------------------------------
# Fetch helper
# ---------------------------------------------------------------------------


class TestFetchHelper:
    """Tests for fetch()."""

    def test_prefers_curl(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(fake_bin, "curl", 'echo "CURL_CALLED"; cat /dev/null')
        result: ScriptResult = run_installer()
        assert "CURL_CALLED" in result.stdout or result.returncode != 0

    def test_falls_back_to_wget(self, run_installer: Any, fake_bin: Path) -> None:
        # Covered implicitly by other tests; curl/wget preference is hard to isolate
        # since we can't hide the real curl from `command -v`
        _make_uname(fake_bin)
        pass

    def test_neither_available_dies(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(fake_bin, "curl", "exit 1")
        write_fake_cmd(fake_bin, "wget", "exit 1")
        result: ScriptResult = run_installer(env_override={"ILUM_VERSION": ""})
        assert result.returncode != 0


# ---------------------------------------------------------------------------
# Install strategies
# ---------------------------------------------------------------------------


class TestInstallStrategies:
    """Tests for tiered fallback install."""

    def test_brew_not_tried_on_linux(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(fake_bin, "brew", 'echo "BREW_CALLED"; exit 0')
        result: ScriptResult = run_installer()
        assert "BREW_CALLED" not in result.stdout

    def test_brew_tried_on_darwin(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(
            fake_bin,
            'case "$1" in -s) echo Darwin;; -m) echo arm64;; *) echo Darwin;; esac',
        )
        write_fake_cmd(fake_bin, "brew", 'echo "BREW_CALLED"; exit 0')
        result: ScriptResult = run_installer()
        assert "Homebrew" in result.output

    def test_pipx_fallback(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(fake_bin, "pipx", 'echo "PIPX_OK"; exit 0')
        result: ScriptResult = run_installer()
        assert "pipx" in result.output

    def test_uv_fallback(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(fake_bin, "pipx", "exit 1")
        write_fake_cmd(fake_bin, "uv", 'echo "UV_OK"; exit 0')
        result: ScriptResult = run_installer()
        assert "uv" in result.output

    def test_binary_download_fallback(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(fake_bin, "pipx", "exit 1")
        write_fake_cmd(fake_bin, "uv", "exit 1")
        result: ScriptResult = run_installer()
        assert "Falling back to binary download" in result.output

    def test_all_strategies_failure_message(self, run_installer: Any, fake_bin: Path) -> None:
        _make_uname(fake_bin)
        write_fake_cmd(fake_bin, "pipx", "exit 1")
        write_fake_cmd(fake_bin, "uv", "exit 1")
        write_fake_cmd(fake_bin, "curl", "exit 1")
        write_fake_cmd(fake_bin, "wget", "exit 1")
        result: ScriptResult = run_installer()
        assert result.returncode != 0


# ---------------------------------------------------------------------------
# Binary download & checksum
# ---------------------------------------------------------------------------


class TestBinaryDownloadChecksum:
    """Tests for download_binary()."""

    def test_successful_download_and_checksum(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        # Sandbox provides writable ILUM_INSTALL_DIR
        result: ScriptResult = run_installer()
        assert "Checksum OK" in result.output
        assert result.returncode == 0

    def test_bad_checksum_dies(self, run_installer: Any, fake_bin: Path, tmp_path: Path) -> None:
        _setup_binary_download(fake_bin, tmp_path, bad_checksum=True)
        result: ScriptResult = run_installer()
        assert result.returncode != 0
        assert "Checksum verification failed" in result.output

    def test_fallback_to_local_bin(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        """When INSTALL_DIR is not writable and no sudo, falls back to FALLBACK_DIR."""
        _setup_binary_download(fake_bin, tmp_path)

        # Make INSTALL_DIR non-writable
        non_writable = tmp_path / "no_write"
        non_writable.mkdir()
        non_writable.chmod(0o555)

        fallback = tmp_path / "fallback_bin"
        fallback.mkdir()

        result: ScriptResult = run_installer(
            env_override={
                "ILUM_INSTALL_DIR": str(non_writable),
                "ILUM_FALLBACK_DIR": str(fallback),
            },
        )
        # Restore permissions for cleanup
        non_writable.chmod(0o755)

        if result.returncode == 0:
            assert "Installed ilum" in result.output


# ---------------------------------------------------------------------------
# PATH setup
# ---------------------------------------------------------------------------


class TestPathSetup:
    """Tests for setup_path()."""

    def test_updates_bashrc_for_bash(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        home = Path(sandbox["HOME"])
        result: ScriptResult = run_installer(env_override={"SHELL": "/bin/bash"})
        assert result.returncode == 0

        bashrc = home / ".bashrc"
        if bashrc.exists():
            content = bashrc.read_text()
            assert "PATH" in content

    def test_updates_zshrc_for_zsh(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        home = Path(sandbox["HOME"])
        result: ScriptResult = run_installer(env_override={"SHELL": "/bin/zsh"})
        assert result.returncode == 0

        zshrc = home / ".zshrc"
        if zshrc.exists():
            content = zshrc.read_text()
            assert "PATH" in content

    def test_fish_uses_set_gx(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        home = Path(sandbox["HOME"])
        result: ScriptResult = run_installer(env_override={"SHELL": "/usr/bin/fish"})
        assert result.returncode == 0

        fish_config = home / ".config" / "fish" / "config.fish"
        if fish_config.exists():
            content = fish_config.read_text()
            assert "set -gx PATH" in content

    def test_no_path_flag_skips_setup(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        home = Path(sandbox["HOME"])
        result: ScriptResult = run_installer("--no-path", env_override={"SHELL": "/bin/bash"})
        assert result.returncode == 0

        bashrc = home / ".bashrc"
        # bashrc should NOT exist or should NOT contain install_dir
        install_dir = sandbox["ILUM_INSTALL_DIR"]
        if bashrc.exists():
            assert install_dir not in bashrc.read_text()

    def test_idempotency_no_duplicate_entries(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        home = Path(sandbox["HOME"])
        install_dir = sandbox["ILUM_INSTALL_DIR"]

        # Pre-create bashrc with the install dir already in it
        bashrc = home / ".bashrc"
        bashrc.write_text(f'export PATH="{install_dir}:$PATH"\n')

        result: ScriptResult = run_installer(env_override={"SHELL": "/bin/bash"})
        assert result.returncode == 0

        content = bashrc.read_text()
        count = content.count(install_dir)
        assert count <= 2  # original + at most one from idempotency check

    def test_unknown_shell_prints_manual_instructions(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        result: ScriptResult = run_installer(env_override={"SHELL": "/bin/tcsh"})
        assert result.returncode == 0
        assert "Add it manually" in result.output or "not in your PATH" in result.output

    def test_already_in_path_skips_update(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        install_dir = sandbox["ILUM_INSTALL_DIR"]
        home = Path(sandbox["HOME"])

        result: ScriptResult = run_installer(
            env_override={
                "SHELL": "/bin/bash",
                # Add install_dir to PATH so setup_path skips
                "PATH": f"{fake_bin}:{install_dir}:/usr/bin:/bin",
            },
        )
        assert result.returncode == 0

        bashrc = home / ".bashrc"
        # bashrc should NOT be created since install_dir is already in PATH
        assert not bashrc.exists() or install_dir not in bashrc.read_text()


# ---------------------------------------------------------------------------
# Pipe mode (no terminal)
# ---------------------------------------------------------------------------


class TestPipeMode:
    """Tests for ANSI code suppression when stdout is not a terminal."""

    def test_no_ansi_when_piped(self, sandbox: dict[str, str]) -> None:
        """When stdout is not a terminal, no ANSI escape codes should appear."""
        result = subprocess.run(
            ["bash", str(INSTALL_SH), "--help"],
            capture_output=True,
            text=True,
            env=sandbox,
            timeout=10,
        )
        assert "\033[" not in result.stdout
        assert "\x1b[" not in result.stdout

    def test_output_is_readable(self, sandbox: dict[str, str]) -> None:
        result = subprocess.run(
            ["bash", str(INSTALL_SH), "--help"],
            capture_output=True,
            text=True,
            env=sandbox,
            timeout=10,
        )
        assert result.returncode == 0
        assert "Usage:" in result.stdout


# ---------------------------------------------------------------------------
# Verification
# ---------------------------------------------------------------------------


class TestVerification:
    """Tests for the verification block at the end of main()."""

    def test_runs_ilum_version(self, run_installer: Any, fake_bin: Path, tmp_path: Path) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        # Place a fake ilum on PATH that reports the right version
        write_fake_cmd(fake_bin, "ilum", 'echo "ilum 0.3.0"')

        result: ScriptResult = run_installer()
        assert result.returncode == 0
        assert "Verified" in result.output or "0.3.0" in result.output

    def test_version_mismatch_warns(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path
    ) -> None:
        _setup_binary_download(fake_bin, tmp_path)
        # Fake ilum that returns wrong version
        write_fake_cmd(fake_bin, "ilum", 'echo "ilum 0.1.0"')

        result: ScriptResult = run_installer()
        assert result.returncode == 0
        assert "mismatch" in result.output or "0.1.0" in result.output

    def test_rc_file_hint_when_not_on_path(
        self, run_installer: Any, fake_bin: Path, tmp_path: Path, sandbox: dict[str, str]
    ) -> None:
        """Regression test for bug #2: rc_file was scoped to setup_path, not available in main."""
        _setup_binary_download(fake_bin, tmp_path)

        # Do NOT put a fake ilum on PATH — so `command -v ilum` fails
        # and the else branch triggers.
        ilum_on_path = fake_bin / "ilum"
        if ilum_on_path.exists():
            ilum_on_path.unlink()

        # Exclude ILUM_INSTALL_DIR from PATH so the installed binary isn't found
        # by `command -v ilum` in the verification step
        result: ScriptResult = run_installer(
            env_override={
                "SHELL": "/bin/bash",
                "PATH": f"{fake_bin}:/usr/bin:/bin",
            },
        )
        assert result.returncode == 0

        # The key check: the message should contain a real rc file path, not empty
        # Before the fix, it would say "source  or restart" (empty $rc_file)
        assert "source" in result.output, f"Expected 'source' in output: {result.output}"
        assert ".bashrc" in result.output or "your shell rc file" in result.output
        # Must NOT contain "source  or" (double space = empty variable)
        assert "source  or" not in result.output
