"""The staged gate — proving it is OFF, and proving what it would do once armed.

The most important tests here are the negative ones: with no environment set, the
write-set recorder and the gate must read nothing, write nothing, and return ``0``.
That is what "shipped disabled" has to mean, and it is the whole of **SHP-11** — no
step may create a state that is only safe if a later step also happens.

Adapted with attribution from the Sooth kit's ``shp/tests/test_adversarial.py``
(attribution-before-blame) and ``test_adopt.py`` (the mode matrix). Copy, not a
dependency.
"""

from __future__ import annotations

import io
from typing import TYPE_CHECKING, Any

import pytest

from mgf.fed.sessionhooks import _events, _gate, _session_start

if TYPE_CHECKING:
    from pathlib import Path


@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


@pytest.fixture
def armed(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("MGF_HOOKS_GATE", "1")


@pytest.fixture
def enforcing(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("MGF_HOOKS_GATE", "1")
    monkeypatch.setenv("MGF_HOOKS_MODE", "enforce")


def _write_payload(repo: Path, path: Path, sid: str = "s1") -> dict[str, Any]:
    return {
        "cwd": str(repo),
        "session_id": sid,
        "tool_name": "Write",
        "tool_input": {"file_path": str(path)},
    }


# --------------------------------------------------------------------------- #
# Shipped disabled — the default posture
# --------------------------------------------------------------------------- #


class TestDisabledByDefault:
    def test_writeset_records_nothing(self, repo: Path) -> None:
        assert _gate.run_writeset(_write_payload(repo, repo / "a.py")) == 0
        assert not _gate.writeset_path(repo, "s1").exists()
        assert not (repo / ".claude").exists()

    def test_gate_evaluates_nothing(self, repo: Path) -> None:
        # An undeclared session WOULD be a finding once armed. Unarmed, the gate must
        # not even look.
        assert _gate.run_gate({"cwd": str(repo), "session_id": "s1"}) == 0
        assert not _gate.pending_path(repo).exists()

    def test_enforce_alone_does_not_arm(self, repo: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        # Two independent keys. Setting only the mode must not produce a gate.
        monkeypatch.setenv("MGF_HOOKS_MODE", "enforce")
        assert _gate.run_gate({"cwd": str(repo), "session_id": "s1"}) == 0
        assert not _gate.pending_path(repo).exists()

    def test_the_installer_never_wires_the_gate(self) -> None:
        from mgf.fed.sessionhooks import _install

        # The shim reaches session-start and nothing else. A Stop or PostToolUse
        # entry in the installed shim would be exactly the unsafe intermediate state
        # SHP-11 forbids.
        assert _install.HOOK_EVENT == "SessionStart"
        assert "gate" not in _install.HOOK_COMMAND
        assert "writeset" not in _install.HOOK_COMMAND


# --------------------------------------------------------------------------- #
# Armed: the write-set recorder
# --------------------------------------------------------------------------- #


@pytest.mark.usefixtures("armed")
class TestWriteset:
    def test_exact_path_tools_are_definitive(self, repo: Path) -> None:
        target = repo / "src" / "a.py"
        assert _gate.run_writeset(_write_payload(repo, target)) == 0
        exact, commands = _gate.read_writeset(repo, "s1")
        assert exact == {str(target.resolve())}
        assert commands == []

    @pytest.mark.parametrize("tool", sorted(_gate.EXACT_PATH_TOOLS))
    def test_every_exact_tool_is_recognised(self, repo: Path, tool: str) -> None:
        field = _gate.EXACT_PATH_TOOLS[tool]
        payload = {
            "cwd": str(repo),
            "session_id": "s1",
            "tool_name": tool,
            "tool_input": {field: str(repo / "x")},
        }
        assert _gate.run_writeset(payload) == 0
        assert _gate.read_writeset(repo, "s1")[0]

    def test_bash_is_recorded_as_a_command_not_a_path(self, repo: Path) -> None:
        # A shell command can write anywhere and proves nothing directly. It is
        # evidence a human can read, never an attribution.
        payload = {
            "cwd": str(repo),
            "session_id": "s1",
            "tool_name": "Bash",
            "tool_input": {"command": "rm -rf ../other-repo/src"},
        }
        assert _gate.run_writeset(payload) == 0
        exact, commands = _gate.read_writeset(repo, "s1")
        assert exact == set()
        assert commands == ["rm -rf ../other-repo/src"]

    def test_long_commands_are_truncated(self, repo: Path) -> None:
        payload = {
            "cwd": str(repo),
            "session_id": "s1",
            "tool_name": "Bash",
            "tool_input": {"command": "x" * (_gate.MAX_COMMAND_RECORDED + 500)},
        }
        _gate.run_writeset(payload)
        assert len(_gate.read_writeset(repo, "s1")[1][0]) == _gate.MAX_COMMAND_RECORDED

    @pytest.mark.parametrize(
        "payload_extra",
        [
            {"tool_name": "Read", "tool_input": {"file_path": "/x"}},
            {"tool_name": "Write", "tool_input": {}},
            {"tool_name": "Bash", "tool_input": {"command": ""}},
            {"tool_name": "", "tool_input": None},
            {},
        ],
    )
    def test_non_writing_calls_record_nothing(
        self, repo: Path, payload_extra: dict[str, Any]
    ) -> None:
        payload = {"cwd": str(repo), "session_id": "s1", **payload_extra}
        assert _gate.run_writeset(payload) == 0
        assert _gate.read_writeset(repo, "s1") == (set(), [])

    def test_a_failed_append_never_breaks_the_tool_call(self, repo: Path) -> None:
        # A PostToolUse hook that fails must not break the tool call it followed.
        (repo / ".claude").write_text("not a directory\n", encoding="utf-8")
        assert _gate.run_writeset(_write_payload(repo, repo / "a.py")) == 0

    def test_the_kill_switch_stops_recording(
        self, repo: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("MGF_HOOKS_DISABLED", "1")
        assert _gate.run_writeset(_write_payload(repo, repo / "a.py")) == 0
        assert not _gate.writeset_path(repo, "s1").exists()

    def test_write_sets_are_per_session(self, repo: Path) -> None:
        # Concurrent sessions are standing practice; one shared write-set would
        # attribute one session's work to another.
        _gate.run_writeset(_write_payload(repo, repo / "a.py", sid="s1"))
        _gate.run_writeset(_write_payload(repo, repo / "b.py", sid="s2"))
        assert len(_gate.read_writeset(repo, "s1")[0]) == 1
        assert len(_gate.read_writeset(repo, "s2")[0]) == 1


# --------------------------------------------------------------------------- #
# Armed: the verdict
# --------------------------------------------------------------------------- #


@pytest.mark.usefixtures("armed")
class TestEvaluate:
    def test_an_undeclared_session_is_a_blocking_finding(self, repo: Path) -> None:
        findings = _gate.evaluate(repo, "s1")
        assert [f["rule"] for f in findings] == ["SEAT-02"]
        assert findings[0]["blocking"] is True
        # SHP-06: the gate blocks silence, never judgment — so the refusal has to
        # carry its own one-command exit.
        assert "mgf-fed hooks emit seat_declared" in findings[0]["finding"]

    def test_a_declared_session_has_no_finding(self, repo: Path) -> None:
        _events.emit(
            repo,
            "seat_declared",
            {"seat": "executor", "model": "opus-5"},
            payload={"session_id": "s1"},
        )
        assert _gate.evaluate(repo, "s1") == []

    def test_a_cross_repo_write_is_advisory_never_blocking(
        self, repo: Path, tmp_path: Path
    ) -> None:
        # mgf has ruled NO cross-repo handoff channel, so this finding has no
        # sanctioned discharge. Blocking on it would be a gate with no exit — which
        # is the gate that gets ripped out.
        other = tmp_path / "sibling"
        (other / ".git").mkdir(parents=True)
        _events.emit(
            repo,
            "seat_declared",
            {"seat": "executor", "model": "opus-5"},
            payload={"session_id": "s1"},
        )
        _gate.run_writeset(_write_payload(repo, other / "src" / "x.py"))
        findings = _gate.evaluate(repo, "s1")
        assert [f["rule"] for f in findings] == ["SHP-05"]
        assert findings[0]["blocking"] is False
        assert str(other) in findings[0]["finding"]

    def test_writes_inside_the_launch_repo_are_not_cross_repo(self, repo: Path) -> None:
        _events.emit(
            repo,
            "seat_declared",
            {"seat": "executor", "model": "opus-5"},
            payload={"session_id": "s1"},
        )
        _gate.run_writeset(_write_payload(repo, repo / "src" / "x.py"))
        assert _gate.evaluate(repo, "s1") == []

    def test_writes_outside_any_repo_are_not_cross_repo(self, repo: Path, tmp_path: Path) -> None:
        _events.emit(
            repo,
            "seat_declared",
            {"seat": "executor", "model": "opus-5"},
            payload={"session_id": "s1"},
        )
        _gate.run_writeset(_write_payload(repo, tmp_path / "loose" / "note.txt"))
        assert _gate.evaluate(repo, "s1") == []


@pytest.mark.usefixtures("armed")
class TestObserveModeNeverBlocks:
    def test_a_blocking_finding_still_exits_zero(self, repo: Path) -> None:
        err = io.StringIO()
        assert _gate.run_gate({"cwd": str(repo), "session_id": "s1"}, err=err) == 0
        assert err.getvalue() == ""

    def test_but_it_is_recorded_for_replay(self, repo: Path) -> None:
        # Observe mode that says nothing is no observe mode at all.
        _gate.run_gate({"cwd": str(repo), "session_id": "s1"})
        pending = _gate.pending_path(repo)
        assert pending.is_file()
        assert "SEAT-02" in pending.read_text(encoding="utf-8")

    def test_the_next_session_start_reads_it_back(self, repo: Path) -> None:
        _gate.run_gate({"cwd": str(repo), "session_id": "s1"})
        buf = io.StringIO()
        _session_start.run({"cwd": str(repo), "session_id": "s2"}, out=buf)
        assert "never declared its seat" in buf.getvalue()

    def test_a_resolved_finding_clears_the_pending_verdict(self, repo: Path) -> None:
        # Always resolved, never only written: a stale verdict tells the NEXT session
        # about a problem that no longer exists.
        _gate.run_gate({"cwd": str(repo), "session_id": "s1"})
        assert _gate.pending_path(repo).is_file()
        _events.emit(
            repo,
            "seat_declared",
            {"seat": "executor", "model": "opus-5"},
            payload={"session_id": "s1"},
        )
        _gate.run_gate({"cwd": str(repo), "session_id": "s1"})
        assert not _gate.pending_path(repo).exists()


@pytest.mark.usefixtures("enforcing")
class TestEnforceMode:
    def test_a_blocking_finding_exits_two_with_a_reason(self, repo: Path) -> None:
        err = io.StringIO()
        assert _gate.run_gate({"cwd": str(repo), "session_id": "s1"}, err=err) == 2
        assert "MGF-HOOKS-BLOCK [SEAT-02]" in err.getvalue()

    def test_it_is_self_releasing_on_the_retry(self, repo: Path) -> None:
        # A gate that can trap a session is a gate that gets ripped out. The harness
        # sets stop_hook_active on the retry; that is the release.
        payload = {"cwd": str(repo), "session_id": "s1", "stop_hook_active": True}
        assert _gate.run_gate(payload) == 0

    def test_an_advisory_finding_never_blocks_even_here(self, repo: Path, tmp_path: Path) -> None:
        other = tmp_path / "sibling"
        (other / ".git").mkdir(parents=True)
        _events.emit(
            repo,
            "seat_declared",
            {"seat": "executor", "model": "opus-5"},
            payload={"session_id": "s1"},
        )
        _gate.run_writeset(_write_payload(repo, other / "x.py"))
        err = io.StringIO()
        assert _gate.run_gate({"cwd": str(repo), "session_id": "s1"}, err=err) == 0
        assert err.getvalue() == ""

    def test_a_declared_session_passes(self, repo: Path) -> None:
        _events.emit(
            repo,
            "seat_declared",
            {"seat": "designer", "model": "fable-5"},
            payload={"session_id": "s1"},
        )
        assert _gate.run_gate({"cwd": str(repo), "session_id": "s1"}) == 0

    def test_subagent_stop_is_never_the_session_ending(self, repo: Path) -> None:
        payload = {"cwd": str(repo), "session_id": "s1", "hook_event_name": "SubagentStop"}
        assert _gate.run_gate(payload) == 0

    def test_the_kill_switch_outranks_enforce(
        self, repo: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        # While disabled, nothing can block. Ever. That is the guarantee.
        monkeypatch.setenv("MGF_HOOKS_DISABLED", "1")
        assert _gate.run_gate({"cwd": str(repo), "session_id": "s1"}) == 0

    def test_a_malformed_event_log_fails_open(self, repo: Path) -> None:
        events = repo / ".claude" / "state" / "agent-events.jsonl"
        events.parent.mkdir(parents=True)
        events.write_text("{{{ not json\n", encoding="utf-8")
        # Unreadable state means "undeclared", which blocks — but it blocks with a
        # reason the human can act on, never with a traceback.
        err = io.StringIO()
        assert _gate.run_gate({"cwd": str(repo), "session_id": "s1"}, err=err) == 2
        assert "SEAT-02" in err.getvalue()
