"""Contract tests pinning `mgf.test_supervisor` ↔ `mgf.common.testing` alignment.

Per TS-23 the supervisor (`mgf.test_supervisor`, published as the
sibling library `mgf-test-supervisor`) MUST be stdlib-only — so it
cannot import from `mgf.common.testing` at runtime (mgf-common is
the project under test; the supervisor must run even when
mgf-common is broken). The two surfaces therefore carry parallel
copies of the schema:

  - `mgf.common.testing.TestRecord` / `SCHEMA_VERSION` /
    `MARKER_TIER` — the importable library shape consumers use
    to read `run.jsonl`.
  - `mgf.test_supervisor.records.TestRecord` / `SCHEMA_VERSION`
    and `mgf.test_supervisor.tiers.MARKER_TIER` — the
    supervisor's local stdlib-only copies.

These tests assert the two stay aligned. If a field is renamed in
one place without the other, this file's tests fail and the
mismatch surfaces immediately — exactly where it can be fixed in
one edit. The federation contract is "the wire formats match"; the
implementation is "the field sets match."

(The alternative — having the supervisor import from
`mgf.common.testing` — would violate TS-23: a broken mgf-common
runtime would break the test runner that's trying to diagnose
why mgf-common is broken. The duplication is the lesser cost; the
contract test is the alignment guarantee.)

Defense in depth: the SAME alignment contract also runs from the
mgf-test-supervisor side at
`mgf-test-supervisor/tests/unit/test_alignment_with_mgf_common.py`.
Field renames need to land in both copies in the same commit; the
two-sided enforcement catches the case where one repo's CI hasn't
run yet.

Pre-2026-05-26 versions of this file imported from `scripts._mtest`
(the in-tree copy). The supervisor was extracted to the published
sibling `mgf-test-supervisor` v0.1.0 on 2026-05-26; this file was
updated to import from `mgf.test_supervisor` in the same migration.

Release-dance guard (M2 Wave-2): when the installed supervisor
PRE-DATES the Wave-2 aligned surface (its MARKER_TIER still maps the
removed `slow` marker, or its TierBudget still carries the old
NamedTuple field names), the whole module skips loudly instead of
failing — CI stays green through the lockstep window and the tests
arm automatically once the resolver installs supervisor >= 0.1.18.
The mirror guard exists on the supervisor's side for a pre-Wave-2
mgf-common.
"""

from __future__ import annotations

import dataclasses

import pytest
from mgf.test_supervisor import records as supervisor_records
from mgf.test_supervisor import tiers as supervisor_tiers

from mgf.common import testing as public

if "slow" in supervisor_tiers.MARKER_TIER or not hasattr(
    supervisor_tiers.TIERS[0], "per_test_seconds"
):  # pragma: no cover - release-dance window only
    pytest.skip(
        "installed mgf-test-supervisor pre-dates the M2 Wave-2 aligned "
        "surface (slow-less MARKER_TIER + unified TierBudget field "
        "names); the alignment suite arms once mgf-test-supervisor "
        ">= 0.1.18 is installed",
        allow_module_level=True,
    )


class TestSchemaVersionAligned:
    def test_schema_versions_match(self) -> None:
        # If you bumped one, bump the other in the same commit.
        assert public.SCHEMA_VERSION == supervisor_records.SCHEMA_VERSION, (
            f"Schema-version drift: public {public.SCHEMA_VERSION!r} "
            f"vs supervisor {supervisor_records.SCHEMA_VERSION!r}. "
            f"Update both in the same commit per AP-04 cycle."
        )


class TestOutcomesAligned:
    def test_supervisor_outcomes_match_public_enum(self) -> None:
        public_outcomes = {o.value for o in public.Outcome}
        supervisor_outcomes = set(supervisor_records.OUTCOMES)
        assert public_outcomes == supervisor_outcomes, (
            f"Outcome-set drift: public {sorted(public_outcomes)} vs "
            f"supervisor {sorted(supervisor_outcomes)}. Add/remove in both."
        )


