"""Tests for Caliper Pydantic models — construction, serialization, aliases."""

import pytest
from pydantic import ValidationError

from timeback_caliper import (
    ActivityCompletedEvent,
    EventResult,
    JobFailedError,
    JobReturnValue,
    JobStatus,
    TimebackActivityContext,
    TimebackActivityMetric,
    TimebackActivityMetricsCollection,
    TimebackApp,
    TimebackTimeSpentMetricsCollection,
    TimebackUser,
    TimeSpentEvent,
    TimeSpentMetric,
)

# ---------------------------------------------------------------------------
# Event models
# ---------------------------------------------------------------------------


class TestTimebackUser:
    """Tests for TimebackUser model."""

    def test_minimal_user(self):
        user = TimebackUser(
            id="https://example.edu/users/123",
            email="student@example.edu",
        )
        assert user.id == "https://example.edu/users/123"
        assert user.type == "TimebackUser"
        assert user.email == "student@example.edu"
        assert user.name is None
        assert user.role is None

    def test_full_user(self):
        user = TimebackUser(
            id="https://example.edu/users/123",
            email="student@example.edu",
            name="Jane Doe",
            role="student",
        )
        assert user.name == "Jane Doe"
        assert user.role == "student"

    def test_empty_id_rejected(self):
        with pytest.raises(ValidationError):
            TimebackUser(id="", email="student@example.edu")


class TestTimebackActivityContext:
    """Tests for TimebackActivityContext model."""

    def test_minimal_context(self):
        context = TimebackActivityContext(
            id="https://myapp.example.com/activities/123",
            subject="Math",
            app=TimebackApp(name="My Learning App"),
        )
        assert context.id == "https://myapp.example.com/activities/123"
        assert context.type == "TimebackActivityContext"
        assert context.subject == "Math"
        assert context.app.name == "My Learning App"

    def test_full_context(self):
        context = TimebackActivityContext(
            id="https://myapp.example.com/activities/123",
            subject="Math",
            app=TimebackApp(name="My App", id="https://myapp.example.com"),
        )
        assert context.app.id == "https://myapp.example.com"


class TestTimebackActivityMetric:
    """Tests for TimebackActivityMetric model."""

    def test_metric(self):
        metric = TimebackActivityMetric(type="correctQuestions", value=8)
        assert metric.type == "correctQuestions"
        assert metric.value == 8

    def test_negative_value_accepted(self):
        metric = TimebackActivityMetric(type="xpEarned", value=-10)
        assert metric.value == -10


