"""Tests for Pydantic models in cde_onboard.models."""

from __future__ import annotations

from datetime import UTC

import pytest
from pydantic import ValidationError

from cde_onboard.models import (
    CodeBlock,
    LessonMeta,
    LessonStep,
    ProgressData,
    ScaffoldFile,
)

# ---------------------------------------------------------------------------
# CodeBlock
# ---------------------------------------------------------------------------


class TestCodeBlock:
    """Tests for the CodeBlock model."""

    def test_minimal_code_block(self) -> None:
        """A CodeBlock only requires source; language defaults to yaml."""
        block = CodeBlock(source="key: value")
        assert block.source == "key: value"
        assert block.language == "yaml"
        assert block.filename is None

    def test_code_block_all_fields(self, sample_code_block: CodeBlock) -> None:
        """A CodeBlock with all fields populated."""
        assert sample_code_block.language == "yaml"
        assert "postgres" in sample_code_block.source
        assert sample_code_block.filename == "docker-compose.yaml"

    def test_code_block_custom_language(self) -> None:
        """Language can be overridden."""
        block = CodeBlock(language="json", source='{"key": "value"}')
        assert block.language == "json"

    def test_code_block_missing_source_raises(self) -> None:
        """source is required."""
        with pytest.raises(ValidationError):
            CodeBlock()  # type: ignore[call-arg]


# ---------------------------------------------------------------------------
# LessonStep
# ---------------------------------------------------------------------------


class TestLessonStep:
    """Tests for the LessonStep model."""

    def test_minimal_lesson_step(self) -> None:
        """Only title and explanation are required."""
        step = LessonStep(title="Intro", explanation="Some text.")
        assert step.title == "Intro"
        assert step.explanation == "Some text."
        assert step.code is None
        assert step.why is None
        assert step.deep_dive is None

    def test_full_lesson_step(self, sample_lesson_step: LessonStep) -> None:
        """All optional fields can be populated."""
        assert sample_lesson_step.code is not None
        assert sample_lesson_step.why is not None
        assert sample_lesson_step.deep_dive is not None

    def test_lesson_step_with_code(self) -> None:
        """A LessonStep can embed a CodeBlock."""
        code = CodeBlock(language="bash", source="echo hello")
        step = LessonStep(title="Run", explanation="Run the command.", code=code)
        assert step.code is not None
        assert step.code.language == "bash"

    def test_lesson_step_missing_title_raises(self) -> None:
        """title is required."""
        with pytest.raises(ValidationError):
            LessonStep(explanation="Missing title")  # type: ignore[call-arg]

    def test_lesson_step_missing_explanation_raises(self) -> None:
        """explanation is required."""
        with pytest.raises(ValidationError):
            LessonStep(title="Missing explanation")  # type: ignore[call-arg]


# ---------------------------------------------------------------------------
# LessonMeta
# ---------------------------------------------------------------------------


class TestLessonMeta:
    """Tests for the LessonMeta model."""

    def test_lesson_meta_all_fields(self, sample_lesson_meta: LessonMeta) -> None:
        """All required fields are set."""
        assert sample_lesson_meta.id == "test-lesson"
        assert sample_lesson_meta.number == 1
        assert sample_lesson_meta.title == "Test Lesson"
        assert sample_lesson_meta.description == "A test lesson for unit tests."
        assert sample_lesson_meta.overview == "Short overview of the test lesson."

    def test_lesson_meta_missing_id_raises(self) -> None:
        """id is required."""
        with pytest.raises(ValidationError):
            LessonMeta(  # type: ignore[call-arg]
                number=1,
                title="T",
                description="D",
                overview="O",
            )

    def test_lesson_meta_missing_number_raises(self) -> None:
        """number is required."""
        with pytest.raises(ValidationError):
            LessonMeta(  # type: ignore[call-arg]
                id="x",
                title="T",
                description="D",
                overview="O",
            )


# ---------------------------------------------------------------------------
# ScaffoldFile
# ---------------------------------------------------------------------------


