"""
Tests for event factory functions.
"""

from __future__ import annotations

from timeback_caliper import (
    CALIPER_DATA_VERSION,
    QUESTION_RESULT_SCORE_TYPE,
    ActivityCompletedEvent,
    ActivityCompletedInput,
    AssessmentItemEvent,
    QuestionAnsweredInput,
    QuestionGradedInput,
    QuestionGradeEvent,
    QuestionObjectInput,
    QuestionSeenInput,
    ResponseGeneratedInput,
    ScoreGeneratedInput,
    TimeSpentEvent,
    TimeSpentInput,
    create_activity_event,
    create_question_answered_event,
    create_question_graded_event,
    create_question_seen_event,
    create_time_spent_event,
)
from timeback_caliper.types.timeback import (
    TimebackActivityContext,
    TimebackActivityMetric,
    TimebackApp,
    TimebackUser,
    TimeSpentMetric,
)


class TestCreateActivityEvent:
    """Tests for create_activity_event factory."""

    def test_creates_event_with_required_fields(self) -> None:
        """Creates ActivityCompletedEvent with all required fields."""
        input_data = ActivityCompletedInput(
            actor=TimebackUser(
                id="urn:uuid:user-123",
                email="student@example.edu",
            ),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[
                TimebackActivityMetric(type="totalQuestions", value=10),
                TimebackActivityMetric(type="correctQuestions", value=8),
            ],
        )

        event = create_activity_event(input_data)

        assert isinstance(event, ActivityCompletedEvent)
        assert event.context == CALIPER_DATA_VERSION
        assert event.type == "ActivityEvent"
        assert event.action == "Completed"
        assert event.profile == "TimebackProfile"
        assert event.actor.email == "student@example.edu"
        assert len(event.generated.items) == 2

    def test_auto_generates_event_id(self) -> None:
        """Auto-generates event ID if not provided."""
        input_data = ActivityCompletedInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_activity_event(input_data)

        assert event.id.startswith("urn:uuid:")
        assert len(event.id) > 10

    def test_uses_provided_event_id(self) -> None:
        """Uses provided event ID if specified."""
        input_data = ActivityCompletedInput(
            id="urn:uuid:custom-event-id",
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_activity_event(input_data)

        assert event.id == "urn:uuid:custom-event-id"

    def test_auto_generates_metrics_id(self) -> None:
        """Auto-generates metrics collection ID if not provided."""
        input_data = ActivityCompletedInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_activity_event(input_data)

        assert event.generated.id.startswith("urn:uuid:")

    def test_uses_provided_metrics_id(self) -> None:
        """Uses provided metrics ID if specified."""
        input_data = ActivityCompletedInput(
            metrics_id="urn:uuid:custom-metrics-id",
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_activity_event(input_data)

        assert event.generated.id == "urn:uuid:custom-metrics-id"

    def test_auto_generates_event_time(self) -> None:
        """Auto-generates event time if not provided."""
        input_data = ActivityCompletedInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_activity_event(input_data)

        # Should be ISO format
        assert "T" in event.event_time

    def test_uses_provided_event_time(self) -> None:
        """Uses provided event time if specified."""
        input_data = ActivityCompletedInput(
            event_time="2024-01-15T10:30:00Z",
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_activity_event(input_data)

        assert event.event_time == "2024-01-15T10:30:00Z"

    def test_includes_extensions(self) -> None:
        """Includes extensions if provided."""
        input_data = ActivityCompletedInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
            extensions={"customField": "customValue"},
        )

        event = create_activity_event(input_data)

        assert event.extensions == {"customField": "customValue"}

    def test_includes_attempt_in_generated_when_provided(self) -> None:
        """Includes attempt at generated.attempt (not inside generated.extensions)."""
        input_data = ActivityCompletedInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
            attempt=2,
        )

        event = create_activity_event(input_data)

        assert event.generated.attempt == 2
        assert event.generated.extensions is None

    def test_includes_session_when_provided(self) -> None:
        """Sets session field when provided in input."""
        input_data = ActivityCompletedInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
            session="urn:uuid:run-abc-123",
        )

        event = create_activity_event(input_data)

        assert event.session == "urn:uuid:run-abc-123"

    def test_session_is_none_when_not_provided(self) -> None:
        """Session is None when not provided in input."""
        input_data = ActivityCompletedInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Math",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_activity_event(input_data)

        assert event.session is None

    def test_accepts_camel_case_fixture_style_fields(self) -> None:
        """Accepts camelCase fixture-style keys while preserving snake_case callers."""
        input_data = ActivityCompletedInput.model_validate(
            {
                "actor": {
                    "id": "urn:uuid:user-123",
                    "email": "student@example.edu",
                },
                "object": {
                    "id": "urn:uuid:activity-123",
                    "subject": "Math",
                    "app": {"name": "My App"},
                },
                "metrics": [],
                "edApp": "https://app.test",
                "eventTime": "2024-01-15T10:30:00Z",
                "metricsId": "urn:uuid:metrics-123",
                "generatedExtensions": {"source": "fixture"},
            }
        )

        event = create_activity_event(input_data)

        assert event.ed_app == "https://app.test"
        assert event.event_time == "2024-01-15T10:30:00Z"
        assert event.generated.id == "urn:uuid:metrics-123"
        assert event.generated.extensions == {"source": "fixture"}


