from __future__ import annotations

import pytest

from pymulator.sidecar.daemon import SidecarError, SidecarRuntime, _bus_signature, _validate_services_for_transport_profile


def test_ecuconnect_signature_uses_reported_identity_settings() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()

    base = {
        "ecuconnect": True,
        "ecuconnectHost": "0.0.0.0",
        "ecuconnectPort": 8129,
        "ecuconnectVersion": "0.9.999",
        "ecuconnectHardwareRevision": "PVIRT",
    }
    changed_version = {**base, "ecuconnectVersion": "1.2.3"}
    changed_hardware = {**base, "ecuconnectHardwareRevision": "HW-REV-A"}

    assert runtime._service_signature("ecuconnect", base, setup_stub) != runtime._service_signature(
        "ecuconnect", changed_version, setup_stub
    )
    assert runtime._service_signature("ecuconnect", base, setup_stub) != runtime._service_signature(
        "ecuconnect", changed_hardware, setup_stub
    )


def test_ecuconnect_signature_normalizes_empty_identity_settings() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()

    implicit_defaults = {
        "ecuconnect": True,
        "ecuconnectHost": "0.0.0.0",
        "ecuconnectPort": 8129,
    }
    explicit_empty_values = {
        **implicit_defaults,
        "ecuconnectVersion": "   ",
        "ecuconnectHardwareRevision": "",
    }

    assert runtime._service_signature("ecuconnect", implicit_defaults, setup_stub) == runtime._service_signature(
        "ecuconnect", explicit_empty_values, setup_stub
    )


def test_reported_identity_event_updates_runtime_service_options() -> None:
    runtime = SidecarRuntime()
    runtime._options = {
        "services": {
            "ecuconnect": True,
            "ecuconnectVersion": "0.9.999",
            "ecuconnectHardwareRevision": "PVIRT",
        }
    }

    runtime._on_service_event(
        "ECUconnect",
        {
            "kind": "reported_identity_changed",
            "timestamp": 1700000000.0,
            "firmware": "0.6.5.213",
            "hardware": "S3/VM",
            "source": "firmware_update",
        },
    )

    services = runtime._options.get("services", {})
    assert services.get("ecuconnectVersion") == "0.6.5.213"
    assert services.get("ecuconnectHardwareRevision") == "S3/VM"
    assert services.get("ecuconnectVersionFromUpdate") is True
    assert services.get("ecuconnectVersionUpdatedAt") == "2023-11-14T22:13:20Z"


def test_elm327_signature_uses_reported_identity_and_voltage_settings() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()

    base = {
        "elm327": True,
        "elm327Host": "0.0.0.0",
        "elm327Port": 35000,
        "elm327Name": "ELM327/STN2xxx",
        "elm327StartupFile": "",
        "elm327LogDirectory": "",
        "elm327AtiVersion": "ELM327 1.4a",
        "elm327At1Descriptor": "OBDII to RS232 Interpreter",
        "elm327At2Name": "ELMpythonator",
        "elm327AtignState": "ON",
        "elm327AtrvVoltage": "12.1V",
        "elm327StiVersion": "STN2200 2.2.0",
        "elm327StixVersion": "STN2200 2.2.0 [26.12.1971]",
        "elm327StsnSerial": "123456789012",
        "elm327StmfrManufacturer": "OBD Solutions LLC",
    }
    changed_ati = {**base, "elm327AtiVersion": "ELM327 v2.0"}
    changed_ign = {**base, "elm327AtignState": "OFF"}
    changed_voltage = {**base, "elm327AtrvVoltage": "13.7V"}
    changed_serial = {**base, "elm327StsnSerial": "SN4242424242"}
    changed_manufacturer = {**base, "elm327StmfrManufacturer": "Acme Diagnostics"}
    changed_log_directory = {**base, "elm327LogDirectory": "/tmp/elm327-logs"}

    assert runtime._service_signature("elm327", base, setup_stub) != runtime._service_signature(
        "elm327", changed_ati, setup_stub
    )
    assert runtime._service_signature("elm327", base, setup_stub) != runtime._service_signature(
        "elm327", changed_ign, setup_stub
    )
    assert runtime._service_signature("elm327", base, setup_stub) != runtime._service_signature(
        "elm327", changed_voltage, setup_stub
    )
    assert runtime._service_signature("elm327", base, setup_stub) != runtime._service_signature(
        "elm327", changed_serial, setup_stub
    )
    assert runtime._service_signature("elm327", base, setup_stub) != runtime._service_signature(
        "elm327", changed_manufacturer, setup_stub
    )
    assert runtime._service_signature("elm327", base, setup_stub) != runtime._service_signature(
        "elm327", changed_log_directory, setup_stub
    )