class TestSeveritiesAligned:
    def test_supervisor_severities_match_public_enum(self) -> None:
        public_sevs = {s.value for s in public.Severity}
        supervisor_sevs = set(supervisor_records.SEVERITIES)
        assert public_sevs == supervisor_sevs, (
            f"Severity-set drift: public {sorted(public_sevs)} vs "
            f"supervisor {sorted(supervisor_sevs)}. Add/remove in both."
        )


class TestMarkerTierAligned:
    def test_marker_tier_mappings_match(self) -> None:
        assert public.MARKER_TIER == supervisor_tiers.MARKER_TIER, (
            f"MARKER_TIER drift between public + supervisor copies:\n"
            f"  public:     {dict(public.MARKER_TIER)}\n"
            f"  supervisor: {dict(supervisor_tiers.MARKER_TIER)}\n"
            f"Update both in the same commit so consumers reading "
            f"run.jsonl resolve tiers consistently with how the "
            f"supervisor selected the tests."
        )

    def test_slow_removed_on_both_sides(self) -> None:
        # M2 Wave-2: the deprecated `slow` marker completed removal.
        assert "slow" not in public.MARKER_TIER
        assert "slow" not in supervisor_tiers.MARKER_TIER

    def test_default_tier_matches(self) -> None:
        assert public.DEFAULT_TIER == supervisor_tiers.DEFAULT_TIER


class TestTierBudgetsAligned:
    """M2 Wave-2 unified TierBudget to ONE dataclass shape (the
    supervisor adopted this package's field names) — so field NAMES
    are pinned now, not just values. The NamedTuple double-shape
    (`typical_test_s` / `total_budget_s` / `intent`) is gone."""

    def test_tier_count(self) -> None:
        assert len(public.TIERS) == len(supervisor_tiers.TIERS) == 4

    def test_field_names_match(self) -> None:
        public_fields = [f.name for f in dataclasses.fields(public.TierBudget)]
        supervisor_fields = [f.name for f in dataclasses.fields(supervisor_tiers.TierBudget)]
        assert public_fields == supervisor_fields, (
            f"TierBudget field drift: public {public_fields} vs supervisor {supervisor_fields}"
        )

    def test_tier_rows_match_field_by_field(self) -> None:
        for pub, sup in zip(public.TIERS, supervisor_tiers.TIERS, strict=True):
            assert pub.name == sup.name
            assert pub.per_test_seconds == sup.per_test_seconds
            assert pub.suite_seconds == sup.suite_seconds
            assert pub.description == sup.description


class TestSeverityMappingAligned:
    def test_default_severity_mapping_matches(self) -> None:
        # The MAPPING, not just the value sets (M2 Wave-2: the
        # supervisor said quarantined→SKIP while DEFAULT_SEVERITY
        # said WARN — this pin makes that drift class impossible).
        for outcome in public.Outcome:
            pub_sev = public.DEFAULT_SEVERITY[outcome].value
            sup_sev = supervisor_records.severity_for_outcome(outcome.value)
            assert pub_sev == sup_sev, (
                f"severity({outcome.value!r}) drift: public {pub_sev!r} vs supervisor {sup_sev!r}"
            )

    def test_quarantined_is_warn(self) -> None:
        assert public.DEFAULT_SEVERITY[public.Outcome.QUARANTINED] is public.Severity.WARN
        assert supervisor_records.severity_for_outcome("quarantined") == "WARN"


class TestFailureBundleSectionsAligned:
    def test_section_order_byte_pinned(self) -> None:
        # The supervisor VENDORS SECTION_ORDER (TS-23 forbids the
        # runtime import). This byte-pin guarantees bundles written
        # by the supervisor are greppable via this package's
        # SECTION_ORDER — the schema is no longer decorative.
        assert tuple(public.SECTION_ORDER) == tuple(supervisor_records.SECTION_ORDER)

    def test_section_header_fmt_byte_pinned(self) -> None:
        assert public.SECTION_HEADER_FMT == supervisor_records.SECTION_HEADER_FMT


