"""Migrated from SWE5 COM Management Integration Test Plan.

Source: ``25IMR003_ForSeven_RGB-SWITD_03-COM Management (Integration Test results).xlsm``

Translation strategy
--------------------
The COM tests are about LIN communication: NAD addressing, LDF/baudrate,
ALM_Req_A signal layout, LID-range targeting, and frame periodicity.

- ``Watch color table`` (firmware lookup) is exercised end-to-end:
  drive a known RGB at full intensity and verify ``PWM_Frame`` matches the
  ``rgb_to_pwm.compute_pwm`` calculator (which encodes the same color
  table).
- NAD: read ``ALM_Status.ALMNadNo`` and confirm it falls inside the
  valid NAD range declared by the LDF.
- Baudrate: physical-layer; not measurable from inside the test runner
  (requires a scope) → the step is recorded for traceability and skipped.
- ``ALM_Req_A`` byte-mapping: send a frame with distinctive RGB+I values
  and confirm the ECU's response (LED reaches ON, PWM matches) — that
  proves byte-level interpretation end-to-end.
- LID-range flag: drive a frame inside vs. outside the node's range and
  observe whether the LED reacts.
- 5 ms periodicity: a master-side LIN-master scheduling property that
  the slave does not echo back; documented as not directly observable.

Marker: ``COM`` — see ``pytest.ini``.

Run only this module:
    pytest -m "COM" tests/hardware/swe5/test_com_management.py
"""
from __future__ import annotations

import sys
import time
from pathlib import Path

import pytest

_HW_DIR = Path(__file__).resolve().parent.parent
if str(_HW_DIR) not in sys.path:
    sys.path.insert(0, str(_HW_DIR))

from frame_io import FrameIO
from alm_helpers import (
    AlmTester,
    LED_STATE_OFF, LED_STATE_ANIMATING, LED_STATE_ON,
    STATE_POLL_INTERVAL, STATE_TIMEOUT_DEFAULT,
)


pytestmark = [pytest.mark.COM]


# Fixtures (fio, alm, _reset_to_off) and the MUM gate come from
# tests/hardware/mum/conftest.py.


# --- tests -----------------------------------------------------------------


def test_com_itd_0001(fio: FrameIO, alm: AlmTester, rp):
    """
    Title: LED color table is configured per spec — PWM_Frame matches the calculator

    Description:
        Verify the SW configures the LED color table as required by
        [SWRS_LIN_0001]. The firmware's color table feeds
        rgb_to_pwm.compute_pwm(). Drive a known RGB at full intensity, wait
        for LED_ON, then assert PWM_Frame matches
        compute_pwm(R,G,B,temp_c=Tj_NTC).pwm_comp within tolerance.
        Mismatch implies the on-ECU table differs from the spec.

    Requirements: SWRS_LIN_0001
    Test ID: COM_ITD_0001
    """
    r, g, b = 0, 180, 80

    # Step: Drive ALM_Req_A mode=0 RGB at full intensity to this NAD
    fio.send(
        "ALM_Req_A",
        AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
        AmbLightIntensity=255,
        AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
        AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
    )

    # Step: Wait for ALMLEDState == LED_ON
    reached, elapsed, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
    rp("led_state_history", history)
    rp("on_elapsed_s", round(elapsed, 3))
    assert reached, f"LED_ON never reached: {history}"

    # Step: Assert PWM_Frame matches the rgb_to_pwm calculator (color-table proxy)
    alm.assert_pwm_matches_rgb(rp, r, g, b)


