"""LIN auto-addressing (BSM-SNPD) test on the MUM.

Ports the BSM-SNPD sequence from `vendor/automated_lin_test/test_auto_addressing.py`
into pytest. The flow:

  1. INIT     subf=0x01, params=(0x02, 0xFF)              wait 50 ms
  2. ASSIGN   subf=0x02, params=(0x02, target_nad)        x 16 frames, 20 ms apart
              (target_nad placed first, then NADs 0x01..0x10 cycle)
  3. STORE    subf=0x03, params=(0x02, 0xFF)              wait 20 ms
  4. FINALIZE subf=0x04, params=(0x02, 0xFF)              wait 20 ms

Each frame is 8 bytes:

  byte 0  NAD = 0x7F        (broadcast)
  byte 1  PCI = 0x06        (6 data bytes)
  byte 2  SID = 0xB5        (BSM-SNPD)
  byte 3  Supplier ID LSB = 0xFF
  byte 4  Supplier ID MSB = 0x7F
  byte 5  subfunction
  byte 6  param 1
  byte 7  param 2

Critically, BSM frames must be sent with **LIN 1.x Classic checksum**, which
the ECU firmware checks. `MumLinInterface.send_raw()` routes through the
transport layer's `ld_put_raw`, which uses Classic; `lin.send()` would use
Enhanced and frames would be silently rejected.

The test changes the ECU's NAD, asserts the change, and restores the original
NAD in `finally` so it leaves the bench in the state it found it.
"""
from __future__ import annotations

import time
from typing import Iterable

import pytest

from ecu_framework.lin.base import LinInterface


pytestmark = [pytest.mark.hardware, pytest.mark.mum, pytest.mark.slow]


# BSM-SNPD constants
BSM_NAD_BROADCAST = 0x7F
BSM_PCI = 0x06
BSM_SID = 0xB5
BSM_SUPPLIER_ID_LSB = 0xFF
BSM_SUPPLIER_ID_MSB = 0x7F

BSM_SUBF_INIT = 0x01
BSM_SUBF_ASSIGN = 0x02
BSM_SUBF_STORE = 0x03
BSM_SUBF_FINALIZE = 0x04

BSM_INIT_DELAY = 0.050
BSM_FRAME_DELAY = 0.020

VALID_NAD_RANGE: Iterable[int] = range(0x01, 0x11)  # 0x01..0x10 inclusive

# Time to wait after FINALIZE for the ECU to commit and resume normal traffic
POST_FINALIZE_SETTLE = 1.0


def _bsm_frame(subfunction: int, param1: int, param2: int) -> bytes:
    """Build the 8-byte BSM-SNPD raw payload."""
    return bytes([
        BSM_NAD_BROADCAST,
        BSM_PCI,
        BSM_SID,
        BSM_SUPPLIER_ID_LSB,
        BSM_SUPPLIER_ID_MSB,
        subfunction & 0xFF,
        param1 & 0xFF,
        param2 & 0xFF,
    ])


def _read_nad(lin: LinInterface, status_frame, attempts: int = 5) -> int | None:
    """Read ALM_Status a few times, return ALMNadNo or None if no response."""
    for _ in range(attempts):
        rx = lin.receive(id=status_frame.id, timeout=0.5)
        if rx is not None:
            decoded = status_frame.unpack(bytes(rx.data))
            return int(decoded["ALMNadNo"])
        time.sleep(0.1)
    return None


