"""Tests for structured message schemas, agent convenience methods, and action routing."""

import json

import httpx
import pytest
import respx

from mrp.agent import Agent
from mrp.auth import Keypair
from mrp.errors import RequestError
from mrp.models import Message
from mrp.schemas import (
    REQUEST_CONTENT_TYPE,
    RESPONSE_CONTENT_TYPE,
    EVENT_CONTENT_TYPE,
    STATUS_CONTENT_TYPE,
    MRPRequest,
    MRPResponse,
    MRPResponseError,
    MRPEvent,
    MRPStatus,
    create_request,
    create_ok_response,
    create_error_response,
    create_event,
    create_status,
    parse_request,
    parse_response,
    parse_event,
    parse_status,
    is_request,
    is_response,
    is_event,
    is_status,
    as_request,
    as_response,
    as_event,
    as_status,
)


# --- Helpers ---


@pytest.fixture
def keypair():
    return Keypair.generate()


def _make_message(content_type: str, body: dict | None = None) -> Message:
    """Create a minimal Message for testing."""
    return Message(
        message_id="msg_1",
        sender_key="sender_key",
        recipient_key="recipient_key",
        content_type=content_type,
        body=body or {},
        attachments=[],
        status="delivered",
        thread_id="t1",
        in_reply_to=None,
        metadata={},
        created_at="2026-03-05T12:00:00Z",
        expires_at="2026-03-12T12:00:00Z",
    )


def _mock_register(keypair):
    """Set up mock for agent registration (PATCH)."""
    pk = keypair.public_key_b64
    respx.patch(f"http://localhost:8080/v1/agents/{pk}").mock(
        return_value=httpx.Response(200, json={
            "public_key": pk, "display_name": "Test", "status": "active",
            "metadata": {}, "capabilities": [],
            "created_at": "2026-03-05T12:00:00Z",
            "last_active_at": "2026-03-05T12:00:00Z",
        })
    )


def _mock_send():
    """Set up mock for message sending (POST)."""
    respx.post("http://localhost:8080/v1/messages").mock(
        return_value=httpx.Response(202, json={
            "message_id": "msg_out", "status": "sent",
            "created_at": "2026-03-05T12:00:00Z",
            "expires_at": "2026-03-12T12:00:00Z",
        })
    )


# --- Schema Builder Tests ---


class TestCreateRequest:
    def test_basic_request(self):
        req = create_request("text:translate", {"text": "hello", "target": "es"})
        assert req.action == "text:translate"
        assert req.params == {"text": "hello", "target": "es"}
        assert req.response_format is None

    def test_request_with_response_format(self):
        req = create_request(
            "text:translate",
            {"text": "hello"},
            response_format="{ translation: string }",
        )
        assert req.response_format == "{ translation: string }"

    def test_to_dict_omits_none_response_format(self):
        req = create_request("do:thing", {"x": 1})
        d = req.to_dict()
        assert "response_format" not in d
        assert d == {"action": "do:thing", "params": {"x": 1}}

    def test_to_dict_includes_response_format(self):
        req = create_request("do:thing", {"x": 1}, response_format="json")
        d = req.to_dict()
        assert d["response_format"] == "json"


class TestCreateResponse:
    def test_ok_response(self):
        resp = create_ok_response({"translation": "hola"})
        assert resp.status == "ok"
        assert resp.result == {"translation": "hola"}
        assert resp.error is None

    def test_ok_response_to_dict(self):
        resp = create_ok_response({"translation": "hola"})
        d = resp.to_dict()
        assert d == {"status": "ok", "result": {"translation": "hola"}}
        assert "error" not in d

    def test_error_response(self):
        resp = create_error_response("too_long", "Text exceeds 10000 chars")
        assert resp.status == "error"
        assert resp.error is not None
        assert resp.error.code == "too_long"
        assert resp.error.message == "Text exceeds 10000 chars"
        assert resp.result is None

    def test_error_response_to_dict(self):
        resp = create_error_response("too_long", "Text exceeds 10000 chars")
        d = resp.to_dict()
        assert d == {
            "status": "error",
            "error": {"code": "too_long", "message": "Text exceeds 10000 chars"},
        }
        assert "result" not in d