def test_elm327_signature_normalizes_empty_identity_values_to_defaults() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()

    implicit_defaults = {
        "elm327": True,
        "elm327Host": "0.0.0.0",
        "elm327Port": 35000,
        "elm327Name": "ELM327/STN2xxx",
        "elm327StartupFile": "",
        "elm327LogDirectory": "",
    }
    explicit_empty_values = {
        **implicit_defaults,
        "elm327AtiVersion": " ",
        "elm327At1Descriptor": "",
        "elm327At2Name": "",
        "elm327AtignState": "",
        "elm327AtrvVoltage": "   ",
        "elm327StiVersion": "",
        "elm327StixVersion": "",
        "elm327StsnSerial": " ",
        "elm327StmfrManufacturer": "   ",
        "elm327LogDirectory": "   ",
    }

    assert runtime._service_signature("elm327", implicit_defaults, setup_stub) == runtime._service_signature(
        "elm327", explicit_empty_values, setup_stub
    )


def test_enet_service_broker_signature_uses_vin_and_url() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()

    base = {
        "enetServiceBroker": True,
        "enetServiceBrokerVin": "WBADE6324VBW12345",
        "enetServiceBrokerUrl": "",
        "enetServiceBrokerMode": "isotp",
    }
    changed_vin = {**base, "enetServiceBrokerVin": "WBADE6324VBW54321"}
    changed_url = {**base, "enetServiceBrokerUrl": "https://esb.example.invalid"}
    changed_mode = {**base, "enetServiceBrokerMode": "raw"}

    assert runtime._service_signature("enetServiceBroker", base, setup_stub) != runtime._service_signature(
        "enetServiceBroker", changed_vin, setup_stub
    )
    assert runtime._service_signature("enetServiceBroker", base, setup_stub) != runtime._service_signature(
        "enetServiceBroker", changed_url, setup_stub
    )
    assert runtime._service_signature("enetServiceBroker", base, setup_stub) != runtime._service_signature(
        "enetServiceBroker", changed_mode, setup_stub
    )


def test_enet_service_broker_requires_vin() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()
    services = {
        "enetServiceBroker": True,
        "enetServiceBrokerVin": "",
        "enetServiceBrokerUrl": "",
    }

    with pytest.raises(SidecarError, match="VIN is required for ENET Service Broker"):
        runtime._create_service_instance("enetServiceBroker", object(), setup_stub, services, log=False)


