"""Tests for launcher.install — tribunal pack installer."""

from __future__ import annotations

import json
import shutil
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _make_pack(directory: Path, *, with_hooks: bool = True, with_rules: bool = False, sha256: str | None = None) -> Path:
    """Create a minimal valid tribunal pack at *directory*."""
    directory.mkdir(parents=True, exist_ok=True)

    manifest: dict = {
        "name": directory.name,
        "version": "1.0.0",
        "description": "Test pack",
    }
    if sha256 is not None:
        manifest["sha256"] = sha256

    (directory / "tribunal-pack.json").write_text(json.dumps(manifest), encoding="utf-8")

    if with_hooks:
        hooks_dir = directory / "hooks"
        hooks_dir.mkdir(exist_ok=True)
        (hooks_dir / "hooks.json").write_text(
            json.dumps({"hooks": {"PreToolUse": [{"type": "command", "command": "echo test"}]}}),
            encoding="utf-8",
        )

    if with_rules:
        rules_dir = directory / "rules"
        rules_dir.mkdir(exist_ok=True)
        (rules_dir / "my-rule.md").write_text("# Rule", encoding="utf-8")

    return directory


# ---------------------------------------------------------------------------
# _resolve_source tests
# ---------------------------------------------------------------------------

class TestResolveSource:
    def test_local_path_dot_prefix(self, tmp_path: Path):
        _make_pack(tmp_path / "mypack")
        from launcher.install import _resolve_source
        kind, resolved = _resolve_source(f"./mypack")
        # Local relative to cwd — we only care that kind=="local"
        assert kind == "local"

    def test_local_existing_directory(self, tmp_path: Path):
        pack = _make_pack(tmp_path / "mypack")
        from launcher.install import _resolve_source
        kind, resolved = _resolve_source(str(pack))
        assert kind == "local"
        assert resolved == str(pack.resolve())

    def test_github_url(self):
        from launcher.install import _resolve_source
        kind, resolved = _resolve_source("https://github.com/org/my-pack")
        assert kind == "github"
        assert resolved == "https://github.com/org/my-pack"

    def test_registry_name(self, tmp_path: Path):
        """Should resolve a registry name to a GitHub URL."""
        reg = tmp_path / "registry.yaml"
        reg.write_text(
            "version: 1\npacks:\n  my-pack:\n    url: https://github.com/org/my-pack\n    sha256: null\n    version: '1.0.0'\n    description: Test\n",
            encoding="utf-8",
        )
        import launcher.install as install_mod
        original = install_mod._REGISTRY_PATH
        install_mod._REGISTRY_PATH = reg
        try:
            from launcher.install import _resolve_source
            kind, resolved = _resolve_source("my-pack")
            assert kind == "github"
            assert resolved == "https://github.com/org/my-pack"
        finally:
            install_mod._REGISTRY_PATH = original

    def test_unknown_source_raises(self):
        from launcher.install import _resolve_source
        with pytest.raises(ValueError, match="Unknown pack source"):
            _resolve_source("totally-unknown-pack-xyz")


# ---------------------------------------------------------------------------
# _validate_pack_dir tests
# ---------------------------------------------------------------------------

class TestValidatePackDir:
    def test_valid_pack_with_hooks(self, tmp_path: Path):
        pack = _make_pack(tmp_path / "good-pack", with_hooks=True)
        from launcher.install import _validate_pack_dir
        _validate_pack_dir(pack)  # should not raise

    def test_valid_pack_with_rules(self, tmp_path: Path):
        pack = _make_pack(tmp_path / "rules-pack", with_hooks=False, with_rules=True)
        from launcher.install import _validate_pack_dir
        _validate_pack_dir(pack)  # should not raise

    def test_missing_manifest_raises(self, tmp_path: Path):
        pack = tmp_path / "bad-pack"
        pack.mkdir()
        (pack / "hooks").mkdir()
        (pack / "hooks" / "hooks.json").write_text("{}", encoding="utf-8")
        from launcher.install import _validate_pack_dir
        with pytest.raises(ValueError, match="missing 'tribunal-pack.json'"):
            _validate_pack_dir(pack)

    def test_missing_hooks_and_rules_raises(self, tmp_path: Path):
        pack = tmp_path / "empty-pack"
        pack.mkdir()
        (pack / "tribunal-pack.json").write_text("{}", encoding="utf-8")
        from launcher.install import _validate_pack_dir
        with pytest.raises(ValueError, match="must contain at least one"):
            _validate_pack_dir(pack)

    def test_empty_hooks_dir_raises(self, tmp_path: Path):
        """An empty hooks/ dir doesn't count."""
        pack = tmp_path / "empty-hooks"
        pack.mkdir()
        (pack / "tribunal-pack.json").write_text("{}", encoding="utf-8")
        (pack / "hooks").mkdir()
        from launcher.install import _validate_pack_dir
        with pytest.raises(ValueError, match="must contain at least one"):
            _validate_pack_dir(pack)