class TestCreateEvent:
    def test_event_with_data(self):
        evt = create_event("task.completed", {"task_id": "123"})
        assert evt.event == "task.completed"
        assert evt.data == {"task_id": "123"}

    def test_event_without_data(self):
        evt = create_event("heartbeat")
        assert evt.event == "heartbeat"
        assert evt.data is None

    def test_to_dict_omits_none_data(self):
        evt = create_event("heartbeat")
        d = evt.to_dict()
        assert d == {"event": "heartbeat"}
        assert "data" not in d

    def test_to_dict_includes_data(self):
        evt = create_event("task.completed", {"task_id": "123"})
        d = evt.to_dict()
        assert d == {"event": "task.completed", "data": {"task_id": "123"}}


class TestCreateStatus:
    def test_full_status(self):
        st = create_status(progress=50, stage="processing", detail="Halfway there")
        assert st.progress == 50
        assert st.stage == "processing"
        assert st.detail == "Halfway there"

    def test_minimal_status(self):
        st = create_status()
        assert st.progress is None
        assert st.stage is None
        assert st.detail is None

    def test_to_dict_omits_none_fields(self):
        st = create_status()
        d = st.to_dict()
        assert d == {}

    def test_to_dict_includes_set_fields(self):
        st = create_status(progress=75, stage="uploading")
        d = st.to_dict()
        assert d == {"progress": 75, "stage": "uploading"}
        assert "detail" not in d


# --- Schema Parser Tests (valid input) ---


class TestParseRequest:
    def test_round_trip(self):
        req = create_request("text:translate", {"text": "hello", "target": "es"}, "{ translation: string }")
        parsed = parse_request(req.to_dict())
        assert parsed is not None
        assert parsed.action == "text:translate"
        assert parsed.params == {"text": "hello", "target": "es"}
        assert parsed.response_format == "{ translation: string }"

    def test_defaults_params_to_empty(self):
        parsed = parse_request({"action": "foo"})
        assert parsed is not None
        assert parsed.params == {}

    def test_returns_none_for_null(self):
        assert parse_request(None) is None

    def test_returns_none_for_non_dict(self):
        assert parse_request(42) is None
        assert parse_request("hello") is None

    def test_returns_none_for_missing_action(self):
        assert parse_request({"params": {}}) is None

    def test_returns_none_for_non_string_action(self):
        assert parse_request({"action": 123, "params": {}}) is None


class TestParseResponse:
    def test_round_trip_ok(self):
        resp = create_ok_response({"translation": "hola"})
        parsed = parse_response(resp.to_dict())
        assert parsed is not None
        assert parsed.status == "ok"
        assert parsed.result == {"translation": "hola"}
        assert parsed.error is None

    def test_round_trip_error(self):
        resp = create_error_response("too_long", "Text exceeds 10000 chars")
        parsed = parse_response(resp.to_dict())
        assert parsed is not None
        assert parsed.status == "error"
        assert parsed.error is not None
        assert parsed.error.code == "too_long"
        assert parsed.error.message == "Text exceeds 10000 chars"

    def test_returns_none_for_null(self):
        assert parse_response(None) is None

    def test_returns_none_for_invalid_status(self):
        assert parse_response({"status": "maybe"}) is None

    def test_returns_none_for_missing_status(self):
        assert parse_response({"result": {}}) is None


class TestParseEvent:
    def test_round_trip_with_data(self):
        evt = create_event("task.completed", {"task_id": "123"})
        parsed = parse_event(evt.to_dict())
        assert parsed is not None
        assert parsed.event == "task.completed"
        assert parsed.data == {"task_id": "123"}

    def test_round_trip_without_data(self):
        evt = create_event("heartbeat")
        parsed = parse_event(evt.to_dict())
        assert parsed is not None
        assert parsed.event == "heartbeat"
        assert parsed.data is None

    def test_returns_none_for_null(self):
        assert parse_event(None) is None

    def test_returns_none_for_missing_event(self):
        assert parse_event({"data": {}}) is None


