import json

import httpx
import pytest
import respx

from unittest.mock import MagicMock, patch

from mrp.agent import Agent
from mrp.auth import Keypair, _b64url_decode
from mrp.crypto import encrypt_message, decrypt_message
from mrp.errors import RequestError
from mrp.models import Message
from mrp.schemas import REQUEST_CONTENT_TYPE, RESPONSE_CONTENT_TYPE, create_request


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


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 _make_request_message(keypair, action="echo", params=None):
    """Create a Message with REQUEST_CONTENT_TYPE for dispatch testing."""
    if params is None:
        params = {"text": "hello"}
    req = create_request(action, params)
    return Message(
        message_id="msg_1",
        sender_key="sender_key_abc",
        recipient_key=keypair.public_key_b64,
        content_type=REQUEST_CONTENT_TYPE,
        body=req.to_dict(),
        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 _make_plain_message(keypair):
    """Create a plain application/json Message for dispatch testing."""
    return Message(
        message_id="msg_2",
        sender_key="sender_key_abc",
        recipient_key=keypair.public_key_b64,
        content_type="application/json",
        body={"text": "hello"},
        attachments=[],
        status="delivered",
        thread_id=None,
        in_reply_to=None,
        metadata={},
        created_at="2026-03-05T12:00:00Z",
        expires_at="2026-03-12T12:00:00Z",
    )


class TestAgentDispatch:
    @respx.mock
    def test_action_routing(self, keypair):
        """Register handler with on_request, dispatch request message, verify handler called."""
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)

        # Mock the respond call (it sends an HTTP message)
        respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_reply", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )

        handler_called_with = []

        def echo_handler(req, msg):
            handler_called_with.append((req, msg))
            return {"echo": req.params["text"]}

        agent.on_request("echo", echo_handler)

        msg = _make_request_message(keypair, action="echo", params={"text": "hello"})
        result = agent._dispatch_message(msg)

        # Action handler returns None (auto-responds via respond())
        assert result is None
        assert len(handler_called_with) == 1
        assert handler_called_with[0][0].action == "echo"
        assert handler_called_with[0][0].params == {"text": "hello"}
        assert handler_called_with[0][1].message_id == "msg_1"

    @respx.mock
    def test_action_error_response(self, keypair):
        """Handler raises RequestError -- verify respond_error is called."""
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)

        # Capture the sent response
        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_err", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )

        def failing_handler(req, msg):
            raise RequestError("NOT_FOUND", "resource not found")

        agent.on_request("lookup", failing_handler)

        msg = _make_request_message(keypair, action="lookup", params={"id": "123"})
        result = agent._dispatch_message(msg)

        assert result is None
        # Verify respond_error sent a message
        assert route.called
        sent_body = json.loads(route.calls.last.request.content)
        assert sent_body["content_type"] == RESPONSE_CONTENT_TYPE
        body = sent_body["body"]
        assert body["status"] == "error"
        assert body["error"]["code"] == "NOT_FOUND"
        assert body["error"]["message"] == "resource not found"

    def test_fallback_on_message(self, keypair):
        """Set on_message, dispatch non-request message, verify called."""
        agent = Agent("http://localhost:8080", keypair=keypair)

        handler_called_with = []

        def generic_handler(msg):
            handler_called_with.append(msg)
            return {"reply": "ok"}

        agent.on_message(generic_handler)

        msg = _make_plain_message(keypair)
        result = agent._dispatch_message(msg)

        assert len(handler_called_with) == 1
        assert handler_called_with[0].message_id == "msg_2"
        assert result == {"reply": "ok"}


class TestAgentEncryption:
    @respx.mock
    def test_send_encrypted(self, keypair):
        """Send with encrypt=True, verify content_type changed to encrypted."""
        agent = Agent("http://localhost:8080", keypair=keypair)
        _mock_register(keypair)

        # Create a recipient keypair
        recipient = Keypair.generate()

        route = respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_enc", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )

        result = agent.send(
            to=recipient.public_key_b64,
            body={"secret": "data"},
            encrypt=True,
        )

        assert result.message_id == "msg_enc"
        assert route.called

        sent_body = json.loads(route.calls.last.request.content)
        assert sent_body["content_type"] == "application/x-m2m-encrypted"
        # The body should have encrypted fields
        encrypted_body = sent_body["body"]
        assert encrypted_body["v"] == 2
        assert "enc" in encrypted_body
        assert "ct" in encrypted_body
        assert encrypted_body["ct_content_type"] == "application/json"

    def test_decrypt_success(self, keypair):
        """Encrypt then decrypt roundtrip via Agent.decrypt()."""
        sender = Keypair.generate()
        recipient = Keypair.generate()

        # Encrypt a message body
        plaintext = json.dumps({"secret": "hello"}).encode()
        encrypted_body = encrypt_message(
            sender.seed,
            recipient.public_key,
            plaintext,
            "application/json",
        )

        # Create a Message with encrypted content
        msg = Message(
            message_id="msg_enc",
            sender_key=sender.public_key_b64,
            recipient_key=recipient.public_key_b64,
            content_type="application/x-m2m-encrypted",
            body=encrypted_body,
            attachments=[],
            status="delivered",
            thread_id=None,
            in_reply_to=None,
            metadata={},
            created_at="2026-03-05T12:00:00Z",
            expires_at="2026-03-12T12:00:00Z",
        )

        # Decrypt via Agent
        agent = Agent("http://localhost:8080", keypair=recipient)
        decrypted_bytes, content_type = agent.decrypt(msg)

        assert json.loads(decrypted_bytes) == {"secret": "hello"}
        assert content_type == "application/json"

    def test_decrypt_not_encrypted(self, keypair):
        """ValueError for non-encrypted content type."""
        agent = Agent("http://localhost:8080", keypair=keypair)

        msg = Message(
            message_id="msg_plain",
            sender_key="sender_key",
            recipient_key=keypair.public_key_b64,
            content_type="application/json",
            body={"text": "hello"},
            attachments=[],
            status="delivered",
            thread_id=None,
            in_reply_to=None,
            metadata={},
            created_at="2026-03-05T12:00:00Z",
            expires_at="2026-03-12T12:00:00Z",
        )

        with pytest.raises(ValueError, match="Message is not encrypted"):
            agent.decrypt(msg)
