"""Tests for MailioClient."""

from __future__ import annotations

import base64
import uuid
from unittest.mock import AsyncMock, patch

import httpx
import respx
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

from mailio_receipt.client import CreateReceiptResult, MailioClient
from mailio_receipt.models import (
    Actors,
    Intent,
    IntentKind,
    MailioAuditEvent,
    MailioReceipt,
)


def _make_config(private_key: Ed25519PrivateKey | None = None) -> dict:
    """Build a valid agent config dict for testing."""
    if private_key is None:
        private_key = Ed25519PrivateKey.generate()

    # Ed25519 raw bytes: 32-byte seed + 32-byte public key
    raw_private = private_key.private_bytes_raw()
    raw_public = private_key.public_key().public_bytes_raw()
    signing_key_b64 = base64.b64encode(raw_private + raw_public).decode()
    public_key_b64 = base64.b64encode(raw_public).decode()

    return {
        "signingPrivateKey": signing_key_b64,
        "signingPublicKey": public_key_b64,
        "did": "did:mailio:0xabc123",
        "didDocumentStoredKey": "did:web:mio.example.com:0xabc123",
        "api_key": "ml_test_key",
        "type": "mailio_agent_keys_ed25519_x25519",
    }


def _make_event() -> MailioAuditEvent:
    """Create a sample event for testing."""
    actors = Actors(issuer="did:mailio:0xabc123", subject="did:web:user.example.com")
    intent = Intent(
        kind=IntentKind.ACTION_PLANNED,
        action="test.action",
        category="test",
        scope={"key": "value"},
    )
    return MailioAuditEvent(
        type="MailioAuditEvent",
        v="v1",
        id=str(uuid.uuid4()),
        flow_id=str(uuid.uuid4()),
        ts="2026-02-13T00:00:00+00:00",
        nonce=str(uuid.uuid4()),
        actors=actors,
        intent=intent,
    )


class TestMailioClientInit:
    """Test config parsing in MailioClient constructor."""

    def test_extracts_backend_host(self) -> None:
        config = _make_config()
        client = MailioClient(config)
        assert client._backend_host == "mio.example.com"
        assert client._base_url == "https://mio.example.com"

    def test_extracts_signer_did(self) -> None:
        config = _make_config()
        client = MailioClient(config)
        assert client._signer_did == "did:mailio:0xabc123"

    def test_extracts_api_key(self) -> None:
        config = _make_config()
        client = MailioClient(config)
        assert client._api_key == "ml_test_key"

    def test_extracts_private_key(self) -> None:
        key = Ed25519PrivateKey.generate()
        config = _make_config(key)
        client = MailioClient(config)
        # Verify the extracted key can sign (same seed produces same key)
        assert client._private_key.private_bytes_raw() == key.private_bytes_raw()


class TestContextManager:
    """Test async context manager lifecycle."""

    async def test_async_context_manager(self) -> None:
        config = _make_config()
        async with MailioClient(config) as client:
            assert client._client is not None
        # After exit, client should be closed
        assert client._client.is_closed


class TestCreateReceipt:
    """Test create_receipt method."""

    @respx.mock
    async def test_create_and_save_success(self) -> None:
        config = _make_config()
        event = _make_event()

        async with MailioClient(config) as client:
            # First create locally to know the shape of the response
            local_result = await client.create_receipt(event, save=False)
            receipt_dict = local_result.receipt.to_dict()

            # Mock the POST endpoint to return the receipt
            respx.post("https://mio.example.com/api/v1/receipts").mock(
                return_value=httpx.Response(200, json=receipt_dict)
            )

            result = await client.create_receipt(event)

        assert isinstance(result, CreateReceiptResult)
        assert result.saved is True
        assert result.save_error is None
        assert result.receipt.event_digest != ""

    async def test_create_without_save(self) -> None:
        config = _make_config()
        event = _make_event()

        async with MailioClient(config) as client:
            result = await client.create_receipt(event, save=False)

        assert result.saved is False
        assert result.save_error is None
        assert result.receipt.event.id == event.id

    @respx.mock
    async def test_save_failure_returns_receipt(self) -> None:
        config = _make_config()
        event = _make_event()

        # Mock POST to always return 500
        respx.post("https://mio.example.com/api/v1/receipts").mock(
            return_value=httpx.Response(500)
        )

        async with MailioClient(config, max_retries=1, backoff_base=0.01) as client:
            result = await client.create_receipt(event)

        assert result.saved is False
        assert result.save_error is not None
        # Receipt is still returned
        assert result.receipt.event.id == event.id
        assert len(result.receipt.signatures) == 1


