"""Shared PSU helpers for hardware tests.

The Owon PSU does not slew instantaneously, and the slew time depends on
the bench (PSU model, load, cable drop). Tests that change supply
voltage must therefore *measure* the rail before assuming the new
voltage is present, instead of waiting a fixed sleep.

This module provides two layers:

  - :func:`wait_until_settled`        — primitive: poll
    ``psu.measure_voltage_v()`` until it falls within a tolerance band
    of the target. Returns the elapsed time and the full poll trace.

  - :func:`apply_voltage_and_settle`  — composite: write a setpoint,
    wait for the rail to actually be there, then hold for a
    configurable ``validation_time`` so any downstream observer (an
    ECU monitoring its supply rail and reporting status over LIN) has
    time to detect and react. Returns a structured dict that callers
    record to the report.

The pattern in tests is:

    apply_voltage_and_settle(psu, OVERVOLTAGE_V,
                             validation_time=ECU_VALIDATION_TIME_S)
    status = fio.read_signal("ALM_Status", "ALMVoltageStatus")
    assert status == VOLTAGE_STATUS_OVER

— a single deterministic status read instead of polling the bus
hoping the ECU has caught up.
"""
from __future__ import annotations

import time
from typing import Optional

from ecu_framework.power import OwonPSU


# ── tunable defaults (override per call when needed) ─────────────────────
# Tolerance band for "the PSU has reached the target". 100 mV is well
# within typical Owon regulation accuracy and tight enough that we're
# really measuring the slewed voltage, not loop noise.
DEFAULT_VOLTAGE_TOL_V = 0.10

# Polling interval. The serial round-trip is ~10 ms; 50 ms gives clean
# samples without saturating the link.
DEFAULT_POLL_INTERVAL_S = 0.05

# Maximum time to wait for the PSU to settle. Owon settling on small
# steps is sub-second; on big steps a few seconds. 10 s is a generous
# fence that surfaces a real bench problem if exceeded.
DEFAULT_SETTLE_TIMEOUT_S = 10.0

# Default time to hold after the PSU settles before the test reads any
# downstream status. This is the **firmware-dependent** budget — how
# long the ECU needs to detect the new voltage and republish status.
# Tune to your firmware spec.
DEFAULT_VALIDATION_TIME_S = 1.0


# ── primitive: poll until settled ────────────────────────────────────────


def wait_until_settled(
    psu: OwonPSU,
    target_v: float,
    *,
    tol: float = DEFAULT_VOLTAGE_TOL_V,
    interval: float = DEFAULT_POLL_INTERVAL_S,
    timeout: float = DEFAULT_SETTLE_TIMEOUT_S,
) -> tuple[Optional[float], list[tuple[float, Optional[float]]]]:
    """Poll ``psu.measure_voltage_v()`` until within ``tol`` of ``target_v``.

    The caller is responsible for issuing the setpoint **just before**
    calling this — the timer starts on the function's first instruction
    so the recorded duration includes the bus latency of the setpoint
    being applied.

    Returns ``(elapsed_seconds, trace)`` when settled, or
    ``(None, trace)`` if ``timeout`` expired. ``trace`` is the full list
    of ``(elapsed_seconds, measured_voltage)`` tuples; the
    ``measured_voltage`` may be ``None`` for samples that failed to
    parse (rare; surfaces firmware response anomalies).
    """
    trace: list[tuple[float, Optional[float]]] = []
    start = time.monotonic()
    deadline = start + timeout
    while time.monotonic() < deadline:
        v = psu.measure_voltage_v()
        elapsed = time.monotonic() - start
        trace.append((round(elapsed, 4), v))
        if v is not None and abs(v - target_v) <= tol:
            return elapsed, trace
        time.sleep(interval)
    return None, trace


def downsample_trace(
    trace: list[tuple[float, Optional[float]]],
    max_samples: int = 30,
) -> list[tuple[float, Optional[float]]]:
    """Reduce a trace to at most ``max_samples`` evenly-spaced entries.

    Keeps the first and last samples so the start/end of the curve are
    always visible, then strides through the middle. Useful for
    attaching a poll trace to a JUnit/HTML report without bloating it.
    """
    n = len(trace)
    if n <= max_samples:
        return list(trace)
    step = max(1, n // max_samples)
    sampled = trace[::step]
    if sampled[-1] != trace[-1]:
        sampled.append(trace[-1])
    return sampled


# ── composite: apply, wait for rail, hold for ECU ───────────────────────


def apply_voltage_and_settle(
    psu: OwonPSU,
    target_v: float,
    *,
    validation_time: float = DEFAULT_VALIDATION_TIME_S,
    tol: float = DEFAULT_VOLTAGE_TOL_V,
    interval: float = DEFAULT_POLL_INTERVAL_S,
    settle_timeout: float = DEFAULT_SETTLE_TIMEOUT_S,
) -> dict:
    """Set ``target_v``, wait for the rail to actually be there, then hold.

    Steps:
      1. ``psu.set_voltage(1, target_v)`` — issue the setpoint.
      2. :func:`wait_until_settled` — poll the PSU meter until measured
         voltage is within ``tol`` of ``target_v`` (or raise on timeout).
      3. ``time.sleep(validation_time)`` — give the firmware-side
         observer (e.g. ECU voltage monitor) time to detect the new
         voltage and update its status frame.

    By the time this function returns the rail is at ``target_v`` and
    the ECU has had ``validation_time`` to react. A single status read
    afterwards is unambiguous — no polling-on-the-bus race.

    Returns a dict with diagnostic data:

        {
          "settled_s":     float,    # PSU slewing time to within tol
          "validation_s":  float,    # validation_time as passed
          "final_v":       float,    # last measured voltage
          "trace":         list,     # full (elapsed_s, v) trace
        }

    Raises:
        AssertionError: PSU did not reach ``target_v`` within
            ``settle_timeout`` seconds (last measured voltage and
            tolerance band included in the message).
    """
    psu.set_voltage(1, target_v)
    elapsed, trace = wait_until_settled(
        psu, target_v,
        tol=tol, interval=interval, timeout=settle_timeout,
    )

    final_v = trace[-1][1] if trace else None
    if elapsed is None:
        raise AssertionError(
            f"PSU did not settle to {target_v} V within {settle_timeout} s "
            f"(last measured: {final_v} V, ±{tol} V tolerance). "
            f"Either the PSU can't slew this far, the load is misbehaving, "
            f"or the timeout is too tight for this transition."
        )

    # Hold the rail steady so the ECU can detect and republish status.
    if validation_time > 0:
        time.sleep(validation_time)

    return {
        "settled_s": elapsed,
        "validation_s": validation_time,
        "final_v": final_v,
        "trace": trace,
    }
