"""Tests for Project entity and related domain objects."""

from __future__ import annotations

from datetime import timedelta
from uuid import uuid4

import pytest

from noteflow.domain.entities.project import (
    ExportRules,
    Project,
    ProjectSettings,
    TriggerRules,
)
from noteflow.domain.errors import CannotArchiveDefaultProjectError, ValidationError
from noteflow.domain.utils.time import utc_now
from noteflow.domain.value_objects import ExportFormat


class TestExportRules:
    """Tests for ExportRules dataclass."""

    def test_export_rules_default_values(self) -> None:
        """Test ExportRules has None defaults for all fields."""
        rules = ExportRules()
        assert rules.default_format is None, "default_format should default to None"
        assert rules.include_audio is None, "include_audio should default to None"
        assert rules.include_timestamps is None, "include_timestamps should default to None"
        assert rules.template_id is None, "template_id should default to None"

    def test_export_rules_with_values(self) -> None:
        """Test ExportRules accepts all field values."""
        template_id = uuid4()
        rules = ExportRules(
            default_format=ExportFormat.PDF,
            include_audio=True,
            include_timestamps=False,
            template_id=template_id,
        )
        assert rules.default_format == ExportFormat.PDF, "default_format should match"
        assert rules.include_audio is True, "include_audio should match"
        assert rules.include_timestamps is False, "include_timestamps should match"
        assert rules.template_id == template_id, "template_id should match"

    def test_export_rules_is_frozen(self) -> None:
        """Test ExportRules is immutable."""
        from dataclasses import FrozenInstanceError

        rules = ExportRules()
        with pytest.raises(FrozenInstanceError, match="cannot assign"):
            rules.default_format = ExportFormat.HTML


class TestTriggerRules:
    """Tests for TriggerRules dataclass."""

    def test_trigger_rules_default_values(self) -> None:
        """Test TriggerRules has None defaults for all fields."""
        rules = TriggerRules()
        assert rules.auto_start_enabled is None, "auto_start_enabled should default to None"
        assert rules.calendar_match_patterns is None, "calendar_match_patterns should default to None"
        assert rules.app_match_patterns is None, "app_match_patterns should default to None"

    def test_trigger_rules_with_values(self) -> None:
        """Test TriggerRules accepts all field values."""
        rules = TriggerRules(
            auto_start_enabled=True,
            calendar_match_patterns=["*standup*", "*meeting*"],
            app_match_patterns=["Zoom", "Teams"],
        )
        assert rules.auto_start_enabled is True, "auto_start_enabled should match"
        assert rules.calendar_match_patterns == ["*standup*", "*meeting*"], "calendar_match_patterns should match"
        assert rules.app_match_patterns == ["Zoom", "Teams"], "app_match_patterns should match"

    def test_empty_list_clears_patterns(self) -> None:
        """Test empty list explicitly clears inherited patterns."""
        rules = TriggerRules(
            calendar_match_patterns=[],
            app_match_patterns=[],
        )
        assert rules.calendar_match_patterns == [], "calendar_match_patterns should be empty list"
        assert rules.app_match_patterns == [], "app_match_patterns should be empty list"

    def test_trigger_rules_is_frozen(self) -> None:
        """Test TriggerRules is immutable."""
        from dataclasses import FrozenInstanceError

        rules = TriggerRules()
        with pytest.raises(FrozenInstanceError, match="cannot assign"):
            rules.auto_start_enabled = True


