"""Tests for DID:web resolution."""

from __future__ import annotations

import base64
import time
from typing import TYPE_CHECKING

import httpx
import pytest
import respx

from mailio_receipt.did_resolver import DIDWebResolver, did_to_https_url
from mailio_receipt.errors import DIDNetworkError, DIDNotFoundError, DIDResolutionError

if TYPE_CHECKING:
    from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey


class TestDidToHttpsUrl:
    """Tests for DID:web URL conversion."""

    def test_simple_domain(self) -> None:
        """Test simple domain conversion."""
        url = did_to_https_url("did:web:example.com")
        assert url == "https://example.com/.well-known/did.json"

    def test_domain_with_path(self) -> None:
        """Test domain with path segments."""
        url = did_to_https_url("did:web:example.com:user:alice")
        assert url == "https://example.com/user/alice/did.json"

    def test_domain_with_port(self) -> None:
        """Test domain with encoded port."""
        url = did_to_https_url("did:web:example.com%3A8443")
        assert url == "https://example.com:8443/.well-known/did.json"

    def test_domain_with_port_and_path(self) -> None:
        """Test domain with port and path."""
        url = did_to_https_url("did:web:example.com%3A8443:user:alice")
        assert url == "https://example.com:8443/user/alice/did.json"

    def test_invalid_did_format(self) -> None:
        """Test that invalid DID format raises error."""
        with pytest.raises(DIDResolutionError, match="Invalid DID:web format"):
            did_to_https_url("did:key:z123")

    def test_not_a_did(self) -> None:
        """Test that non-DID raises error."""
        with pytest.raises(DIDResolutionError, match="Invalid DID:web format"):
            did_to_https_url("not-a-did")


def _public_key_to_jwk(public_key: EllipticCurvePublicKey) -> dict:
    """Convert EC public key to JWK."""
    numbers = public_key.public_numbers()
    x_bytes = numbers.x.to_bytes(32, byteorder="big")
    y_bytes = numbers.y.to_bytes(32, byteorder="big")
    return {
        "kty": "EC",
        "crv": "P-256",
        "x": base64.urlsafe_b64encode(x_bytes).decode("ascii").rstrip("="),
        "y": base64.urlsafe_b64encode(y_bytes).decode("ascii").rstrip("="),
    }


