"""End-to-end hardware test: power the ECU on via Owon PSU, switch to the
'CCO' schedule, and publish an RGB activation frame on ALM_Req_A (ID 0x0A).

Frame layout (from vendor/4SEVEN_color_lib_test.ldf, ALM_Req_A @ ID 0x0A, 8B):
  byte 0  AmbLightColourRed   (0..255)
  byte 1  AmbLightColourGreen (0..255)
  byte 2  AmbLightColourBlue  (0..255)
  byte 3  AmbLightIntensity   (0..255)
  byte 4  AmbLightUpdate (bits 0-1) | AmbLightMode (bits 2-7)
  byte 5  AmbLightDuration
  byte 6  AmbLightLIDFrom
  byte 7  AmbLightLIDTo

Schedule 'CCO' polls ALM_Req_A every 10 ms (LDF line 252-263). Updating the
master-published frame data via BLC_mon_set_xmit makes the next CCO slot
publish the new RGB values. The slave answers ALM_Status (ID 0x11) which we
use as evidence the bus is alive.
"""
from __future__ import annotations

import time

import pytest
import serial

from ecu_framework.config import EcuTestConfig
from ecu_framework.lin.base import LinFrame, LinInterface
from ecu_framework.power import OwonPSU, SerialParams


pytestmark = [pytest.mark.hardware, pytest.mark.babylin]

# Frame IDs from the LDF
ALM_REQ_A_ID = 0x0A   # master-published RGB control frame
ALM_STATUS_ID = 0x11  # slave-published status frame

# Default RGB activation: full white at full intensity, immediate setpoint.
DEFAULT_RGB = (0xFF, 0xFF, 0xFF)
DEFAULT_INTENSITY = 0xFF


_PARITY_MAP = {
    "N": serial.PARITY_NONE,
    "E": serial.PARITY_EVEN,
    "O": serial.PARITY_ODD,
}
_STOPBITS_MAP = {
    1: serial.STOPBITS_ONE,
    2: serial.STOPBITS_TWO,
}


def _build_serial_params(psu_cfg) -> SerialParams:
    return SerialParams(
        baudrate=int(psu_cfg.baudrate),
        timeout=float(psu_cfg.timeout),
        parity=_PARITY_MAP.get(str(psu_cfg.parity or "N").upper(), serial.PARITY_NONE),
        stopbits=_STOPBITS_MAP.get(int(float(psu_cfg.stopbits or 1)), serial.STOPBITS_ONE),
        xonxoff=bool(psu_cfg.xonxoff),
        rtscts=bool(psu_cfg.rtscts),
        dsrdtr=bool(psu_cfg.dsrdtr),
    )


def _build_alm_req_a_payload(
    r: int, g: int, b: int,
    intensity: int = DEFAULT_INTENSITY,
    update: int = 0,    # 0 = Immediate color update
    mode: int = 0,      # 0 = Immediate Setpoint
    duration: int = 0,
    lid_from: int = 0,
    lid_to: int = 0,
) -> bytes:
    """Pack RGB-activation signals into the 8-byte ALM_Req_A payload."""
    # byte 4 packs Update (2 bits, LSB) and Mode (6 bits) per the LDF offsets.
    byte4 = (update & 0x03) | ((mode & 0x3F) << 2)
    return bytes([
        r & 0xFF, g & 0xFF, b & 0xFF,
        intensity & 0xFF,
        byte4 & 0xFF,
        duration & 0xFF,
        lid_from & 0xFF,
        lid_to & 0xFF,
    ])


