import os
import pathlib
import sys
import typing as t
import warnings

import pytest

from ecu_framework.config import load_config, EcuTestConfig
from ecu_framework.lin.base import LinInterface
from ecu_framework.lin.mock import MockBabyLinInterface

try:
    from ecu_framework.lin.babylin import BabyLinInterface  # type: ignore  # deprecated
except Exception:
    BabyLinInterface = None  # type: ignore

try:
    from ecu_framework.lin.mum import MumLinInterface  # type: ignore
except Exception:
    MumLinInterface = None  # type: ignore


WORKSPACE_ROOT = pathlib.Path(__file__).resolve().parents[1]


@pytest.fixture(scope="session")
def config() -> EcuTestConfig:
    cfg = load_config(str(WORKSPACE_ROOT))
    return cfg


@pytest.fixture(scope="session")
def lin(config: EcuTestConfig) -> t.Iterator[LinInterface]:
    iface_type = config.interface.type
    if iface_type == "mock":
        lin = MockBabyLinInterface(bitrate=config.interface.bitrate, channel=config.interface.channel)
    elif iface_type == "babylin":
        if BabyLinInterface is None:
            pytest.skip("BabyLin interface not available in this environment")
        warnings.warn(
            "interface.type='babylin' selects the deprecated BabyLIN adapter; "
            "switch to interface.type='mum' for new tests.",
            DeprecationWarning,
            stacklevel=2,
        )
        lin = BabyLinInterface(
            dll_path=config.interface.dll_path,
            bitrate=config.interface.bitrate,
            channel=config.interface.channel,
            node_name=config.interface.node_name,
            func_names=config.interface.func_names,
            sdf_path=config.interface.sdf_path,
            schedule_nr=config.interface.schedule_nr,
        )
    elif iface_type == "mum":
        if MumLinInterface is None:
            pytest.skip("MUM interface not available in this environment")
        if not config.interface.host:
            pytest.skip("interface.host is required when interface.type == 'mum'")
        # Merge frame lengths: LDF (if any) provides defaults; YAML
        # `frame_lengths` overrides on a per-id basis.
        merged_lengths: dict = {}
        if config.interface.ldf_path:
            try:
                from ecu_framework.lin.ldf import LdfDatabase
                merged_lengths.update(LdfDatabase(config.interface.ldf_path).frame_lengths())
            except Exception as e:
                # Don't fail connect just because the LDF couldn't be parsed —
                # the `ldf` fixture will surface the real error if a test asks.
                sys.stderr.write(f"[lin fixture] LDF load failed, ignoring: {e!r}\n")
        if config.interface.frame_lengths:
            merged_lengths.update(config.interface.frame_lengths)
        lin = MumLinInterface(
            host=config.interface.host,
            lin_device=config.interface.lin_device,
            power_device=config.interface.power_device,
            baudrate=config.interface.bitrate,
            boot_settle_seconds=config.interface.boot_settle_seconds,
            frame_lengths=merged_lengths or None,
        )
    else:
        raise RuntimeError(f"Unknown interface type: {iface_type}")

    lin.connect()
    yield lin
    lin.disconnect()


@pytest.fixture(scope="session")
def ldf(config: EcuTestConfig):
    """Session-scoped LDF database loaded from `interface.ldf_path`.

    Tests that depend on LDF-defined frames request this fixture; tests that
    don't need it can ignore it. Skips with a clear message if `ldf_path`
    isn't set or the file isn't parseable.
    """
    if not config.interface.ldf_path:
        pytest.skip("interface.ldf_path is not set in config")
    # Resolve relative paths against the workspace root for convenience.
    p = pathlib.Path(config.interface.ldf_path)
    if not p.is_absolute():
        p = (WORKSPACE_ROOT / p).resolve()
    if not p.is_file():
        pytest.skip(f"LDF file not found: {p}")
    try:
        from ecu_framework.lin.ldf import LdfDatabase
    except Exception as e:
        pytest.skip(f"ldfparser not available: {e!r}")
    return LdfDatabase(p)


@pytest.fixture(scope="session", autouse=False)
def flash_ecu(config: EcuTestConfig, lin: LinInterface) -> None:
    if not config.flash.enabled:
        pytest.skip("Flashing disabled in config")
    # Lazy import to avoid dependency during mock-only runs
    from ecu_framework.flashing import HexFlasher

    if not config.flash.hex_path:
        pytest.skip("No HEX path provided in config")

    flasher = HexFlasher(lin)
    ok = flasher.flash_hex(config.flash.hex_path)
    if not ok:
        pytest.fail("ECU flashing failed")


@pytest.fixture
def rp(record_property: "pytest.RecordProperty"):
    """Convenience reporter: attaches a key/value as a test property and echoes to captured output.

    Usage in tests:
        def test_something(rp):
            rp("key", value)
    """
    def _rp(key: str, value):
        # Attach property (pytest-html will show in Properties table)
        record_property(str(key), value)
        # Echo to captured output for quick scanning in report details
        try:
            print(f"[prop] {key}={value}")
        except Exception:
            pass
    return _rp
