"""Unit tests for ProjectService.

Tests cover:
- create_project: slug generation, database requirement
- update_project: slug validation via domain method
- archive_project: default project protection (via repository)
- count_projects/count_project_members: graceful degradation when unsupported
- get_effective_rules: settings inheritance chain
- resolve_project_role: workspace role → project role mapping
"""


from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock
from uuid import UUID, uuid4

import pytest

from noteflow.application.services.project_service import ProjectService
from noteflow.domain.entities.project import (
    ExportRules,
    Project,
    ProjectSettings,
    TriggerRules,
)
from noteflow.domain.errors import ValidationError
from noteflow.domain.identity.entities import ProjectMembership, WorkspaceSettings
from noteflow.domain.identity.roles import ProjectRole, WorkspaceRole
from noteflow.domain.value_objects import ExportFormat

# =============================================================================
# Fixtures
# =============================================================================


@pytest.fixture
def project_service() -> ProjectService:
    """Create ProjectService instance."""
    return ProjectService()


@pytest.fixture
def mock_project_uow() -> MagicMock:
    """Create mock UnitOfWork with project repository support.

    Provides a fully-configured mock UnitOfWork with projects and
    project_memberships repositories for testing ProjectService.
    """
    uow = MagicMock(spec=["supports_projects", "projects", "project_memberships", "commit"])
    uow.supports_projects = True
    uow.commit = AsyncMock()

    # Projects repository
    uow.projects = MagicMock()
    uow.projects.create = AsyncMock()
    uow.projects.get = AsyncMock(return_value=None)
    uow.projects.get_by_slug = AsyncMock(return_value=None)
    uow.projects.get_default_for_workspace = AsyncMock(return_value=None)
    uow.projects.list_for_workspace = AsyncMock(return_value=[])
    uow.projects.update = AsyncMock()
    uow.projects.archive = AsyncMock(return_value=None)
    uow.projects.restore = AsyncMock(return_value=None)
    uow.projects.delete = AsyncMock(return_value=False)
    uow.projects.count_for_workspace = AsyncMock(return_value=0)

    # Project memberships repository
    uow.project_memberships = MagicMock()
    uow.project_memberships.add = AsyncMock()
    uow.project_memberships.get = AsyncMock(return_value=None)
    uow.project_memberships.update_role = AsyncMock(return_value=None)
    uow.project_memberships.remove = AsyncMock(return_value=False)
    uow.project_memberships.list_for_project = AsyncMock(return_value=[])
    uow.project_memberships.count_for_project = AsyncMock(return_value=0)

    return uow


@pytest.fixture
def unsupported_project_uow() -> MagicMock:
    """Create mock UnitOfWork without project support (memory mode)."""
    uow = MagicMock(spec=["supports_projects"])
    uow.supports_projects = False
    return uow


@pytest.fixture
def service_test_project() -> Project:
    """Create sample project for service testing."""
    return Project(
        id=uuid4(),
        workspace_id=uuid4(),
        name="Test Project",
        slug="test-project",
        description="A test project",
    )


# =============================================================================
# Test: create_project
# =============================================================================


class TestCreateProject:
    """Tests for ProjectService.create_project."""

    async def test_create_project_generates_slug_from_name(
        self,
        project_service: ProjectService,
        mock_project_uow: MagicMock,
    ) -> None:
        """create_project auto-generates slug from name when not provided."""
        workspace_id = uuid4()
        created_project = Project(
            id=uuid4(),
            workspace_id=workspace_id,
            name="My Test Project",
            slug="my-test-project",
        )
        mock_project_uow.projects.create.return_value = created_project

        result = await project_service.create_project(
            mock_project_uow,
            workspace_id=workspace_id,
            name="My Test Project",
            # slug not provided
        )

        assert result.slug == "my-test-project", "Slug should be auto-generated from name"
        call_kwargs = mock_project_uow.projects.create.call_args[1]
        assert call_kwargs["slug"] == "my-test-project", "Generated slug should be passed to repository"

    async def test_create_project_uses_provided_slug(
        self,
        project_service: ProjectService,
        mock_project_uow: MagicMock,
    ) -> None:
        """create_project uses explicit slug when provided."""
        workspace_id = uuid4()
        created_project = Project(
            id=uuid4(),
            workspace_id=workspace_id,
            name="My Test Project",
            slug="custom-slug",
        )
        mock_project_uow.projects.create.return_value = created_project

        result = await project_service.create_project(
            mock_project_uow,
            workspace_id=workspace_id,
            name="My Test Project",
            slug="custom-slug",
        )

        assert result.slug == "custom-slug", "Should use provided slug"
        call_kwargs = mock_project_uow.projects.create.call_args[1]
        assert call_kwargs["slug"] == "custom-slug", "Provided slug should be passed to repository"

    async def test_create_project_requires_database_persistence(
        self,
        project_service: ProjectService,
        unsupported_project_uow: MagicMock,
    ) -> None:
        """create_project raises NotImplementedError when projects not supported."""
        with pytest.raises(NotImplementedError, match="database persistence"):
            await project_service.create_project(
                unsupported_project_uow,
                workspace_id=uuid4(),
                name="Test",
            )

    async def test_create_project_commits_transaction(
        self,
        project_service: ProjectService,
        mock_project_uow: MagicMock,
    ) -> None:
        """create_project commits the unit of work after creation."""
        created_project = Project(
            id=uuid4(),
            workspace_id=uuid4(),
            name="Test",
            slug="test",
        )
        mock_project_uow.projects.create.return_value = created_project

        await project_service.create_project(
            mock_project_uow,
            workspace_id=uuid4(),
            name="Test",
        )

        mock_project_uow.commit.assert_called_once()


