"""PSU voltage settling-time characterization.

WHAT THIS TEST DOES
-------------------
Measures how long the bench Owon PSU actually takes to deliver a new
voltage at its output terminals after a setpoint change. Other tests
(notably ``test_overvolt.py``) rely on a settle delay before they read
``ALMVoltageStatus``; this characterization gives you the real number
to budget for instead of guessing.

HOW IT WORKS
------------
For each parametrized transition ``start_v → target_v``:

  1. SETUP    — park the PSU at ``start_v`` and wait, *un-timed*, until
                the measured voltage actually settles there. This step
                isolates the timer from any leftover state from the
                previous test.
  2. PROCEDURE — issue ``set_voltage(target_v)`` and immediately begin
                polling ``psu.measure_voltage_v()`` at
                ``POLL_INTERVAL_S``; the timer starts the moment the
                setpoint is sent.
  3. ASSERT   — record ``settling_time_s`` (and the full voltage
                trace) as report properties; assert that the PSU
                actually reached the target within
                ``MAX_SETTLE_TIME_S``.
  4. TEARDOWN — set the supply back to ``NOMINAL_V`` so subsequent
                tests start from the bench's normal state.

WHY THIS DESERVES ITS OWN MARKER (``psu_settling``)
---------------------------------------------------
The test takes tens of seconds (4 transitions × several seconds each)
and is only useful occasionally — typically when changing the bench
PSU model or when other voltage-tolerance tests start failing on the
detect timeout. Selecting it explicitly with ``-m psu_settling``
keeps everyday MUM/PSU runs fast.

It's also marked ``slow`` so default ``-m hardware`` runs that pass
``-m "not slow"`` skip it without the user having to know it exists.

REPORT PROPERTIES (per case)
----------------------------
- ``transition``          — the parametrize label, e.g. ``13_to_18_OV``
- ``start_voltage_v``     — the requested start voltage (SETUP target)
- ``target_voltage_v``    — the final target voltage (PROCEDURE target)
- ``settling_time_s``     — headline result: seconds from setpoint
                            to first within-tolerance sample. ``None``
                            if the timeout was reached
- ``final_voltage_v``     — last measured voltage (whether settled or not)
- ``sample_count``        — number of measurements taken
- ``voltage_trace``       — list of (elapsed_s, measured_v) tuples
                            (downsampled to ~30 entries to keep the
                            report readable)
"""
from __future__ import annotations

import time

import pytest

from ecu_framework.power import OwonPSU

from psu_helpers import (
    DEFAULT_POLL_INTERVAL_S,
    DEFAULT_SETTLE_TIMEOUT_S,
    DEFAULT_VOLTAGE_TOL_V,
    downsample_trace,
    wait_until_settled,
)


# ── markers ───────────────────────────────────────────────────────────────
# `hardware`     — needs the bench
# `psu_settling` — opt-in marker: run with `pytest -m psu_settling`
# `slow`         — excludable from quick runs via `-m "not slow"`
pytestmark = [
    pytest.mark.hardware,
    pytest.mark.psu_settling,
    pytest.mark.slow,
]


# ── characterization knobs ────────────────────────────────────────────────
# These are the defaults from psu_helpers, re-exported here so the
# numbers used in the report properties match the tunables visible at
# the top of the test file.
VOLTAGE_TOL_V       = DEFAULT_VOLTAGE_TOL_V
POLL_INTERVAL_S     = DEFAULT_POLL_INTERVAL_S
MAX_SETTLE_TIME_S   = DEFAULT_SETTLE_TIMEOUT_S

# How long to let the PSU settle during the un-timed SETUP step. We want
# this comfortably longer than typical settling so we never start the
# timer with the rail still moving.
SETUP_SETTLE_TIMEOUT_S = MAX_SETTLE_TIME_S
SETUP_SETTLE_GRACE_S   = 0.3   # extra hold once within tolerance, just in case

# Voltage we leave the bench at on TEARDOWN, so the next test starts
# from a known state. Matches the value used by test_overvolt.py.
NOMINAL_V = 13.0

# Trace size cap for report properties.
TRACE_MAX_SAMPLES = 30


# ── parameter matrix ──────────────────────────────────────────────────────
# Cover the four transitions actually used by test_overvolt.py so the
# extracted timings translate directly into wait budgets there.
_TRANSITIONS = [
    # (start_v, target_v, label)
    (13.0, 18.0, "13_to_18_OV"),
    (18.0, 13.0, "18_to_13_back"),
    (13.0,  7.0, "13_to_7_UV"),
    ( 7.0, 13.0, "7_to_13_back"),
]