class TestScaffoldFile:
    """Tests for the ScaffoldFile model."""

    def test_scaffold_file_fields(self, sample_scaffold_file: ScaffoldFile) -> None:
        """All required fields are set."""
        assert sample_scaffold_file.relative_path == ".ona/automations.yaml"
        assert sample_scaffold_file.template_name == "automations_task.yaml"
        assert "testing" in sample_scaffold_file.description.lower()

    def test_scaffold_file_missing_relative_path_raises(self) -> None:
        """relative_path is required."""
        with pytest.raises(ValidationError):
            ScaffoldFile(  # type: ignore[call-arg]
                template_name="tpl.yaml",
                description="D",
            )


# ---------------------------------------------------------------------------
# ProgressData
# ---------------------------------------------------------------------------


class TestProgressData:
    """Tests for the ProgressData model."""

    def test_empty_progress(self) -> None:
        """A fresh ProgressData has no completions and no timestamps."""
        data = ProgressData()
        assert data.completed_lessons == []
        assert data.started_at is None
        assert data.last_activity is None
        assert data.total_completed == 0

    def test_mark_completed_adds_lesson(self) -> None:
        """mark_completed adds a lesson ID to the list."""
        data = ProgressData()
        data.mark_completed("devcontainer-basics")
        assert data.is_completed("devcontainer-basics")
        assert data.total_completed == 1

    def test_mark_completed_sets_timestamps(self) -> None:
        """mark_completed sets started_at and last_activity."""
        data = ProgressData()
        data.mark_completed("tasks")
        assert data.started_at is not None
        assert data.last_activity is not None
        assert data.started_at.tzinfo == UTC

    def test_mark_completed_idempotent(self) -> None:
        """Marking the same lesson twice doesn't duplicate it."""
        data = ProgressData()
        data.mark_completed("tasks")
        data.mark_completed("tasks")
        assert data.total_completed == 1
        assert data.completed_lessons.count("tasks") == 1

    def test_mark_completed_preserves_started_at(self) -> None:
        """started_at is only set on the first completion, not overwritten."""
        data = ProgressData()
        data.mark_completed("devcontainer-basics")
        first_started = data.started_at
        data.mark_completed("tasks")
        assert data.started_at == first_started

    def test_mark_completed_updates_last_activity(self) -> None:
        """last_activity is updated on every completion."""
        data = ProgressData()
        data.mark_completed("devcontainer-basics")
        first_activity = data.last_activity
        # Mark a different lesson
        data.mark_completed("tasks")
        assert data.last_activity is not None
        assert first_activity is not None
        assert data.last_activity >= first_activity

    def test_is_completed_false_for_unknown(self) -> None:
        """is_completed returns False for lessons not yet completed."""
        data = ProgressData()
        assert not data.is_completed("nonexistent-lesson")

    def test_total_completed_property(self, populated_progress_data: ProgressData) -> None:
        """total_completed returns the correct count."""
        assert populated_progress_data.total_completed == 2

    def test_multiple_completions(self) -> None:
        """Completing multiple lessons tracks each one."""
        data = ProgressData()
        ids = ["devcontainer-basics", "first-environment", "tasks", "services"]
        for lesson_id in ids:
            data.mark_completed(lesson_id)
        assert data.total_completed == 4
        for lesson_id in ids:
            assert data.is_completed(lesson_id)

    def test_serialization_round_trip(self) -> None:
        """ProgressData can be serialized to JSON and deserialized back."""
        data = ProgressData()
        data.mark_completed("devcontainer-basics")
        data.mark_completed("services")

        json_str = data.model_dump_json()
        restored = ProgressData.model_validate_json(json_str)

        assert restored.completed_lessons == data.completed_lessons
        assert restored.started_at == data.started_at
        assert restored.last_activity == data.last_activity
        assert restored.total_completed == 2

    def test_default_factory_creates_independent_lists(self) -> None:
        """Each ProgressData instance gets its own completed_lessons list."""
        a = ProgressData()
        b = ProgressData()
        a.mark_completed("tasks")
        assert not b.is_completed("tasks")