# =============================================================================
# Test: update_project
# =============================================================================


class TestUpdateProject:
    """Tests for ProjectService.update_project."""

    async def test_update_project_validates_slug_via_domain_method(
        self,
        project_service: ProjectService,
        mock_project_uow: MagicMock,
        service_test_project: Project,
    ) -> None:
        """update_project uses Project.update_slug for validation."""
        mock_project_uow.projects.get.return_value = service_test_project

        # Invalid slug should raise ValidationError from domain
        with pytest.raises(ValidationError, match="slug"):
            await project_service.update_project(
                mock_project_uow,
                project_id=service_test_project.id,
                slug="Invalid-Slug",  # uppercase not allowed
            )

    async def test_update_project_accepts_valid_slug(
        self,
        project_service: ProjectService,
        mock_project_uow: MagicMock,
        service_test_project: Project,
    ) -> None:
        """update_project accepts valid slug pattern."""
        mock_project_uow.projects.get.return_value = service_test_project
        mock_project_uow.projects.update.return_value = service_test_project

        result = await project_service.update_project(
            mock_project_uow,
            project_id=service_test_project.id,
            slug="valid-slug-123",
        )

        assert result is not None, "Update should succeed with valid slug"
        assert service_test_project.slug == "valid-slug-123", "Slug should be updated on domain entity"

    async def test_update_project_returns_none_when_not_found(
        self,
        project_service: ProjectService,
        mock_project_uow: MagicMock,
    ) -> None:
        """update_project returns None when project doesn't exist."""
        mock_project_uow.projects.get.return_value = None

        result = await project_service.update_project(
            mock_project_uow,
            project_id=uuid4(),
            name="Updated",
        )

        assert result is None, "Should return None for non-existent project"

    async def test_update_project_returns_none_when_unsupported(
        self,
        project_service: ProjectService,
        unsupported_project_uow: MagicMock,
    ) -> None:
        """update_project returns None when projects not supported."""
        result = await project_service.update_project(
            unsupported_project_uow,
            project_id=uuid4(),
            name="Updated",
        )

        assert result is None, "Should return None when projects not supported"


# =============================================================================
# Test: count operations (graceful degradation)
# =============================================================================


class TestCountOperations:
    """Tests for count_projects and count_project_members."""

    async def test_count_projects_returns_zero_when_unsupported(
        self,
        project_service: ProjectService,
        unsupported_project_uow: MagicMock,
    ) -> None:
        """count_projects returns 0 when projects not supported."""
        result = await project_service.count_projects(
            unsupported_project_uow,
            workspace_id=uuid4(),
        )

        assert result == 0, "Should return 0 when projects not supported"

    async def test_count_projects_delegates_to_repository(
        self,
        project_service: ProjectService,
        mock_project_uow: MagicMock,
    ) -> None:
        """count_projects returns repository count when supported."""
        workspace_id = uuid4()
        mock_project_uow.projects.count_for_workspace.return_value = 42

        result = await project_service.count_projects(
            mock_project_uow,
            workspace_id=workspace_id,
            include_archived=True,
        )

        EXPECTED_COUNT = 42
        assert result == EXPECTED_COUNT, "Should return repository count"
        mock_project_uow.projects.count_for_workspace.assert_called_once_with(
            workspace_id, True
        )

    async def test_count_project_members_returns_zero_when_unsupported(
        self,
        project_service: ProjectService,
        unsupported_project_uow: MagicMock,
    ) -> None:
        """count_project_members returns 0 when projects not supported."""
        result = await project_service.count_project_members(
            unsupported_project_uow,
            project_id=uuid4(),
        )

        assert result == 0, "Should return 0 when projects not supported"

    async def test_count_project_members_delegates_to_repository(
        self,
        project_service: ProjectService,
        mock_project_uow: MagicMock,
    ) -> None:
        """count_project_members returns repository count when supported."""
        project_id = uuid4()
        mock_project_uow.project_memberships.count_for_project.return_value = 15

        result = await project_service.count_project_members(
            mock_project_uow,
            project_id=project_id,
        )

        EXPECTED_MEMBER_COUNT = 15
        assert result == EXPECTED_MEMBER_COUNT, "Should return repository count"
        mock_project_uow.project_memberships.count_for_project.assert_called_once_with(
            project_id
        )


