"""Tests for MailioReceipt data models."""

from __future__ import annotations

import uuid
from datetime import datetime, timezone

import pytest

from mailio_receipt.errors import ValidationError
from mailio_receipt.models import (
    Actors,
    ArtifactBinding,
    Chain,
    Intent,
    IntentKind,
    MailioAuditEvent,
    MailioReceipt,
    MessageBinding,
    Signature,
    SignedMetadata,
    create_event,
)


class TestActors:
    """Tests for Actors dataclass."""

    def test_valid_actors(self) -> None:
        """Test creating valid Actors."""
        actors = Actors(
            issuer="did:web:agent.example.com",
            subject="did:web:user.example.com",
        )
        assert actors.issuer == "did:web:agent.example.com"
        assert actors.subject == "did:web:user.example.com"
        assert actors.authority is None
        assert actors.others is None

    def test_actors_with_authority(self) -> None:
        """Test Actors with authority."""
        actors = Actors(
            issuer="did:web:agent.example.com",
            subject="did:web:user.example.com",
            authority="did:web:authority.example.com",
        )
        assert actors.authority == "did:web:authority.example.com"

    def test_actors_with_others(self) -> None:
        """Test Actors with others."""
        actors = Actors(
            issuer="did:web:agent.example.com",
            subject="did:web:user.example.com",
            others=("did:web:other1.example.com", "did:web:other2.example.com"),
        )
        assert actors.others == ("did:web:other1.example.com", "did:web:other2.example.com")

    def test_invalid_issuer_empty(self) -> None:
        """Test that empty issuer raises ValidationError."""
        with pytest.raises(ValidationError, match="issuer must not be empty"):
            Actors(
                issuer="",
                subject="did:web:user.example.com",
            )

    def test_invalid_subject_empty(self) -> None:
        """Test that empty subject raises ValidationError."""
        with pytest.raises(ValidationError, match="subject must not be empty"):
            Actors(
                issuer="did:web:agent.example.com",
                subject="",
            )

    def test_to_dict(self) -> None:
        """Test Actors serialization to dict."""
        actors = Actors(
            issuer="did:web:agent.example.com",
            subject="did:web:user.example.com",
            authority="did:web:authority.example.com",
            others=("did:web:other.example.com",),
        )
        d = actors.to_dict()
        assert d["issuer"] == "did:web:agent.example.com"
        assert d["subject"] == "did:web:user.example.com"
        assert d["authority"] == "did:web:authority.example.com"
        assert d["others"] == ["did:web:other.example.com"]

    def test_from_dict(self) -> None:
        """Test Actors deserialization from dict."""
        data = {
            "issuer": "did:web:agent.example.com",
            "subject": "did:web:user.example.com",
        }
        actors = Actors.from_dict(data)
        assert actors.issuer == "did:web:agent.example.com"
        assert actors.subject == "did:web:user.example.com"


class TestIntent:
    """Tests for Intent dataclass."""

    def test_valid_intent(self) -> None:
        """Test creating valid Intent."""
        intent = Intent(
            kind=IntentKind.ACTION_PLANNED,
            action="invoice.send",
            category="financial",
            scope={"amount": 1500.00},
        )
        assert intent.kind == IntentKind.ACTION_PLANNED
        assert intent.action == "invoice.send"
        assert intent.scope == {"amount": 1500.00}

    def test_intent_kinds(self) -> None:
        """Test all intent kind constants."""
        assert IntentKind.ACTION_PLANNED == "mailio.workflow.action.planned"
        assert IntentKind.APPROVAL_REQUESTED == "mailio.workflow.approval.requested"
        assert IntentKind.APPROVAL_GRANTED == "mailio.workflow.approval.granted"
        assert IntentKind.APPROVAL_REVOKED == "mailio.workflow.approval.revoked"
        assert IntentKind.ACTION_EXECUTED == "mailio.workflow.action.executed"
        assert IntentKind.ACTION_FAILED == "mailio.workflow.action.failed"
        assert IntentKind.NOTIFY == "mailio.workflow.notify"

    def test_to_dict(self) -> None:
        """Test Intent serialization."""
        intent = Intent(
            kind=IntentKind.ACTION_PLANNED,
            action="invoice.send",
            category="financial",
            scope={"amount": 1500.00},
        )
        d = intent.to_dict()
        assert d["kind"] == IntentKind.ACTION_PLANNED
        assert d["action"] == "invoice.send"
        assert d["scope"] == {"amount": 1500.00}