class TestProjectSettings:
    """Tests for ProjectSettings dataclass."""

    def test_project_settings_default_values(self) -> None:
        """Test ProjectSettings has None defaults for all fields."""
        settings = ProjectSettings()
        assert settings.export_rules is None, "export_rules should default to None"
        assert settings.trigger_rules is None, "trigger_rules should default to None"
        assert settings.rag_enabled is None, "rag_enabled should default to None"
        assert settings.default_summarization_template is None, "default_summarization_template should default to None"

    def test_with_nested_rules(self) -> None:
        """Test ProjectSettings accepts nested rule objects."""
        export_rules = ExportRules(default_format=ExportFormat.MARKDOWN)
        trigger_rules = TriggerRules(auto_start_enabled=True)
        settings = ProjectSettings(
            export_rules=export_rules,
            trigger_rules=trigger_rules,
            rag_enabled=True,
            default_summarization_template="professional",
        )
        assert settings.export_rules == export_rules, "export_rules should match"
        assert settings.trigger_rules == trigger_rules, "trigger_rules should match"
        assert settings.rag_enabled is True, "rag_enabled should match"
        assert settings.default_summarization_template == "professional", "template should match"


class TestProjectCreation:
    """Tests for Project creation and initialization."""

    def test_create_with_required_fields(self) -> None:
        """Test Project creation with only required fields."""
        project_id = uuid4()
        workspace_id = uuid4()
        project = Project(
            id=project_id,
            workspace_id=workspace_id,
            name="Test Project",
        )
        assert project.id == project_id, "id should match"
        assert project.workspace_id == workspace_id, "workspace_id should match"
        assert project.name == "Test Project", "name should match"

    @pytest.mark.parametrize(
        ("attr", "expected"),
        [
            ("slug", None),
            ("description", None),
            ("is_default", False),
            ("archived_at", None),
        ],
    )
    def test_default_attribute_values(self, attr: str, expected: object) -> None:
        """Test Project has expected default values."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        actual = getattr(project, attr)
        assert actual == expected, f"expected {attr}={expected!r}, got {actual!r}"

    def test_default_settings_factory(self) -> None:
        """Test Project creates empty ProjectSettings by default."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        assert isinstance(project.settings, ProjectSettings), "settings should be ProjectSettings"
        assert project.settings.export_rules is None, "export_rules should default to None"
        assert project.settings.trigger_rules is None, "trigger_rules should default to None"

    def test_default_metadata_factory(self) -> None:
        """Test Project creates empty metadata dict by default."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        assert project.metadata == {}, "metadata should default to empty dict"
        assert isinstance(project.metadata, dict), "metadata should be a dict"

    def test_timestamps_are_set(self) -> None:
        """Test created_at and updated_at are set on creation."""
        before = utc_now()
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        after = utc_now()

        assert project.created_at is not None, "created_at should be set"
        assert project.updated_at is not None, "updated_at should be set"
        assert before <= project.created_at <= after, "created_at should be within test window"
        assert before <= project.updated_at <= after, "updated_at should be within test window"


class TestProjectSlugValidation:
    """Tests for Project slug validation."""

    @pytest.mark.parametrize(
        "valid_slug",
        [
            "my-project",
            "project123",
            "a",
            "test-project-2024",
            "123",
            "a-b-c",
        ],
    )
    def test_valid_slug_patterns(self, valid_slug: str) -> None:
        """Test valid slug patterns are accepted."""
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Test",
            slug=valid_slug,
        )
        assert project.slug == valid_slug, f"expected slug={valid_slug!r}, got {project.slug!r}"

    @pytest.mark.parametrize(
        "invalid_slug",
        [
            "My-Project",  # uppercase
            "my_project",  # underscore
            "my project",  # space
            "project!",  # special char
            "UPPERCASE",  # all uppercase
            "Project",  # mixed case
            "",  # empty string
        ],
    )
    def test_invalid_slug_patterns_raise(self, invalid_slug: str) -> None:
        """Test invalid slug patterns raise ValidationError."""
        with pytest.raises(ValidationError, match="slug"):
            Project(
                id=uuid4(),
                workspace_id=uuid4(),
                name="Test",
                slug=invalid_slug,
            )

    def test_none_slug_allowed(self) -> None:
        """Test None slug is allowed (no validation)."""
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Test",
            slug=None,
        )
        assert project.slug is None, f"expected slug=None, got {project.slug!r}"


class TestProjectArchiveRestore:
    """Tests for Project archive and restore operations."""

    def test_archive_sets_timestamp(self) -> None:
        """Test archive() sets archived_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        assert project.archived_at is None, "archived_at should be None before archive"

        before = utc_now()
        project.archive()
        after = utc_now()

        assert project.archived_at is not None, "archived_at should be set"
        assert before <= project.archived_at <= after, "archived_at should be within test window"

    def test_archive_updates_timestamp(self) -> None:
        """Test archive() updates updated_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        original_updated = project.updated_at

        project.archive()

        assert project.updated_at >= original_updated, "updated_at should be >= original after archive"

    def test_archive_default_project_raises(self) -> None:
        """Test archiving default project raises CannotArchiveDefaultProjectError."""
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Default",
            is_default=True,
        )
        with pytest.raises(CannotArchiveDefaultProjectError, match="Cannot archive"):
            project.archive()

    def test_restore_clears_archived_at(self) -> None:
        """Test restore() clears archived_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.archive()
        assert project.archived_at is not None, "archived_at should be set after archive"

        project.restore()

        assert project.archived_at is None, "archived_at should be cleared"

    def test_restore_updates_timestamp(self) -> None:
        """Test restore() updates updated_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.archive()
        archived_updated = project.updated_at

        project.restore()

        assert project.updated_at >= archived_updated, "updated_at should be >= archived_updated after restore"


class TestProjectProperties:
    """Tests for Project computed properties."""

    def test_is_archived_false_by_default(self) -> None:
        """Test is_archived returns False for new project."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        assert project.is_archived is False, "is_archived should be False for new project"

    def test_is_archived_true_when_archived(self) -> None:
        """Test is_archived returns True after archive()."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.archive()
        assert project.is_archived is True, "is_archived should be True after archive"

    def test_is_active_true_by_default(self) -> None:
        """Test is_active returns True for new project."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        assert project.is_active is True, "is_active should be True for new project"

    def test_is_active_false_when_archived(self) -> None:
        """Test is_active returns False after archive()."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.archive()
        assert project.is_active is False, "is_active should be False after archive"

    def test_is_archived_and_is_active_are_inverse(self) -> None:
        """Test is_archived and is_active are always inverse."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        assert project.is_archived != project.is_active, "should be inverse when new"

        project.archive()
        assert project.is_archived != project.is_active, "should be inverse when archived"

        project.restore()
        assert project.is_archived != project.is_active, "should be inverse when restored"