class TestRetryBehavior:
    """Test HTTP retry and backoff logic."""

    @respx.mock
    async def test_retry_on_5xx_then_success(self) -> None:
        config = _make_config()
        event = _make_event()

        call_count = 0

        def side_effect(_request: httpx.Request) -> httpx.Response:
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return httpx.Response(503)
            # Return a valid receipt on second attempt
            # We need to create it here since we need a valid response
            return httpx.Response(200, json=MailioReceipt.from_json(
                _request.content.decode()
            ).to_dict())

        respx.post("https://mio.example.com/api/v1/receipts").mock(
            side_effect=side_effect
        )

        async with MailioClient(config, max_retries=3, backoff_base=0.01) as client:
            result = await client.create_receipt(event)

        assert result.saved is True
        assert call_count == 2

    @respx.mock
    async def test_retry_on_429_with_retry_after(self) -> None:
        config = _make_config()

        call_count = 0

        def side_effect(_request: httpx.Request) -> httpx.Response:
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return httpx.Response(429, headers={"Retry-After": "0"})
            return httpx.Response(200, json={"type": "MailioReceipt", "v": "v1",
                "event": {}, "event_digest": "", "signatures": []})

        receipt_dict = _make_receipt_dict(config)
        respx.post("https://mio.example.com/api/v1/receipts").mock(
            side_effect=lambda req: _rate_limit_then_ok(req, receipt_dict)
        )

        event = _make_event()
        async with MailioClient(config, max_retries=3, backoff_base=0.01) as client:
            result = await client.create_receipt(event)

        assert result.saved is True

    @respx.mock
    async def test_no_retry_on_4xx(self) -> None:
        config = _make_config()
        event = _make_event()

        route = respx.post("https://mio.example.com/api/v1/receipts").mock(
            return_value=httpx.Response(400, json={"error": "bad request"})
        )

        async with MailioClient(config, max_retries=3, backoff_base=0.01) as client:
            result = await client.create_receipt(event)

        # Should fail immediately without retry
        assert result.saved is False
        assert result.save_error is not None
        assert route.call_count == 1


class TestSignReceipt:
    """Test sign_receipt co-signature method."""

    async def test_adds_cosignature(self) -> None:
        config = _make_config()
        event = _make_event()

        async with MailioClient(config) as client:
            result = await client.create_receipt(event, save=False)
            original = result.receipt

            sign_result = await client.sign_receipt(original, role="authority", save=False)
            cosigned = sign_result.receipt

        assert len(cosigned.signatures) == 2
        assert cosigned.signatures[0].jws == original.signatures[0].jws
        assert cosigned.signatures[1].role == "authority"
        assert cosigned.signatures[1].signer == "did:mailio:0xabc123"

    async def test_preserves_original(self) -> None:
        config = _make_config()
        event = _make_event()

        async with MailioClient(config) as client:
            result = await client.create_receipt(event, save=False)
            original = result.receipt

            _ = await client.sign_receipt(original, role="authority", save=False)

        # Original should be unchanged (frozen model)
        assert len(original.signatures) == 1

    async def test_cosigned_event_digest_unchanged(self) -> None:
        config = _make_config()
        event = _make_event()

        async with MailioClient(config) as client:
            result = await client.create_receipt(event, save=False)
            original = result.receipt

            sign_result = await client.sign_receipt(original, save=False)

        assert sign_result.receipt.event_digest == original.event_digest
        assert sign_result.receipt.event == original.event

    @respx.mock
    async def test_cosign_and_save(self) -> None:
        config = _make_config()
        event = _make_event()

        async with MailioClient(config) as client:
            result = await client.create_receipt(event, save=False)
            original = result.receipt

            # Mock POST for saving the co-signed receipt
            respx.post("https://mio.example.com/api/v1/receipts").mock(
                side_effect=lambda req: httpx.Response(
                    200, json=MailioReceipt.from_json(req.content.decode()).to_dict()
                )
            )

            sign_result = await client.sign_receipt(original, role="authority", save=True)

        assert sign_result.saved is True
        assert sign_result.save_error is None
        assert len(sign_result.receipt.signatures) == 2