class TestParseStatus:
    def test_round_trip(self):
        st = create_status(progress=50, stage="processing", detail="Halfway there")
        parsed = parse_status(st.to_dict())
        assert parsed is not None
        assert parsed.progress == 50
        assert parsed.stage == "processing"
        assert parsed.detail == "Halfway there"

    def test_minimal(self):
        parsed = parse_status({})
        assert parsed is not None
        assert parsed.progress is None
        assert parsed.stage is None
        assert parsed.detail is None

    def test_returns_none_for_null(self):
        assert parse_status(None) is None

    def test_returns_none_for_non_dict(self):
        assert parse_status(42) is None


# --- Message Helper Tests ---


class TestIsHelpers:
    def test_is_request(self):
        assert is_request(_make_message(REQUEST_CONTENT_TYPE)) is True
        assert is_request(_make_message("application/json")) is False

    def test_is_response(self):
        assert is_response(_make_message(RESPONSE_CONTENT_TYPE)) is True
        assert is_response(_make_message("application/json")) is False

    def test_is_event(self):
        assert is_event(_make_message(EVENT_CONTENT_TYPE)) is True
        assert is_event(_make_message("application/json")) is False

    def test_is_status(self):
        assert is_status(_make_message(STATUS_CONTENT_TYPE)) is True
        assert is_status(_make_message("application/json")) is False


class TestAsHelpers:
    def test_as_request_parses(self):
        msg = _make_message(REQUEST_CONTENT_TYPE, {"action": "text:translate", "params": {"text": "hi"}})
        req = as_request(msg)
        assert req is not None
        assert req.action == "text:translate"
        assert req.params == {"text": "hi"}

    def test_as_request_returns_none_for_wrong_type(self):
        msg = _make_message("application/json", {"action": "foo"})
        assert as_request(msg) is None

    def test_as_response_parses(self):
        msg = _make_message(RESPONSE_CONTENT_TYPE, {"status": "ok", "result": {"x": 1}})
        resp = as_response(msg)
        assert resp is not None
        assert resp.status == "ok"
        assert resp.result == {"x": 1}

    def test_as_response_returns_none_for_wrong_type(self):
        assert as_response(_make_message("application/json")) is None

    def test_as_event_parses(self):
        msg = _make_message(EVENT_CONTENT_TYPE, {"event": "done", "data": {"id": "1"}})
        evt = as_event(msg)
        assert evt is not None
        assert evt.event == "done"
        assert evt.data == {"id": "1"}

    def test_as_event_returns_none_for_wrong_type(self):
        assert as_event(_make_message("application/json")) is None

    def test_as_status_parses(self):
        msg = _make_message(STATUS_CONTENT_TYPE, {"progress": 75, "stage": "uploading"})
        st = as_status(msg)
        assert st is not None
        assert st.progress == 75
        assert st.stage == "uploading"

    def test_as_status_returns_none_for_wrong_type(self):
        assert as_status(_make_message("application/json")) is None


# --- Agent Convenience Method Tests ---


class TestAgentRequest:
    @respx.mock
    def test_sends_structured_request(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)
        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_out", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        result = agent.request("recipient", "text:translate", {"text": "hello", "target": "es"})
        assert result.message_id == "msg_out"

        sent_body = json.loads(route.calls[0].request.content)
        assert sent_body["content_type"] == REQUEST_CONTENT_TYPE
        assert sent_body["body"]["action"] == "text:translate"
        assert sent_body["body"]["params"] == {"text": "hello", "target": "es"}


class TestAgentRespond:
    @respx.mock
    def test_sends_ok_response(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)
        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_out", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        msg = _make_message(REQUEST_CONTENT_TYPE, {"action": "test", "params": {}})
        result = agent.respond(msg, {"translation": "hola"})
        assert result.message_id == "msg_out"

        sent_body = json.loads(route.calls[0].request.content)
        assert sent_body["content_type"] == RESPONSE_CONTENT_TYPE
        assert sent_body["body"]["status"] == "ok"
        assert sent_body["body"]["result"] == {"translation": "hola"}
        assert sent_body["in_reply_to"] == "msg_1"


class TestAgentRespondError:
    @respx.mock
    def test_sends_error_response(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)
        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_out", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        msg = _make_message(REQUEST_CONTENT_TYPE, {"action": "test", "params": {}})
        result = agent.respond_error(msg, "too_long", "Text too long")
        assert result.message_id == "msg_out"

        sent_body = json.loads(route.calls[0].request.content)
        assert sent_body["content_type"] == RESPONSE_CONTENT_TYPE
        assert sent_body["body"]["status"] == "error"
        assert sent_body["body"]["error"] == {"code": "too_long", "message": "Text too long"}


