"""Tests for the HLE wire protocol."""

import pytest
from pydantic import ValidationError

from hle_common.protocol import (
    PROTOCOL_VERSION,
    ErrorPayload,
    HttpRequest,
    HttpResponse,
    MessageType,
    ProtocolMessage,
    TunnelOpenRequest,
    TunnelOpenResponse,
    WsFrame,
)


class TestProtocolVersion:
    def test_protocol_version_exists(self):
        assert PROTOCOL_VERSION == "1.1"

    def test_protocol_version_is_string(self):
        assert isinstance(PROTOCOL_VERSION, str)


class TestMessageType:
    def test_message_types_are_strings(self):
        assert MessageType.AUTH_REQUEST == "auth_request"
        assert MessageType.WS_FRAME == "ws_frame"
        assert MessageType.WEBHOOK_INCOMING == "webhook_incoming"

    def test_tunnel_register_type(self):
        assert MessageType.TUNNEL_REGISTER == "tunnel_register"

    def test_all_message_types_exist(self):
        expected = {
            "auth_request",
            "auth_response",
            "tunnel_open",
            "tunnel_close",
            "tunnel_ack",
            "tunnel_register",
            "http_request",
            "http_response",
            "http_response_start",
            "http_response_chunk",
            "http_response_end",
            "ws_open",
            "ws_close",
            "ws_frame",
            "webhook_incoming",
            "webhook_response",
            "speed_test_data",
            "speed_test_result",
            "ping",
            "pong",
            "error",
        }
        actual = {member.value for member in MessageType}
        assert actual == expected


class TestProtocolMessage:
    def test_minimal_message(self):
        msg = ProtocolMessage(type=MessageType.PING)
        assert msg.type == MessageType.PING
        assert msg.tunnel_id is None

    def test_full_message(self):
        msg = ProtocolMessage(
            type=MessageType.HTTP_REQUEST,
            tunnel_id="t-123",
            request_id="r-456",
            payload={"key": "value"},
        )
        assert msg.tunnel_id == "t-123"
        assert msg.payload == {"key": "value"}

    def test_payload_accepts_typed_dict(self):
        msg = ProtocolMessage(
            type=MessageType.ERROR,
            payload={"code": "not_found", "message": "Tunnel not found", "count": 42},
        )
        assert msg.payload["code"] == "not_found"
        assert msg.payload["count"] == 42

    def test_payload_defaults_to_none(self):
        msg = ProtocolMessage(type=MessageType.PONG)
        assert msg.payload is None


class TestTunnelOpenRequest:
    def test_defaults(self):
        req = TunnelOpenRequest(service_url="http://localhost:8080")
        assert req.auth_mode == "sso"
        assert req.websocket_enabled is True
        assert req.domain is None


class TestTunnelOpenResponse:
    def test_all_fields(self):
        resp = TunnelOpenResponse(
            tunnel_id="t-abc",
            subdomain="myapp",
            public_url="https://myapp.hle.example.com",
        )
        assert resp.tunnel_id == "t-abc"
        assert resp.subdomain == "myapp"
        assert resp.public_url == "https://myapp.hle.example.com"

    def test_missing_required_field_raises(self):
        with pytest.raises((TypeError, ValidationError)):
            TunnelOpenResponse(tunnel_id="t-1", subdomain="sub")  # missing public_url

    def test_roundtrip_serialization(self):
        resp = TunnelOpenResponse(
            tunnel_id="t-1",
            subdomain="demo",
            public_url="https://demo.hle.example.com",
        )
        data = resp.model_dump()
        restored = TunnelOpenResponse(**data)
        assert restored == resp


class TestHttpRequest:
    def test_required_fields(self):
        req = HttpRequest(
            request_id="r-1",
            method="GET",
            path="/api/data",
            headers={"Host": "example.com"},
        )
        assert req.request_id == "r-1"
        assert req.method == "GET"
        assert req.body is None
        assert req.query_string == ""

    def test_with_body_and_query_string(self):
        req = HttpRequest(
            request_id="r-2",
            method="POST",
            path="/submit",
            headers={"Content-Type": "application/json"},
            body="eyJrZXkiOiAidmFsdWUifQ==",  # base64 of {"key": "value"}
            query_string="page=1&limit=10",
        )
        assert req.body == "eyJrZXkiOiAidmFsdWUifQ=="
        assert req.query_string == "page=1&limit=10"

    def test_body_is_str_not_bytes(self):
        req = HttpRequest(
            request_id="r-3",
            method="PUT",
            path="/update",
            headers={},
            body="base64content",
        )
        assert isinstance(req.body, str)

    def test_missing_request_id_raises(self):
        with pytest.raises((TypeError, ValidationError)):
            HttpRequest(method="GET", path="/", headers={})


class TestHttpResponse:
    def test_required_fields(self):
        resp = HttpResponse(
            request_id="r-1",
            status_code=200,
            headers={"Content-Type": "text/plain"},
        )
        assert resp.request_id == "r-1"
        assert resp.status_code == 200
        assert resp.body is None

    def test_with_body(self):
        resp = HttpResponse(
            request_id="r-2",
            status_code=201,
            headers={},
            body="cmVzcG9uc2U=",  # base64 of "response"
        )
        assert resp.body == "cmVzcG9uc2U="
        assert isinstance(resp.body, str)

    def test_missing_request_id_raises(self):
        with pytest.raises((TypeError, ValidationError)):
            HttpResponse(status_code=200, headers={})


class TestWsFrame:
    def test_text_frame(self):
        frame = WsFrame(stream_id="s-1", data="hello")
        assert frame.is_binary is False
        assert frame.data == "hello"
        assert frame.path == ""

    def test_binary_frame(self):
        frame = WsFrame(stream_id="s-1", data="AAEB", is_binary=True)
        assert frame.is_binary is True
        assert isinstance(frame.data, str)

    def test_data_is_str(self):
        frame = WsFrame(stream_id="s-1", data="text payload")
        assert isinstance(frame.data, str)

    def test_path_for_ws_open(self):
        frame = WsFrame(stream_id="s-1", data="", path="/ws/notifications")
        assert frame.path == "/ws/notifications"

    def test_no_is_close_attribute(self):
        frame = WsFrame(stream_id="s-1", data="x")
        assert not hasattr(frame, "is_close") or "is_close" not in frame.model_fields


class TestErrorPayload:
    def test_required_fields(self):
        err = ErrorPayload(code="tunnel_not_found", message="Tunnel does not exist")
        assert err.code == "tunnel_not_found"
        assert err.message == "Tunnel does not exist"
        assert err.request_id is None

    def test_with_request_id(self):
        err = ErrorPayload(
            code="auth_failed",
            message="Invalid token",
            request_id="r-99",
        )
        assert err.request_id == "r-99"

    def test_missing_code_raises(self):
        with pytest.raises((TypeError, ValidationError)):
            ErrorPayload(message="oops")

    def test_missing_message_raises(self):
        with pytest.raises((TypeError, ValidationError)):
            ErrorPayload(code="bad")

    def test_roundtrip_serialization(self):
        err = ErrorPayload(code="rate_limit", message="Too many requests", request_id="r-5")
        data = err.model_dump()
        restored = ErrorPayload(**data)
        assert restored == err
