"""Unit tests for LdfDatabase / Frame using the 4SEVEN LDF as fixture data."""
from __future__ import annotations

import pathlib

import pytest

# Skip the whole module if ldfparser isn't installed.
pytest.importorskip("ldfparser", reason="ldfparser is required for LDF unit tests")

from ecu_framework.lin.ldf import Frame, FrameNotFound, LdfDatabase


WORKSPACE_ROOT = pathlib.Path(__file__).resolve().parents[2]
LDF_PATH = WORKSPACE_ROOT / "vendor" / "4SEVEN_color_lib_test.ldf"


@pytest.fixture(scope="module")
def db() -> LdfDatabase:
    return LdfDatabase(LDF_PATH)


@pytest.mark.unit
def test_loads_metadata(db: LdfDatabase):
    assert db.protocol_version in ("2.1", "2.0", "1.3")
    assert db.baudrate == 19200


@pytest.mark.unit
def test_lookup_by_name_and_id(db: LdfDatabase):
    by_name = db.frame("ALM_Req_A")
    by_id = db.frame(0x0A)
    assert by_name.id == 0x0A == by_id.id
    assert by_name.name == "ALM_Req_A" == by_id.name
    assert by_name.length == 8


@pytest.mark.unit
def test_unknown_frame_raises(db: LdfDatabase):
    with pytest.raises(FrameNotFound):
        db.frame("not_a_real_frame")


@pytest.mark.unit
def test_signal_layout_matches_ldf(db: LdfDatabase):
    layout = db.frame("ALM_Req_A").signal_layout()
    # spot-check a couple of entries from the LDF Frames block
    assert (0, "AmbLightColourRed", 8) in layout
    assert (32, "AmbLightUpdate", 2) in layout
    assert (34, "AmbLightMode", 6) in layout
    assert (56, "AmbLightLIDTo", 8) in layout


@pytest.mark.unit
def test_pack_kwargs_full_payload(db: LdfDatabase):
    frame = db.frame("ALM_Req_A")
    payload = frame.pack(
        AmbLightColourRed=0xFF,
        AmbLightColourGreen=0xFF,
        AmbLightColourBlue=0xFF,
        AmbLightIntensity=0xFF,
        AmbLightUpdate=0,
        AmbLightMode=0,
        AmbLightDuration=0,
        AmbLightLIDFrom=0x01,
        AmbLightLIDTo=0x01,
    )
    assert isinstance(payload, bytes)
    assert len(payload) == 8
    assert payload == bytes.fromhex("ffffffff00000101")


@pytest.mark.unit
def test_pack_unspecified_signals_use_init_value(db: LdfDatabase):
    """LDF defines non-zero init_values for ColorConfigFrameRed signals;
    pack() with no kwargs should fall back to those defaults."""
    frame = db.frame("ColorConfigFrameRed")
    payload = frame.pack()
    decoded = frame.unpack(payload)
    # ColorConfigFrameRed_X init_value is 5665, _Y is 2396, _Z is 0, _Vf_Cal is 2031
    assert decoded["ColorConfigFrameRed_X"] == 5665
    assert decoded["ColorConfigFrameRed_Y"] == 2396
    assert decoded["ColorConfigFrameRed_Z"] == 0
    assert decoded["ColorConfigFrameRed_Vf_Cal"] == 2031


@pytest.mark.unit
def test_pack_dict_argument(db: LdfDatabase):
    frame = db.frame("ALM_Req_A")
    a = frame.pack(AmbLightColourRed=0x12, AmbLightColourBlue=0x34)
    b = frame.pack({"AmbLightColourRed": 0x12, "AmbLightColourBlue": 0x34})
    assert a == b


@pytest.mark.unit
def test_pack_rejects_args_and_kwargs_together(db: LdfDatabase):
    frame = db.frame("ALM_Req_A")
    with pytest.raises(TypeError):
        frame.pack({"AmbLightColourRed": 1}, AmbLightColourGreen=2)


@pytest.mark.unit
def test_unpack_round_trip(db: LdfDatabase):
    frame = db.frame("ALM_Req_A")
    values = {
        "AmbLightColourRed": 0xAB,
        "AmbLightColourGreen": 0xCD,
        "AmbLightColourBlue": 0x12,
        "AmbLightIntensity": 0x80,
        "AmbLightUpdate": 2,         # 2 bits
        "AmbLightMode": 0x15,        # 6 bits
        "AmbLightDuration": 0x40,
        "AmbLightLIDFrom": 0x01,
        "AmbLightLIDTo": 0xFE,
    }
    payload = frame.pack(**values)
    decoded = frame.unpack(payload)
    for k, v in values.items():
        assert decoded[k] == v, f"signal {k} mismatch: {decoded[k]} vs {v}"


@pytest.mark.unit
def test_alm_status_decode_real_payload(db: LdfDatabase):
    """ALM_Status: byte 0 carries ALMNadNo (8 bits at offset 0)."""
    frame = db.frame("ALM_Status")
    assert frame.length == 4
    decoded = frame.unpack(b"\x07\x00\x00\x00")
    assert decoded["ALMNadNo"] == 7


@pytest.mark.unit
def test_frame_lengths_includes_all_unconditional_frames(db: LdfDatabase):
    lengths = db.frame_lengths()
    assert lengths[0x0A] == 8   # ALM_Req_A
    assert lengths[0x11] == 4   # ALM_Status
    assert lengths[0x06] == 3   # ConfigFrame
    # Every entry should map to a positive length
    assert all(l >= 1 for l in lengths.values())


@pytest.mark.unit
def test_frames_returns_wrapped_frame_objects(db: LdfDatabase):
    frames = db.frames()
    assert all(isinstance(f, Frame) for f in frames)
    names = {f.name for f in frames}
    assert {"ALM_Req_A", "ALM_Status", "ConfigFrame"}.issubset(names)


@pytest.mark.unit
def test_ldf_repr_does_not_explode(db: LdfDatabase):
    s = repr(db)
    assert "LdfDatabase" in s


@pytest.mark.unit
def test_missing_file_raises_filenotfounderror(tmp_path):
    with pytest.raises(FileNotFoundError):
        LdfDatabase(tmp_path / "nope.ldf")