# ---------------------------------------------------------------------------
# _sha256_dir and _verify_sha256 tests
# ---------------------------------------------------------------------------

class TestSha256:
    def test_sha256_dir_deterministic(self, tmp_path: Path):
        pack = _make_pack(tmp_path / "sha-pack")
        from launcher.install import _sha256_dir
        h1 = _sha256_dir(pack)
        h2 = _sha256_dir(pack)
        assert h1 == h2
        assert len(h1) == 64  # hex SHA-256

    def test_sha256_dir_changes_with_content(self, tmp_path: Path):
        pack = _make_pack(tmp_path / "sha-pack2")
        from launcher.install import _sha256_dir
        h1 = _sha256_dir(pack)
        (pack / "hooks" / "extra.json").write_text("hello", encoding="utf-8")
        h2 = _sha256_dir(pack)
        assert h1 != h2

    def test_verify_sha256_passes_when_none(self, tmp_path: Path):
        pack = _make_pack(tmp_path / "nosha")
        from launcher.install import _verify_sha256
        _verify_sha256(pack, None)  # should not raise

    def test_verify_sha256_passes_with_correct_hash(self, tmp_path: Path):
        pack = _make_pack(tmp_path / "goodsha")
        from launcher.install import _sha256_dir, _verify_sha256
        h = _sha256_dir(pack)
        _verify_sha256(pack, h)  # should not raise

    def test_verify_sha256_fails_with_wrong_hash(self, tmp_path: Path):
        pack = _make_pack(tmp_path / "badsha")
        from launcher.install import _verify_sha256
        with pytest.raises(ValueError, match="SHA-256 mismatch"):
            _verify_sha256(pack, "a" * 64)


# ---------------------------------------------------------------------------
# install_pack integration tests (local path)
# ---------------------------------------------------------------------------

