"""Tests for qremotex.protocol — messages, serializer, router."""

import json

import pytest

from qremotex.protocol.messages import (
    ExecutionStatus,
    LogLevel,
    MessageType,
    ack_message,
    error_message,
    heartbeat_message,
    hello_message,
    log_message,
    pong_message,
    result_message,
    status_message,
)
from qremotex.protocol.router import MessageRouter
from qremotex.protocol.serializer import deserialize, serialize


# ──────────────────────────── Message factory tests ──

class TestMessageFactories:

    def test_hello_message(self) -> None:
        msg = hello_message("ex1", "ak_abc", "1.0.0", ["python:execute:unrestricted"], {"env": "prod"}, {"os": "linux"})
        assert msg["type"] == "hello"
        assert msg["executor_id"] == "ex1"
        assert msg["api_key_id"] == "ak_abc"
        assert msg["capabilities"] == ["python:execute:unrestricted"]

    def test_heartbeat_message(self) -> None:
        msg = heartbeat_message("ex1", 1234567890, {"cpu_percent": 42})
        assert msg["type"] == "heartbeat"
        assert msg["ts"] == 1234567890
        assert msg["metrics"]["cpu_percent"] == 42

    def test_pong_message(self) -> None:
        msg = pong_message("ex1")
        assert msg["type"] == "pong"
        assert msg["executor_id"] == "ex1"

    def test_ack_message(self) -> None:
        msg = ack_message("req-1", "ex1")
        assert msg["type"] == "ack"
        assert msg["request_id"] == "req-1"

    def test_status_message_with_position(self) -> None:
        msg = status_message("req-1", "ex1", ExecutionStatus.QUEUED, position=3)
        assert msg["status"] == "queued"
        assert msg["position"] == 3

    def test_status_message_without_position(self) -> None:
        msg = status_message("req-1", "ex1", ExecutionStatus.STARTED)
        assert "position" not in msg

    def test_log_message(self) -> None:
        msg = log_message("req-1", "ex1", "info", "stdout", "hello world", "2025-01-01T00:00:00Z")
        assert msg["type"] == "log"
        assert msg["message"] == "hello world"

    def test_result_message_full(self) -> None:
        msg = result_message("req-1", "ex1", True, {"answer": 42}, 150, peak_memory_mb=512)
        assert msg["success"] is True
        assert msg["result"] == {"answer": 42}
        assert msg["peak_memory_mb"] == 512

    def test_error_message_with_trace(self) -> None:
        msg = error_message("req-1", "ex1", "division by zero", trace="Traceback ...")
        assert msg["error"] == "division by zero"
        assert msg["trace"] == "Traceback ..."

    def test_error_message_without_trace(self) -> None:
        msg = error_message("req-1", "ex1", "oops")
        assert "trace" not in msg


# ──────────────────────────── Enum tests ──

class TestEnums:

    def test_message_type_values(self) -> None:
        assert MessageType.HELLO.value == "hello"
        assert MessageType.EXECUTE_SCRIPT.value == "execute_script"

    def test_execution_status(self) -> None:
        assert ExecutionStatus.QUEUED.value == "queued"

    def test_log_level(self) -> None:
        assert LogLevel.STDERR.value == "stderr"


# ──────────────────────────── Serializer tests ──

class TestSerializer:

    def test_serialize_round_trip(self) -> None:
        msg = ack_message("req-1", "ex1")
        raw = serialize(msg)
        parsed = json.loads(raw)
        assert parsed["type"] == "ack"
        assert parsed["request_id"] == "req-1"

    def test_deserialize_valid(self) -> None:
        raw = '{"type":"ping","ts":1234}'
        result = deserialize(raw)
        assert result is not None
        assert result["type"] == "ping"

    def test_deserialize_invalid_json(self) -> None:
        assert deserialize("{bad json}") is None

    def test_deserialize_not_object(self) -> None:
        assert deserialize("[1,2,3]") is None

    def test_deserialize_missing_type(self) -> None:
        assert deserialize('{"foo":"bar"}') is None


# ──────────────────────────── Router tests ──

class TestMessageRouter:

    @pytest.mark.asyncio
    async def test_dispatch_to_handler(self) -> None:
        router = MessageRouter()
        received: list[dict] = []

        async def handler(msg: dict) -> None:
            received.append(msg)

        router.register(MessageType.EXECUTE_SCRIPT, handler)
        await router.dispatch({"type": "execute_script", "request_id": "r1"})
        assert len(received) == 1
        assert received[0]["request_id"] == "r1"

    @pytest.mark.asyncio
    async def test_dispatch_unknown_type_no_error(self) -> None:
        router = MessageRouter()
        # Should not raise
        await router.dispatch({"type": "unknown_type"})

    @pytest.mark.asyncio
    async def test_dispatch_missing_type(self) -> None:
        router = MessageRouter()
        await router.dispatch({"foo": "bar"})  # No crash

    @pytest.mark.asyncio
    async def test_dispatch_handler_error_is_caught(self) -> None:
        router = MessageRouter()

        async def bad_handler(msg: dict) -> None:
            raise ValueError("boom")

        router.register("test_type", bad_handler)
        # Should not propagate the exception
        await router.dispatch({"type": "test_type"})

    @pytest.mark.asyncio
    async def test_register_string_key(self) -> None:
        router = MessageRouter()
        called = False

        async def handler(msg: dict) -> None:
            nonlocal called
            called = True

        router.register("custom_type", handler)
        await router.dispatch({"type": "custom_type"})
        assert called is True