def test_external_can_signature_uses_mode_and_bitrates() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()
    base = {
        "externalCan": True,
        "externalCanInterface": "can0",
        "externalCanMode": "classic",
        "externalCanAccessMode": "normal",
        "externalCanBitrate": 500000,
    }
    changed_mode = {**base, "externalCanMode": "fd"}
    changed_access_mode = {**base, "externalCanAccessMode": "listen_only"}
    changed_bitrate = {**base, "externalCanBitrate": 250000}
    changed_data_bitrate = {**changed_mode, "externalCanDataBitrate": 2000000}
    changed_termination = {**base, "externalCanTermination": True}

    assert runtime._service_signature("externalCan", base, setup_stub) != runtime._service_signature(
        "externalCan", changed_mode, setup_stub
    )
    assert runtime._service_signature("externalCan", base, setup_stub) != runtime._service_signature(
        "externalCan", changed_bitrate, setup_stub
    )
    assert runtime._service_signature("externalCan", base, setup_stub) != runtime._service_signature(
        "externalCan", changed_access_mode, setup_stub
    )
    assert runtime._service_signature("externalCan", changed_mode, setup_stub) != runtime._service_signature(
        "externalCan", changed_data_bitrate, setup_stub
    )
    assert runtime._service_signature("externalCan", base, setup_stub) != runtime._service_signature(
        "externalCan", changed_termination, setup_stub
    )


def test_external_can_signature_rejects_invalid_mode_or_bitrates() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()
    with pytest.raises(SidecarError, match="externalCanMode must be one of"):
        runtime._service_signature(
            "externalCan",
            {
                "externalCan": True,
                "externalCanInterface": "can0",
                "externalCanMode": "invalid",
            },
            setup_stub,
        )
    with pytest.raises(SidecarError, match="externalCanAccessMode must be one of"):
        runtime._service_signature(
            "externalCan",
            {
                "externalCan": True,
                "externalCanInterface": "can0",
                "externalCanMode": "classic",
                "externalCanAccessMode": "invalid",
                "externalCanBitrate": 500000,
            },
            setup_stub,
        )
    with pytest.raises(SidecarError, match="externalCanBitrate must be greater than 0"):
        runtime._service_signature(
            "externalCan",
            {
                "externalCan": True,
                "externalCanInterface": "can0",
                "externalCanMode": "classic",
                "externalCanBitrate": 0,
            },
            setup_stub,
        )
    with pytest.raises(SidecarError, match="externalCanDataBitrate must be greater than 0"):
        runtime._service_signature(
            "externalCan",
            {
                "externalCan": True,
                "externalCanInterface": "can0",
                "externalCanMode": "fd",
                "externalCanBitrate": 500000,
                "externalCanDataBitrate": 0,
            },
            setup_stub,
        )


def test_external_can_signature_ignores_mode_and_bitrate_when_disabled() -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()
    signature = runtime._service_signature(
        "externalCan",
        {
            "externalCan": False,
            "externalCanInterface": "can0",
            "externalCanMode": "invalid",
            "externalCanAccessMode": "invalid",
            "externalCanBitrate": 0,
            "externalCanDataBitrate": 0,
        },
        setup_stub,
    )
    assert signature == (False, "can0")


