from __future__ import annotations

import socket
import time

from pymulator.core.bus import CANBusEmulation
from pymulator.core.can import NullInterface
from pymulator.ecuconnect.protocol import ATT, PDU, PDUType
from pymulator.ecuconnect.server import ECUconnectServer


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 _recv_exact(sock: socket.socket, length: int, timeout: float = 1.0) -> bytes:
    sock.settimeout(timeout)
    data = bytearray()
    while len(data) < length:
        chunk = sock.recv(length - len(data))
        if not chunk:
            raise AssertionError("Socket closed before full payload")
        data.extend(chunk)
    return bytes(data)


def _recv_pdu(sock: socket.socket, timeout: float = 1.0) -> PDU:
    header = _recv_exact(sock, 4, timeout=timeout)
    assert header[0] == ATT
    length = int.from_bytes(header[2:4], "big")
    payload = _recv_exact(sock, length, timeout=timeout)
    return PDU.from_bytes(header + payload)


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_ecuconnect_update_then_reset_changes_reported_firmware() -> None:
    bus = CANBusEmulation(NullInterface())
    server = ECUconnectServer(
        bus,
        host="127.0.0.1",
        port=0,
        firmware_version="0.9.999",
        update_prepare_delay_seconds=0.0,
        update_chunk_delay_seconds=0.0,
        update_commit_delay_seconds=0.0,
    )
    server.start()
    try:
        server_socket = _wait_for_socket(lambda: server._socket)
        port = _wait_for_bound_port(server_socket)

        first = socket.create_connection(("127.0.0.1", port), timeout=1.0)
        with first:
            blob = bytearray(128)
            blob[0x30:0x3A] = b"0.6.5.213\x00"
            blob[0x50:0x64] = b"ECUconnectOS-ecuconnect\x00"

            first.sendall(PDU(PDUType.prepare_for_update, []).frame)
            assert _recv_pdu(first).pdu_type == PDUType.ok

            first.sendall(PDU(PDUType.send_update_data, list(blob)).frame)
            assert _recv_pdu(first).pdu_type == PDUType.ok

            first.sendall(PDU(PDUType.commit_update, []).frame)
            assert _recv_pdu(first).pdu_type == PDUType.ok

            first.sendall(PDU(PDUType.reset, []).frame)
            assert _recv_pdu(first).pdu_type == PDUType.ok
            assert _wait_for_disconnect(first, timeout=1.0)

        second = socket.create_connection(("127.0.0.1", port), timeout=1.0)
        with second:
            second.sendall(PDU(PDUType.request_info, []).frame)
            info = _recv_pdu(second)
            assert info.pdu_type == PDUType.info
            lines = bytes(info.payload).decode("utf-8").splitlines()
            assert lines[4] == "0.6.5.213"
    finally:
        server.stop()
        bus.shutdown()