class TestInstallPackLocal:
    def test_install_local_pack(self, tmp_path: Path):
        """Should copy pack to packs_dir and merge hooks."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        dest = install_pack(
            str(pack_src),
            packs_dir=packs_dir,
            hooks_json=hooks_json,
        )

        assert dest.exists()
        assert (dest / "tribunal-pack.json").exists()
        # Merged hooks
        merged = json.loads(hooks_json.read_text())
        assert "PreToolUse" in merged["hooks"]

    def test_install_overwrites_existing(self, tmp_path: Path):
        """Re-installing should replace the existing pack."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        dest1 = install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)
        # Add a sentinel file to the first install
        (dest1 / "sentinel.txt").write_text("old")
        # Reinstall
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")
        dest2 = install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)
        # Sentinel should be gone
        assert not (dest2 / "sentinel.txt").exists()

    def test_install_succeeds_without_sha256_in_manifest(self, tmp_path: Path):
        """Pack without sha256 field in manifest should install without errors."""
        pack_src = _make_pack(tmp_path / "verified-pack")
        # Ensure no sha256 in manifest (default _make_pack doesn't add one)
        manifest = json.loads((pack_src / "tribunal-pack.json").read_text())
        assert "sha256" not in manifest or manifest.get("sha256") is None

        from launcher.install import install_pack
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        dest = install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)
        assert dest.exists()

    def test_install_invalid_pack_raises(self, tmp_path: Path):
        """Should raise ValueError for invalid pack structure."""
        bad_pack = tmp_path / "bad"
        bad_pack.mkdir()
        # No manifest
        (bad_pack / "hooks").mkdir()
        (bad_pack / "hooks" / "hooks.json").write_text("{}")

        from launcher.install import install_pack
        with pytest.raises(ValueError, match="missing 'tribunal-pack.json'"):
            install_pack(str(bad_pack), packs_dir=tmp_path / "packs")

    def test_install_no_active_hooks_json(self, tmp_path: Path, capsys):
        """Should warn but not crash when active hooks.json is missing."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        nonexistent_hooks = tmp_path / "nonexistent" / "hooks.json"

        from launcher.install import install_pack
        dest = install_pack(
            str(pack_src),
            packs_dir=packs_dir,
            hooks_json=nonexistent_hooks,
        )
        assert dest.exists()
        captured = capsys.readouterr()
        assert "skipping merge" in captured.out


# ---------------------------------------------------------------------------
# list_packs tests
# ---------------------------------------------------------------------------

class TestListPacks:
    def test_list_packs_shows_registry(self, tmp_path: Path, capsys):
        reg = tmp_path / "registry.yaml"
        reg.write_text(
            "version: 1\npacks:\n  python-strict:\n    url: https://github.com/org/python-strict\n    sha256: null\n    version: '1.0.0'\n    description: Strict Python\n",
            encoding="utf-8",
        )

        import launcher.install as install_mod
        original = install_mod._REGISTRY_PATH
        install_mod._REGISTRY_PATH = reg
        try:
            from launcher.install import list_packs
            list_packs()
        finally:
            install_mod._REGISTRY_PATH = original

        # Rich renders to a non-capturing stream normally; just ensure no exception
        # If rich writes to stdout, it'll appear here
        # (Rich Console defaults to stdout for non-TTY)

    def test_list_packs_empty_registry(self, tmp_path: Path, capsys):
        reg = tmp_path / "empty.yaml"
        reg.write_text("version: 1\npacks: {}\n", encoding="utf-8")

        import launcher.install as install_mod
        original = install_mod._REGISTRY_PATH
        install_mod._REGISTRY_PATH = reg
        try:
            from launcher.install import list_packs
            list_packs()
        finally:
            install_mod._REGISTRY_PATH = original

        captured = capsys.readouterr()
        assert "No packs" in captured.out

    def test_list_packs_missing_registry(self, tmp_path: Path, capsys):
        nonexistent = tmp_path / "missing.yaml"

        import launcher.install as install_mod
        original = install_mod._REGISTRY_PATH
        install_mod._REGISTRY_PATH = nonexistent
        try:
            from launcher.install import list_packs
            list_packs()
        finally:
            install_mod._REGISTRY_PATH = original

        captured = capsys.readouterr()
        assert "No packs" in captured.out


# ---------------------------------------------------------------------------
# CLI integration: install and list-packs subcommands
# ---------------------------------------------------------------------------

class TestCliInstallSubcommand:
    def test_cli_registers_install(self):
        from launcher.cli import _build_parser
        parser = _build_parser()
        args, _ = parser.parse_known_args(["install", "python-strict"])
        assert args.command == "install"
        assert args.pack_source == "python-strict"

    def test_cli_registers_list_packs(self):
        from launcher.cli import _build_parser
        parser = _build_parser()
        args, _ = parser.parse_known_args(["list-packs"])
        assert args.command == "list-packs"

    def test_cli_install_dispatches(self, tmp_path: Path):
        """main() install command should call install_pack."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        with patch("launcher.install.install_pack") as mock_install:
            mock_install.return_value = packs_dir / "my-pack"
            from launcher.cli import main
            main(["install", str(pack_src)])
            mock_install.assert_called_once_with(str(pack_src))

    def test_cli_list_packs_dispatches(self):
        """main() list-packs command should call list_packs."""
        with patch("launcher.install.list_packs") as mock_list:
            from launcher.cli import main
            main(["list-packs"])
            mock_list.assert_called_once()