def test_external_can_open_error_includes_interface_mode_and_bitrates(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()

    class _BusStub:
        wires = object()

    class _BridgeStub:
        def __init__(
            self,
            bus,  # noqa: ANN001
            interface: str,
            bitrate: int,
            fd_mode: bool = False,
            data_bitrate: int | None = None,
            listen_only: bool = False,
            termination: bool | None = None,
            log: bool = False,
        ) -> None:
            del bus, log, termination
            self.interface = interface
            self.bitrate = bitrate
            self.fd_mode = fd_mode
            self.data_bitrate = data_bitrate
            self.listen_only = listen_only

        def start(self) -> bool:
            raise RuntimeError("No suitable bit timings found.")

    monkeypatch.setattr("pymulator.sidecar.daemon.ExternalCANBridge", _BridgeStub)
    monkeypatch.setattr("pymulator.sidecar.daemon.missing_python_can_modules", lambda _interface: [])
    monkeypatch.setattr("pymulator.sidecar.daemon.selected_python_can_backend", lambda _interface: "socketcan")

    with pytest.raises(
        SidecarError,
        match=(
            r"External CAN open failed "
            r"\(interface=can0, mode=fd, access_mode=listen_only, bitrate=666666, termination=off, data_bitrate=2000000\): "
            r"No suitable bit timings found\."
        ),
    ):
        runtime._create_service_instance(
            "externalCan",
            _BusStub(),
            setup_stub,
            {
                "externalCan": True,
                "externalCanInterface": "can0",
                "externalCanMode": "fd",
                "externalCanAccessMode": "listen_only",
                "externalCanBitrate": 666666,
                "externalCanDataBitrate": 2000000,
            },
            log=False,
        )


def test_external_can_keeps_fd_mode_for_gs_usb_backend(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    runtime = SidecarRuntime()
    setup_stub = type("SetupStub", (), {"_path_map": {}})()

    class _BusStub:
        wires = object()

    created: dict[str, object] = {}

    class _BridgeStub:
        def __init__(
            self,
            bus,  # noqa: ANN001
            interface: str,
            bitrate: int,
            fd_mode: bool = False,
            data_bitrate: int | None = None,
            listen_only: bool = False,
            termination: bool | None = None,
            log: bool = False,
        ) -> None:
            del bus, log
            created["interface"] = interface
            created["bitrate"] = bitrate
            created["fd_mode"] = fd_mode
            created["data_bitrate"] = data_bitrate
            created["listen_only"] = listen_only
            created["termination"] = termination

        def start(self) -> bool:
            return True

        def stop(self) -> None:
            return

    monkeypatch.setattr("pymulator.sidecar.daemon.ExternalCANBridge", _BridgeStub)
    monkeypatch.setattr("pymulator.sidecar.daemon.missing_python_can_modules", lambda _interface: [])
    monkeypatch.setattr("pymulator.sidecar.daemon.selected_python_can_backend", lambda _interface: "gs_usb")

    runtime._create_service_instance(
        "externalCan",
        _BusStub(),
        setup_stub,
        {
            "externalCan": True,
            "externalCanInterface": "gs_usb:0",
            "externalCanMode": "fd",
            "externalCanAccessMode": "listen_only",
            "externalCanBitrate": 500000,
            "externalCanDataBitrate": 2000000,
        },
        log=False,
    )

    assert created == {
        "interface": "gs_usb:0",
        "bitrate": 500000,
        "fd_mode": True,
        "data_bitrate": 2000000,
        "listen_only": True,
        "termination": False,
    }


def test_bus_signature_defaults_transport_profile_to_isotp() -> None:
    default_signature = _bus_signature({"mode": "virtual", "bitrate": 500000, "timingFactor": 0})
    explicit_signature = _bus_signature(
        {
            "mode": "virtual",
            "bitrate": 500000,
            "timingFactor": 0,
            "transportProfile": "isotp",
        }
    )
    assert default_signature == explicit_signature


def test_bus_signature_changes_when_transport_profile_changes() -> None:
    classic = _bus_signature({"mode": "virtual", "bitrate": 500000, "timingFactor": 0, "transportProfile": "isotp"})
    fd = _bus_signature({"mode": "virtual", "bitrate": 500000, "timingFactor": 0, "transportProfile": "isotp_fd"})
    assert classic != fd


def test_transport_profile_validation_rejects_hsfz_doip_or_elm327_on_isotp_fd() -> None:
    with pytest.raises(SidecarError, match="HSFZ, DoIP, and ELM327 cannot be enabled"):
        _validate_services_for_transport_profile({"hsfz": True}, "isotp_fd")
    with pytest.raises(SidecarError, match="HSFZ, DoIP, and ELM327 cannot be enabled"):
        _validate_services_for_transport_profile({"doip": True}, "isotp_fd")
    with pytest.raises(SidecarError, match="HSFZ, DoIP, and ELM327 cannot be enabled"):
        _validate_services_for_transport_profile({"elm327": True}, "isotp_fd")


def test_transport_profile_validation_allows_hsfz_doip_and_elm327_on_classic_isotp() -> None:
    _validate_services_for_transport_profile({"hsfz": True, "doip": True, "elm327": True}, "isotp")