class TestCreateTimeSpentEvent:
    """Tests for create_time_spent_event factory."""

    def test_creates_event_with_required_fields(self) -> None:
        """Creates TimeSpentEvent with all required fields."""
        input_data = TimeSpentInput(
            actor=TimebackUser(
                id="urn:uuid:user-123",
                email="student@example.edu",
            ),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Reading",
                app=TimebackApp(name="My App"),
            ),
            metrics=[
                TimeSpentMetric(type="active", value=1800),
                TimeSpentMetric(type="inactive", value=300),
            ],
        )

        event = create_time_spent_event(input_data)

        assert isinstance(event, TimeSpentEvent)
        assert event.context == CALIPER_DATA_VERSION
        assert event.type == "TimeSpentEvent"
        assert event.action == "SpentTime"
        assert event.profile == "TimebackProfile"
        assert event.actor.email == "student@example.edu"
        assert len(event.generated.items) == 2

    def test_auto_generates_ids(self) -> None:
        """Auto-generates event and metrics IDs if not provided."""
        input_data = TimeSpentInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Reading",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_time_spent_event(input_data)

        assert event.id.startswith("urn:uuid:")
        assert event.generated.id.startswith("urn:uuid:")

    def test_uses_provided_ids(self) -> None:
        """Uses provided IDs if specified."""
        input_data = TimeSpentInput(
            id="urn:uuid:my-event",
            metrics_id="urn:uuid:my-metrics",
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Reading",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_time_spent_event(input_data)

        assert event.id == "urn:uuid:my-event"
        assert event.generated.id == "urn:uuid:my-metrics"

    def test_includes_session_when_provided(self) -> None:
        """Sets session field when provided in input."""
        input_data = TimeSpentInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Reading",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
            session="urn:uuid:run-abc-123",
        )

        event = create_time_spent_event(input_data)

        assert event.session == "urn:uuid:run-abc-123"

    def test_session_is_none_when_not_provided(self) -> None:
        """Session is None when not provided in input."""
        input_data = TimeSpentInput(
            actor=TimebackUser(id="urn:uuid:user-123", email="test@example.edu"),
            object=TimebackActivityContext(
                id="urn:uuid:activity-123",
                subject="Reading",
                app=TimebackApp(name="My App"),
            ),
            metrics=[],
        )

        event = create_time_spent_event(input_data)

        assert event.session is None

    def test_accepts_camel_case_fixture_style_fields(self) -> None:
        """Accepts camelCase fixture-style keys while preserving snake_case callers."""
        input_data = TimeSpentInput.model_validate(
            {
                "actor": {
                    "id": "urn:uuid:user-123",
                    "email": "student@example.edu",
                },
                "object": {
                    "id": "urn:uuid:activity-123",
                    "subject": "Reading",
                    "app": {"name": "My App"},
                },
                "metrics": [],
                "edApp": "https://app.test",
                "eventTime": "2024-01-15T10:30:00Z",
                "metricsId": "urn:uuid:metrics-123",
            }
        )

        event = create_time_spent_event(input_data)

        assert event.ed_app == "https://app.test"
        assert event.event_time == "2024-01-15T10:30:00Z"
        assert event.generated.id == "urn:uuid:metrics-123"