def _run_bsm_sequence(lin: LinInterface, target_nad: int) -> None:
    """Drive one full INIT→ASSIGN×16→STORE→FINALIZE cycle, target NAD first."""
    # 1. INIT
    lin.send_raw(_bsm_frame(BSM_SUBF_INIT, 0x02, 0xFF))
    time.sleep(BSM_INIT_DELAY)

    # 2. 16x ASSIGN, target_nad placed first
    nad_sequence = list(VALID_NAD_RANGE)
    if target_nad in nad_sequence:
        nad_sequence.remove(target_nad)
    nad_sequence.insert(0, target_nad)
    for nad in nad_sequence:
        lin.send_raw(_bsm_frame(BSM_SUBF_ASSIGN, 0x02, nad))
        time.sleep(BSM_FRAME_DELAY)

    # 3. STORE
    lin.send_raw(_bsm_frame(BSM_SUBF_STORE, 0x02, 0xFF))
    time.sleep(BSM_FRAME_DELAY)

    # 4. FINALIZE
    lin.send_raw(_bsm_frame(BSM_SUBF_FINALIZE, 0x02, 0xFF))
    time.sleep(BSM_FRAME_DELAY)


def test_bsm_auto_addressing_changes_nad(
    lin: LinInterface, ldf, rp
):
    """
    Title: BSM-SNPD auto-addressing assigns a new NAD and ALM_Status reflects it

    Description:
        Runs the full BSM-SNPD sequence (INIT, 16x ASSIGN, STORE, FINALIZE)
        with a target NAD different from the ECU's current NAD, then reads
        ALM_Status and asserts ALMNadNo equals the target. Restores the
        original NAD in a finally block to leave the bench unchanged.

    Requirements: REQ-MUM-BSM-AUTOADDR

    Test Steps:
        1. Skip unless interface.type == 'mum'
        2. Read initial NAD from ALM_Status
        3. Pick a target NAD in 0x01..0x10 different from initial
        4. Run BSM sequence with target_nad first
        5. Read ALM_Status; assert ALMNadNo == target_nad
        6. Run BSM sequence again to restore initial NAD
        7. Read ALM_Status; record the final NAD

    Expected Result:
        - Initial NAD is in 0x01..0xFE
        - After BSM sequence, ALM_Status.ALMNadNo == target_nad
        - After restore sequence, ALM_Status.ALMNadNo == initial_nad
    """
    # MUM gate is enforced by tests/hardware/mum/conftest.py::_require_mum
    # send_raw is MUM-only; gate on capability so the failure mode is clean
    if not hasattr(lin, "send_raw"):
        pytest.skip("LIN adapter does not expose send_raw() (need MumLinInterface)")

    status = ldf.frame("ALM_Status")
    rp("ldf_path", str(ldf.path))

    # Step 2: read current NAD
    initial_nad = _read_nad(lin, status)
    assert initial_nad is not None, "ECU not responding on ALM_Status — wiring/power?"
    rp("initial_nad", f"0x{initial_nad:02X}")
    assert 0x01 <= initial_nad <= 0xFE, f"ECU initial NAD {initial_nad:#x} is out of range"

    # Step 3: pick a target NAD different from current
    candidates = [n for n in VALID_NAD_RANGE if n != initial_nad]
    target_nad = candidates[0]
    rp("target_nad", f"0x{target_nad:02X}")

    try:
        # Step 4: run the BSM sequence
        _run_bsm_sequence(lin, target_nad)
        time.sleep(POST_FINALIZE_SETTLE)

        # Step 5: verify
        new_nad = _read_nad(lin, status)
        rp("post_bsm_nad", f"0x{new_nad:02X}" if new_nad is not None else "no_response")
        assert new_nad == target_nad, (
            f"NAD did not change to target: expected 0x{target_nad:02X}, "
            f"got {new_nad if new_nad is None else f'0x{new_nad:02X}'}"
        )
    finally:
        # Step 6 + 7: restore the original NAD so the bench is left as we found it
        try:
            _run_bsm_sequence(lin, initial_nad)
            time.sleep(POST_FINALIZE_SETTLE)
            restored_nad = _read_nad(lin, status)
            rp("restored_nad", f"0x{restored_nad:02X}" if restored_nad is not None else "no_response")
            if restored_nad != initial_nad:
                # Don't fail the test on restore failure (the original assertion is
                # what we care about), but make it visible.
                rp("restore_warning", f"failed to restore initial NAD ({restored_nad})")
        except Exception as e:
            rp("restore_error", repr(e))
