"""Migrated from SWE6 COM Management Validation Test Plan.

Source: ``25IMR003_ForSeven-SWVTD_01-COM Management (Validation Test Plan).xlsm``

Translation strategy
--------------------
Both qualification tests in this workbook reference frames and signals
that are **not present in the current production LDF**
(``vendor/4SEVEN_color_lib_test.ldf``):

- ``ALM_NodeSelection`` (per-NAD selection bytes)
- ``ALM_Req_B`` (a second request frame for LED-on/-off commit)
- ``ALM_LED_Idx`` (per-LED bitmask within ``ALM_Req_A``)

The current LDF uses a different addressing scheme: each ALM_Node has a
single LED, addressed by the ``AmbLightLIDFrom``/``AmbLightLIDTo`` range
inside ``ALM_Req_A``. Until the LDF is updated to expose the workbook's
signals (or the workbook is updated to match the deployed LDF), these
tests cannot be executed end-to-end.

Each test below performs a **real LDF probe** for the missing signals.
If the LDF later starts exposing them, the probe stops skipping and the
remaining steps execute against the real bus. The skip reason names the
exact missing signal so a reviewer can see what's blocking.

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

Run only this module:
    pytest -m "COM_VTD" tests/hardware/swe6/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_ON,
    STATE_POLL_INTERVAL, STATE_TIMEOUT_DEFAULT,
)


pytestmark = [pytest.mark.COM_VTD]


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


def _require_signals_in_frame(fio: FrameIO, frame_name: str, signal_names: list[str]) -> None:
    """Skip if the LDF doesn't define ``frame_name`` with all ``signal_names``.

    Lets the test become live automatically when the LDF is updated.
    """
    try:
        f = fio.frame(frame_name)
    except Exception as e:
        pytest.skip(f"LDF does not define frame {frame_name!r}: {e!r}")
        return
    declared = {s.name for s in getattr(f, "signals", []) or []}
    missing = [s for s in signal_names if s not in declared]
    if missing:
        pytest.skip(
            f"LDF frame {frame_name!r} is missing signal(s) {missing!r}; "
            f"this validation test cannot run against the current LDF."
        )


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


def test_com_vtd_0001(fio: FrameIO, alm: AlmTester, rp):
    """
    Title: SW responds only when its NAD bit is set in ALM_NodeSelection

    Description:
        Verify the SW responds only when the current NAD's bit is set in
        the ``ALM_NodeSelection`` bytes; the LED-on/-off command is then
        committed by a follow-up ``ALM_Req_B`` frame.

        Steps from workbook:
          1. Send ALM_Req_A with current NAD bit = 1 + LED_0 ON parameters
          2. Send ALM_Req_B → expect LED_0 ON
          3. Send ALM_Req_A with current NAD bit = 0 + LED_0 OFF parameters
          4. Send ALM_Req_B → expect LED_0 still ON (command was not addressed)

        Status: ``ALM_NodeSelection`` and ``ALM_Req_B`` are NOT in the
        current production LDF (``vendor/4SEVEN_color_lib_test.ldf``);
        addressing is currently done via ``AmbLightLIDFrom``/
        ``AmbLightLIDTo`` inside ``ALM_Req_A``. The probe below names the
        exact missing artifact and the test becomes live automatically
        when the LDF is updated.

    Requirements: SWRS_LIN_0008
    Test ID: COM_VTD_0001
    """
    # Step: Probe LDF for the validation-spec frames/signals.
    # If/when the LDF is updated to expose these, the skip below
    # disappears and the remaining steps will execute.
    _require_signals_in_frame(fio, "ALM_Req_A", ["ALM_NodeSelection"])
    _require_signals_in_frame(fio, "ALM_Req_B", [])  # frame existence

    # The steps below are ready to run as soon as the LDF exposes the
    # missing signals. They mirror the workbook procedure.

    # Step 1: ALM_Req_A with this NAD selected + LED_0 ON parameters
    fio.send(
        "ALM_Req_A",
        ALM_NodeSelection=(1 << (alm.nad - 1)),  # bitmask for this NAD
        ALM_LED_Idx=0x01,                         # LED_0
        AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255,
        AmbLightIntensity=255,
        AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
    )

    # Step 2: ALM_Req_B commits the command — expect LED_0 ON
    fio.send("ALM_Req_B")
    reached, _, history = alm.wait_for_state(LED_STATE_ON, timeout=STATE_TIMEOUT_DEFAULT)
    rp("on_history", history)
    assert reached, f"LED_0 did not turn ON after ALM_Req_B commit: {history}"

    # Step 3: ALM_Req_A with this NAD NOT selected + LED_0 OFF parameters
    fio.send(
        "ALM_Req_A",
        ALM_NodeSelection=0,
        ALM_LED_Idx=0x01,
        AmbLightColourRed=0, AmbLightColourGreen=0, AmbLightColourBlue=0,
        AmbLightIntensity=0,
        AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
    )

    # Step 4: ALM_Req_B — LED_0 must remain ON (un-addressed command)
    fio.send("ALM_Req_B")
    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("post_history", history)
    assert LED_STATE_ON in history, (
        f"LED_0 turned off — un-addressed command was wrongly applied: {history}"
    )


@pytest.mark.parametrize(
    "led_idx,description",
    [
        pytest.param(0x00, "all OFF",                       id="led_idx_0x00"),
        pytest.param(0xAA, "LEDs 1,3,5,7 ON; 0,2,4,6 OFF",  id="led_idx_0xAA"),
        pytest.param(0x55, "LEDs 0,2,4,6 ON; 1,3,5,7 OFF",  id="led_idx_0x55"),
        pytest.param(0xFF, "all ON",                        id="led_idx_0xFF"),
    ],
)
def test_com_vtd_0002(fio: FrameIO, alm: AlmTester, rp, led_idx, description):
    """
    Title: SW interprets ALM_LED_Idx as a per-LED bitmask

    Description:
        Verify ``ALM_LED_Idx`` is interpreted as a bitmask, one bit per LED:
          - 0x00 → all LEDs OFF
          - 0xAA → LEDs 1,3,5,7 ON, LEDs 0,2,4,6 OFF
          - 0x55 → LEDs 0,2,4,6 ON, LEDs 1,3,5,7 OFF
          - 0xFF → all LEDs ON

        Status: ``ALM_LED_Idx`` is NOT in the current production LDF; the
        deployed ECU exposes a single LED via ``AmbLightLIDFrom/To``.
        Per-LED verification additionally requires either an extended
        ALM_Status frame or external optical instrumentation — individual
        ON/OFF states of 8 LEDs are not LIN-observable from the current
        ALM_Status payload.

        The probe below skips the test naming the exact missing signal so
        the test becomes live automatically when the LDF is updated.

    Requirements: SWRS_LIN_0009, SWRS_LIN_0010, SWRS_LIN_0011
    Test ID: COM_VTD_0002
    """
    # Step: Probe LDF for ALM_LED_Idx and ALM_NodeSelection
    _require_signals_in_frame(fio, "ALM_Req_A", ["ALM_LED_Idx", "ALM_NodeSelection"])

    # Step: Send ALM_Req_A with this NAD selected, ALM_LED_Idx=<bitmask>
    fio.send(
        "ALM_Req_A",
        ALM_NodeSelection=(1 << (alm.nad - 1)),
        ALM_LED_Idx=led_idx,
        AmbLightColourRed=255, AmbLightColourGreen=255, AmbLightColourBlue=255,
        AmbLightIntensity=255,
        AmbLightUpdate=0, AmbLightMode=0, AmbLightDuration=0,
    )

    # Step: Verify per-LED ON/OFF pattern matches mask
    rp("led_idx_mask", f"0x{led_idx:02X}")
    rp("expected_pattern", description)
    # When the LDF is extended with a per-LED status frame, replace
    # this skip with an actual signal read + bit-by-bit assertion.
    pytest.skip(
        "Per-LED state is not exposed in the current ALM_Status frame; "
        "individual LED verification requires either an extended status "
        "frame or external optical instrumentation."
    )