class TestAgentEmitEvent:
    @respx.mock
    def test_sends_event(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)
        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_out", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        result = agent.emit_event("recipient", "task.completed", {"task_id": "42"})
        assert result.message_id == "msg_out"

        sent_body = json.loads(route.calls[0].request.content)
        assert sent_body["content_type"] == EVENT_CONTENT_TYPE
        assert sent_body["body"]["event"] == "task.completed"
        assert sent_body["body"]["data"] == {"task_id": "42"}


class TestAgentSendStatus:
    @respx.mock
    def test_sends_status(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)
        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_out", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        result = agent.send_status("recipient", progress=50, stage="processing")
        assert result.message_id == "msg_out"

        sent_body = json.loads(route.calls[0].request.content)
        assert sent_body["content_type"] == STATUS_CONTENT_TYPE
        assert sent_body["body"]["progress"] == 50
        assert sent_body["body"]["stage"] == "processing"


# --- Action Routing Tests ---


class TestActionRouting:
    @respx.mock
    def test_dispatches_to_registered_handler(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)
        _mock_send()

        handler_called = False

        def handler(req, msg):
            nonlocal handler_called
            handler_called = True
            assert req.action == "text:translate"
            assert req.params == {"text": "hello"}
            return {"translation": "hola"}

        agent.on_request("text:translate", handler)

        request_msg = _make_message(
            REQUEST_CONTENT_TYPE,
            {"action": "text:translate", "params": {"text": "hello"}},
        )
        agent._dispatch_message(request_msg)
        assert handler_called is True

    @respx.mock
    def test_auto_sends_ok_response(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)
        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_out", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )

        agent.on_request("text:translate", lambda req, msg: {"translation": "hola"})

        request_msg = _make_message(
            REQUEST_CONTENT_TYPE,
            {"action": "text:translate", "params": {"text": "hello"}},
        )
        agent._dispatch_message(request_msg)

        sent_body = json.loads(route.calls[0].request.content)
        assert sent_body["body"]["status"] == "ok"
        assert sent_body["body"]["result"] == {"translation": "hola"}

    @respx.mock
    def test_sends_error_response_on_request_error(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)
        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_out", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )

        def handler(req, msg):
            raise RequestError("too_long", "Text exceeds limit")

        agent.on_request("text:translate", handler)

        request_msg = _make_message(
            REQUEST_CONTENT_TYPE,
            {"action": "text:translate", "params": {}},
        )
        agent._dispatch_message(request_msg)

        sent_body = json.loads(route.calls[0].request.content)
        assert sent_body["body"]["status"] == "error"
        assert sent_body["body"]["error"] == {"code": "too_long", "message": "Text exceeds limit"}

    @respx.mock
    def test_falls_back_to_on_message_for_unmatched_action(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)

        fallback_called = False

        def on_msg(msg):
            nonlocal fallback_called
            fallback_called = True
            return None

        agent.on_message(on_msg)
        agent.on_request("text:translate", lambda req, msg: {"done": True})

        # Send a request with an unregistered action
        msg = _make_message(REQUEST_CONTENT_TYPE, {"action": "other:action", "params": {}})
        agent._dispatch_message(msg)
        assert fallback_called is True

    @respx.mock
    def test_falls_back_to_on_message_for_non_request(self, keypair):
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)

        fallback_called = False

        def on_msg(msg):
            nonlocal fallback_called
            fallback_called = True
            return None

        agent.on_message(on_msg)
        agent.on_request("text:translate", lambda req, msg: {"done": True})

        msg = _make_message("application/json", {"text": "hello"})
        agent._dispatch_message(msg)
        assert fallback_called is True


# --- RequestError Tests ---


class TestRequestError:
    def test_has_code_and_message(self):
        err = RequestError("invalid_input", "Missing required field")
        assert err.code == "invalid_input"
        assert err.message == "Missing required field"
        assert isinstance(err, Exception)

    def test_str_representation(self):
        err = RequestError("invalid_input", "Missing required field")
        assert str(err) == "Missing required field"