class TestGetReceipt:
    """Test get_receipt method."""

    @respx.mock
    async def test_get_receipt_by_id(self) -> None:
        config = _make_config()
        event = _make_event()

        # Create a receipt to use as mock response
        async with MailioClient(config) as client:
            result = await client.create_receipt(event, save=False)
            receipt_dict = result.receipt.to_dict()
            receipt_id = result.receipt.event.id

            respx.get(f"https://mio.example.com/api/v1/receipts/{receipt_id}").mock(
                return_value=httpx.Response(200, json=receipt_dict)
            )

            fetched = await client.get_receipt(receipt_id)

        assert fetched.event.id == receipt_id
        assert fetched.event_digest == result.receipt.event_digest


class TestGetFlowReceipts:
    """Test get_flow_receipts method."""

    @respx.mock
    async def test_get_flow_receipts(self) -> None:
        config = _make_config()
        event = _make_event()
        flow_id = event.flow_id

        async with MailioClient(config) as client:
            result = await client.create_receipt(event, save=False)
            receipt_dict = result.receipt.to_dict()

            respx.get(
                f"https://mio.example.com/api/v1/receipts/flow/{flow_id}"
            ).mock(
                return_value=httpx.Response(
                    200, json={"receipts": [receipt_dict, receipt_dict]}
                )
            )

            receipts = await client.get_flow_receipts(flow_id)

        assert len(receipts) == 2
        assert all(r.event.flow_id == flow_id for r in receipts)

    @respx.mock
    async def test_get_flow_receipts_empty(self) -> None:
        config = _make_config()
        flow_id = str(uuid.uuid4())

        respx.get(
            f"https://mio.example.com/api/v1/receipts/flow/{flow_id}"
        ).mock(return_value=httpx.Response(200, json={"receipts": []}))

        async with MailioClient(config) as client:
            receipts = await client.get_flow_receipts(flow_id)

        assert receipts == []


class TestVerifyReceipt:
    """Test verify_receipt delegation."""

    async def test_verify_delegates_to_verification_module(self) -> None:
        config = _make_config()
        event = _make_event()

        async with MailioClient(config) as client:
            result = await client.create_receipt(event, save=False)
            receipt = result.receipt

            with patch(
                "mailio_receipt.client._verify_receipt",
                new_callable=AsyncMock,
            ) as mock_verify:
                from mailio_receipt.verification import VerificationResult

                mock_verify.return_value = VerificationResult(
                    valid=True, signers=(), error=None
                )
                vr = await client.verify_receipt(receipt)

        assert vr.valid is True
        mock_verify.assert_called_once_with(
            receipt,
            mio_backend_url="mio.example.com",
            mio_backend_api_key="ml_test_key",
        )


# ── Helpers ──────────────────────────────────────────────────────


def _make_receipt_dict(config: dict) -> dict:
    """Create a valid receipt dict for mock responses."""
    event = _make_event()
    client = MailioClient(config)
    # We can't await here, so build manually
    from mailio_receipt.canonicalize import canonicalize, compute_digest
    from mailio_receipt.signing import create_jws_detached

    event_dict = event.to_dict()
    event_digest = compute_digest(event_dict)
    canonical = canonicalize(event_dict)
    jws = create_jws_detached(canonical, client._private_key, client._signer_did)
    return {
        "type": "MailioReceipt",
        "v": "v1",
        "event": event_dict,
        "event_digest": event_digest,
        "signatures": [
            {
                "role": "agent",
                "signer": client._signer_did,
                "alg": "EdDSA",
                "signed": {
                    "canonicalization": "RFC8785",
                    "digest": event_digest,
                    "target": "/event",
                },
                "jws": jws,
            }
        ],
    }


_rate_limit_call_count = 0


def _rate_limit_then_ok(
    _request: httpx.Request, receipt_dict: dict
) -> httpx.Response:
    global _rate_limit_call_count
    _rate_limit_call_count += 1
    if _rate_limit_call_count == 1:
        return httpx.Response(429, headers={"Retry-After": "0"})
    return httpx.Response(200, json=receipt_dict)