def test_com_itd_0002(fio: FrameIO, alm: AlmTester, rp, ldf):
    """
    Title: LDF implementation — NAD and baudrate match the LDF

    Description:
        Verify the SW implements the LDF from "4SEVEN_LDF.ldf" — confirm
        the NAD and the bus baudrate.
        NAD: read ALMNadNo via ALM_Status; confirm it is a valid LIN slave
        NAD (0x01..0xFE) — i.e. matches the value the LDF declares for ALM_Node.
        Baudrate: physical-layer property of the LIN bus; verifying it
        requires an oscilloscope on the LIN line, so the step is recorded
        for traceability but cannot be asserted from inside the test runner.

    Requirements: SWRS_LIN_0001, SWRS_LIN_0002, SWRS_LIN_0003
    Test ID: COM_ITD_0002
    """
    # Step: Read ALM_Status and confirm ALMNadNo is a valid LIN slave NAD
    nad = fio.read_signal("ALM_Status", "ALMNadNo")
    assert nad is not None, "ALM_Status not received within timeout"
    rp("alm_nad", int(nad))
    assert 0x01 <= int(nad) <= 0xFE, (
        f"ALMNadNo {int(nad):#x} is outside the valid LIN slave range 0x01..0xFE"
    )

    # Step: Confirm the LDF declares the same NAD for ALM_Node (introspection)
    # The LdfDatabase wrapper exposes the parsed ldf via .ldf in some
    # implementations; fall back to attribute access otherwise.
    ldf_nad = None
    try:
        for node in getattr(ldf.ldf, "slaves", []) or []:
            # ldfparser slaves carry .name and .configured_nad
            if getattr(node, "name", "").lower().startswith("alm"):
                ldf_nad = int(getattr(node, "configured_nad", 0))
                break
    except Exception as e:  # pragma: no cover — best effort
        rp("ldf_introspection_error", repr(e))
    rp("ldf_declared_nad", ldf_nad)
    if ldf_nad is not None:
        assert int(nad) == ldf_nad, (
            f"Runtime NAD {int(nad):#x} != LDF-declared NAD {ldf_nad:#x}"
        )

    # Step: Baudrate verification requires an external scope on the LIN bus.
    # LIN baudrate is a physical-layer parameter; the master configures
    # it from the LDF when opening the interface, but it is not echoed
    # back in any frame the slave publishes. Recording for traceability.
    rp("baudrate_check", "requires oscilloscope — not asserted in software")


def test_com_itd_0003(fio: FrameIO, alm: AlmTester, rp):
    """
    Title: ALM_Req_A interpretation — distinctive RGBI bytes drive the expected PWM

    Description:
        Verify the SW correctly interprets the bytes of ALM_Req_A
        (AmbLightLIDFrom/To, AmbLightColourRed/Green/Blue, AmbLightIntensity).
        Per-byte verification at the firmware level (Byte_3 ==
        AmbLightColourRed, etc.) is not LIN-observable. Instead, send a frame
        with distinctive R/G/B values, addressed to this node's NAD only,
        then verify (a) the LED reaches ON (LIDFrom/To were honoured) and
        (b) PWM_wo_Comp matches the calculator for those R/G/B at full
        intensity (Red/Green/Blue/Intensity bytes were interpreted correctly).

    Requirements: SWRS_LIN_0004, SWRS_LIN_0012, SWRS_LIN_0013, SWRS_LIN_0014, SWRS_LIN_0015
    Test ID: COM_ITD_0003
    """
    # Distinctive values so a swap of two bytes would be detected.
    r, g, b, intensity = 0xA0, 0x40, 0x10, 0xFF

    # Step: Disable temperature compensation so PWM_wo_Comp == calculator
    fio.send(
        "ConfigFrame",
        ConfigFrame_Calibration=0, ConfigFrame_EnableDerating=1,
        ConfigFrame_EnableCompensation=0, ConfigFrame_MaxLM=3840,
    )
    time.sleep(0.2)

    try:
        # Step: Send ALM_Req_A LIDFrom=LIDTo=alm.nad, RGBI distinctive values
        fio.send(
            "ALM_Req_A",
            AmbLightColourRed=r, AmbLightColourGreen=g, AmbLightColourBlue=b,
            AmbLightIntensity=intensity,
            AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
            AmbLightLIDFrom=alm.nad, AmbLightLIDTo=alm.nad,
        )

        # Step: LIDFrom/To honoured — LED reaches ON
        reached, elapsed, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
        rp("led_state_history", history)
        rp("on_elapsed_s", round(elapsed, 3))
        assert reached, (
            f"ECU did not respond to a frame addressed to its own NAD: {history}"
        )

        # Step: RGB+Intensity bytes correctly interpreted — PWM_wo_Comp matches calculator
        alm.assert_pwm_wo_comp_matches_rgb(rp, r, g, b)
    finally:
        # Step: Restore EnableCompensation=1
        fio.send(
            "ConfigFrame",
            ConfigFrame_Calibration=0, ConfigFrame_EnableDerating=1,
            ConfigFrame_EnableCompensation=1, ConfigFrame_MaxLM=3840,
        )
        time.sleep(0.2)