class TestActivityCompletedEvent:
    """Tests for ActivityCompletedEvent model."""

    def test_full_event(self):
        event = ActivityCompletedEvent(
            id="urn:uuid:123e4567-e89b-12d3-a456-426614174000",
            actor=TimebackUser(
                id="https://example.edu/users/123",
                email="student@example.edu",
            ),
            object=TimebackActivityContext(
                id="https://myapp.example.com/activities/456",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            event_time="2024-01-15T14:30:00Z",
            generated=TimebackActivityMetricsCollection(
                id="urn:uuid:789",
                items=[
                    TimebackActivityMetric(type="totalQuestions", value=10),
                    TimebackActivityMetric(type="correctQuestions", value=8),
                ],
            ),
        )
        assert event.type == "ActivityEvent"
        assert event.action == "Completed"
        assert event.profile == "TimebackProfile"
        assert len(event.generated.items) == 2

    def test_serialization(self):
        event = ActivityCompletedEvent(
            id="urn:uuid:123",
            actor=TimebackUser(id="...", email="a@b.com"),
            object=TimebackActivityContext(
                id="...",
                subject="Math",
                app=TimebackApp(name="App"),
            ),
            event_time="2024-01-15T14:30:00Z",
            generated=TimebackActivityMetricsCollection(
                id="urn:uuid:456",
                items=[TimebackActivityMetric(type="xpEarned", value=100)],
            ),
        )
        data = event.model_dump(mode="json", by_alias=True)
        assert "@context" in data
        assert "eventTime" in data
        assert data["eventTime"] == "2024-01-15T14:30:00Z"


class TestTimeSpentEvent:
    """Tests for TimeSpentEvent model."""

    def test_time_spent_event(self):
        event = TimeSpentEvent(
            id="urn:uuid:123",
            actor=TimebackUser(id="...", email="a@b.com"),
            object=TimebackActivityContext(
                id="...",
                subject="Reading",
                app=TimebackApp(name="App"),
            ),
            event_time="2024-01-15T15:00:00Z",
            generated=TimebackTimeSpentMetricsCollection(
                id="urn:uuid:456",
                items=[
                    TimeSpentMetric(type="active", value=1800),
                    TimeSpentMetric(type="inactive", value=300),
                ],
            ),
        )
        assert event.type == "TimeSpentEvent"
        assert event.action == "SpentTime"
        assert len(event.generated.items) == 2


class TestTimeSpentMetric:
    """Tests for TimeSpentMetric model."""

    def test_metric(self):
        metric = TimeSpentMetric(
            type="active",
            value=1800,
            start_date="2024-01-15T10:00:00Z",
            end_date="2024-01-15T10:30:00Z",
        )
        assert metric.value == 1800
        assert metric.start_date == "2024-01-15T10:00:00Z"

    def test_value_over_24h_accepted(self):
        metric = TimeSpentMetric(type="active", value=90000)
        assert metric.value == 90000


class TestEventSerializationExcludesNone:
    """Caliper API rejects null values — fields should be omitted, not set to null."""

    def test_activity_event_excludes_none_values(self):
        event = ActivityCompletedEvent(
            id="urn:uuid:123e4567-e89b-12d3-a456-426614174000",
            actor=TimebackUser(id="https://example.edu/users/123", email="student@example.edu"),
            object=TimebackActivityContext(
                id="https://myapp.example.com/activities/456",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            event_time="2024-01-15T14:30:00Z",
            generated=TimebackActivityMetricsCollection(
                id="urn:uuid:789",
                items=[TimebackActivityMetric(type="correctQuestions", value=8)],
            ),
        )
        data = event.model_dump(mode="json", by_alias=True, exclude_none=True)

        assert "edApp" not in data
        assert "extensions" not in data
        assert "name" not in data["actor"]
        assert "role" not in data["actor"]
        assert "extensions" not in data["actor"]
        assert "course" not in data["object"]
        assert "activity" not in data["object"]
        assert "process" not in data["object"]
        assert "extensions" not in data["object"]
        assert "extensions" not in data["generated"]

        assert data["actor"]["id"] == "https://example.edu/users/123"
        assert data["actor"]["email"] == "student@example.edu"
        assert data["object"]["subject"] == "Math"

    def test_time_spent_event_excludes_none_values(self):
        event = TimeSpentEvent(
            id="urn:uuid:123",
            actor=TimebackUser(id="https://example.edu/users/456", email="student@example.edu"),
            object=TimebackActivityContext(
                id="https://myapp.example.com/activities/789",
                subject="Reading",
                app=TimebackApp(name="App"),
            ),
            event_time="2024-01-15T15:00:00Z",
            generated=TimebackTimeSpentMetricsCollection(
                id="urn:uuid:456",
                items=[TimeSpentMetric(type="active", value=1800)],
            ),
        )
        data = event.model_dump(mode="json", by_alias=True, exclude_none=True)

        assert "edApp" not in data
        assert "extensions" not in data
        assert "name" not in data["actor"]
        assert "extensions" not in data["actor"]
        assert "extensions" not in data["object"]
        assert "extensions" not in data["generated"]

    def test_event_with_values_includes_them(self):
        event = ActivityCompletedEvent(
            id="urn:uuid:123",
            actor=TimebackUser(
                id="https://example.edu/users/123",
                email="student@example.edu",
                name="Jane Doe",
                role="student",
                extensions={"customField": "value"},
            ),
            object=TimebackActivityContext(
                id="https://myapp.example.com/activities/456",
                subject="Math",
                app=TimebackApp(name="My App"),
                extensions={"contextData": 123},
            ),
            event_time="2024-01-15T14:30:00Z",
            generated=TimebackActivityMetricsCollection(
                id="urn:uuid:789",
                items=[TimebackActivityMetric(type="correctQuestions", value=8)],
                extensions={"pctCompleteApp": 0.85},
            ),
            ed_app="https://myapp.example.com",
            extensions={"courseId": "course-123"},
        )
        data = event.model_dump(mode="json", by_alias=True, exclude_none=True)

        assert data["edApp"] == "https://myapp.example.com"
        assert data["extensions"] == {"courseId": "course-123"}
        assert data["actor"]["name"] == "Jane Doe"
        assert data["actor"]["role"] == "student"
        assert data["actor"]["extensions"] == {"customField": "value"}
        assert data["object"]["extensions"] == {"contextData": 123}
        assert data["generated"]["extensions"] == {"pctCompleteApp": 0.85}

    def test_without_exclude_none_includes_nulls(self):
        event = ActivityCompletedEvent(
            id="urn:uuid:123",
            actor=TimebackUser(id="...", email="a@b.com"),
            object=TimebackActivityContext(id="...", subject="Math", app=TimebackApp(name="App")),
            event_time="2024-01-15T14:30:00Z",
            generated=TimebackActivityMetricsCollection(
                id="urn:uuid:456",
                items=[TimebackActivityMetric(type="xpEarned", value=100)],
            ),
        )
        data_with_nulls = event.model_dump(mode="json", by_alias=True)
        assert data_with_nulls["edApp"] is None
        assert data_with_nulls["extensions"] is None
        assert data_with_nulls["actor"]["name"] is None
        assert data_with_nulls["actor"]["extensions"] is None

        data_clean = event.model_dump(mode="json", by_alias=True, exclude_none=True)
        assert "edApp" not in data_clean
        assert "extensions" not in data_clean
        assert "name" not in data_clean["actor"]
        assert "extensions" not in data_clean["actor"]


# ---------------------------------------------------------------------------
# Job models
# ---------------------------------------------------------------------------


class TestJobStatusSchema:
    """Tests for JobStatus model."""

    def test_uses_state_field(self):
        status = JobStatus(id="job-123", state="completed")
        assert status.state == "completed"
        assert status.id == "job-123"

    def test_state_values(self):
        for state in ["waiting", "active", "completed", "failed"]:
            status = JobStatus(id="job-123", state=state)
            assert status.state == state

    def test_with_return_value(self):
        status = JobStatus(
            id="job-123",
            state="completed",
            return_value=JobReturnValue(
                status="success",
                results=[
                    EventResult(allocated_id="1", external_id="urn:uuid:abc"),
                    EventResult(allocated_id="2", external_id="urn:uuid:def"),
                ],
            ),
        )
        assert status.return_value is not None
        assert len(status.return_value.results) == 2
        assert status.return_value.results[0].external_id == "urn:uuid:abc"
        assert status.return_value.results[1].allocated_id == "2"

    def test_from_api_json_with_camel_case(self):
        data = {
            "id": "job-456",
            "state": "active",
            "processedOn": "2024-01-15T10:00:00Z",
        }
        status = JobStatus(**data)
        assert status.id == "job-456"
        assert status.state == "active"
        assert status.processed_on == "2024-01-15T10:00:00Z"

    def test_optional_fields_default_to_none(self):
        status = JobStatus(id="job-123", state="waiting")
        assert status.return_value is None
        assert status.processed_on is None


class TestEventResult:
    """Tests for EventResult model."""

    def test_basic_fields(self):
        result = EventResult(allocated_id="123", external_id="urn:uuid:abc")
        assert result.allocated_id == "123"
        assert result.external_id == "urn:uuid:abc"

    def test_from_camel_case(self):
        data = {"allocatedId": "123", "externalId": "urn:uuid:abc"}
        result = EventResult(**data)
        assert result.allocated_id == "123"
        assert result.external_id == "urn:uuid:abc"


class TestJobReturnValue:
    """Tests for JobReturnValue model."""

    def test_status_and_results(self):
        return_value = JobReturnValue(
            status="success",
            results=[EventResult(allocated_id="1", external_id="urn:uuid:a")],
        )
        assert return_value.status == "success"
        assert len(return_value.results) == 1


class TestJobFailedError:
    """Tests for JobFailedError."""

    def test_message_includes_job_id(self):
        error = JobFailedError("job-123")
        assert "job-123" in str(error)

    def test_message_includes_error_detail(self):
        error = JobFailedError("job-123", error="Validation failed")
        assert "job-123" in str(error)
        assert "Validation failed" in str(error)

    def test_job_id_attribute(self):
        error = JobFailedError("job-123")
        assert error.job_id == "job-123"
