"""``mgf-fed hooks`` — the verb group over the session-hook kit.

The kit's own behaviour is pinned in ``tests/unit/sessionhooks/``; this file pins the
CLI contract on top of it: the argument shape, the exit codes, the registration, and
— the one that matters operationally — that every **harness** action exits ``0``
however it is invoked, because those are the ones a live session depends on.
"""

from __future__ import annotations

import argparse
import io
import json
import os
import subprocess
import sys
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path
from typing import Any

import pytest

from mgf.fed.sessionhooks import _events, _install
from mgf.fed.sessionhooks._cli import ACTIONS, add_hooks_args, run_hooks


@pytest.fixture(autouse=True)
def _hermetic(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("MGF_HOOKS_DISABLE_FILE", str(tmp_path / "no-such-kill-switch"))
    for name in ("MGF_HOOKS_DISABLED", "MGF_HOOKS_MODE", "MGF_HOOKS_GATE", "CLAUDE_SESSION_ID"):
        monkeypatch.delenv(name, raising=False)


@pytest.fixture
def repo(tmp_path: Path) -> Path:
    root = tmp_path / "repo"
    (root / ".git").mkdir(parents=True)
    return root


def _args(*argv: str) -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    add_hooks_args(parser)
    return parser.parse_args(list(argv))


def _run(*argv: str) -> tuple[int, str, str]:
    out, err = io.StringIO(), io.StringIO()
    with redirect_stdout(out), redirect_stderr(err):
        code = run_hooks(_args(*argv))
    return code, out.getvalue(), err.getvalue()


class TestArgumentShape:
    def test_default_action_is_status(self) -> None:
        assert _args().action == "status"

    def test_every_action_parses(self) -> None:
        for action in ACTIONS:
            assert _args(action).action == action

    def test_an_unknown_action_is_rejected_by_argparse(self) -> None:
        with pytest.raises(SystemExit):
            _args("frobnicate")

    def test_emit_takes_type_then_pairs(self) -> None:
        args = _args("emit", "seat_declared", "seat=executor", "model=opus-5")
        assert args.rest == ["seat_declared", "seat=executor", "model=opus-5"]


class TestRegistration:
    def test_hooks_is_registered_as_a_subcommand(self) -> None:
        from mgf.common.cli._subcommand_registry import _iter_registered

        import mgf.fed.cli  # noqa: F401  (registration side-effect)

        entry = next((e for e in _iter_registered() if e.name == "hooks"), None)
        assert entry is not None
        assert entry.handler is run_hooks
        assert entry.parser_setup is add_hooks_args

    def test_hooks_is_a_NATIVE_verb(self) -> None:
        # F1: native, not registry-merged. Reaching it through the registry would
        # make the hook path depend on the [cli] extra — see TestBaseInstall.
        from mgf.fed.__main__ import _native_commands

        assert "hooks" in _native_commands()

    def test_the_native_ladder_does_not_double_register_it(self) -> None:
        # Being in _native_commands() is also what stops _add_registered_subcommands
        # adding a SECOND parser for the same name when [cli] IS installed.
        from mgf.fed.__main__ import build_parser

        parser = build_parser()
        actions = [a for a in parser._subparsers._group_actions if a.choices]
        names = [n for a in actions for n in a.choices]
        assert names.count("hooks") == 1

    def test_it_surfaces_through_the_real_parser(self) -> None:
        from mgf.fed.__main__ import build_parser

        args = build_parser().parse_args(["hooks", "status"])
        assert (args.command, args.action) == ("hooks", "status")


class TestInstallUninstall:
    def test_install_status_uninstall_cycle(self, repo: Path) -> None:
        code, out, _ = _run("status", "--repo", str(repo))
        assert (code, "not installed" in out) == (0, True)

        code, out, _ = _run("install", "--repo", str(repo))
        assert code == 0
        assert "installed the SessionStart shim" in out
        # SHP-08: a committed kill-switch would silence the fleet invisibly.
        assert ".gitignore" in out

        code, out, _ = _run("status", "--repo", str(repo))
        assert code == 0
        assert "shim installed (current)" in out

        code, out, _ = _run("uninstall", "--repo", str(repo))
        assert (code, "removed" in out) == (0, True)

    def test_install_is_idempotent_through_the_cli(self, repo: Path) -> None:
        _run("install", "--repo", str(repo))
        code, out, _ = _run("install", "--repo", str(repo))
        assert (code, "already installed" in out) == (0, True)

    def test_refusal_is_an_error_exit_with_the_reason_on_stderr(self, repo: Path) -> None:
        settings = _install.settings_path(repo)
        settings.parent.mkdir(parents=True)
        settings.write_text("{ not json", encoding="utf-8")
        code, _, err = _run("install", "--repo", str(repo))
        assert code == 1
        assert "not valid JSON" in err
        assert settings.read_text(encoding="utf-8") == "{ not json"

    def test_uninstall_refusal_too(self, repo: Path) -> None:
        settings = _install.settings_path(repo)
        settings.parent.mkdir(parents=True)
        settings.write_text("{ not json", encoding="utf-8")
        code, _, err = _run("uninstall", "--repo", str(repo))
        assert (code, "not valid JSON" in err) == (1, True)


class TestEmit:
    def test_the_seat_declaration_the_reteach_asks_for(self, repo: Path) -> None:
        code, out, _ = _run(
            "emit",
            "seat_declared",
            "seat=executor",
            "model=opus-5",
            "--repo",
            str(repo),
            "--session",
            "s1",
        )
        assert code == 0
        assert "recorded seat_declared" in out
        declared = _events.declaration_for(repo, "s1")
        assert declared is not None
        assert declared["model"] == "opus-5"

    def test_a_closed_vocabulary_refusal_is_a_nonzero_exit(self, repo: Path) -> None:
        code, _, err = _run("emit", "vibes_check", "--repo", str(repo))
        assert code == 1
        assert "REFUSED" in err
        assert not _events.events_path(repo).exists()

    def test_no_type_prints_usage(self, repo: Path) -> None:
        code, _, err = _run("emit", "--repo", str(repo))
        assert code == 1
        assert "usage: mgf-fed hooks emit" in err
        assert "seat_declared" in err

    def test_a_field_without_an_equals_is_refused_before_any_write(self, repo: Path) -> None:
        code, _, err = _run("emit", "journal_step", "stepone", "--repo", str(repo))
        assert code == 1
        assert "key=value" in err
        assert not _events.events_path(repo).exists()

    def test_values_may_contain_equals_signs(self, repo: Path) -> None:
        _run("emit", "journal_step", "step=a=b=c", "--repo", str(repo), "--session", "s1")
        assert _events.read(repo)[0]["step"] == "a=b=c"


class TestLog:
    def test_empty_log_says_so(self, repo: Path) -> None:
        code, out, _ = _run("log", "--repo", str(repo))
        assert (code, "never run here" in out) == (0, True)

    def test_records_render_with_a_summary(self, repo: Path) -> None:
        _events.emit(repo, "session_start", payload={"session_id": "s1"})
        _events.emit(
            repo,
            "seat_declared",
            {"seat": "designer", "model": "fable-5"},
            payload={"session_id": "s1"},
        )
        code, out, _ = _run("log", "--repo", str(repo))
        assert code == 0
        assert "seat_declared" in out
        assert "2 record(s), 1 session(s)" in out

    def test_json_mode_is_machine_readable(self, repo: Path) -> None:
        _events.emit(repo, "session_start", payload={"session_id": "s1"})
        code, out, _ = _run("log", "--repo", str(repo), "--json")
        assert code == 0
        assert json.loads(out)[0]["event"] == "session_start"

    def test_filters(self, repo: Path) -> None:
        for sid in ("s1", "s2", "s3"):
            _events.emit(repo, "session_start", payload={"session_id": sid})
        assert len(json.loads(_run("log", "--repo", str(repo), "--json", "--tail", "1")[1])) == 1
        filtered = _run("log", "--repo", str(repo), "--json", "--session", "s2")[1]
        assert [r["session"] for r in json.loads(filtered)] == ["s2"]


class TestStatusOutput:
    def test_json_status_carries_the_whole_posture(self, repo: Path) -> None:
        code, out, _ = _run("status", "--repo", str(repo), "--json")
        assert code == 0
        report: dict[str, Any] = json.loads(out)
        assert report["mode"] == "observe"
        assert report["gate_armed"] is False
        assert report["disabled"] is None
        assert report["installed"] is False

    def test_status_names_undeclared_sessions(self, repo: Path) -> None:
        _events.emit(repo, "session_start", payload={"session_id": "quiet"})
        code, out, _ = _run("status", "--repo", str(repo))
        assert code == 0
        assert "never declared a seat" in out

    def test_status_announces_the_kill_switch(
        self, repo: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("MGF_HOOKS_DISABLED", "1")
        code, out, _ = _run("status", "--repo", str(repo))
        assert code == 0
        assert "KILL-SWITCH ACTIVE" in out

    def test_status_reports_the_armed_gate(
        self, repo: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("MGF_HOOKS_GATE", "1")
        code, out, _ = _run("status", "--repo", str(repo))
        assert (code, "gate=ARMED" in out) == (0, True)


class TestHarnessActionsAlwaysExitZero:
    """The operational floor: a live session depends on these never failing."""

    @pytest.mark.parametrize("action", ["session-start", "writeset", "gate"])
    @pytest.mark.parametrize("stdin", ["", "not json {", '{"cwd": "/nope"}', "[1,2]", "null"])
    def test_every_harness_action_on_every_stdin(
        self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, action: str, stdin: str
    ) -> None:
        # chdir into the sandbox on purpose: unusable stdin falls back to the process
        # cwd, and without this the suite writes .claude/state/ into the SOURCE TREE.
        # (It did, once. That is why the assertion below exists.)
        monkeypatch.chdir(tmp_path)
        monkeypatch.setattr("sys.stdin", io.StringIO(stdin))
        out, err = io.StringIO(), io.StringIO()
        with redirect_stdout(out), redirect_stderr(err):
            assert run_hooks(_args(action)) == 0

    def test_the_cwd_fallback_writes_inside_the_cwd_and_nowhere_else(
        self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    ) -> None:
        source_tree = Path(__file__).resolve().parents[3]
        monkeypatch.chdir(tmp_path)
        monkeypatch.setattr("sys.stdin", io.StringIO(""))
        with redirect_stdout(io.StringIO()):
            assert run_hooks(_args("session-start")) == 0
        assert (tmp_path / ".claude" / "state" / "agent-events.jsonl").is_file()
        assert not (source_tree / ".claude" / "state").exists()

    def test_session_start_reads_stdin_and_teaches(
        self, monkeypatch: pytest.MonkeyPatch, repo: Path
    ) -> None:
        payload = json.dumps({"cwd": str(repo), "session_id": "s1"})
        monkeypatch.setattr("sys.stdin", io.StringIO(payload))
        out = io.StringIO()
        with redirect_stdout(out):
            assert run_hooks(_args("session-start")) == 0
        assert "SEAT-02" in out.getvalue()
        assert _events.read(repo, session="s1", etype="session_start")

    def test_the_installed_shim_names_the_action_this_cli_serves(self) -> None:
        # If these ever diverge, every installed repo silently stops working.
        assert _install.HOOK_ENTRYPOINT == "mgf-fed hooks session-start"
        assert "session-start" in ACTIONS


# --------------------------------------------------------------------------- #
# F1 — the hook path must survive a BASE install (no mgf-common)
# --------------------------------------------------------------------------- #


def _shadow_mgf_common(tmp_path: Path) -> Path:
    """A sys.path entry that makes ``import mgf.common`` raise ModuleNotFoundError.

    Simulates a base ``pip install mgf-fed`` — the ``[cli]`` extra absent — without
    uninstalling anything. ``mgf`` stays a namespace package here so ``mgf.fed`` still
    resolves from the real location; only ``mgf.common`` is poisoned.
    """
    shadow = tmp_path / "shadow"
    (shadow / "mgf" / "common").mkdir(parents=True)
    (shadow / "mgf" / "common" / "__init__.py").write_text(
        "raise ModuleNotFoundError(\"No module named 'mgf.common'\")\n", encoding="utf-8"
    )
    return shadow


def _run_base_install(
    shadow: Path, args: list[str], stdin: str = ""
) -> subprocess.CompletedProcess[str]:
    env = dict(os.environ)
    env["PYTHONPATH"] = str(shadow)
    env["MGF_HOOKS_DISABLE_FILE"] = str(shadow / "no-such-kill-switch")
    for name in ("MGF_HOOKS_DISABLED", "MGF_HOOKS_MODE", "MGF_HOOKS_GATE", "CLAUDE_SESSION_ID"):
        env.pop(name, None)
    return subprocess.run(  # noqa: S603
        [sys.executable, "-m", "mgf.fed", *args],
        input=stdin,
        capture_output=True,
        text=True,
        env=env,
        timeout=60,
        check=False,
    )


@pytest.mark.integration
class TestBaseInstallHasTheHookPath:
    """F1: `pip install mgf-fed` with no extras must still serve the shim.

    The blocker this pins: `hooks` used to be registry-merged, and the registry lives
    behind `mgf.common`. On a base install the verb did not exist at all — so every
    session in every adopting repo would have lost its re-teach in silence, which is
    the invisible-absence failure the whole kit was written to end.
    """

    def test_the_shadow_actually_blocks_mgf_common(self, tmp_path: Path) -> None:
        # Guard the guard: a simulation that does not simulate proves nothing.
        shadow = _shadow_mgf_common(tmp_path)
        probe = _run_base_install(shadow, ["--version"])
        assert probe.returncode == 0
        env = dict(os.environ, PYTHONPATH=str(shadow))
        blocked = subprocess.run(
            [sys.executable, "-c", "import mgf.common"],
            capture_output=True,
            text=True,
            env=env,
            timeout=60,
            check=False,
        )
        assert blocked.returncode != 0
        assert "No module named 'mgf.common'" in blocked.stderr

    def test_session_start_exits_zero_and_teaches(self, tmp_path: Path) -> None:
        shadow = _shadow_mgf_common(tmp_path)
        repo = tmp_path / "repo"
        (repo / ".git").mkdir(parents=True)
        payload = json.dumps({"cwd": str(repo), "session_id": "base-1"})
        done = _run_base_install(shadow, ["hooks", "session-start"], stdin=payload)
        assert done.returncode == 0, done.stderr
        assert "SEAT-02" in done.stdout
        assert "Declare:" in done.stdout
        assert (repo / ".claude" / "state" / "agent-events.jsonl").is_file()

    def test_install_and_status_work_too(self, tmp_path: Path) -> None:
        # The whole verb group, not just the entrypoint: `_install` falls back to a
        # stdlib atomic write when mgf.common.io is unreachable.
        shadow = _shadow_mgf_common(tmp_path)
        repo = tmp_path / "repo2"
        (repo / ".git").mkdir(parents=True)
        installed = _run_base_install(shadow, ["hooks", "install", "--repo", str(repo)])
        assert installed.returncode == 0, installed.stderr
        settings = json.loads((repo / ".claude" / "settings.json").read_text(encoding="utf-8"))
        assert settings["hooks"]["SessionStart"]
        status = _run_base_install(shadow, ["hooks", "status", "--repo", str(repo), "--json"])
        assert status.returncode == 0, status.stderr
        assert json.loads(status.stdout)["installed"] is True

    def test_hooks_is_listed_in_help(self, tmp_path: Path) -> None:
        shadow = _shadow_mgf_common(tmp_path)
        helped = _run_base_install(shadow, ["--help"])
        assert helped.returncode == 0
        assert "hooks" in helped.stdout


# --------------------------------------------------------------------------- #
# F2 — the installed shim, executed as a shell would execute it
# --------------------------------------------------------------------------- #


def _stub_path(tmp_path: Path, body: str) -> Path:
    """A directory holding an executable `mgf-fed` stub, for PATH."""
    bindir = tmp_path / "stubbin"
    bindir.mkdir(exist_ok=True)
    stub = bindir / "mgf-fed"
    stub.write_text(body, encoding="utf-8")
    stub.chmod(0o755)
    return bindir


def _run_shim(path_dirs: list[str]) -> subprocess.CompletedProcess[str]:
    env = dict(os.environ, PATH=os.pathsep.join(path_dirs))
    return subprocess.run(  # noqa: S603
        ["/bin/sh", "-c", _install.HOOK_COMMAND],
        capture_output=True,
        text=True,
        env=env,
        timeout=60,
        check=False,
    )


@pytest.mark.integration
class TestTheShimLineItself:
    """The shim is a shell one-liner in a JSON string. It is worth executing."""

    def test_no_mgf_fed_at_all_is_silent_and_zero(self, tmp_path: Path) -> None:
        # Absence is the operator's choice, not a fault.
        empty = tmp_path / "emptybin"
        empty.mkdir()
        done = _run_shim([str(empty)])
        assert done.returncode == 0
        assert done.stderr == ""
        assert done.stdout == ""

    def test_a_stale_mgf_fed_says_so_ONCE_and_still_exits_zero(self, tmp_path: Path) -> None:
        # An mgf-fed too old for `hooks`: argparse exits 2. Without the notice that
        # repo's sessions lose the re-teach in complete silence.
        bindir = _stub_path(
            tmp_path,
            "#!/bin/sh\necho \"mgf-fed: error: invalid choice: 'hooks'\" >&2\nexit 2\n",
        )
        done = _run_shim([str(bindir)])
        assert done.returncode == 0
        notice_lines = [ln for ln in done.stderr.splitlines() if "session-start unavailable" in ln]
        assert len(notice_lines) == 1, done.stderr
        assert "upgrade mgf-fed" in notice_lines[0]

    def test_a_working_mgf_fed_prints_no_notice(self, tmp_path: Path) -> None:
        bindir = _stub_path(tmp_path, "#!/bin/sh\necho '## SEAT-02 — declare'\nexit 0\n")
        done = _run_shim([str(bindir)])
        assert done.returncode == 0
        assert "SEAT-02" in done.stdout
        assert "unavailable" not in done.stderr

    def test_even_a_crashing_mgf_fed_cannot_stop_the_session(self, tmp_path: Path) -> None:
        bindir = _stub_path(tmp_path, "#!/bin/sh\nkill -SEGV $$\n")
        assert _run_shim([str(bindir)]).returncode == 0

    def test_the_notice_survives_both_quoting_layers(self) -> None:
        # It is embedded in a single-quoted shell word inside a JSON string. A notice
        # that only survives one of those layers breaks the shim it explains.
        assert "'" not in _install.HOOK_STALE_NOTICE
        assert '"' not in _install.HOOK_STALE_NOTICE
        assert "`" not in _install.HOOK_STALE_NOTICE
        assert "$" not in _install.HOOK_STALE_NOTICE
        assert json.loads(json.dumps(_install.HOOK_COMMAND)) == _install.HOOK_COMMAND


@pytest.mark.integration
class TestOutputFailureNeverStopsASession:
    """F6: `> /dev/full` — the one write failure that can actually take a session out."""

    def test_dev_full_stdout_still_exits_zero(self, tmp_path: Path) -> None:
        if not Path("/dev/full").exists():  # pragma: no cover - platform guard
            pytest.skip("/dev/full is Linux-only")
        repo = tmp_path / "repo"
        (repo / ".git").mkdir(parents=True)
        payload = json.dumps({"cwd": str(repo), "session_id": "full-1"})
        env = dict(os.environ)
        env["MGF_HOOKS_DISABLE_FILE"] = str(tmp_path / "no-such-kill-switch")
        with Path("/dev/full").open("w") as full:
            done = subprocess.run(
                [sys.executable, "-m", "mgf.fed", "hooks", "session-start"],
                input=payload,
                stdout=full,
                stderr=subprocess.PIPE,
                text=True,
                env=env,
                timeout=60,
                check=False,
            )
        assert done.returncode == 0, done.stderr
        # And it still did its job: the record landed even though nobody could read it.
        assert (repo / ".claude" / "state" / "agent-events.jsonl").is_file()