_BASE_QUESTION_ACTOR = "urn:uuid:student-123"
_BASE_QUESTION_ED_APP = "https://myapp.example.com"
_BASE_QUESTION_OBJECT = QuestionObjectInput(id="urn:uuid:q-456", name="What is 2+2?")


class TestCreateQuestionSeenEvent:
    """Tests for create_question_seen_event factory."""

    def test_creates_event_with_required_fields(self) -> None:
        """Creates AssessmentItemEvent.Started with all required fields."""
        event = create_question_seen_event(
            QuestionSeenInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert isinstance(event, AssessmentItemEvent)
        assert event.context == CALIPER_DATA_VERSION
        assert event.type == "AssessmentItemEvent"
        assert event.action == "Started"
        assert event.profile == "AssessmentProfile"
        assert event.actor == _BASE_QUESTION_ACTOR
        assert event.ed_app == _BASE_QUESTION_ED_APP

    def test_object_has_assessment_item_type(self) -> None:
        """Object has type 'AssessmentItem' auto-injected."""
        event = create_question_seen_event(
            QuestionSeenInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.object["type"] == "AssessmentItem"
        assert event.object["id"] == "urn:uuid:q-456"
        assert event.object["name"] == "What is 2+2?"

    def test_auto_generates_event_id(self) -> None:
        """Auto-generates event ID if not provided."""
        event = create_question_seen_event(
            QuestionSeenInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.id.startswith("urn:uuid:")

    def test_uses_provided_event_id(self) -> None:
        """Uses provided event ID if specified."""
        event = create_question_seen_event(
            QuestionSeenInput(
                id="urn:uuid:custom-event-id",
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.id == "urn:uuid:custom-event-id"

    def test_auto_generates_event_time(self) -> None:
        """Auto-generates event time if not provided."""
        event = create_question_seen_event(
            QuestionSeenInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert "T" in event.event_time

    def test_uses_provided_event_time(self) -> None:
        """Uses provided event time if specified."""
        event = create_question_seen_event(
            QuestionSeenInput(
                event_time="2026-01-15T10:30:00Z",
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.event_time == "2026-01-15T10:30:00Z"

    def test_session_is_none_when_not_provided(self) -> None:
        """Session is None when not provided."""
        event = create_question_seen_event(
            QuestionSeenInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.session is None

    def test_includes_session_when_provided(self) -> None:
        """Sets session field when provided."""
        event = create_question_seen_event(
            QuestionSeenInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
                session="urn:uuid:session-abc",
            )
        )

        assert event.session == "urn:uuid:session-abc"

    def test_object_preserves_is_part_of(self) -> None:
        """isPartOf is preserved when provided."""
        event = create_question_seen_event(
            QuestionSeenInput(
                actor=_BASE_QUESTION_ACTOR,
                object=QuestionObjectInput(
                    id="urn:uuid:q-1",
                    is_part_of={"id": "urn:uuid:assessment-1", "type": "Assessment"},
                ),
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.object["isPartOf"] == {"id": "urn:uuid:assessment-1", "type": "Assessment"}


class TestCreateQuestionAnsweredEvent:
    """Tests for create_question_answered_event factory."""

    def test_creates_event_with_required_fields(self) -> None:
        """Creates AssessmentItemEvent.Completed with all required fields."""
        event = create_question_answered_event(
            QuestionAnsweredInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert isinstance(event, AssessmentItemEvent)
        assert event.type == "AssessmentItemEvent"
        assert event.action == "Completed"
        assert event.profile == "AssessmentProfile"
        assert event.object["type"] == "AssessmentItem"

    def test_generated_is_none_when_not_provided(self) -> None:
        """Generated is None when not provided."""
        event = create_question_answered_event(
            QuestionAnsweredInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.generated is None

    def test_includes_generated_response_when_provided(self) -> None:
        """Generated entity has type 'Response' auto-injected."""
        event = create_question_answered_event(
            QuestionAnsweredInput(
                actor=_BASE_QUESTION_ACTOR,
                object=_BASE_QUESTION_OBJECT,
                ed_app=_BASE_QUESTION_ED_APP,
                generated=ResponseGeneratedInput(
                    id="urn:uuid:response-789",
                    attempt="urn:uuid:attempt-101",
                    started_at_time="2026-01-15T10:00:00Z",
                    ended_at_time="2026-01-15T10:00:30Z",
                ),
            )
        )

        assert event.generated is not None
        assert event.generated["type"] == "Response"
        assert event.generated["id"] == "urn:uuid:response-789"
        assert event.generated["attempt"] == "urn:uuid:attempt-101"
        assert event.generated["startedAtTime"] == "2026-01-15T10:00:00Z"
        assert event.generated["endedAtTime"] == "2026-01-15T10:00:30Z"


class TestCreateQuestionGradedEvent:
    """Tests for create_question_graded_event factory."""

    def test_creates_event_with_required_fields(self) -> None:
        """Creates GradeEvent.Graded with all required fields."""
        event = create_question_graded_event(
            QuestionGradedInput(
                actor=_BASE_QUESTION_ACTOR,
                object="urn:uuid:attempt-101",
                generated=ScoreGeneratedInput(score_given=1, max_score=1),
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert isinstance(event, QuestionGradeEvent)
        assert event.type == "GradeEvent"
        assert event.action == "Graded"
        assert event.profile == "GradingProfile"
        assert event.context == CALIPER_DATA_VERSION

    def test_object_is_attempt_iri(self) -> None:
        """Object is the attempt IRI string."""
        event = create_question_graded_event(
            QuestionGradedInput(
                actor=_BASE_QUESTION_ACTOR,
                object="urn:uuid:attempt-101",
                generated=ScoreGeneratedInput(score_given=1, max_score=1),
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.object == "urn:uuid:attempt-101"

    def test_generated_score_has_correct_type_and_score_type(self) -> None:
        """Score entity has type='Score' and scoreType='QUESTION_RESULT' auto-injected."""
        event = create_question_graded_event(
            QuestionGradedInput(
                actor=_BASE_QUESTION_ACTOR,
                object="urn:uuid:attempt-101",
                generated=ScoreGeneratedInput(
                    score_given=0.85,
                    max_score=1.0,
                    attempt="urn:uuid:attempt-101",
                ),
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.generated["type"] == "Score"
        assert event.generated["scoreType"] == QUESTION_RESULT_SCORE_TYPE
        assert event.generated["scoreGiven"] == 0.85
        assert event.generated["maxScore"] == 1.0
        assert event.generated["attempt"] == "urn:uuid:attempt-101"

    def test_score_id_is_auto_generated_when_not_provided(self) -> None:
        """Score ID is auto-generated when not provided."""
        event = create_question_graded_event(
            QuestionGradedInput(
                actor=_BASE_QUESTION_ACTOR,
                object="urn:uuid:attempt-101",
                generated=ScoreGeneratedInput(score_given=1, max_score=1),
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.generated["id"].startswith("urn:uuid:")

    def test_score_id_is_preserved_when_provided(self) -> None:
        """Score ID is preserved when provided."""
        event = create_question_graded_event(
            QuestionGradedInput(
                actor=_BASE_QUESTION_ACTOR,
                object="urn:uuid:attempt-101",
                generated=ScoreGeneratedInput(
                    id="urn:uuid:score-custom",
                    score_given=1,
                    max_score=1,
                ),
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.generated["id"] == "urn:uuid:score-custom"

    def test_session_is_none_when_not_provided(self) -> None:
        """Session is None when not provided."""
        event = create_question_graded_event(
            QuestionGradedInput(
                actor=_BASE_QUESTION_ACTOR,
                object="urn:uuid:attempt-101",
                generated=ScoreGeneratedInput(score_given=1, max_score=1),
                ed_app=_BASE_QUESTION_ED_APP,
            )
        )

        assert event.session is None

    def test_includes_session_when_provided(self) -> None:
        """Sets session field when provided."""
        event = create_question_graded_event(
            QuestionGradedInput(
                actor=_BASE_QUESTION_ACTOR,
                object="urn:uuid:attempt-101",
                generated=ScoreGeneratedInput(score_given=1, max_score=1),
                ed_app=_BASE_QUESTION_ED_APP,
                session="urn:uuid:session-abc",
            )
        )

        assert event.session == "urn:uuid:session-abc"