def test_e2e_power_on_then_cco_rgb_activate(config: EcuTestConfig, lin: LinInterface, rp):
    """
    Title: E2E - Power ECU, Switch to CCO Schedule, Activate RGB

    Description:
        Powers the ECU via the Owon PSU, switches the BabyLIN master to the
        'CCO' schedule (which polls ALM_Req_A every 10 ms per the LDF), and
        publishes an RGB activation payload on ALM_Req_A (ID 0x0A). Captures
        bus traffic for a short window to confirm activity (typically the
        slave-published ALM_Status at ID 0x11 will appear).

    Requirements: REQ-E2E-CCO-RGB

    Test Steps:
        1. Skip unless interface.type == 'babylin'
        2. Skip unless power_supply is enabled and a port is configured
        3. Open the PSU, IDN check, set V/I, enable output
        4. Wait for ECU boot (boot_settle_seconds, default 1.5 s)
        5. Stop any current schedule and start schedule 'CCO'
        6. Build the ALM_Req_A payload from RGB+intensity+mode+update
        7. Publish the payload via lin.send(LinFrame(0x0A, ...))
        8. Drain RX briefly and collect frames seen during the activation window
        9. Assert at least one frame was observed; report IDs/lengths
       10. Disable PSU output (always)

    Expected Result:
        - PSU comes up, ECU boots, CCO starts without SDK errors
        - At least one LIN frame is observed on the bus during the window
        - PSU output is disabled at end of test
    """
    # Step 1 / 2: gate on hardware availability
    if config.interface.type != "babylin":
        pytest.skip("interface.type must be 'babylin' for this E2E test")

    psu_cfg = config.power_supply
    if not psu_cfg.enabled:
        pytest.skip("Power supply disabled in config.power_supply.enabled")
    if not psu_cfg.port:
        pytest.skip("No power supply 'port' configured (config.power_supply.port)")

    set_v = float(psu_cfg.set_voltage)
    print(f"Debug: set_v={set_v}, type={type(set_v)}")
    set_i = float(psu_cfg.set_current)
    print(f"Debug: set_i={set_i}, type={type(set_i)}")
    eol = psu_cfg.eol or "\n"
    port = str(psu_cfg.port).strip()

    boot_settle_s = float(getattr(psu_cfg, "boot_settle_seconds", 1.5))
    activation_window_s = float(getattr(psu_cfg, "activation_window", 1.0))

    # The adapter is hardware-only here; the test is gated on interface.type=='babylin'.
    send_command = getattr(lin, "send_command", None)
    start_schedule = getattr(lin, "start_schedule", None)
    if send_command is None or start_schedule is None:
        pytest.skip("LIN adapter does not expose send_command/start_schedule (need BabyLinInterface)")

    rgb = (DEFAULT_RGB[0], DEFAULT_RGB[1], DEFAULT_RGB[2])
    rp("interface_type", config.interface.type)
    rp("psu_port", port)
    rp("set_voltage", set_v)
    rp("set_current", set_i)
    rp("schedule", "CCO")
    rp("rgb", list(rgb))
    rp("intensity", DEFAULT_INTENSITY)

    sparams = _build_serial_params(psu_cfg)

    with OwonPSU(port, sparams, eol=eol) as psu:
        # Step 3: bring up PSU
        idn = psu.idn()
        rp("psu_idn", idn)
        assert isinstance(idn, str) and idn != "", "PSU *IDN? returned empty"
        if psu_cfg.idn_substr:
            assert str(psu_cfg.idn_substr).lower() in idn.lower(), (
                f"PSU IDN does not contain expected substring "
                f"{psu_cfg.idn_substr!r}; got {idn!r}"
            )

        psu.set_voltage(1, set_v)
        psu.set_current(1, set_i)

        try:
            psu.set_output(True)

            # Step 4: let ECU boot
            time.sleep(boot_settle_s)
            try:
                rp("measured_voltage", psu.measure_voltage())
                rp("measured_current", psu.measure_current())
            except Exception as meas_err:
                rp("measure_error", repr(meas_err))

            # Step 5: switch to schedule CCO. The BabyLIN firmware only accepts
            # 'start schedule <index>;', so we resolve the name to its SDF index
            # via BLC_SDF_getScheduleNr (handled inside start_schedule).
            try:
                send_command("stop;")
            except Exception as e:
                rp("stop_error", repr(e))
            cco_idx = start_schedule("CCO")
            rp("schedule_index", cco_idx)

            # Step 6 + 7: build and publish the RGB activation frame.
            payload = _build_alm_req_a_payload(*rgb, intensity=DEFAULT_INTENSITY)
            rp("tx_id", f"0x{ALM_REQ_A_ID:02X}")
            rp("tx_data_hex", payload.hex())
            lin.send(LinFrame(id=ALM_REQ_A_ID, data=payload))

            # Step 8: collect frames over the activation window. CCO publishes
            # ALM_Req_A (0x0A) and ALM_Status (0x11) every ~10 ms each.
            try:
                lin.flush()
            except Exception:
                pass

            seen = []
            deadline = time.monotonic() + activation_window_s
            while time.monotonic() < deadline:
                rx = lin.receive(timeout=0.1)
                if rx is None:
                    continue
                seen.append((rx.id, bytes(rx.data)))

            ids = sorted({fid for fid, _ in seen})
            rp("rx_count", len(seen))
            rp("rx_ids", [f"0x{i:02X}" for i in ids])
            if seen:
                last_id, last_data = seen[-1]
                rp("rx_last_id", f"0x{last_id:02X}")
                rp("rx_last_data_hex", last_data.hex())

            # Step 9: minimal liveness assertion. We don't require ALM_Status
            # specifically because absence-of-slave is a separate failure mode
            # to diagnose; we just want to know the bus moved at all.
            assert seen, (
                f"No LIN frames observed during {activation_window_s:.2f}s on schedule CCO. "
                f"Check wiring, SDF, and that 'CCO' exists in the loaded SDF."
            )
            if ALM_STATUS_ID in ids:
                rp("alm_status_seen", True)
            else:
                # Not asserted, but logged so the report shows it clearly.
                rp("alm_status_seen", False)
        finally:
            # Step 10: always cut power
            try:
                psu.set_output(False)
            except Exception as off_err:
                rp("set_output_off_error", repr(off_err))
