"""Session-scoped fixtures for the hardware test suite.

WHY THIS FILE EXISTS
--------------------
On this bench the Owon PSU **powers the ECU** — the MUM only carries
LIN traffic. So the PSU output must stay on for the **entire** test
session, not just for the duration of an individual PSU test. If
each test file opened/closed its own PSU connection (which by
default sends ``output 0`` on close) the bench would brown out
between modules and every subsequent MUM test would fail.

WHAT THIS FILE PROVIDES
-----------------------
- ``_psu_or_none``       : session-scoped, tolerant. Opens the PSU
                          once at session start, parks it at the
                          configured nominal voltage, enables output,
                          and leaves it that way for the whole
                          session. Yields the live ``OwonPSU`` or
                          ``None`` if the PSU isn't reachable.
- ``_psu_powers_bench``  : session-scoped, ``autouse=True``. Realizes
                          ``_psu_or_none`` so even tests that don't
                          request the PSU by name benefit from the
                          power-up. No-op when PSU isn't configured.
- ``psu``                : session-scoped, public. Tests that read
                          measurements or perturb voltage request
                          this fixture; it skips cleanly when the
                          PSU isn't available.

CONTRACT FOR TESTS
------------------
Tests SHOULD:
  - request ``psu`` if they need to read measurements or change voltage
  - restore the bench to nominal voltage in their ``finally`` block
    (the session fixture will not restore between tests)

Tests MUST NOT:
  - call ``psu.set_output(False)`` — this kills the ECU power for
    every test that follows in the same session
  - call ``psu.close()`` — the session fixture owns the lifecycle
"""
from __future__ import annotations

import time

import pytest
from serial import SerialException

from ecu_framework.config import EcuTestConfig
from ecu_framework.power import OwonPSU, SerialParams, resolve_port


# ── nominal supply settings used at session start ─────────────────────────
# Sourced from ``config.power_supply.set_voltage`` / ``set_current`` so the
# bench operator controls them via YAML rather than via Python edits.
# These constants are fallbacks if the YAML omits them.
_FALLBACK_NOMINAL_VOLTAGE = 13.0   # V
_FALLBACK_NOMINAL_CURRENT = 1.0    # A
_PSU_PARK_SETTLE_SECONDS = 0.5     # let the rails stabilize before tests run


@pytest.fixture(scope="session")
def _psu_or_none(config: EcuTestConfig):
    """Open the Owon PSU once per session, park at nominal, leave output ON.

    Returns the live :class:`OwonPSU` instance, or ``None`` if the PSU
    isn't enabled / configured / reachable. Always yields exactly once
    (no exceptions propagate out of this fixture for the unavailable
    cases) so tests that don't request it directly can proceed.

    The session-end teardown closes the port; with
    ``safe_off_on_close=True`` that also sends ``output 0`` — the
    session ends with the bench safely de-energized.
    """
    cfg = config.power_supply

    if not cfg.enabled or not cfg.port:
        # PSU not configured. Yield None so the autouse fixture and
        # the public ``psu`` fixture can both decide what to do.
        yield None
        return

    params = SerialParams.from_config(cfg)
    resolved = resolve_port(cfg.port, idn_substr=cfg.idn_substr, params=params)
    if resolved is None:
        # Configured but not reachable. Treat the same as not present —
        # tests that need it will skip via the public ``psu`` fixture.
        yield None
        return

    port, idn = resolved
    p = OwonPSU(
        port=port,
        params=params,
        eol=cfg.eol or "\n",
        safe_off_on_close=True,   # session-end safety net
    )
    try:
        p.open()
    except SerialException:
        # Race: another process grabbed the port between resolve and
        # open. Tests that need the PSU will skip cleanly.
        yield None
        return

    # Park at the configured nominal supply and enable output. Stays
    # this way for the whole session unless individual tests perturb
    # it (and restore in finally).
    nominal_v = float(cfg.set_voltage) if cfg.set_voltage else _FALLBACK_NOMINAL_VOLTAGE
    nominal_i = float(cfg.set_current) if cfg.set_current else _FALLBACK_NOMINAL_CURRENT
    p.set_voltage(1, nominal_v)
    p.set_current(1, nominal_i)
    p.set_output(True)
    time.sleep(_PSU_PARK_SETTLE_SECONDS)

    print(
        f"\n[psu] session power on: port={port!r} idn={idn!r} "
        f"V={nominal_v} I_lim={nominal_i}"
    )
    try:
        yield p
    finally:
        # Session-end: close() sends ``output 0`` first because of
        # safe_off_on_close=True, then releases the port.
        p.close()


@pytest.fixture(scope="session", autouse=True)
def _psu_powers_bench(_psu_or_none):
    """Autouse: realizes :func:`_psu_or_none` so the PSU comes up at
    session start even for tests that don't request ``psu`` by name.

    Without this, a MUM-only test (which never references ``psu``)
    would never trigger PSU setup, and on a bench where the Owon
    powers the ECU the test would fail with "ECU not responding".

    No assertions, no skips — purely a lifecycle hook.
    """
    yield


@pytest.fixture(scope="session")
def psu(_psu_or_none) -> OwonPSU:
    """Public PSU fixture for tests that read measurements or perturb voltage.

    Skips cleanly when the PSU isn't configured / reachable so tests
    targeting the PSU stay portable across benches that don't have one
    wired up.
    """
    if _psu_or_none is None:
        pytest.skip(
            "PSU not available (config.power_supply.enabled=false, "
            "no port configured, or port not reachable)."
        )
    return _psu_or_none