class TestTierFunctionsAligned:
    """`tier_for_markers` (supervisor) gained override/mapping in M2
    Wave-2 and must behave exactly like `effective_tier` (public)."""

    @pytest.mark.parametrize(
        "markers",
        [
            (),
            ("smoke",),
            ("integration",),
            ("contract", "perf"),
            ("unknown_project_marker",),
        ],
    )
    def test_same_result(self, markers: tuple[str, ...]) -> None:
        assert public.effective_tier(list(markers)) == supervisor_tiers.tier_for_markers(markers)

    @pytest.mark.parametrize("override", [0, 3])
    def test_same_override_semantics(self, override: int) -> None:
        assert public.effective_tier(
            ["e2e"], override=override
        ) == supervisor_tiers.tier_for_markers(("e2e",), override=override)

    def test_same_invalid_override_error(self) -> None:
        with pytest.raises(ValueError, match=r"0\.\.3"):
            public.effective_tier(["smoke"], override=9)
        with pytest.raises(ValueError, match=r"0\.\.3"):
            supervisor_tiers.tier_for_markers(("smoke",), override=9)


class TestRecordFieldsAligned:
    """The DATACLASS shapes differ deliberately (public is frozen +
    no slots for legibility; supervisor is frozen + slots for
    write-path performance). The FIELD SETS MUST match so JSONL
    written by the supervisor parses cleanly via the public reader.
    """

    def test_field_names_match(self) -> None:
        import dataclasses

        public_fields = {f.name for f in dataclasses.fields(public.TestRecord)}
        supervisor_fields = {f.name for f in dataclasses.fields(supervisor_records.TestRecord)}
        assert public_fields == supervisor_fields, (
            f"TestRecord field-set drift:\n"
            f"  public-only:     {public_fields - supervisor_fields}\n"
            f"  supervisor-only: {supervisor_fields - public_fields}"
        )

    def test_jsonl_round_trip_through_public_reader(self) -> None:
        # Build a supervisor-shape record, serialise, parse via the
        # public reader — this proves the wire formats interop.
        sup_rec = supervisor_records.TestRecord(
            test_id="tests/unit/foo.py::test_bar",
            outcome="passed",
            severity="OK",
            duration_ms=12,
            tier=1,
            markers=("contract",),
            run_id="run-id",
            ts_iso="2026-05-26T01:11:37.000Z",
        )
        line = sup_rec.to_json()
        public_rec = public.TestRecord.from_jsonl_line(line)
        assert public_rec.test_id == sup_rec.test_id
        assert public_rec.outcome is public.Outcome.PASSED
        assert public_rec.tier == sup_rec.tier
        assert public_rec.markers == list(sup_rec.markers)

    def test_perf_field_round_trips(self) -> None:
        # TS-28: a supervisor record carrying perf attribution must
        # survive serialise → public-reader parse with the perf payload
        # intact (and absence → None).
        perf_payload = {
            "schema_version": "1.0.0",
            "test_id": "tests/unit/foo.py::test_bar",
            "total_ms": 12.5,
            "functions": [{"func": "a.py:1(f)", "ncalls": 2, "tottime_ms": 3.0, "cumtime_ms": 4.0}],
            "spans": [{"label": "load", "ms": 5.0, "depth": 0}],
        }
        sup_rec = supervisor_records.TestRecord(
            test_id="tests/unit/foo.py::test_bar",
            outcome="passed",
            severity="OK",
            duration_ms=12,
            tier=1,
            run_id="run-id",
            ts_iso="2026-05-29T00:00:00.000Z",
            perf=perf_payload,
        )
        public_rec = public.TestRecord.from_jsonl_line(sup_rec.to_json())
        assert public_rec.perf == perf_payload
        # Absent perf → None on both sides.
        sup_none = supervisor_records.TestRecord(
            test_id="t::t",
            outcome="passed",
            severity="OK",
            duration_ms=0,
            tier=1,
            run_id="r",
            ts_iso="t",
        )
        assert public.TestRecord.from_jsonl_line(sup_none.to_json()).perf is None
