from __future__ import annotations

import re
import socket
import time

from elmpythonator.service import ELMService
from pymulator.core.bus import CANBusEmulation
from pymulator.core.can import NullInterface
from pymulator.core.uds import ServiceId
from pymulator.doip import server as doip_server
from pymulator.hsfz import server as hsfz_server

from .conftest import attach_ecu, load_handler


def _wait_for_socket(getter, timeout: float = 1.0):
    deadline = time.time() + timeout
    while time.time() < deadline:
        sock = getter()
        if sock is not None:
            return sock
        time.sleep(0.01)
    raise AssertionError("Timed out waiting for server socket")


def _wait_for_bound_port(sock: socket.socket, timeout: float = 1.0) -> int:
    deadline = time.time() + timeout
    while time.time() < deadline:
        port = sock.getsockname()[1]
        if port != 0:
            return port
        time.sleep(0.01)
    raise AssertionError("Timed out waiting for bound port")


def _read_until(sock: socket.socket, terminator: bytes, timeout: float = 1.0) -> bytes:
    sock.settimeout(timeout)
    buffer = bytearray()
    while not buffer.endswith(terminator):
        chunk = sock.recv(1024)
        if not chunk:
            break
        buffer.extend(chunk)
    return bytes(buffer)


def _wait_for_disconnect(sock: socket.socket, timeout: float = 1.0) -> bool:
    deadline = time.time() + timeout
    sock.settimeout(0.1)
    while time.time() < deadline:
        try:
            data = sock.recv(1)
        except (ConnectionResetError, OSError):
            return True
        if data == b"":
            return True
    return False


def test_doip_end_to_end(monkeypatch):
    bus = CANBusEmulation(NullInterface())
    session = load_handler("uds.json")
    attach_ecu(bus, "UDS", session)
    monkeypatch.setattr(doip_server, "DOIP_PORT_UDP", 0)

    server = doip_server.DoIPServer(bus, host="127.0.0.1", port=0)
    server.start()
    try:
        server_socket = _wait_for_socket(lambda: server._socket)
        port = _wait_for_bound_port(server_socket)
        client = socket.create_connection(("127.0.0.1", port), timeout=1.0)
        with client:
            tester_addr = 0x0E00
            payload = tester_addr.to_bytes(2, "big") + b"\x00"
            client.sendall(
                doip_server._build_header(doip_server.DOIP_PAYLOAD_ROUTING_ACTIVATION_REQUEST, len(payload)) + payload
            )
            message = doip_server.DoIPMessage.from_socket(client)
            assert message is not None
            assert message.payload_type == doip_server.DOIP_PAYLOAD_ROUTING_ACTIVATION_RESPONSE
            assert message.payload[:2] == tester_addr.to_bytes(2, "big")

            uds_payload = [ServiceId.read_data_by_identifier, 0xF1, 0x90]
            target = int(session.arbitration_id)
            diag_payload = tester_addr.to_bytes(2, "big") + target.to_bytes(2, "big") + bytes(uds_payload)
            client.sendall(
                doip_server._build_header(doip_server.DOIP_PAYLOAD_DIAGNOSTIC_MESSAGE, len(diag_payload))
                + diag_payload
            )
            first = doip_server.DoIPMessage.from_socket(client)
            assert first is not None
            if first.payload_type == doip_server.DOIP_PAYLOAD_DIAGNOSTIC_ACK:
                ack = first
                response = doip_server.DoIPMessage.from_socket(client)
            else:
                response = first
                ack = doip_server.DoIPMessage.from_socket(client)

            assert ack is not None
            assert response is not None
            assert ack.payload_type == doip_server.DOIP_PAYLOAD_DIAGNOSTIC_ACK
            assert response.payload_type == doip_server.DOIP_PAYLOAD_DIAGNOSTIC_MESSAGE
            assert response.payload[4:7] == bytes([0x62, 0xF1, 0x90])
    finally:
        server.stop()
        bus.shutdown()


def test_doip_routing_activation_invalid_source(monkeypatch):
    bus = CANBusEmulation(NullInterface())
    monkeypatch.setattr(doip_server, "DOIP_PORT_UDP", 0)

    server = doip_server.DoIPServer(bus, host="127.0.0.1", port=0, tester_address=0x3001)
    server.start()
    try:
        server_socket = _wait_for_socket(lambda: server._socket)
        port = _wait_for_bound_port(server_socket)
        client = socket.create_connection(("127.0.0.1", port), timeout=1.0)
        with client:
            tester_addr = 0x3000
            payload = tester_addr.to_bytes(2, "big") + b"\x00"
            client.sendall(
                doip_server._build_header(doip_server.DOIP_PAYLOAD_ROUTING_ACTIVATION_REQUEST, len(payload)) + payload
            )
            message = doip_server.DoIPMessage.from_socket(client)
            assert message is not None
            assert message.payload_type == doip_server.DOIP_PAYLOAD_ROUTING_ACTIVATION_RESPONSE
            assert message.payload[:2] == tester_addr.to_bytes(2, "big")
            assert message.payload[4] == doip_server.DOIP_ROUTING_INVALID_SOURCE
            assert _wait_for_disconnect(client, timeout=1.0)
    finally:
        server.stop()
        bus.shutdown()


