"""Marker → tier mapping for the federation test supervisor (TS-22).

The four tiers reflect per-test runtime budget; markers map to
tiers via this module's :data:`MARKER_TIER`. A test inherits its
tier from its **strongest** (highest) marker; explicit
``@pytest.mark.tier(N)`` overrides. Unmarked tests are implicitly
tier-1 (unit).

The mapping is the federation default. Consumers MAY override via
their own ``[tool.pytest.ini_options]`` marker registration plus
a project-local ``MARKER_TIER`` derived from this one.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Final

MARKER_TIER: Final[dict[str, int]] = {
    "smoke": 0,
    "contract": 1,
    "property": 1,
    "regression": 1,
    "fuzz": 1,
    "conformance": 2,
    "use_case": 2,
    "concurrency": 2,
    "integration": 2,
    "perf": 3,
    "stress": 3,
    "e2e": 3,
}
"""Canonical marker → tier mapping. See ``TESTING.md`` §13.11 for
the per-marker definitions. Consumers MAY override per-project but
SHOULD only ADD entries, not change tier values (preserves
"run my tier-0" cross-project meaning).

PAPER-62 (v0.41.6+): ``integration`` previously fell through to
:data:`DEFAULT_TIER` (tier-1) because it had no entry, which
silently mis-budgeted it — an ``integration`` test that genuinely
launches subprocesses doesn't fit tier-1's <0.3s per-test budget.
It now has an explicit tier: ``integration`` → tier-2
(cross-module / may launch subprocesses; fits the 2s per-test
budget).

Migration note (M2 Wave-2, 2026-07): the deprecated ``slow``
marker completed its removal cycle and is GONE from the mapping
(it was tier-3 from PAPER-62 until then). Tests that still carry
``@pytest.mark.slow`` should instead carry the specific tier-3
marker that says WHY they are slow (``perf`` / ``stress`` /
``e2e``) — or no long-signal marker at all, and let the
supervisor's observed-duration auto-tier promote them. Consumers
must also drop the ``slow`` row from their registered markers
(``[tool.pytest.ini_options].markers``) — under
``--strict-markers`` a leftover ``@pytest.mark.slow`` fails
collection, which is the loud migration signal we want."""

DEFAULT_TIER: Final[int] = 1
"""Tier for an unmarked test. Unit tests are the implicit default
(no marker required); tier-1 matches their <0.3s per-test budget."""


@dataclass(frozen=True)
class TierBudget:
    """Per-tier wall-clock budget."""

    name: str
    """Human label, e.g. ``"tier-0"``."""

    per_test_seconds: float
    """Soft per-test budget. Auto-tier heuristic bumps a test up
    one tier if its rolling-median runtime is ≥5× this value."""

    suite_seconds: float
    """Total suite budget for tests in this tier. The supervisor
    uses this as the default ``--budget T`` when none is given."""

    description: str
    """One-line purpose. Surfaced in ``mgf-fed test --help``."""


TIERS: Final[tuple[TierBudget, ...]] = (
    TierBudget("tier-0", 0.10, 1.0, "Build-broken signal"),
    TierBudget("tier-1", 0.30, 30.0, "Daily contract"),
    TierBudget("tier-2", 2.00, 120.0, "Promise pins"),
    TierBudget("tier-3", 10.00, 1800.0, "Long signal (stress / perf / e2e)"),
)
"""The four-tier budget set. Index `n` is `tier-n`. The
supervisor MAY parameterise per project but SHOULD keep the
shapes consistent so cross-project comparisons stay meaningful."""


def effective_tier(
    markers: list[str] | tuple[str, ...],
    override: int | None = None,
    mapping: dict[str, int] | None = None,
) -> int:
    """Compute a test's effective tier from its markers.

    Resolution:

    1. If ``override`` is not None, return it (an explicit
       ``@pytest.mark.tier(N)`` on the test).
    2. Otherwise, the test's tier is the **maximum** of its
       markers' tiers (a test with ``smoke`` AND ``e2e`` is
       tier-3 — the stronger marker wins so it doesn't sneak
       into tier-0 selection).
    3. If no markers are mapped, return :data:`DEFAULT_TIER`
       (unit-shape default).

    ``mapping`` lets callers supply a project-local override
    of :data:`MARKER_TIER` (the federation default). Unknown
    markers are silently ignored — projects can ship custom
    markers without breaking tier selection.
    """
    if override is not None:
        if not 0 <= override <= 3:
            msg = f"tier override must be 0..3, got {override}"
            raise ValueError(msg)
        return override

    table = mapping or MARKER_TIER
    tiers = [table[m] for m in markers if m in table]
    if not tiers:
        return DEFAULT_TIER
    return max(tiers)


__all__ = [
    "DEFAULT_TIER",
    "MARKER_TIER",
    "TIERS",
    "TierBudget",
    "effective_tier",
]
