"""The event log's closed vocabulary — the refusal path is as load-bearing as the append.

Seeded-bad probes throughout: a rule that is only ever exercised by valid input is a
rule nobody has proved. Adapted with attribution from the Sooth kit's
``shp/eventlog.py`` self-test. Copy, not a dependency.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from mgf.fed.sessionhooks import _events

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"))
    monkeypatch.delenv("CLAUDE_SESSION_ID", raising=False)


class TestVocabularyIsClosed:
    def test_unknown_type_is_refused(self, tmp_path: Path) -> None:
        ok, why = _events.emit(tmp_path, "vibes_check")
        assert ok is False
        assert "vocabulary is closed" in why
        assert not _events.events_path(tmp_path).exists()

    def test_missing_required_field_is_refused(self, tmp_path: Path) -> None:
        ok, why = _events.emit(tmp_path, "seat_declared", {"seat": "designer"})
        assert ok is False
        assert "requires fields ['model']" in why

    def test_seat_outside_the_three_is_refused(self, tmp_path: Path) -> None:
        # SEAT-01 defines three seats. A seat the corpus does not define cannot be
        # reasoned about later, and accepting it silently drifts the vocabulary.
        ok, why = _events.emit(tmp_path, "seat_declared", {"seat": "vibes", "model": "x"})
        assert ok is False
        assert "SEAT-01" in why

    def test_escalation_trigger_outside_the_enum_is_refused(self, tmp_path: Path) -> None:
        ok, why = _events.emit(tmp_path, "escalation", {"trigger": "felt_like_it"})
        assert ok is False
        assert "must be one of" in why

    def test_reserved_fields_cannot_be_shadowed(self, tmp_path: Path) -> None:
        # A caller that can set `session` or `event` can forge the log's own frame.
        ok, why = _events.emit(tmp_path, "journal_step", {"step": "x", "session": "someone-else"})
        assert ok is False
        assert "reserved field" in why

    def test_every_declared_type_has_a_required_set(self) -> None:
        assert set(_events.REQUIRED_FIELDS) == set(_events.EVENT_TYPES)

    def test_the_vocabulary_is_exactly_what_the_docs_claim(self) -> None:
        # Growth is a smell: adding a type is a deliberate act, and this pin makes it
        # one. `session_start` is mgf's addition (Sooth's `preflight` is deliberately
        # NOT ported — an event type nobody emits is dead vocabulary).
        assert _events.EVENT_TYPES == frozenset(
            {
                "session_start",
                "seat_declared",
                "seat_override",
                "escalation",
                "journal_step",
                "context_ambiguity_filed",
            }
        )


class TestRoundTrip:
    def test_valid_event_lands_and_reads_back(self, tmp_path: Path) -> None:
        ok, why = _events.emit(
            tmp_path,
            "seat_declared",
            {"seat": "executor", "model": "opus-5", "task_class": "implementation"},
            payload={"session_id": "s1"},
        )
        assert (ok, why) == (True, "")
        records = _events.read(tmp_path)
        assert len(records) == 1
        assert records[0]["event"] == "seat_declared"
        assert records[0]["session"] == "s1"
        assert records[0]["seat"] == "executor"
        assert records[0]["protocol"].startswith("mgf-sessionhooks/")
        assert records[0]["utc"].endswith("Z")

    def test_filters(self, tmp_path: Path) -> None:
        _events.emit(tmp_path, "session_start", payload={"session_id": "a"})
        _events.emit(tmp_path, "journal_step", {"step": "one"}, payload={"session_id": "a"})
        _events.emit(tmp_path, "session_start", payload={"session_id": "b"})
        assert len(_events.read(tmp_path, session="a")) == 2
        assert len(_events.read(tmp_path, etype="session_start")) == 2
        assert len(_events.read(tmp_path, session="a", etype="journal_step")) == 1
        assert len(_events.read(tmp_path, tail=1)) == 1
        assert _events.read(tmp_path, session="nobody") == []

    def test_append_failure_is_reported_but_accepted(self, tmp_path: Path) -> None:
        # A hook whose behaviour changes when the disk fills is a hook nobody can
        # reason about. Report, then carry on.
        blocker = tmp_path / ".claude"
        blocker.write_text("not a directory\n", encoding="utf-8")
        ok, why = _events.emit(tmp_path, "session_start")
        assert ok is True
        assert "not persisted" in why


class TestDeclarationLookup:
    def test_absence_is_the_finding(self, tmp_path: Path) -> None:
        _events.emit(tmp_path, "session_start", payload={"session_id": "s1"})
        assert _events.declaration_for(tmp_path, "s1") is None

    def test_declaration_is_found(self, tmp_path: Path) -> None:
        _events.emit(
            tmp_path,
            "seat_declared",
            {"seat": "designer", "model": "fable-5"},
            payload={"session_id": "s1"},
        )
        declared = _events.declaration_for(tmp_path, "s1")
        assert declared is not None
        assert declared["seat"] == "designer"

    def test_another_session_declaration_does_not_count(self, tmp_path: Path) -> None:
        _events.emit(
            tmp_path,
            "seat_declared",
            {"seat": "designer", "model": "fable-5"},
            payload={"session_id": "other"},
        )
        assert _events.declaration_for(tmp_path, "s1") is None

    def test_last_declaration_wins(self, tmp_path: Path) -> None:
        for model in ("first", "second"):
            _events.emit(
                tmp_path,
                "seat_declared",
                {"seat": "executor", "model": model},
                payload={"session_id": "s1"},
            )
        declared = _events.declaration_for(tmp_path, "s1")
        assert declared is not None
        assert declared["model"] == "second"


class TestSummarise:
    def test_undeclared_sessions_are_named(self, tmp_path: Path) -> None:
        _events.emit(tmp_path, "session_start", payload={"session_id": "quiet"})
        _events.emit(tmp_path, "session_start", payload={"session_id": "loud"})
        _events.emit(
            tmp_path,
            "seat_declared",
            {"seat": "executor", "model": "opus-5"},
            payload={"session_id": "loud"},
        )
        summary = _events.summarise(_events.read(tmp_path))
        assert summary["records"] == 3
        assert summary["sessions"] == 2
        assert summary["undeclared_sessions"] == ["quiet"]
        assert summary["per_session"]["loud"]["seat"] == "executor"

    def test_empty_is_empty(self, tmp_path: Path) -> None:
        summary = _events.summarise([])
        assert summary["records"] == 0
        assert summary["undeclared_sessions"] == []


def test_validate_is_pure(tmp_path: Path) -> None:
    # The validator is separately callable so a caller can pre-check without a write.
    assert _events.validate("session_start", {}) is None
    assert _events.validate("nope", {}) is not None
    assert not _events.events_path(tmp_path).exists()