class TestChain:
    """Tests for Chain dataclass."""

    def test_valid_chain(self) -> None:
        """Test creating valid Chain."""
        chain = Chain(
            prev_receipt_digest="a" * 64,
            reason="Chaining to previous receipt",
        )
        assert chain.prev_receipt_digest == "a" * 64
        assert chain.reason == "Chaining to previous receipt"

    def test_invalid_digest_length(self) -> None:
        """Test that invalid digest length raises ValidationError."""
        with pytest.raises(ValidationError, match="prev_receipt_digest must be a valid SHA-256 digest"):
            Chain(prev_receipt_digest="short")

    def test_invalid_digest_chars(self) -> None:
        """Test that invalid hex chars raise ValidationError."""
        with pytest.raises(ValidationError, match="prev_receipt_digest must be a valid SHA-256 digest"):
            Chain(prev_receipt_digest="g" * 64)  # 'g' is not a hex char


class TestBinding:
    """Tests for Binding dataclass."""

    def test_artifact_binding(self) -> None:
        """Test creating ArtifactBinding."""
        binding = ArtifactBinding(
            name="invoice.pdf",
            digest="b" * 64,
            media_type="application/pdf",
        )
        assert binding.name == "invoice.pdf"
        assert binding.digest == "b" * 64
        assert binding.media_type == "application/pdf"

    def test_invalid_artifact_digest(self) -> None:
        """Test that invalid artifact digest raises ValidationError."""
        with pytest.raises(ValidationError, match="digest must be a valid SHA-256 digest"):
            ArtifactBinding(name="file.txt", digest="invalid")

    def test_message_binding(self) -> None:
        """Test creating MessageBinding."""
        binding = MessageBinding(
            message_id="<msg123@example.com>",
            thread_id="thread-456",
        )
        assert binding.message_id == "<msg123@example.com>"
        assert binding.thread_id == "thread-456"


class TestMailioAuditEvent:
    """Tests for MailioAuditEvent dataclass."""

    def test_valid_event(self, sample_actors: Actors, sample_intent: Intent) -> None:
        """Test creating valid event."""
        event = MailioAuditEvent(
            id=str(uuid.uuid4()),
            flow_id=str(uuid.uuid4()),
            ts=datetime.now(timezone.utc).isoformat(),
            actors=sample_actors,
            intent=sample_intent,
        )
        assert event.type == "MailioAuditEvent"
        assert event.v == "v1"

    def test_invalid_event_id(self, sample_actors: Actors, sample_intent: Intent) -> None:
        """Test that invalid event ID raises ValidationError."""
        with pytest.raises(ValidationError, match="id must be a valid UUID"):
            MailioAuditEvent(
                id="not-a-uuid",
                flow_id=str(uuid.uuid4()),
                ts=datetime.now(timezone.utc).isoformat(),
                actors=sample_actors,
                intent=sample_intent,
            )

    def test_invalid_flow_id(self, sample_actors: Actors, sample_intent: Intent) -> None:
        """Test that invalid flow ID raises ValidationError."""
        with pytest.raises(ValidationError, match="flow_id must be a valid UUID"):
            MailioAuditEvent(
                id=str(uuid.uuid4()),
                flow_id="not-a-uuid",
                ts=datetime.now(timezone.utc).isoformat(),
                actors=sample_actors,
                intent=sample_intent,
            )

    def test_valid_ttl(self, sample_actors: Actors, sample_intent: Intent) -> None:
        """Test valid TTL format."""
        event = MailioAuditEvent(
            id=str(uuid.uuid4()),
            flow_id=str(uuid.uuid4()),
            ts=datetime.now(timezone.utc).isoformat(),
            actors=sample_actors,
            intent=sample_intent,
            ttl="PT1H",
        )
        assert event.ttl == "PT1H"

    def test_invalid_ttl(self, sample_actors: Actors, sample_intent: Intent) -> None:
        """Test that invalid TTL raises ValidationError."""
        with pytest.raises(ValidationError, match="ttl must be valid ISO8601 duration"):
            MailioAuditEvent(
                id=str(uuid.uuid4()),
                flow_id=str(uuid.uuid4()),
                ts=datetime.now(timezone.utc).isoformat(),
                actors=sample_actors,
                intent=sample_intent,
                ttl="invalid",
            )

    def test_to_dict_camel_case(self, sample_actors: Actors, sample_intent: Intent) -> None:
        """Test that to_dict produces camelCase keys."""
        event = MailioAuditEvent(
            id=str(uuid.uuid4()),
            flow_id=str(uuid.uuid4()),
            ts=datetime.now(timezone.utc).isoformat(),
            actors=sample_actors,
            intent=sample_intent,
        )
        d = event.to_dict()
        assert "flowId" in d
        assert "flow_id" not in d