def test_hsfz_end_to_end(monkeypatch):
    bus = CANBusEmulation(NullInterface())
    session = load_handler("uds.json")
    session._arbitration_id = 0x10
    session._reply_id = 0x11
    session.reset_arbitration_id = 0x10
    session.reset_reply_id = 0x11
    attach_ecu(bus, "UDS", session)
    monkeypatch.setattr(hsfz_server, "HSFZ_PORT_UDP", 0)

    server = hsfz_server.HSFZServer(bus, host="127.0.0.1", port=0)
    server.start()
    try:
        server_socket = _wait_for_socket(lambda: server._socket)
        port = _wait_for_bound_port(server_socket)
        client = socket.create_connection(("127.0.0.1", port), timeout=1.0)
        with client:
            request = hsfz_server.HSFZMessage(
                msg_type=hsfz_server.HSFZ_TYPE_DIAGNOSTIC_REQ_RES,
                source=0xF1,
                destination=session.arbitration_id,
                payload=[ServiceId.read_data_by_identifier, 0xF1, 0x90],
            )
            client.sendall(request.bytes)
            echo = hsfz_server.HSFZMessage.from_socket(client)
            assert echo is not None
            assert echo.msg_type == hsfz_server.HSFZ_TYPE_ACKNOWLEDGE_TRANSFER

            response = hsfz_server.HSFZMessage.from_socket(client)
            assert response is not None
            assert response.msg_type == hsfz_server.HSFZ_TYPE_DIAGNOSTIC_REQ_RES
            assert response.payload[:3] == [0x62, 0xF1, 0x90]
            assert response.source == session.reply_id
            assert response.destination == 0xF1
    finally:
        server.stop()
        bus.shutdown()


def test_elm327_end_to_end():
    bus = CANBusEmulation(NullInterface())
    service = ELMService(bus, host="127.0.0.1", port=0)
    service.start()
    try:
        server_socket = _wait_for_socket(lambda: service.server.server)
        port = _wait_for_bound_port(server_socket)
        snapshot = service.ui_snapshot()
        assert snapshot.get("listen_host") == "127.0.0.1"
        assert snapshot.get("listen_display_host") == "127.0.0.1"
        assert snapshot.get("listen_port") == port
        assert snapshot.get("listen_is_wildcard") is False
        client = socket.create_connection(("127.0.0.1", port), timeout=1.0)
        with client:
            client.sendall(b"ATI\r")
            data = _read_until(client, b">", timeout=1.0)
            assert b"ELM327 1.4a" in data
            client.sendall(b"STMFR\r")
            data = _read_until(client, b">", timeout=1.0)
            assert b"OBD Solutions LLC" in data
            client.sendall(b"ATSH0007DF\r")
            data = _read_until(client, b">", timeout=1.0)
            assert b"OK" in data
            assert data.endswith(b">")
    finally:
        service.stop()
        bus.shutdown()


def test_elm327_custom_reported_identity_and_voltage():
    bus = CANBusEmulation(NullInterface())
    service = ELMService(
        bus,
        host="127.0.0.1",
        port=0,
        ati_version="ELM327 v2.3-custom",
        at1_descriptor="Custom RS232 Bridge",
        at2_name="BenchAdapter",
        atrv_voltage="13.2V",
        sti_version="STN2230 5.0.0",
        stix_version="STN2230 5.0.0 [01.01.2026]",
        stsn_serial="SN000042424242",
        stmfr_manufacturer="Bench Labs Inc",
        atign_state="OFF",
    )
    service.start()
    try:
        server_socket = _wait_for_socket(lambda: service.server.server)
        port = _wait_for_bound_port(server_socket)
        client = socket.create_connection(("127.0.0.1", port), timeout=1.0)
        with client:
            for command, expected in (
                (b"ATI\r", b"ELM327 v2.3-custom"),
                (b"AT@1\r", b"Custom RS232 Bridge"),
                (b"AT@2\r", b"BenchAdapter"),
                (b"ATIGN\r", b"OFF"),
                (b"ATRV\r", b"13.2V"),
                (b"STI\r", b"STN2230 5.0.0"),
                (b"STIX\r", b"STN2230 5.0.0 [01.01.2026]"),
                (b"STSN\r", b"SN000042424242"),
                (b"STMFR\r", b"Bench Labs Inc"),
                (b"STUIL1\r", b"OK"),
                (b"STDIX\r", b"Device:       ELM327/STN2xxx"),
            ):
                client.sendall(command)
                data = _read_until(client, b">", timeout=1.0)
                assert expected in data
                assert data.endswith(b">")
    finally:
        service.stop()
        bus.shutdown()


def test_elm327_connection_transcript_per_tcp_connection(tmp_path):
    bus = CANBusEmulation(NullInterface())
    service = ELMService(bus, host="127.0.0.1", port=0, log_directory=str(tmp_path))
    service.start()
    try:
        server_socket = _wait_for_socket(lambda: service.server.server)
        port = _wait_for_bound_port(server_socket)
        client = socket.create_connection(("127.0.0.1", port), timeout=1.0)
        with client:
            client.sendall(b"ATI\r")
            data = _read_until(client, b">", timeout=1.0)
            assert b"ELM327 1.4a" in data
            assert data.endswith(b">")
    finally:
        service.stop()
        bus.shutdown()

    logs = sorted(tmp_path.glob("elm327-*.log"))
    assert len(logs) == 1
    assert re.fullmatch(r"elm327-127_0_0_1-\d{8}-\d{6}-\d{6}\.log", logs[0].name) is not None

    contents = logs[0].read_text(encoding="utf-8")
    assert "CMD ATI" in contents
    assert "RX " in contents
    assert "TX " in contents
    assert "ELM327 1.4a" in contents
