"""Per-failure detail-bundle shape (TS-27).

A :class:`FailureBundle` carries every diagnostic a triager could
want — traceback, captured I/O, process state, dmesg, env
fingerprint, history — in one file at
``.gates/last-run/failures/<safe-test-id>.txt``.

The supervisor writes the bundle; downstream tooling (a future
Magogi dashboard, Slack relay) reads it. The schema lives here so
both sides share one definition.

The serialised format is plain text with section markers:

    === TEST ===
    tests/unit/foo.py::TestBar::test_baz

    === OUTCOME ===
    failed (AssertionError)
    chunk wall-time: 0.18s
    chunk exit code: 1
    chunk signal: -

    === TRACEBACK ===
    ...

(see :data:`SECTION_ORDER` for the full sequence)

The plain-text format is deliberately not JSON. A triager opens
the file in a terminal and reads it top-to-bottom; the section
markers carry enough structure to be machine-parseable when a
downstream tool wants it.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Final

# Section markers — frozen so consumers can grep / re-parse.
SECTION_ORDER: Final[tuple[str, ...]] = (
    "TEST",
    "OUTCOME",
    "TRACEBACK",
    "STDOUT",
    "STDERR",
    "PROCESS STATE",
    "DMESG TAIL",
    "ENV",
    "HISTORY",
    "NEXT STEPS",
)
"""Canonical section order in the failure-bundle text. Consumers
that grep for ``=== <section> ===`` markers MUST follow this
order — supervisors writing bundles MUST emit in this order
(empty sections are allowed but their header MUST still appear,
so consumers can rely on the structure)."""

SECTION_HEADER_FMT: Final[str] = "=== {name} ==="
"""Format for a section header line. Centralised so a future
schema change (e.g., bumping to ``### {name} ###``) is one edit."""


@dataclass(frozen=True)
class FailureBundle:
    """One per-failure detail bundle.

    Field-to-section mapping:

    - ``test_id`` → ``=== TEST ===``
    - ``outcome_line`` → ``=== OUTCOME ===``
    - ``traceback`` → ``=== TRACEBACK ===``
    - ``stdout_tail`` → ``=== STDOUT ===`` (last 4 KB by
      convention; the tail note lives in the body, never the
      header — headers are exactly the :data:`SECTION_ORDER`
      names so consumers can grep them)
    - ``stderr_tail`` → ``=== STDERR ===`` (same convention)
    - ``process_state`` → ``=== PROCESS STATE ===`` (project
      daemon PID / wchan / thread count; empty for projects with
      no daemon)
    - ``dmesg_tail`` → ``=== DMESG TAIL ===``
    - ``env`` → ``=== ENV ===``
    - ``history`` → ``=== HISTORY ===`` (last 5 outcomes)
    - ``next_steps`` → ``=== NEXT STEPS ===``
    """

    test_id: str
    outcome_line: str
    """A one-or-more-line block describing the outcome: status,
    error class, chunk wall-time, exit code, signal name."""

    traceback: str = ""
    stdout_tail: str = ""
    stderr_tail: str = ""
    process_state: str = ""
    dmesg_tail: str = ""
    env: str = ""
    history: str = ""
    next_steps: str = ""
    sections: dict[str, str] = field(default_factory=dict)
    """Optional named sections beyond the canonical set, appended
    after :data:`SECTION_ORDER` in insertion order. Use sparingly
    — every project-specific section reduces the cross-federation
    triage muscle memory."""

    def serialize(self) -> str:
        """Render to the canonical plain-text format. The output
        is deterministic given the same inputs (frozen + ordered
        section sequence) so the bundle file is reviewable in
        diff form across runs."""
        section_contents: dict[str, str] = {
            "TEST": self.test_id,
            "OUTCOME": self.outcome_line,
            "TRACEBACK": self.traceback,
            "STDOUT": self.stdout_tail,
            "STDERR": self.stderr_tail,
            "PROCESS STATE": self.process_state,
            "DMESG TAIL": self.dmesg_tail,
            "ENV": self.env,
            "HISTORY": self.history,
            "NEXT STEPS": self.next_steps,
        }

        parts: list[str] = []
        for section in SECTION_ORDER:
            parts.append(SECTION_HEADER_FMT.format(name=section))
            body = section_contents[section]
            parts.append(body if body else "(empty)")
            parts.append("")  # blank line between sections

        for name, body in self.sections.items():
            parts.append(SECTION_HEADER_FMT.format(name=name))
            parts.append(body if body else "(empty)")
            parts.append("")

        return "\n".join(parts)


__all__ = [
    "SECTION_HEADER_FMT",
    "SECTION_ORDER",
    "FailureBundle",
]