class TestCreateEvent:
    """Tests for create_event factory function."""

    def test_create_event_auto_generates_id(
        self, sample_actors: Actors, sample_intent: Intent, sample_flow_id: str
    ) -> None:
        """Test that create_event auto-generates event ID."""
        event = create_event(
            flow_id=sample_flow_id,
            actors=sample_actors,
            intent=sample_intent,
        )
        assert event.id != ""
        assert uuid.UUID(event.id)  # Valid UUID

    def test_create_event_auto_generates_timestamp(
        self, sample_actors: Actors, sample_intent: Intent, sample_flow_id: str
    ) -> None:
        """Test that create_event auto-generates timestamp."""
        event = create_event(
            flow_id=sample_flow_id,
            actors=sample_actors,
            intent=sample_intent,
        )
        assert event.ts != ""
        # Verify it's a valid ISO format
        datetime.fromisoformat(event.ts)

    def test_create_event_uses_provided_id(
        self,
        sample_actors: Actors,
        sample_intent: Intent,
        sample_flow_id: str,
        sample_event_id: str,
    ) -> None:
        """Test that create_event uses provided event ID."""
        event = create_event(
            flow_id=sample_flow_id,
            actors=sample_actors,
            intent=sample_intent,
            event_id=sample_event_id,
        )
        assert event.id == sample_event_id


class TestSignature:
    """Tests for Signature dataclass."""

    def test_valid_signature(self) -> None:
        """Test creating valid Signature."""
        sig = Signature(
            role="agent",
            signer="did:web:agent.example.com",
            alg="ES256",
            signed=SignedMetadata(
                canonicalization="RFC8785",
                digest="a" * 64,
                target="/event",
            ),
            jws="eyJhbGciOiJFUzI1NiJ9..signature",
        )
        assert sig.role == "agent"
        assert sig.signer == "did:web:agent.example.com"
        assert sig.alg == "ES256"

    def test_invalid_signer_empty(self) -> None:
        """Test that empty signer raises ValidationError."""
        with pytest.raises(ValidationError, match="signer must not be empty"):
            Signature(
                role="agent",
                signer="",
                alg="ES256",
                signed=SignedMetadata(digest="a" * 64),
                jws="eyJhbGciOiJFUzI1NiJ9..signature",
            )