# =============================================================================
# Test: get_effective_rules (settings inheritance)
# =============================================================================


class TestGetEffectiveRules:
    """Tests for ProjectService.get_effective_rules inheritance chain."""

    def test_effective_rules_uses_system_defaults_when_no_overrides(
        self,
        project_service: ProjectService,
    ) -> None:
        """Effective rules use system defaults when no settings provided."""
        result = project_service.get_effective_rules(
            workspace_settings=None,
            project_settings=None,
        )

        # System defaults from SYSTEM_DEFAULTS
        assert result.export.default_format == ExportFormat.MARKDOWN, "Should use system default format"
        assert result.export.include_audio is False, "Should use system default for include_audio"
        assert result.export.include_timestamps is True, "Should use system default for timestamps"
        assert result.rag_enabled is False, "Should use system default for rag_enabled"

    def test_effective_rules_workspace_overrides_system(
        self,
        project_service: ProjectService,
    ) -> None:
        """Workspace settings override system defaults."""
        workspace_settings = WorkspaceSettings(
            export_rules=ExportRules(default_format=ExportFormat.PDF),
            rag_enabled=True,
        )

        result = project_service.get_effective_rules(
            workspace_settings=workspace_settings,
            project_settings=None,
        )

        assert result.export.default_format == ExportFormat.PDF, "Workspace should override system format"
        assert result.rag_enabled is True, "Workspace should override system rag_enabled"

    def test_effective_rules_project_overrides_workspace(
        self,
        project_service: ProjectService,
    ) -> None:
        """Project settings override workspace settings."""
        workspace_settings = WorkspaceSettings(
            export_rules=ExportRules(default_format=ExportFormat.PDF),
            rag_enabled=True,
        )
        project_settings = ProjectSettings(
            export_rules=ExportRules(default_format=ExportFormat.HTML),
            rag_enabled=False,
        )

        result = project_service.get_effective_rules(
            workspace_settings=workspace_settings,
            project_settings=project_settings,
        )

        assert result.export.default_format == ExportFormat.HTML, "Project should override workspace format"
        assert result.rag_enabled is False, "Project should override workspace rag_enabled"

    def test_effective_rules_empty_list_clears_parent_patterns(
        self,
        project_service: ProjectService,
    ) -> None:
        """Empty list [] explicitly clears inherited patterns (not same as None)."""
        workspace_settings = WorkspaceSettings(
            trigger_rules=TriggerRules(
                calendar_match_patterns=["*standup*", "*meeting*"],
            ),
        )
        project_settings = ProjectSettings(
            trigger_rules=TriggerRules(
                calendar_match_patterns=[],  # Explicit clear
            ),
        )

        result = project_service.get_effective_rules(
            workspace_settings=workspace_settings,
            project_settings=project_settings,
        )

        assert result.trigger.calendar_match_patterns == [], "Empty list should clear workspace patterns"

    def test_effective_rules_none_inherits_from_parent(
        self,
        project_service: ProjectService,
    ) -> None:
        """None values inherit from parent (workspace or system)."""
        workspace_settings = WorkspaceSettings(
            trigger_rules=TriggerRules(
                calendar_match_patterns=["*standup*"],
            ),
        )
        project_settings = ProjectSettings(
            trigger_rules=TriggerRules(
                calendar_match_patterns=None,  # Inherit
                auto_start_enabled=True,  # Override
            ),
        )

        result = project_service.get_effective_rules(
            workspace_settings=workspace_settings,
            project_settings=project_settings,
        )

        assert result.trigger.calendar_match_patterns == ["*standup*"], "None should inherit workspace patterns"
        assert result.trigger.auto_start_enabled is True, "Should use project override"


# =============================================================================
# Test: resolve_project_role
# =============================================================================


