from __future__ import annotations

from urllib.error import URLError

import pymulator.enet_service_broker.server as enet_server_module
import pytest
from pymulator.enet_service_broker.server import ENETServiceBrokerService, resolve_broker_endpoint
from pymulator.sidecar.daemon import SidecarRuntime, _build_light_snapshot
from pymulator.ui.status import build_bus_snapshot


class _DummyBus:
    participants = {}
    last_activity = 0.0

    def attach(self, _participant) -> None:
        return None

    def detach(self, _participant) -> None:
        return None

    def write(self, _frame, originator=None) -> None:
        return None

    def recent_frames(self) -> list[str]:
        return []


class _FakeResponse:
    def __init__(self, payload: str, status: int = 200) -> None:
        self.status = status
        self._payload = payload.encode("utf-8")

    def __enter__(self) -> "_FakeResponse":
        return self

    def __exit__(self, exc_type, exc, tb) -> bool:
        return False

    def read(self) -> bytes:
        return self._payload


def test_enet_service_snapshot_reports_port_request_error(monkeypatch) -> None:
    service = ENETServiceBrokerService(
        _DummyBus(),
        vin="WBADE6324VBW12345",
        broker_url="https://esb.invalid.local",
        mode="raw",
    )

    def _failing_urlopen(_request, timeout=0.0):  # noqa: ANN001
        raise URLError("Name or service not known")

    monkeypatch.setattr(enet_server_module, "urlopen", _failing_urlopen)

    endpoint = resolve_broker_endpoint(service.broker_url)
    assert service._request_ports(endpoint) is None

    snapshot = service.ui_snapshot()
    assert snapshot["error"] is not None
    assert "Port request failed" in str(snapshot["error"])


def test_enet_service_snapshot_clears_error_after_successful_port_request(monkeypatch) -> None:
    service = ENETServiceBrokerService(
        _DummyBus(),
        vin="WBADE6324VBW12345",
        broker_url="https://esb.invalid.local",
        mode="raw",
    )

    def _failing_urlopen(_request, timeout=0.0):  # noqa: ANN001
        raise URLError("Name or service not known")

    monkeypatch.setattr(enet_server_module, "urlopen", _failing_urlopen)
    endpoint = resolve_broker_endpoint(service.broker_url)
    assert service._request_ports(endpoint) is None
    assert service.ui_snapshot()["error"] is not None

    success_payload = (
        '{"vehicle":{"ip":"127.0.0.1","port":7001},"tester":{"ip":"127.0.0.1","port":8001}}'
    )

    def _successful_urlopen(_request, timeout=0.0):  # noqa: ANN001
        return _FakeResponse(success_payload, status=200)

    monkeypatch.setattr(enet_server_module, "urlopen", _successful_urlopen)
    assert service._request_ports(endpoint) == ("127.0.0.1", 7001, 8001)
    assert service.ui_snapshot()["error"] is None


def test_enet_errors_are_included_in_full_and_light_snapshots() -> None:
    bus = _DummyBus()

    class _DummyService:
        def ui_snapshot(self) -> dict[str, object]:
            return {
                "connected": False,
                "clients": 0,
                "last_rx": None,
                "last_tx": None,
                "rx_bytes": 0,
                "tx_bytes": 0,
                "tester_port": None,
                "broker_host": "192.168.1.17",
                "broker_name": "esb-lab.local",
                "listen_host": "0.0.0.0",
                "listen_display_host": "192.168.1.17",
                "listen_port": 35000,
                "listen_is_wildcard": True,
                "error": "Port request failed: Name or service not known",
            }

    service = _DummyService()

    full = build_bus_snapshot(bus, services=[("ENET Service Broker", service)])
    assert full.services[0].error == "Port request failed: Name or service not known"
    assert full.services[0].broker_host == "192.168.1.17"
    assert full.services[0].broker_name == "esb-lab.local"

    light = _build_light_snapshot(bus, services=[("ENET Service Broker", service)])
    assert light["services"][0]["error"] == "Port request failed: Name or service not known"
    assert light["services"][0]["broker_host"] == "192.168.1.17"
    assert light["services"][0]["broker_name"] == "esb-lab.local"
    assert light["services"][0]["listen_host"] == "0.0.0.0"
    assert light["services"][0]["listen_display_host"] == "192.168.1.17"
    assert light["services"][0]["listen_port"] == 35000
    assert light["services"][0]["listen_is_wildcard"] is True


def test_enet_resolve_endpoint_uses_zeroconf_when_url_override_empty(monkeypatch: pytest.MonkeyPatch) -> None:
    service = ENETServiceBrokerService(
        _DummyBus(),
        vin="WBADE6324VBW12345",
        broker_url="",
        mode="raw",
    )
    expected = enet_server_module.BrokerEndpoint(
        base_url="http://127.0.0.1:8090",
        stream_host="127.0.0.1",
    )

    monkeypatch.setattr(enet_server_module, "discover_broker_endpoint", lambda: expected)

    assert service._resolve_endpoint() == expected


def test_enet_resolve_endpoint_uses_url_override_when_present() -> None:
    service = ENETServiceBrokerService(
        _DummyBus(),
        vin="WBADE6324VBW12345",
        broker_url="https://esb.example.invalid",
        mode="raw",
    )

    endpoint = service._resolve_endpoint()
    assert endpoint.base_url == "https://esb.example.invalid"
    assert endpoint.stream_host == "esb.example.invalid"


def test_discover_broker_endpoint_requires_zeroconf_dependency(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(enet_server_module, "Zeroconf", None)

    with pytest.raises(enet_server_module.BrokerDiscoveryError, match="Install zeroconf"):
        enet_server_module.discover_broker_endpoint()


def test_discover_broker_endpoint_uses_service_record(monkeypatch: pytest.MonkeyPatch) -> None:
    class _FakeServiceInfo:
        port = 8090
        properties = {b"scheme": b"http"}
        server = "esb.local."

        @staticmethod
        def parsed_addresses() -> list[str]:
            return ["192.168.1.77"]

    calls: list[tuple[str, str, int]] = []

    class _FakeZeroconf:
        def get_service_info(self, service_type: str, full_name: str, timeout: int = 0):
            calls.append((service_type, full_name, timeout))
            return _FakeServiceInfo()

        def close(self) -> None:
            return None

    monkeypatch.setattr(enet_server_module, "Zeroconf", _FakeZeroconf)

    endpoint = enet_server_module.discover_broker_endpoint("enet-service-broker._enetbroker._tcp", timeout_s=1.5)
    assert endpoint.base_url == "http://192.168.1.77:8090"
    assert endpoint.stream_host == "192.168.1.77"
    assert calls == [("_enetbroker._tcp.local.", "enet-service-broker._enetbroker._tcp.local.", 1500)]


def test_sidecar_runtime_discover_enet_broker_returns_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
    expected = enet_server_module.BrokerEndpoint(
        base_url="http://192.168.1.77:8090",
        stream_host="192.168.1.77",
    )
    monkeypatch.setattr(enet_server_module, "discover_broker_endpoint", lambda: expected)
    monkeypatch.setattr(enet_server_module, "_resolve_host_name", lambda _host: "esb-lab.local")

    runtime = SidecarRuntime()
    assert runtime.discover_enet_broker() == {
        "url": "http://192.168.1.77:8090",
        "host": "192.168.1.77",
        "name": "esb-lab.local",
    }
