import json

import httpx
import pytest
import respx

from mrp.auth import Keypair
from mrp.client import Client
from mrp.errors import (
    AuthError,
    ConflictError,
    ForbiddenError,
    NotFoundError,
    PayloadTooLargeError,
    RateLimitError,
    StorageError,
    ValidationError,
)
from mrp.models import Attachment


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


@pytest.fixture
def client(keypair):
    c = Client("http://localhost:8080", keypair)
    yield c
    c.close()


class TestRequestAuth:
    @respx.mock
    def test_auth_headers_present(self, client, keypair):
        route = respx.get("http://localhost:8080/v1/agents/test").mock(
            return_value=httpx.Response(200, json={
                "public_key": "test", "status": "active",
                "metadata": {}, "capabilities": [],
                "created_at": "2026-03-05T12:00:00Z",
                "last_active_at": "2026-03-05T12:00:00Z",
            })
        )
        client.get_agent("test")
        req = route.calls.last.request
        assert req.headers["x-m2m-public-key"] == keypair.public_key_b64
        assert "x-m2m-timestamp" in req.headers
        assert "x-m2m-signature" in req.headers


class TestErrorMapping:
    @respx.mock
    def test_401_auth_error(self, client):
        respx.get("http://localhost:8080/v1/agents/x").mock(
            return_value=httpx.Response(401, json={"error": {"code": "unauthorized", "message": "bad"}})
        )
        with pytest.raises(AuthError) as exc_info:
            client.get_agent("x")
        assert exc_info.value.code == "unauthorized"

    @respx.mock
    def test_403_forbidden(self, client):
        respx.delete("http://localhost:8080/v1/agents/x").mock(
            return_value=httpx.Response(403, json={"error": {"code": "forbidden", "message": "nope"}})
        )
        with pytest.raises(ForbiddenError):
            client.delete_agent("x")

    @respx.mock
    def test_404_not_found(self, client):
        respx.get("http://localhost:8080/v1/agents/x").mock(
            return_value=httpx.Response(404, json={"error": {"code": "agent_not_found", "message": "not found"}})
        )
        with pytest.raises(NotFoundError):
            client.get_agent("x")

    @respx.mock
    def test_409_conflict(self, client):
        respx.delete("http://localhost:8080/v1/blobs/x").mock(
            return_value=httpx.Response(409, json={"error": {"code": "blob_referenced", "message": "in use"}})
        )
        with pytest.raises(ConflictError):
            client.delete_blob("x")

    @respx.mock
    def test_400_validation(self, client):
        respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(400, json={"error": {"code": "bad_request", "message": "invalid"}})
        )
        with pytest.raises(ValidationError):
            client.send_message("recipient", body={"test": True})

    @respx.mock
    def test_429_rate_limit(self, client):
        respx.get("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(
                429,
                json={"error": {"code": "rate_limited", "message": "slow down"}},
                headers={
                    "Retry-After": "5",
                    "X-RateLimit-Limit": "100",
                    "X-RateLimit-Remaining": "0",
                    "X-RateLimit-Reset": "1709640000",
                },
            )
        )
        with pytest.raises(RateLimitError) as exc_info:
            client.poll_messages()
        assert exc_info.value.retry_after == 5.0
        assert exc_info.value.limit == 100
        assert exc_info.value.remaining == 0

    @respx.mock
    def test_413_payload(self, client):
        respx.post("http://localhost:8080/v1/blobs").mock(
            return_value=httpx.Response(413, json={"error": {"code": "payload_too_large", "message": "too big"}})
        )
        with pytest.raises(PayloadTooLargeError):
            client.upload_blob(b"data")

    @respx.mock
    def test_507_storage(self, client):
        respx.post("http://localhost:8080/v1/blobs").mock(
            return_value=httpx.Response(507, json={"error": {"code": "insufficient_storage", "message": "full"}})
        )
        with pytest.raises(StorageError):
            client.upload_blob(b"data")


class TestAgentMethods:
    @respx.mock
    def test_get_agent(self, client):
        respx.get("http://localhost:8080/v1/agents/pk1").mock(
            return_value=httpx.Response(200, json={
                "public_key": "pk1", "display_name": "Agent1", "status": "active",
                "metadata": {}, "capabilities": ["chat"],
                "created_at": "2026-03-05T12:00:00Z",
                "last_active_at": "2026-03-05T13:00:00Z",
            })
        )
        agent = client.get_agent("pk1")
        assert agent.public_key == "pk1"
        assert agent.display_name == "Agent1"

    @respx.mock
    def test_update_agent(self, client):
        respx.patch("http://localhost:8080/v1/agents/pk1").mock(
            return_value=httpx.Response(200, json={
                "public_key": "pk1", "display_name": "Updated", "status": "active",
                "metadata": {}, "capabilities": [],
                "created_at": "2026-03-05T12:00:00Z",
                "last_active_at": "2026-03-05T13:00:00Z",
            })
        )
        agent = client.update_agent("pk1", display_name="Updated")
        assert agent.display_name == "Updated"

    @respx.mock
    def test_delete_agent(self, client):
        respx.delete("http://localhost:8080/v1/agents/pk1").mock(
            return_value=httpx.Response(204)
        )
        client.delete_agent("pk1")  # Should not raise