class TestMailioReceipt:
    """Tests for MailioReceipt dataclass."""

    def test_receipt_requires_signatures(
        self, sample_actors: Actors, sample_intent: Intent
    ) -> None:
        """Test that receipt requires at least one signature."""
        event = MailioAuditEvent(
            id=str(uuid.uuid4()),
            flow_id=str(uuid.uuid4()),
            ts=datetime.now(timezone.utc).isoformat(),
            actors=sample_actors,
            intent=sample_intent,
        )
        with pytest.raises(ValidationError, match="at least one signature"):
            MailioReceipt(
                event=event,
                event_digest="a" * 64,
                signatures=(),  # Empty tuple
            )

    def test_receipt_json_roundtrip(
        self, sample_actors: Actors, sample_intent: Intent
    ) -> None:
        """Test that receipt can be serialized and deserialized."""
        event = MailioAuditEvent(
            id=str(uuid.uuid4()),
            flow_id=str(uuid.uuid4()),
            ts=datetime.now(timezone.utc).isoformat(),
            actors=sample_actors,
            intent=sample_intent,
        )
        sig = Signature(
            role="agent",
            signer="did:web:agent.example.com",
            alg="ES256",
            signed=SignedMetadata(digest="a" * 64),
            jws="eyJhbGciOiJFUzI1NiJ9..signature",
        )
        receipt = MailioReceipt(
            event=event,
            event_digest="a" * 64,
            signatures=(sig,),
        )

        json_str = receipt.to_json()
        loaded = MailioReceipt.from_json(json_str)

        assert loaded.event.id == receipt.event.id
        assert loaded.event_digest == receipt.event_digest
        assert len(loaded.signatures) == 1

    def test_parse_webauthn_receipt(self) -> None:
        """Test parsing a receipt with non-DID identifiers and base64url digest."""
        data = {
            "type": "MailioReceipt",
            "v": "v1",
            "event": {
                "type": "MailioAuditEvent",
                "v": "v1",
                "id": "fd769520-42d1-4b84-bc4a-42d32df039bc",
                "flowId": "2eeb0bdc-b885-4547-8514-d4e3f33206cb",
                "ts": "2026-02-11T05:38:19.862Z",
                "actors": {
                    "issuer": "test2@mail.io",
                    "subject": "igordemo-local-69676f72406d61696c2e696f"
                },
                "intent": {
                    "kind": "trigger_watch_condition",
                    "action": "price_above",
                    "category": "stock_price",
                    "scope": {
                        "ticker": "MIO",
                        "direction": "price_above",
                        "threshold": 23
                    },
                },
            },
            "event_digest": "AyMOTKC0GZsk74MsxUJcfcqA8LUfZ1NZ9nCwBpvc5YM",
            "signatures": [
                {
                    "role": "user",
                    "signer": "igordemo-local-69676f72406d61696c2e696f",
                    "alg": "ES256",
                    "signed": {
                        "canonicalization": "RFC8785",
                        "digest": "AyMOTKC0GZsk74MsxUJcfcqA8LUfZ1NZ9nCwBpvc5YM",
                        "target": "/event",
                    },
                    "jws": "MEUCIBO_ypa3TetBHcfVnO1-Yh4xbDzNYmiL_1AP9pO8hFOIAiEApfefA8OL2ydn8vVrZm-weSi2fPI2DVKwgRu_APHV81c",
                },
            ],
        }

        receipt = MailioReceipt.from_dict(data)

        # Event fields
        assert receipt.event.id == "fd769520-42d1-4b84-bc4a-42d32df039bc"
        assert receipt.event.flow_id == "2eeb0bdc-b885-4547-8514-d4e3f33206cb"
        assert receipt.event.ts == "2026-02-11T05:38:19.862Z"
        assert receipt.event.actors.issuer == "test2@mail.io"
        assert receipt.event.actors.subject == "igordemo-local-69676f72406d61696c2e696f"
        assert receipt.event.intent.kind == "trigger_watch_condition"
        assert receipt.event.intent.action == "price_above"
        assert receipt.event.intent.category == "stock_price"
        assert receipt.event.intent.scope == {
            "ticker": "MIO",
            "direction": "price_above",
            "threshold": 23,
        }

        # Digest (base64url format)
        assert receipt.event_digest == "AyMOTKC0GZsk74MsxUJcfcqA8LUfZ1NZ9nCwBpvc5YM"

        # Signature
        assert len(receipt.signatures) == 1
        sig = receipt.signatures[0]
        assert sig.role == "user"
        assert sig.signer == "igordemo-local-69676f72406d61696c2e696f"
        assert sig.alg == "ES256"
        assert sig.signed.canonicalization == "RFC8785"
        assert sig.signed.digest == "AyMOTKC0GZsk74MsxUJcfcqA8LUfZ1NZ9nCwBpvc5YM"
        assert sig.signed.target == "/event"

        # Roundtrip
        loaded = MailioReceipt.from_json(receipt.to_json())
        assert loaded.event.id == receipt.event.id
        assert loaded.event_digest == receipt.event_digest
        assert loaded.signatures[0].jws == sig.jws