class TestProjectMutations:
    """Tests for Project mutation methods."""

    def test_update_name(self) -> None:
        """Test update_name() changes name field."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Original")
        project.update_name("Updated Name")
        assert project.name == "Updated Name", f"expected name='Updated Name', got {project.name!r}"

    def test_update_name_updates_timestamp(self) -> None:
        """Test update_name() updates updated_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Original")
        original_updated = project.updated_at

        project.update_name("Updated")

        assert project.updated_at >= original_updated, "updated_at should be >= original after update_name"

    def test_update_description(self) -> None:
        """Test update_description() changes description field."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.update_description("New description")
        assert project.description == "New description", f"expected description='New description', got {project.description!r}"

    def test_update_description_to_none(self) -> None:
        """Test update_description() can set description to None."""
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Test",
            description="Original",
        )
        project.update_description(None)
        assert project.description is None, f"expected description=None, got {project.description!r}"

    def test_update_description_updates_timestamp(self) -> None:
        """Test update_description() updates updated_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        original_updated = project.updated_at

        project.update_description("Description")

        assert project.updated_at >= original_updated, "updated_at should be >= original after update_description"

    def test_update_settings(self) -> None:
        """Test update_settings() replaces settings object."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        new_settings = ProjectSettings(rag_enabled=True)

        project.update_settings(new_settings)

        assert project.settings == new_settings, "settings should match new settings"
        assert project.settings.rag_enabled is True, "rag_enabled should be True"

    def test_update_settings_updates_timestamp(self) -> None:
        """Test update_settings() updates updated_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        original_updated = project.updated_at

        project.update_settings(ProjectSettings())

        assert project.updated_at >= original_updated, "updated_at should be >= original after update_settings"

    def test_update_slug_with_valid_pattern(self) -> None:
        """Test update_slug() accepts valid slug patterns."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.update_slug("new-valid-slug")
        assert project.slug == "new-valid-slug", "Slug should be updated"

    def test_update_slug_with_invalid_pattern_raises(self) -> None:
        """Test update_slug() raises ValidationError for invalid patterns."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        with pytest.raises(ValidationError, match="slug"):
            project.update_slug("Invalid-Slug")

    def test_update_slug_to_none_allowed(self) -> None:
        """Test update_slug() can set slug to None."""
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Test",
            slug="original-slug",
        )
        project.update_slug(None)
        assert project.slug is None, "Slug should be cleared to None"

    def test_update_slug_updates_timestamp(self) -> None:
        """Test update_slug() updates updated_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        original_updated = project.updated_at

        project.update_slug("new-slug")

        assert project.updated_at >= original_updated, "updated_at should be updated"

    def test_set_metadata(self) -> None:
        """Test set_metadata() adds key-value pair."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.set_metadata("key", "value")
        assert project.metadata["key"] == "value", f"expected metadata['key']='value', got {project.metadata.get('key')!r}"

    def test_set_metadata_overwrites_existing(self) -> None:
        """Test set_metadata() overwrites existing key."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.set_metadata("key", "original")
        project.set_metadata("key", "updated")
        assert project.metadata["key"] == "updated", f"expected metadata['key']='updated', got {project.metadata.get('key')!r}"

    def test_set_metadata_updates_timestamp(self) -> None:
        """Test set_metadata() updates updated_at timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        original_updated = project.updated_at

        project.set_metadata("key", "value")

        assert project.updated_at >= original_updated, "updated_at should be >= original after set_metadata"


