"""Tests for `mgf.common.testing._failure_bundle` — TS-27 detail bundle."""

from __future__ import annotations

from dataclasses import FrozenInstanceError

from mgf.common.testing import (
    SECTION_HEADER_FMT,
    SECTION_ORDER,
    FailureBundle,
)


class TestSectionOrderContract:
    def test_section_order_is_a_tuple(self) -> None:
        # Frozen — consumer code that pins on the order must not see
        # mutation across runs.
        assert isinstance(SECTION_ORDER, tuple)

    def test_section_order_contains_canonical_set(self) -> None:
        # The federation contract: every bundle has these sections in
        # this order. Reorder = breaking change (AP-04 cycle).
        assert SECTION_ORDER == (
            "TEST",
            "OUTCOME",
            "TRACEBACK",
            "STDOUT",
            "STDERR",
            "PROCESS STATE",
            "DMESG TAIL",
            "ENV",
            "HISTORY",
            "NEXT STEPS",
        )

    def test_header_format(self) -> None:
        # The plain-text marker `=== <NAME> ===` is the grep target
        # for downstream tooling.
        assert SECTION_HEADER_FMT.format(name="TEST") == "=== TEST ==="


class TestFailureBundleSerialize:
    def test_minimal_bundle_emits_all_sections(self) -> None:
        bundle = FailureBundle(
            test_id="tests/unit/foo.py::test_bar",
            outcome_line="failed (AssertionError)\nchunk wall-time: 0.18s",
        )
        text = bundle.serialize()
        # All ten canonical section headers MUST appear, in order.
        idx = -1
        for section in SECTION_ORDER:
            header = f"=== {section} ==="
            new_idx = text.index(header)
            assert new_idx > idx, (
                f"section {section!r} appeared out of order — federation "
                f"contract requires the canonical sequence"
            )
            idx = new_idx

    def test_empty_section_renders_as_placeholder(self) -> None:
        # An empty body for a section MUST still render — consumers
        # rely on every header being present so a missing one signals
        # a malformed bundle (not "this section was skipped").
        bundle = FailureBundle(
            test_id="tests/unit/foo.py::test_bar",
            outcome_line="failed",
        )
        text = bundle.serialize()
        assert "=== STDOUT ===" in text
        # The placeholder string is "(empty)" — opaque enough to not
        # be confused with real captured output.
        assert "(empty)" in text

    def test_full_bundle_serialises_all_content(self) -> None:
        bundle = FailureBundle(
            test_id="tests/unit/foo.py::TestBar::test_baz",
            outcome_line="failed (AssertionError)\nchunk wall-time: 0.18s\nchunk exit code: 1",
            traceback="Traceback (most recent call last):\n  File...",
            stdout_tail="last 4 KB of stdout",
            stderr_tail="last 4 KB of stderr",
            process_state="broker pid 12345, wchan futex_wait, threads 3",
            dmesg_tail="(last 30 lines)",
            env="python 3.13.12\nmgf-common 0.40.0",
            history="5 prior outcomes: ...",
            next_steps="See docs/testing/DIAGNOSING_HANGS.md",
        )
        text = bundle.serialize()
        # Each content string lands somewhere in the output, in order.
        for content in [
            "tests/unit/foo.py::TestBar::test_baz",
            "failed (AssertionError)",
            "Traceback (most recent call last)",
            "last 4 KB of stdout",
            "last 4 KB of stderr",
            "broker pid 12345",
            "(last 30 lines)",
            "python 3.13.12",
            "5 prior outcomes",
            "DIAGNOSING_HANGS.md",
        ]:
            assert content in text

    def test_custom_sections_appended_after_canonical_set(self) -> None:
        bundle = FailureBundle(
            test_id="tests/unit/foo.py::test_bar",
            outcome_line="failed",
            sections={"PROJECT-SPECIFIC": "extra context"},
        )
        text = bundle.serialize()
        # Custom section appears AFTER the last canonical one (NEXT STEPS).
        assert text.index("=== NEXT STEPS ===") < text.index("=== PROJECT-SPECIFIC ===")
        assert "extra context" in text

    def test_serialise_is_deterministic(self) -> None:
        # Same inputs → byte-identical output (frozen dataclass +
        # ordered section sequence). Means bundles can be diffed
        # across runs to see what changed.
        a = FailureBundle(
            test_id="tests/unit/foo.py::test_bar",
            outcome_line="failed",
        )
        b = FailureBundle(
            test_id="tests/unit/foo.py::test_bar",
            outcome_line="failed",
        )
        assert a.serialize() == b.serialize()

    def test_frozen(self) -> None:
        bundle = FailureBundle(
            test_id="tests/unit/foo.py::test_bar",
            outcome_line="failed",
        )
        import pytest as _pytest

        with _pytest.raises(FrozenInstanceError):
            bundle.test_id = "other"  # type: ignore[misc]


class TestSerialisedFormatIsGrepFriendly:
    def test_section_markers_appear_on_their_own_line(self) -> None:
        bundle = FailureBundle(
            test_id="tests/unit/foo.py::test_bar",
            outcome_line="failed",
        )
        text = bundle.serialize()
        # Every section header SHOULD be on its own line — that's
        # the contract that lets `grep '^=== '` enumerate sections.
        for section in SECTION_ORDER:
            header = f"=== {section} ==="
            assert f"\n{header}\n" in text or text.startswith(header + "\n")
