"""Shared fixtures for the MUM hardware test suite.

WHY THIS FILE EXISTS
--------------------
Every test under ``tests/hardware/mum/**`` needs the same three things:

  1. The session to be a MUM session (``config.interface.type == "mum"``).
  2. A live ``FrameIO`` bound to the session ``lin`` + ``ldf``.
  3. The ECU's live NAD, discovered by reading ``ALM_Status``.

Before this conftest existed, each test module repeated those fixtures
verbatim — 9 copies of ``fio``, 8 of ``alm``, 8 of ``_reset_to_off``,
and 8 inline ``if config.interface.type != "mum": pytest.skip(...)``
gates. They are all consolidated here.

SCOPE STRATEGY
--------------
``FrameIO``, ``AlmTester``, and the discovered ``nad`` are immutable
relative to a session connection. Keeping them at ``scope="session"``
means one NAD discovery per run instead of one per module, and a single
shared cache of LDF frame lookups across the whole suite. The only
function-scoped fixture is ``_reset_to_off`` — it MUST be per-test so
each test starts with the LED in a known state.

ACCESS CONTROL
--------------
This conftest is at ``tests/hardware/mum/`` deliberately: tests under
``tests/hardware/psu/`` and ``tests/hardware/babylin/`` cannot see
``fio``/``alm``/``nad`` because pytest only walks **upward** through
``conftest.py`` files. A PSU-only test that accidentally requests
``fio`` will fail at collection with "fixture not found" — that is
the access-control mechanism.

OVERRIDE NOTES
--------------
Two files override fixtures here for documented reasons:

- ``test_mum_alm_animation_generated.py`` keeps a local ``_reset_to_off``
  + ``_force_off`` so its "no AlmTester anywhere" demonstration stays
  true. The local ``_reset_to_off`` shadows this conftest's.

- ``test_overvolt.py`` defines its own ``_reset_to_off`` that ALSO
  parks the PSU at the nominal voltage. Its override is necessary —
  without it, both autouse fixtures would run and the LED would be
  toggled twice per test (harmless but wasteful).
"""
from __future__ import annotations

import pytest

from ecu_framework.config import EcuTestConfig
from ecu_framework.lin.base import LinInterface

from frame_io import FrameIO
from alm_helpers import AlmTester


# ---------------------------------------------------------------------------
# Session-wide gate
# ---------------------------------------------------------------------------

@pytest.fixture(scope="session", autouse=True)
def _require_mum(config: EcuTestConfig) -> None:
    """Single skip point for the whole MUM suite.

    Replaces the inline ``if config.interface.type != "mum": pytest.skip(...)``
    that used to live inside every ``fio`` fixture. ``autouse=True`` means
    every test under ``tests/hardware/mum/**`` honors this without having
    to opt in.
    """
    if config.interface.type != "mum":
        pytest.skip("interface.type must be 'mum' for tests under tests/hardware/mum/")


# ---------------------------------------------------------------------------
# Shared MUM-suite fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(scope="session")
def fio(lin: LinInterface, ldf) -> FrameIO:
    """LDF-driven I/O over the session LIN connection.

    Session-scoped because ``FrameIO`` only holds ``(lin, ldf)`` and caches
    frame lookups — sharing it across the whole suite is a feature, not a
    risk. Tests that need a fresh cache can build their own ``FrameIO(lin, ldf)``
    inside the test body.
    """
    return FrameIO(lin, ldf)


@pytest.fixture(scope="session")
def nad(fio: FrameIO) -> int:
    """Live NAD reported by the ECU's ALM_Status frame.

    Used as ``LIDFrom`` / ``LIDTo`` in unicast sends and as the slave
    address bound into ``AlmTester``. Discovered once per session because
    the address doesn't change while the ECU is powered.

    Skips cleanly when:
      - The ECU isn't responding (no ``ALM_Status`` within 1 s) — likely
        a wiring or power problem.
      - The reported NAD is outside the valid 0x01-0xFE range — usually
        means auto-addressing hasn't been performed yet.
    """
    decoded = fio.receive("ALM_Status", timeout=1.0)
    if decoded is None:
        pytest.skip("ECU not responding on ALM_Status — check wiring/power")
    n = int(decoded["ALMNadNo"])
    if not (0x01 <= n <= 0xFE):
        pytest.skip(f"ECU reports invalid NAD {n:#x} — auto-addressing first")
    return n


@pytest.fixture(scope="session")
def alm(fio: FrameIO, nad: int) -> AlmTester:
    """ALM_Node domain helper bound to the live NAD.

    Session-scoped because ``AlmTester`` is stateless beyond ``(fio, nad)``;
    per-test state hygiene is handled by ``_reset_to_off`` below, not by
    rebuilding the helper.
    """
    return AlmTester(fio, nad)


# ---------------------------------------------------------------------------
# Per-test state reset
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def _reset_to_off(alm: AlmTester):
    """Drive the LED to OFF before AND after every test.

    Function-scoped + autouse so that state cannot leak between tests.
    The post-test ``force_off()`` runs even when the test body fails —
    that is the contract: regardless of how the test exits, the next
    one starts on a known baseline.

    Override this fixture locally in a test module to change the reset
    semantics (see the OVERRIDE NOTES in the module docstring).
    """
    alm.force_off()
    yield
    alm.force_off()