class TestResolveProjectRole:
    """Tests for ProjectService.resolve_project_role."""

    @pytest.mark.parametrize(
        ("workspace_role", "expected_project_role"),
        [
            pytest.param(WorkspaceRole.OWNER, ProjectRole.ADMIN, id="owner_gets_admin"),
            pytest.param(WorkspaceRole.ADMIN, ProjectRole.ADMIN, id="admin_gets_admin"),
        ],
    )
    def test_workspace_admin_roles_become_project_admin(
        self,
        project_service: ProjectService,
        workspace_role: WorkspaceRole,
        expected_project_role: ProjectRole,
    ) -> None:
        """Workspace OWNER and ADMIN always get ProjectRole.ADMIN."""
        result = project_service.resolve_project_role(
            workspace_role=workspace_role,
            project_membership=None,
        )

        assert result == expected_project_role, f"{workspace_role.value} should map to {expected_project_role.value}"

    def test_explicit_membership_overrides_default(
        self,
        project_service: ProjectService,
    ) -> None:
        """Explicit project membership role is used when present."""
        membership = ProjectMembership(
            project_id=uuid4(),
            user_id=uuid4(),
            role=ProjectRole.EDITOR,
        )

        result = project_service.resolve_project_role(
            workspace_role=WorkspaceRole.MEMBER,
            project_membership=membership,
        )

        assert result == ProjectRole.EDITOR, "Should use explicit membership role"

    def test_member_without_membership_defaults_to_viewer(
        self,
        project_service: ProjectService,
    ) -> None:
        """Workspace MEMBER without project membership defaults to VIEWER."""
        result = project_service.resolve_project_role(
            workspace_role=WorkspaceRole.MEMBER,
            project_membership=None,
        )

        assert result == ProjectRole.VIEWER, "Member without membership should default to VIEWER"

    def test_workspace_admin_ignores_lower_membership_role(
        self,
        project_service: ProjectService,
    ) -> None:
        """Workspace OWNER/ADMIN always gets ADMIN even with lower membership role."""
        membership = ProjectMembership(
            project_id=uuid4(),
            user_id=uuid4(),
            role=ProjectRole.VIEWER,  # Lower than ADMIN
        )

        result = project_service.resolve_project_role(
            workspace_role=WorkspaceRole.OWNER,
            project_membership=membership,
        )

        assert result == ProjectRole.ADMIN, "Workspace OWNER should get ADMIN regardless of membership"


# =============================================================================
# Test: CRUD operations return None when unsupported
# =============================================================================


class TestGracefulDegradation:
    """Tests for graceful degradation when projects not supported."""

    @pytest.mark.parametrize(
        ("method_name", "args"),
        [
            pytest.param("get_project", [uuid4()], id="get_project"),
            pytest.param("get_project_by_slug", [uuid4(), "slug"], id="get_project_by_slug"),
            pytest.param("get_default_project", [uuid4()], id="get_default_project"),
            pytest.param("archive_project", [uuid4()], id="archive_project"),
            pytest.param("restore_project", [uuid4()], id="restore_project"),
            pytest.param("get_project_membership", [uuid4(), uuid4()], id="get_project_membership"),
            pytest.param("update_project_member_role", [uuid4(), uuid4(), ProjectRole.ADMIN], id="update_member_role"),
            pytest.param("add_project_member", [uuid4(), uuid4(), ProjectRole.EDITOR], id="add_member"),
        ],
    )
    async def test_method_returns_none_when_unsupported(
        self,
        project_service: ProjectService,
        unsupported_project_uow: MagicMock,
        method_name: str,
        args: list[UUID | str | ProjectRole],
    ) -> None:
        """CRUD methods return None when projects not supported."""
        method = getattr(project_service, method_name)

        result = await method(unsupported_project_uow, *args)

        assert result is None, f"{method_name} should return None when unsupported"

    @pytest.mark.parametrize(
        ("method_name", "args"),
        [
            pytest.param("delete_project", [uuid4()], id="delete_project"),
            pytest.param("remove_project_member", [uuid4(), uuid4()], id="remove_member"),
        ],
    )
    async def test_boolean_method_returns_false_when_unsupported(
        self,
        project_service: ProjectService,
        unsupported_project_uow: MagicMock,
        method_name: str,
        args: list[UUID],
    ) -> None:
        """Boolean methods return False when projects not supported."""
        method = getattr(project_service, method_name)

        result = await method(unsupported_project_uow, *args)

        assert result is False, f"{method_name} should return False when unsupported"

    async def test_list_projects_returns_empty_when_unsupported(
        self,
        project_service: ProjectService,
        unsupported_project_uow: MagicMock,
    ) -> None:
        """list_projects returns empty list when projects not supported."""
        result = await project_service.list_projects(
            unsupported_project_uow,
            workspace_id=uuid4(),
        )

        assert result == [], "Should return empty list when unsupported"

    async def test_list_project_members_returns_empty_when_unsupported(
        self,
        project_service: ProjectService,
        unsupported_project_uow: MagicMock,
    ) -> None:
        """list_project_members returns empty list when projects not supported."""
        result = await project_service.list_project_members(
            unsupported_project_uow,
            project_id=uuid4(),
        )

        assert result == [], "Should return empty list when unsupported"