class TestDIDWebResolver:
    """Tests for DIDWebResolver."""

    @pytest.fixture
    def resolver(self) -> DIDWebResolver:
        """Create a DID resolver with short cache TTL for testing."""
        return DIDWebResolver(cache_ttl=1)

    @pytest.fixture
    def sample_did_document(self, agent_public_key: EllipticCurvePublicKey) -> dict:
        """Create a sample DID document."""
        return {
            "id": "did:web:agent.example.com",
            "@context": ["https://www.w3.org/ns/did/v1"],
            "verificationMethod": [
                {
                    "id": "did:web:agent.example.com#key-1",
                    "type": "JsonWebKey2020",
                    "controller": "did:web:agent.example.com",
                    "publicKeyJwk": _public_key_to_jwk(agent_public_key),
                }
            ],
        }

    @respx.mock
    @pytest.mark.asyncio
    async def test_resolve_fetches_document(
        self, resolver: DIDWebResolver, sample_did_document: dict
    ) -> None:
        """Test that resolve fetches the DID document."""
        respx.get("https://agent.example.com/.well-known/did.json").respond(
            json=sample_did_document
        )

        doc = await resolver.resolve("did:web:agent.example.com")

        assert doc["id"] == "did:web:agent.example.com"
        assert "verificationMethod" in doc

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_resolve_not_found(self, resolver: DIDWebResolver) -> None:
        """Test that 404 raises DIDNotFoundError."""
        respx.get("https://missing.example.com/.well-known/did.json").respond(status_code=404)

        with pytest.raises(DIDNotFoundError, match="not found"):
            await resolver.resolve("did:web:missing.example.com")

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_resolve_network_error(self, resolver: DIDWebResolver) -> None:
        """Test that network errors raise DIDNetworkError."""
        respx.get("https://unreachable.example.com/.well-known/did.json").mock(
            side_effect=httpx.ConnectError("Connection refused")
        )

        with pytest.raises(DIDNetworkError, match="Network error"):
            await resolver.resolve("did:web:unreachable.example.com")

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_resolve_timeout(self, resolver: DIDWebResolver) -> None:
        """Test that timeout raises DIDNetworkError (retryable)."""
        respx.get("https://slow.example.com/.well-known/did.json").mock(
            side_effect=httpx.TimeoutException("Timeout")
        )

        with pytest.raises(DIDNetworkError, match="Timeout"):
            await resolver.resolve("did:web:slow.example.com")

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_resolve_caches_result(
        self, resolver: DIDWebResolver, sample_did_document: dict
    ) -> None:
        """Test that resolved documents are cached."""
        route = respx.get("https://agent.example.com/.well-known/did.json").respond(
            json=sample_did_document
        )

        # First call
        await resolver.resolve("did:web:agent.example.com")
        assert route.call_count == 1

        # Second call should use cache
        await resolver.resolve("did:web:agent.example.com")
        assert route.call_count == 1  # No additional request

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_cache_expires(
        self, resolver: DIDWebResolver, sample_did_document: dict
    ) -> None:
        """Test that cache expires after TTL."""
        route = respx.get("https://agent.example.com/.well-known/did.json").respond(
            json=sample_did_document
        )

        # First call
        await resolver.resolve("did:web:agent.example.com")
        assert route.call_count == 1

        # Wait for cache to expire (TTL is 1 second)
        time.sleep(1.1)

        # Should fetch again
        await resolver.resolve("did:web:agent.example.com")
        assert route.call_count == 2

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_get_verification_key(
        self,
        resolver: DIDWebResolver,
        sample_did_document: dict,
        agent_public_key: EllipticCurvePublicKey,
    ) -> None:
        """Test extracting verification key from DID document."""
        respx.get("https://agent.example.com/.well-known/did.json").respond(
            json=sample_did_document
        )

        key = await resolver.get_verification_key("did:web:agent.example.com")

        # Verify it's the same key by comparing encoded points
        original_numbers = agent_public_key.public_numbers()
        resolved_numbers = key.public_numbers()

        assert original_numbers.x == resolved_numbers.x
        assert original_numbers.y == resolved_numbers.y

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_get_verification_key_by_id(
        self,
        resolver: DIDWebResolver,
        agent_public_key: EllipticCurvePublicKey,
        user_public_key: EllipticCurvePublicKey,
    ) -> None:
        """Test selecting verification key by key ID."""
        doc = {
            "id": "did:web:multi.example.com",
            "verificationMethod": [
                {
                    "id": "did:web:multi.example.com#key-1",
                    "type": "JsonWebKey2020",
                    "controller": "did:web:multi.example.com",
                    "publicKeyJwk": _public_key_to_jwk(agent_public_key),
                },
                {
                    "id": "did:web:multi.example.com#key-2",
                    "type": "JsonWebKey2020",
                    "controller": "did:web:multi.example.com",
                    "publicKeyJwk": _public_key_to_jwk(user_public_key),
                },
            ],
        }
        respx.get("https://multi.example.com/.well-known/did.json").respond(json=doc)

        # Get specific key
        key = await resolver.get_verification_key("did:web:multi.example.com", key_id="key-2")

        # Should be the user key
        user_numbers = user_public_key.public_numbers()
        resolved_numbers = key.public_numbers()

        assert user_numbers.x == resolved_numbers.x
        assert user_numbers.y == resolved_numbers.y

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_get_verification_key_not_found(self, resolver: DIDWebResolver) -> None:
        """Test that missing key ID raises error."""
        doc = {
            "id": "did:web:agent.example.com",
            "verificationMethod": [
                {
                    "id": "did:web:agent.example.com#key-1",
                    "type": "JsonWebKey2020",
                    "controller": "did:web:agent.example.com",
                    "publicKeyJwk": {"kty": "EC", "crv": "P-256", "x": "AA", "y": "AA"},
                }
            ],
        }
        respx.get("https://agent.example.com/.well-known/did.json").respond(json=doc)

        with pytest.raises(DIDResolutionError, match="Key ID not found"):
            await resolver.get_verification_key(
                "did:web:agent.example.com", key_id="nonexistent-key"
            )

        await resolver.close()

    @respx.mock
    @pytest.mark.asyncio
    async def test_no_verification_methods(self, resolver: DIDWebResolver) -> None:
        """Test that missing verification methods raises error."""
        doc = {
            "id": "did:web:empty.example.com",
            "verificationMethod": [],
        }
        respx.get("https://empty.example.com/.well-known/did.json").respond(json=doc)

        with pytest.raises(DIDResolutionError, match="No verification methods"):
            await resolver.get_verification_key("did:web:empty.example.com")

        await resolver.close()