@pytest.mark.parametrize(
    "start_v,target_v,label",
    _TRANSITIONS,
    ids=[t[2] for t in _TRANSITIONS],
)
def test_psu_voltage_settling_time(
    psu: OwonPSU,
    rp,
    start_v: float,
    target_v: float,
    label: str,
):
    """
    Title: PSU voltage settling time — {label}

    Description:
        Measures how long the Owon PSU actually takes to deliver
        ``target_v`` after a setpoint change from ``start_v``.
        Records the settling time and a downsampled voltage trace
        as report properties so other tests (e.g. test_overvolt)
        can size their detect timeouts from real data instead of
        guesses.

    Test Steps:
        1. SETUP:    park PSU at start_v and wait *un-timed* until
                     the measured voltage falls within VOLTAGE_TOL_V
        2. PROCEDURE: set_voltage(target_v), immediately start the
                     timer, poll measure_voltage_v() every
                     POLL_INTERVAL_S
        3. ASSERT:   measured voltage reached target_v within
                     MAX_SETTLE_TIME_S
        4. TEARDOWN: restore NOMINAL_V

    Expected Result:
        - The PSU reaches target_v within MAX_SETTLE_TIME_S
        - settling_time_s is recorded for downstream tuning
    """
    # ── SETUP ─────────────────────────────────────────────────────────
    # Park at the starting voltage and wait, un-timed, for the rail to
    # actually be there. This isolates the PROCEDURE timer from any
    # voltage left over from a previous test.
    psu.set_voltage(1, start_v)
    settled_to_start, _setup_trace = wait_until_settled(
        psu, start_v,
        timeout=SETUP_SETTLE_TIMEOUT_S,
    )
    assert settled_to_start is not None, (
        f"SETUP: PSU never reached start voltage {start_v} V within "
        f"{SETUP_SETTLE_TIMEOUT_S} s — bench may be unable to slew "
        f"to that point or measurement is not parsing correctly."
    )
    # A short hold so we're sampling a *steady* voltage, not the tail
    # of a slew, when the PROCEDURE timer starts.
    time.sleep(SETUP_SETTLE_GRACE_S)

    try:
        # ── PROCEDURE ─────────────────────────────────────────────────
        # The setpoint write happens here; ``wait_until_settled`` starts
        # polling immediately so the recorded duration captures bus
        # latency + slew time.
        psu.set_voltage(1, target_v)
        elapsed, trace = wait_until_settled(psu, target_v)
        final_v = trace[-1][1] if trace else None
        sample_count = len(trace)

        # ── ASSERT ────────────────────────────────────────────────────
        # Record headline numbers first so they're in the report even
        # on assertion failure.
        rp("transition", label)
        rp("start_voltage_v", start_v)
        rp("target_voltage_v", target_v)
        rp("settling_time_s", round(elapsed, 4) if elapsed is not None else None)
        rp("final_voltage_v", final_v)
        rp("sample_count", sample_count)
        rp("voltage_trace", downsample_trace(trace, max_samples=TRACE_MAX_SAMPLES))
        rp("voltage_tol_v", VOLTAGE_TOL_V)
        rp("poll_interval_s", POLL_INTERVAL_S)

        # Print a human-readable summary so the timing shows up
        # immediately in `pytest -s` runs.
        if elapsed is not None:
            print(
                f"\n[psu_settling] {label}: {start_v} V → {target_v} V "
                f"settled in {elapsed:.3f} s (final={final_v} V, "
                f"samples={sample_count})"
            )
        else:
            print(
                f"\n[psu_settling] {label}: {start_v} V → {target_v} V "
                f"DID NOT SETTLE within {MAX_SETTLE_TIME_S} s "
                f"(final={final_v} V, samples={sample_count})"
            )

        assert elapsed is not None, (
            f"PSU did not reach {target_v} V within {MAX_SETTLE_TIME_S} s "
            f"(last measurement: {final_v} V, ±{VOLTAGE_TOL_V} V tolerance). "
            f"Either the PSU can't slew this far, the load is misbehaving, "
            f"or MAX_SETTLE_TIME_S is too tight."
        )

    finally:
        # ── TEARDOWN ──────────────────────────────────────────────────
        # Always restore nominal so the next test starts cleanly.
        # Don't time this — it runs for safety, not measurement.
        psu.set_voltage(1, NOMINAL_V)
        # A short pause so any later-running test that polls
        # immediately sees a voltage near nominal rather than the tail
        # of this teardown's slew.
        time.sleep(0.5)