class TestProjectEdgeCases:
    """Edge case tests for Project entity."""

    def test_archive_already_archived_project(self) -> None:
        """Test archiving already archived project updates timestamp."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        project.archive()
        first_archived = project.archived_at
        assert first_archived is not None, "first_archived should be set"

        # Small delay to ensure different timestamp
        project.archive()

        assert project.archived_at is not None, "archived_at should still be set after second archive"
        assert project.archived_at >= first_archived, "archived_at should be >= first_archived after second archive"

    def test_restore_non_archived_project(self) -> None:
        """Test restoring non-archived project is idempotent."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        assert project.archived_at is None, "archived_at should be None before restore"

        project.restore()

        assert project.archived_at is None, "archived_at should remain None after restore on non-archived project"

    def test_metadata_complex_structures(self) -> None:
        """Test metadata can store complex nested structures."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        complex_value = {
            "nested": {"key": "value"},
            "list": [1, 2, 3],
            "boolean": True,
        }
        project.set_metadata("complex", complex_value)
        assert project.metadata["complex"] == complex_value, "complex nested structure should be stored in metadata"

    def test_very_long_name(self) -> None:
        """Test project accepts very long names."""
        long_name = "A" * 1000
        project = Project(id=uuid4(), workspace_id=uuid4(), name=long_name)
        assert len(project.name) == 1000, f"expected name length 1000, got {len(project.name)}"

    def test_unicode_name_and_description(self) -> None:
        """Test project accepts unicode in name and description."""
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="项目名称",  # Chinese for "project name"
            description="プロジェクトの説明",  # Japanese for "project description"
        )
        assert project.name == "项目名称", f"expected Chinese name, got {project.name!r}"
        assert project.description == "プロジェクトの説明", f"expected Japanese description, got {project.description!r}"

    def test_emoji_in_name(self) -> None:
        """Test project accepts emoji in name."""
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="My Project 🚀",
        )
        assert "🚀" in project.name, f"expected emoji in name, got {project.name!r}"

    def test_settings_with_full_configuration(self) -> None:
        """Test project with fully configured settings."""
        template_id = uuid4()
        settings = ProjectSettings(
            export_rules=ExportRules(
                default_format=ExportFormat.PDF,
                include_audio=True,
                include_timestamps=True,
                template_id=template_id,
            ),
            trigger_rules=TriggerRules(
                auto_start_enabled=True,
                calendar_match_patterns=["*standup*"],
                app_match_patterns=["Zoom"],
            ),
            rag_enabled=True,
            default_summarization_template="professional",
        )
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Fully Configured",
            settings=settings,
        )
        assert project.settings.export_rules is not None, "export_rules should not be None"
        assert project.settings.export_rules.default_format == ExportFormat.PDF, "default_format should be PDF"
        assert project.settings.trigger_rules is not None, "trigger_rules should not be None"
        assert project.settings.trigger_rules.auto_start_enabled is True, "auto_start_enabled should be True"


class TestProjectInvariants:
    """Tests for Project domain invariants."""

    def test_default_project_cannot_be_archived(self) -> None:
        """Test default project always rejects archive operation."""
        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Default Project",
            is_default=True,
        )
        # Multiple attempts should all fail
        with pytest.raises(CannotArchiveDefaultProjectError, match="Cannot archive"):
            project.archive()

        # Project state should be unchanged
        assert project.archived_at is None, "archived_at should remain None for default project"
        assert project.is_active is True, "is_active should remain True for default project"

    def test_updated_at_never_before_created_at(self) -> None:
        """Test updated_at is always >= created_at."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        assert project.updated_at >= project.created_at, "updated_at should be >= created_at on creation"

        project.update_name("New Name")
        assert project.updated_at >= project.created_at, "updated_at should be >= created_at after update_name"

        project.archive()
        assert project.updated_at >= project.created_at, "updated_at should be >= created_at after archive"

    def test_explicit_timestamps_preserved(self) -> None:
        """Test explicit timestamps are preserved on creation."""
        created = utc_now() - timedelta(days=30)
        updated = utc_now() - timedelta(days=15)

        project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Historical",
            created_at=created,
            updated_at=updated,
        )
        assert project.created_at == created, f"expected created_at={created}, got {project.created_at}"
        assert project.updated_at == updated, f"expected updated_at={updated}, got {project.updated_at}"

    def test_id_immutability(self) -> None:
        """Test project id cannot be changed after creation."""
        project = Project(id=uuid4(), workspace_id=uuid4(), name="Test")
        original_id = project.id

        # Dataclass allows reassignment but domain logic should prevent it
        # This documents current behavior - consider making id immutable
        project.id = uuid4()

        # Just verifying the field was changed (dataclass allows this)
        assert project.id != original_id, "id should have been changed (dataclass allows reassignment)"


class TestCannotArchiveDefaultProjectError:
    """Tests for CannotArchiveDefaultProjectError."""

    def test_error_message_includes_project_id(self) -> None:
        """Test error message includes the project ID."""
        project_id = "test-project-123"
        error = CannotArchiveDefaultProjectError(project_id)
        # Use structured details instead of string comparison
        assert error.details is not None, "error.details should not be None"
        assert error.details.get("project_id") == project_id, f"expected project_id='{project_id}' in details"

    def test_error_details_contain_project_id(self) -> None:
        """Test error details contain project_id key."""
        project_id = "test-project-456"
        error = CannotArchiveDefaultProjectError(project_id)
        assert error.details is not None, "error.details should not be None"
        assert error.details.get("project_id") == project_id, f"expected project_id='{project_id}' in details, got {error.details.get('project_id')!r}"