def test_com_itd_0006(fio: FrameIO, alm: AlmTester, rp):
    """
    Title: LID range targeting — broadcast hits, out-of-range frame is ignored

    Description:
        Verify the SW respects the AmbLightLIDFrom/To range — frames whose
        range covers this node's NAD are processed; out-of-range frames are
        ignored.
        Note: the workbook step ``Watch AmbLightLIDFrom/AmbLightLIDTo range
        flag`` refers to a firmware-internal flag that is not echoed on the
        LIN bus. The LIN-observable proxy is whether the LED reacts (ON) or
        stays at OFF.

    Requirements: SWRS_LIN_0053
    Test ID: COM_ITD_0006
    """
    # Step: Broadcast LIDFrom=0x00, LIDTo=0xFF — node is in range, must react
    fio.send(
        "ALM_Req_A",
        AmbLightColourRed=120, AmbLightColourGreen=0, AmbLightColourBlue=255,
        AmbLightIntensity=255,
        AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
        AmbLightLIDFrom=0x00, AmbLightLIDTo=0xFF,
    )
    reached_on, elapsed, h_in = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
    rp("in_range_history", h_in)
    rp("in_range_elapsed_s", round(elapsed, 3))
    assert reached_on, f"Node ignored an in-range broadcast: {h_in}"

    alm.force_off()

    # Step: Out-of-range LID (range that excludes this NAD) — must be ignored
    # Pick a non-trivial range that intentionally excludes this node.
    if alm.nad <= 0x10:
        lid_from, lid_to = 0x80, 0xFE
    else:
        lid_from, lid_to = 0x01, max(0x02, alm.nad - 1)
    fio.send(
        "ALM_Req_A",
        AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255,
        AmbLightIntensity=255,
        AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
        AmbLightLIDFrom=lid_from, AmbLightLIDTo=lid_to,
    )
    deadline = time.monotonic() + 1.0
    history = []
    while time.monotonic() < deadline:
        st = alm.read_led_state()
        if not history or history[-1] != st:
            history.append(st)
        time.sleep(STATE_POLL_INTERVAL)
    rp("out_of_range_lid", (lid_from, lid_to))
    rp("out_of_range_history", history)
    assert LED_STATE_ON not in history and LED_STATE_ANIMATING not in history, (
        f"Out-of-range LID frame [{lid_from:#x}..{lid_to:#x}] (NAD={alm.nad:#x}) "
        f"unexpectedly drove the LED: {history}"
    )


def test_com_itd_0001_b(fio: FrameIO, alm: AlmTester, rp):
    """
    Title: Input frame reading periodicity (5 ms) — master-side scheduling, scope-only

    Description:
        Verify the SW respects 5 ms periodicity for reading input frames.
        5 ms is the LIN master's schedule cadence: it is configured by the
        master (MUM/BabyLin) when the schedule table is loaded from the LDF,
        and is not echoed back by the slave. Confirming the actual on-bus
        inter-frame gap requires bus-tracing hardware (oscilloscope or LIN
        analyzer). This step is recorded for traceability.

    Requirements: SWRS_LIN_0001
    Test ID: COM_ITD_0001_b
    """
    # Step: Document: 5 ms scheduling is master-side and not asserted from the slave
    rp("note", (
        "5 ms periodicity is set by the LIN master's schedule table "
        "(loaded from the LDF). Slave-side timing of inter-frame gaps "
        "requires an external LIN bus analyzer or oscilloscope to "
        "verify; pytest cannot observe it directly."
    ))
    # Sanity: confirm we can at least round-trip a frame within a few
    # schedule periods, which verifies the bus is up at the configured
    # baudrate (a coarse sanity check, not a 5 ms timing assertion).
    decoded = fio.receive("ALM_Status", timeout=1.0)
    assert decoded is not None, "ALM_Status not received — bus may be down"

    pytest.skip(
        "5 ms inter-frame periodicity is master-side / physical-layer; "
        "verify with a LIN bus analyzer or oscilloscope, not pytest."
    )