# ---------------------------------------------------------------------------
# TD-7: Hook deduplication tests
# ---------------------------------------------------------------------------

class TestHookDeduplication:
    def test_install_twice_no_duplicate_hooks(self, tmp_path: Path):
        """Re-installing a pack should not create duplicate hook entries."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        # First install
        install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)

        # Second install (reinstall)
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")
        install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)

        # Verify no duplicates
        merged = json.loads(hooks_json.read_text())
        pre_tool_use = merged.get("hooks", {}).get("PreToolUse", [])
        # Should have exactly 1 entry (the pack's hook)
        assert len(pre_tool_use) == 1

    def test_different_packs_both_hooks_present(self, tmp_path: Path):
        """Installing different packs should include hooks from both."""
        pack1 = _make_pack(tmp_path / "pack1")
        pack2 = _make_pack(tmp_path / "pack2")
        # Give pack2 different hooks
        (pack2 / "hooks" / "hooks.json").write_text(
            json.dumps({"hooks": {"PostToolUse": [{"type": "command", "command": "echo post"}]}}),
            encoding="utf-8",
        )

        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        install_pack(str(pack1), packs_dir=packs_dir, hooks_json=hooks_json)
        install_pack(str(pack2), packs_dir=packs_dir, hooks_json=hooks_json)

        merged = json.loads(hooks_json.read_text())
        assert "PreToolUse" in merged["hooks"]
        assert "PostToolUse" in merged["hooks"]

    def test_deduplication_by_command_string(self, tmp_path: Path):
        """Hooks with same event but different commands should both be present."""
        pack1 = _make_pack(tmp_path / "pack1")
        (pack1 / "hooks" / "hooks.json").write_text(
            json.dumps({"hooks": {"PreToolUse": [{"type": "command", "command": "echo one"}]}}),
            encoding="utf-8",
        )
        pack2 = _make_pack(tmp_path / "pack2")
        (pack2 / "hooks" / "hooks.json").write_text(
            json.dumps({"hooks": {"PreToolUse": [{"type": "command", "command": "echo two"}]}}),
            encoding="utf-8",
        )

        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        install_pack(str(pack1), packs_dir=packs_dir, hooks_json=hooks_json)
        install_pack(str(pack2), packs_dir=packs_dir, hooks_json=hooks_json)

        merged = json.loads(hooks_json.read_text())
        commands = [h.get("command") for h in merged["hooks"]["PreToolUse"]]
        assert "echo one" in commands
        assert "echo two" in commands

    def test_same_hook_in_multiple_packs_deduplicated(self, tmp_path: Path):
        """Same hook from different packs should be deduplicated."""
        pack1 = _make_pack(tmp_path / "pack1")
        pack2 = _make_pack(tmp_path / "pack2")
        # Both have the same hook
        same_hook = [{"type": "command", "command": "echo same"}]
        (pack1 / "hooks" / "hooks.json").write_text(
            json.dumps({"hooks": {"PreToolUse": same_hook}}),
            encoding="utf-8",
        )
        (pack2 / "hooks" / "hooks.json").write_text(
            json.dumps({"hooks": {"PreToolUse": same_hook}}),
            encoding="utf-8",
        )

        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        install_pack(str(pack1), packs_dir=packs_dir, hooks_json=hooks_json)
        install_pack(str(pack2), packs_dir=packs_dir, hooks_json=hooks_json)

        merged = json.loads(hooks_json.read_text())
        # Should have only one entry (deduplicated)
        assert len(merged["hooks"]["PreToolUse"]) == 1


# ---------------------------------------------------------------------------
# TD-12: SHA-256 verification tests
# ---------------------------------------------------------------------------

class TestPackHashVerification:
    def test_compute_pack_hash_deterministic(self, tmp_path: Path):
        """_compute_pack_hash should produce consistent results."""
        pack = _make_pack(tmp_path / "my-pack")
        from launcher.install import _compute_pack_hash
        h1 = _compute_pack_hash(pack)
        h2 = _compute_pack_hash(pack)
        assert h1 == h2

    def test_save_and_get_installed_hash(self, tmp_path: Path):
        """Should save and retrieve hash from .installed-hash file."""
        pack = _make_pack(tmp_path / "my-pack")
        from launcher.install import _save_installed_hash, _get_installed_hash, _compute_pack_hash

        expected = _compute_pack_hash(pack)
        _save_installed_hash(pack, expected)

        retrieved = _get_installed_hash(pack)
        assert retrieved == expected

    def test_verify_installed_hash_no_stored_hash(self, tmp_path: Path):
        """When no stored hash exists, verification should pass."""
        pack = _make_pack(tmp_path / "my-pack")
        from launcher.install import _verify_installed_hash

        is_valid, hash_val = _verify_installed_hash(pack)
        assert is_valid is True
        assert hash_val is None  # No stored hash

    def test_verify_installed_hash_matching(self, tmp_path: Path):
        """Verification should pass when hashes match."""
        pack = _make_pack(tmp_path / "my-pack")
        from launcher.install import _compute_pack_hash, _save_installed_hash, _verify_installed_hash

        computed = _compute_pack_hash(pack)
        _save_installed_hash(pack, computed)

        is_valid, hash_val = _verify_installed_hash(pack)
        assert is_valid is True

    def test_verify_installed_hash_mismatch(self, tmp_path: Path):
        """Verification should fail when hashes don't match."""
        pack = _make_pack(tmp_path / "my-pack")
        from launcher.install import _save_installed_hash, _verify_installed_hash

        _save_installed_hash(pack, "a" * 64)  # Wrong hash

        is_valid, hash_val = _verify_installed_hash(pack)
        assert is_valid is False
        assert hash_val is not None

    def test_install_pack_stores_hash(self, tmp_path: Path):
        """install_pack should store hash in .installed-hash file."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack, _compute_pack_hash
        dest = install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)

        hash_file = dest / ".installed-hash"
        assert hash_file.exists()
        stored = hash_file.read_text().strip()
        expected = _compute_pack_hash(dest)
        assert stored == expected

    def test_install_pack_warns_on_reinstall_mismatch(self, tmp_path: Path, capsys):
        """Reinstall with modified pack should warn about hash mismatch."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        # First install
        dest = install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)

        # Modify the pack files to cause hash mismatch
        (dest / "hooks" / "extra.json").write_text("modified", encoding="utf-8")

        # Reinstall - should warn
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")
        install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)

        captured = capsys.readouterr()
        assert "Warning: Hash mismatch" in captured.out or "hash mismatch" in captured.out.lower()

    def test_install_output_shows_hash(self, tmp_path: Path, capsys):
        """install_pack should display the computed hash in output."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)

        captured = capsys.readouterr()
        assert "Pack hash:" in captured.out
        # Should show a 64-character hex hash somewhere in output
        assert any(len(line) == 64 for line in captured.out.split("\n")) or any(
            len(word) == 64 for line in captured.out.split("\n") for word in line.split()
        )

    def test_verify_pack_cli_command(self, tmp_path: Path):
        """verify-pack CLI should check pack integrity."""
        pack_src = _make_pack(tmp_path / "my-pack")
        packs_dir = tmp_path / "packs"
        hooks_json = tmp_path / "hooks.json"
        hooks_json.write_text(json.dumps({"hooks": {}}), encoding="utf-8")

        from launcher.install import install_pack
        install_pack(str(pack_src), packs_dir=packs_dir, hooks_json=hooks_json)

        # Now test the CLI verify-pack
        with patch("launcher.install._PACKS_DIR", packs_dir):
            from launcher.cli import main
            main(["verify-pack", "my-pack"])

    def test_verify_pack_cli_missing_pack(self, tmp_path: Path):
        """verify-pack should error when pack doesn't exist."""
        packs_dir = tmp_path / "packs"
        packs_dir.mkdir(parents=True, exist_ok=True)

        with patch("launcher.install._PACKS_DIR", packs_dir):
            from launcher.cli import main
            with pytest.raises(SystemExit):
                main(["verify-pack", "nonexistent-pack"])
