"""Plumbing pins for the session-hook kit — the parts every hook path depends on.

Adapted with attribution from the Sooth kit's ``shp/tests/`` (F12's contracted-cwd
probe, R27's kill-switch triple, the torn-write reader). Copy, not a dependency.
"""

from __future__ import annotations

import io
import json
import shutil
import subprocess
import threading
import zipfile
from pathlib import Path

import pytest

from mgf.fed.sessionhooks import _state


@pytest.fixture(autouse=True)
def _hermetic(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """No real kill-switch may leak into a test run — nor a real one be created."""
    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)


class TestHookInput:
    """Absent / broken stdin is a manual run, never an error."""

    def test_valid_object(self) -> None:
        assert _state.hook_input(io.StringIO('{"cwd": "/x"}')) == {"cwd": "/x"}

    @pytest.mark.parametrize("raw", ["", "   ", "not json {", "[1, 2]", '"a string"', "null"])
    def test_unusable_stdin_is_empty_not_an_error(self, raw: str) -> None:
        assert _state.hook_input(io.StringIO(raw)) == {}

    def test_unreadable_stream_is_empty(self) -> None:
        closed = io.StringIO("{}")
        closed.close()
        assert _state.hook_input(closed) == {}


class TestSessionId:
    def test_contracted_field_wins(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("CLAUDE_SESSION_ID", "from-env")
        assert _state.session_id({"session_id": "from-stdin"}) == "from-stdin"

    def test_env_is_the_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("CLAUDE_SESSION_ID", "from-env")
        assert _state.session_id({}) == "from-env"

    def test_absent_is_manual(self) -> None:
        assert _state.session_id({}) == "manual"
        assert _state.session_id(None) == "manual"

    @pytest.mark.parametrize(
        ("raw", "expected"),
        [
            ("../../etc/passwd", ".._.._etc_passwd"),
            ("a/b", "a_b"),
            ("///", "manual"),
            ("ok-ID.9", "ok-ID.9"),
        ],
    )
    def test_never_escapes_the_state_dir(self, raw: str, expected: str) -> None:
        # A session id lands in a filename. One that can contain a separator is a
        # path-traversal waiting to be handed to us by the harness.
        assert _state.session_id({"session_id": raw}) == expected


class TestRepoRoot:
    def test_prefers_the_contracted_cwd(self, tmp_path: Path) -> None:
        repo = tmp_path / "repo"
        (repo / ".git").mkdir(parents=True)
        nested = repo / "src" / "deep"
        nested.mkdir(parents=True)
        assert _state.repo_root({"cwd": str(nested)}) == repo.resolve()

    def test_worktree_git_file_counts(self, tmp_path: Path) -> None:
        # A linked worktree has a .git FILE, not a directory. Missing that makes
        # every worktree session write its state into the parent directory.
        repo = tmp_path / "wt"
        repo.mkdir()
        (repo / ".git").write_text("gitdir: /elsewhere\n", encoding="utf-8")
        assert _state.repo_root({"cwd": str(repo)}) == repo.resolve()

    def test_no_repo_returns_the_starting_point(self, tmp_path: Path) -> None:
        plain = tmp_path / "plain"
        plain.mkdir()
        assert _state.repo_root({"cwd": str(plain)}) == plain.resolve()

    def test_missing_cwd_falls_back_to_the_process(self, monkeypatch: pytest.MonkeyPatch) -> None:
        assert _state.repo_root({}).is_absolute()
        assert _state.repo_root(None).is_absolute()


class TestKillSwitch:
    """SHP-08 — three scopes, first match wins, and it never raises."""

    def test_live_by_default(self, tmp_path: Path) -> None:
        assert _state.disabled_reason(tmp_path) is None

    def test_env_scope(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        monkeypatch.setenv("MGF_HOOKS_DISABLED", "1")
        reason = _state.disabled_reason(tmp_path)
        assert reason is not None
        assert "MGF_HOOKS_DISABLED" in reason

    def test_zero_does_not_disable(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        # "0" reads as off-the-switch, not on it; anything else is on.
        monkeypatch.setenv("MGF_HOOKS_DISABLED", "0")
        assert _state.disabled_reason(tmp_path) is None

    def test_fleet_scope(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        sentinel = tmp_path / "DISABLED"
        sentinel.touch()
        monkeypatch.setenv("MGF_HOOKS_DISABLE_FILE", str(sentinel))
        reason = _state.disabled_reason(tmp_path)
        assert reason is not None
        assert "fleet-wide" in reason

    def test_per_repo_scope(self, tmp_path: Path) -> None:
        (tmp_path / _state.REPO_DISABLE_REL).parent.mkdir(parents=True)
        (tmp_path / _state.REPO_DISABLE_REL).touch()
        reason = _state.disabled_reason(tmp_path)
        assert reason is not None
        assert "per-repo" in reason


class TestModeAndArming:
    def test_defaults_are_the_safe_ones(self) -> None:
        assert _state.mode() == _state.MODE_OBSERVE
        assert _state.gate_armed() is False

    @pytest.mark.parametrize("raw", ["enforce", "ENFORCE", " Enforce "])
    def test_enforce_is_explicit(self, monkeypatch: pytest.MonkeyPatch, raw: str) -> None:
        monkeypatch.setenv("MGF_HOOKS_MODE", raw)
        assert _state.mode() == _state.MODE_ENFORCE

    @pytest.mark.parametrize("raw", ["", "observe", "yes", "block", "nonsense"])
    def test_anything_unrecognised_is_observe(
        self, monkeypatch: pytest.MonkeyPatch, raw: str
    ) -> None:
        # A typo in the mode must never be read as "enforce". Fail towards not
        # blocking, always.
        monkeypatch.setenv("MGF_HOOKS_MODE", raw)
        assert _state.mode() == _state.MODE_OBSERVE

    @pytest.mark.parametrize(
        ("raw", "armed"),
        [
            ("1", True),
            (" 1 ", True),  # surrounding whitespace is an accident, not a decision
            # STRICT: `1` is the only arming value, which is what the how-to
            # documents. A truthiness test would arm the gate on strings the operator
            # was never told would arm it — and a mistake here must fall towards NOT
            # blocking, exactly as MGF_HOOKS_MODE does.
            ("true", False),
            ("True", False),
            ("yes", False),
            ("on", False),
            ("2", False),
            ("01", False),
            ("enforce", False),
            ("", False),
            ("0", False),
        ],
    )
    def test_gate_arming_is_strict(
        self, monkeypatch: pytest.MonkeyPatch, raw: str, armed: bool
    ) -> None:
        monkeypatch.setenv("MGF_HOOKS_GATE", raw)
        assert _state.gate_armed() is armed

    def test_the_two_keys_are_independent(self, monkeypatch: pytest.MonkeyPatch) -> None:
        # SHP-11: neither key alone can produce a blocking gate.
        monkeypatch.setenv("MGF_HOOKS_MODE", "enforce")
        assert _state.gate_armed() is False
        monkeypatch.delenv("MGF_HOOKS_MODE")
        monkeypatch.setenv("MGF_HOOKS_GATE", "1")
        assert _state.mode() == _state.MODE_OBSERVE


class TestClaimOnce:
    """O_EXCL: the one-time claim that a read-then-write check cannot make."""

    def test_exactly_one_caller_wins(self, tmp_path: Path) -> None:
        marker = tmp_path / "deep" / "marker"
        assert _state.claim_once(marker) is True
        assert _state.claim_once(marker) is False
        assert _state.claim_once(marker) is False

    def test_it_creates_the_parent(self, tmp_path: Path) -> None:
        assert _state.claim_once(tmp_path / "a" / "b" / "c" / "m") is True

    def test_an_unwritable_target_loses_rather_than_raises(self, tmp_path: Path) -> None:
        # Losing is the safe direction: the caller then does nothing. A duplicate
        # record is noise; a hook that dies on its own bookkeeping is an outage.
        blocker = tmp_path / "blocker"
        blocker.write_text("a file, not a directory\n", encoding="utf-8")
        assert _state.claim_once(blocker / "sub" / "marker") is False

    @pytest.mark.integration
    def test_it_holds_under_real_concurrency(self, tmp_path: Path) -> None:
        # The barrier repro from the court finding: 16 threads released at once.
        # A read-then-write check loses this every time.
        marker = tmp_path / "marker"
        barrier = threading.Barrier(16)
        wins: list[bool] = []
        lock = threading.Lock()

        def race() -> None:
            barrier.wait()
            won = _state.claim_once(marker)
            with lock:
                wins.append(won)

        threads = [threading.Thread(target=race) for _ in range(16)]
        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()
        assert sum(wins) == 1, f"expected exactly one winner, got {sum(wins)}"


class TestBoundedReads:
    """F5: the hook path must not pay for the whole history of a repo."""

    def test_a_window_returns_only_the_tail(self, tmp_path: Path) -> None:
        path = tmp_path / "log.jsonl"
        for i in range(2000):
            _state.append_jsonl(path, {"i": i, "pad": "x" * 200})
        assert path.stat().st_size > 400_000
        windowed = _state.read_jsonl(path, max_bytes=64 * 1024)
        unbounded = _state.read_jsonl(path)
        assert len(unbounded) == 2000
        assert 0 < len(windowed) < 2000
        # The tail is what a hook needs, and the tail is what it gets.
        assert windowed[-1]["i"] == 1999
        assert windowed[0]["i"] > 0

    def test_the_partial_first_line_in_a_window_is_discarded(self, tmp_path: Path) -> None:
        # A window almost certainly starts mid-record. Half a line must never parse
        # into something plausible.
        path = tmp_path / "log.jsonl"
        for i in range(200):
            _state.append_jsonl(path, {"i": i, "pad": "y" * 100})
        window = _state.read_jsonl(path, max_bytes=1024)
        assert window, "the window returned nothing at all"
        assert all("i" in rec and "pad" in rec for rec in window)

    def test_a_window_larger_than_the_file_reads_everything(self, tmp_path: Path) -> None:
        path = tmp_path / "log.jsonl"
        for i in range(5):
            _state.append_jsonl(path, {"i": i})
        assert len(_state.read_jsonl(path, max_bytes=1024 * 1024)) == 5

    def test_a_window_on_a_missing_file_is_empty(self, tmp_path: Path) -> None:
        assert _state.read_jsonl(tmp_path / "nope.jsonl", max_bytes=1024) == []


class TestJsonl:
    def test_append_and_read_round_trip(self, tmp_path: Path) -> None:
        path = tmp_path / "deep" / "log.jsonl"
        assert _state.append_jsonl(path, {"a": 1}) is None
        assert _state.append_jsonl(path, {"b": 2}) is None
        assert _state.read_jsonl(path) == [{"a": 1}, {"b": 2}]

    def test_torn_write_never_breaks_the_reader(self, tmp_path: Path) -> None:
        # A crash mid-write leaves a partial last line. An audit trail that becomes
        # unreadable at the first bad byte is not an audit trail.
        path = tmp_path / "log.jsonl"
        _state.append_jsonl(path, {"good": 1})
        with path.open("a", encoding="utf-8") as handle:
            handle.write('{"torn": \n')
        _state.append_jsonl(path, {"good": 2})
        assert _state.read_jsonl(path) == [{"good": 1}, {"good": 2}]

    def test_non_object_lines_are_skipped(self, tmp_path: Path) -> None:
        path = tmp_path / "log.jsonl"
        path.write_text('[1,2]\n"str"\n{"ok": true}\n\n', encoding="utf-8")
        assert _state.read_jsonl(path) == [{"ok": True}]

    def test_missing_file_is_empty_not_an_error(self, tmp_path: Path) -> None:
        assert _state.read_jsonl(tmp_path / "never-written.jsonl") == []

    def test_unwritable_target_warns_and_never_raises(self, tmp_path: Path) -> None:
        # Observability must never become the failure: a full disk changes what the
        # log says, never what the hook does.
        blocker = tmp_path / "blocker"
        blocker.write_text("I am a file, not a directory\n", encoding="utf-8")
        warning = _state.append_jsonl(blocker / "sub" / "log.jsonl", {"a": 1})
        assert warning is not None
        assert "not persisted" in warning

    def test_unreadable_path_reads_empty(self, tmp_path: Path) -> None:
        directory = tmp_path / "a-directory"
        directory.mkdir()
        assert _state.read_jsonl(directory) == []


class TestStateDir:
    def test_default_is_the_repo_local_claude_state(self, tmp_path: Path) -> None:
        assert _state.state_dir(tmp_path) == tmp_path / ".claude" / "state"

    def test_env_override(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
        monkeypatch.setenv("MGF_HOOKS_STATE_DIR", str(tmp_path / "elsewhere"))
        assert _state.state_dir(tmp_path) == tmp_path / "elsewhere"


def test_utc_stamp_shape() -> None:
    stamp = _state.utc_stamp()
    assert stamp.endswith("Z")
    assert len(stamp) == len("2026-07-31T00:00:00Z")


def test_records_are_stable_json(tmp_path: Path) -> None:
    # Deterministic output: sorted keys, one line per record, no trailing blanks.
    path = tmp_path / "log.jsonl"
    _state.append_jsonl(path, {"b": 2, "a": 1})
    line = path.read_text(encoding="utf-8").rstrip("\n")
    assert line == json.dumps({"a": 1, "b": 2}, sort_keys=True)


class TestTheKillSwitchIsNeverShipped:
    """F7: the fleet-wide sentinel resolves next to the INSTALLED package.

    In an editable install that path is inside the source tree, which means a stray
    `touch` plus a sweeping `git add` would commit it — and the next wheel would ship
    a permanently, silently disabled kit to every consumer at once. Two guards, one
    cheap and always-on, one expensive and exact.
    """

    def test_the_editable_path_is_git_ignored(self) -> None:
        if shutil.which("git") is None:
            # The CI test matrix runs in slim containers with no git on
            # PATH (the same container property app-doctor documents for
            # AG-18). The always-on halves of this guard — no DISABLED
            # anywhere under src/, and the wheel-content test — still run
            # there; this git-authority leg runs wherever git exists
            # (every dev checkout, the lint step's image).
            pytest.skip("no git on PATH (slim CI container); always-on guards still cover")
        repo = Path(__file__).resolve().parents[3]
        sentinel = repo / "src" / "mgf" / "fed" / "sessionhooks" / "DISABLED"
        # `git check-ignore` is the authority — not a substring search of .gitignore,
        # which would pass on a pattern that does not actually match.
        done = subprocess.run(  # noqa: S603
            ["git", "-C", str(repo), "check-ignore", "-q", str(sentinel)],  # noqa: S607
            capture_output=True,
            text=True,
            timeout=30,
            check=False,
        )
        assert done.returncode == 0, (
            f"{sentinel} is NOT git-ignored — a stray `touch` + `git add -A` would "
            f"commit the fleet-wide kill-switch and ship a dead kit."
        )

    def test_no_sentinel_exists_in_the_source_tree_right_now(self) -> None:
        # The wheel is built from `packages = ["src/mgf"]` with no exclude rules, so
        # anything present here ships. This is the assertion that never skips.
        repo = Path(__file__).resolve().parents[3]
        strays = sorted(str(p) for p in (repo / "src").rglob("DISABLED"))
        assert not strays, f"kill-switch sentinel(s) present in the source tree: {strays}"

    @pytest.mark.integration
    def test_a_built_wheel_contains_no_sentinel(self, tmp_path: Path) -> None:
        """Build the real wheel and look inside it.

        Skipped only when the build backend cannot be provisioned at all (no network,
        cold cache) — never when the build succeeds and the check could have failed.
        The always-on assertions above keep the invariant covered either way.
        """
        repo = Path(__file__).resolve().parents[3]
        out = tmp_path / "dist"
        built = None
        for extra in (["--offline"], []):
            done = subprocess.run(  # noqa: S603
                ["uv", "build", "--wheel", "--out-dir", str(out), *extra],  # noqa: S607
                cwd=str(repo),
                capture_output=True,
                text=True,
                timeout=300,
                check=False,
            )
            if done.returncode == 0:
                built = done
                break
        if built is None:
            pytest.skip("uv build unavailable (no backend, offline and cold cache)")
        wheels = sorted(out.glob("*.whl"))
        assert wheels, "uv build reported success but produced no wheel"
        with zipfile.ZipFile(wheels[0]) as archive:
            names = archive.namelist()
        assert any(n.endswith("sessionhooks/_state.py") for n in names), (
            "the wheel does not contain the session-hook kit at all — this test is "
            "looking at the wrong artifact"
        )
        shipped = [n for n in names if n.rsplit("/", 1)[-1] == "DISABLED"]
        assert not shipped, f"the wheel ships a kill-switch sentinel: {shipped}"