class TestMessageMethods:
    @respx.mock
    def test_send_message(self, client):
        respx.post("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(202, json={
                "message_id": "msg_1", "status": "sent",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        result = client.send_message("recipient_key", body={"text": "hi"})
        assert result.message_id == "msg_1"
        assert result.status == "sent"

    @respx.mock
    def test_poll_messages(self, client):
        respx.get("http://localhost:8080/v1/messages").mock(
            return_value=httpx.Response(200, json={
                "messages": [
                    {
                        "message_id": "msg_1", "sender_key": "s", "recipient_key": "r",
                        "content_type": "application/json", "body": {"x": 1},
                        "attachments": [], "status": "delivered",
                        "metadata": {},
                        "created_at": "2026-03-05T12:00:00Z",
                        "expires_at": "2026-03-12T12:00:00Z",
                    }
                ],
                "next_cursor": None,
            })
        )
        result = client.poll_messages()
        assert len(result.messages) == 1

    @respx.mock
    def test_get_message(self, client):
        respx.get("http://localhost:8080/v1/messages/msg_1").mock(
            return_value=httpx.Response(200, json={
                "message_id": "msg_1", "sender_key": "s", "recipient_key": "r",
                "content_type": "application/json", "body": {},
                "attachments": [], "status": "sent",
                "metadata": {},
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        msg = client.get_message("msg_1")
        assert msg.message_id == "msg_1"

    @respx.mock
    def test_get_message_status(self, client):
        respx.get("http://localhost:8080/v1/messages/msg_1/status").mock(
            return_value=httpx.Response(200, json={
                "status": "delivered",
                "created_at": "2026-03-05T12:00:00Z",
                "delivered_at": "2026-03-05T12:01:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        status = client.get_message_status("msg_1")
        assert status.status == "delivered"

    @respx.mock
    def test_get_thread(self, client):
        respx.get("http://localhost:8080/v1/messages/threads/t1").mock(
            return_value=httpx.Response(200, json={"messages": [], "next_cursor": None})
        )
        result = client.get_thread("t1")
        assert result.messages == []


class TestDiscovery:
    @respx.mock
    def test_discover(self, client):
        respx.get("http://localhost:8080/v1/discover").mock(
            return_value=httpx.Response(200, json={
                "agents": [
                    {
                        "public_key": "pk1", "display_name": "A1",
                        "last_active_at": "2026-03-05T12:00:00Z",
                        "capabilities": ["chat"],
                    }
                ],
                "next_cursor": None,
            })
        )
        result = client.discover("chat")
        assert len(result.agents) == 1

    @respx.mock
    def test_discover_all_pagination(self, client):
        # First page
        respx.get("http://localhost:8080/v1/discover").mock(
            side_effect=[
                httpx.Response(200, json={
                    "agents": [{"public_key": "pk1", "display_name": None,
                                "last_active_at": "2026-03-05T12:00:00Z", "capabilities": ["x"]}],
                    "next_cursor": "cur1",
                }),
                httpx.Response(200, json={
                    "agents": [{"public_key": "pk2", "display_name": None,
                                "last_active_at": "2026-03-05T12:00:00Z", "capabilities": ["x"]}],
                    "next_cursor": None,
                }),
            ]
        )
        agents = client.discover_all("x")
        assert len(agents) == 2


class TestBlobs:
    @respx.mock
    def test_upload_blob(self, client):
        respx.post("http://localhost:8080/v1/blobs").mock(
            return_value=httpx.Response(201, json={
                "blob_id": "blob_1", "hash": "abc", "size": 4,
                "content_type": "application/octet-stream",
                "created_at": "2026-03-05T12:00:00Z",
                "expires_at": "2026-03-12T12:00:00Z",
            })
        )
        blob = client.upload_blob(b"data")
        assert blob.blob_id == "blob_1"

    @respx.mock
    def test_download_blob(self, client):
        respx.get("http://localhost:8080/v1/blobs/blob_1").mock(
            return_value=httpx.Response(200, content=b"hello", headers={"Content-Type": "text/plain"})
        )
        data, ct = client.download_blob("blob_1")
        assert data == b"hello"
        assert ct == "text/plain"

    @respx.mock
    def test_get_blob_metadata(self, client):
        respx.head("http://localhost:8080/v1/blobs/blob_1").mock(
            return_value=httpx.Response(200, headers={
                "Content-Type": "image/png",
                "Content-Length": "2048",
                "X-M2M-Blob-Hash": "abc123",
            })
        )
        meta = client.get_blob_metadata("blob_1")
        assert meta.content_type == "image/png"
        assert meta.size == 2048
        assert meta.hash == "abc123"

    @respx.mock
    def test_delete_blob(self, client):
        respx.delete("http://localhost:8080/v1/blobs/blob_1").mock(
            return_value=httpx.Response(204)
        )
        client.delete_blob("blob_1")
