# tests/crud/syndarix/test_project_crud.py
"""
Tests for Project CRUD operations.
"""

import uuid

import pytest

from app.crud.syndarix import project as project_crud
from app.models.syndarix import AutonomyLevel, ProjectStatus
from app.schemas.syndarix import ProjectCreate, ProjectUpdate


class TestProjectCreate:
    """Tests for project creation."""

    @pytest.mark.asyncio
    async def test_create_project_success(self, async_test_db, test_owner_crud):
        """Test successfully creating a project."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            project_data = ProjectCreate(
                name="New Project",
                slug="new-project",
                description="A brand new project",
                autonomy_level=AutonomyLevel.MILESTONE,
                status=ProjectStatus.ACTIVE,
                settings={"key": "value"},
                owner_id=test_owner_crud.id,
            )
            result = await project_crud.create(session, obj_in=project_data)

            assert result.id is not None
            assert result.name == "New Project"
            assert result.slug == "new-project"
            assert result.description == "A brand new project"
            assert result.autonomy_level == AutonomyLevel.MILESTONE
            assert result.status == ProjectStatus.ACTIVE
            assert result.settings == {"key": "value"}
            assert result.owner_id == test_owner_crud.id

    @pytest.mark.asyncio
    async def test_create_project_duplicate_slug_fails(self, async_test_db, test_project_crud):
        """Test creating project with duplicate slug raises ValueError."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            project_data = ProjectCreate(
                name="Duplicate Project",
                slug=test_project_crud.slug,  # Duplicate slug
                description="This should fail",
            )

            with pytest.raises(ValueError) as exc_info:
                await project_crud.create(session, obj_in=project_data)

            assert "already exists" in str(exc_info.value).lower()

    @pytest.mark.asyncio
    async def test_create_project_minimal_fields(self, async_test_db):
        """Test creating project with minimal required fields."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            project_data = ProjectCreate(
                name="Minimal Project",
                slug="minimal-project",
            )
            result = await project_crud.create(session, obj_in=project_data)

            assert result.name == "Minimal Project"
            assert result.slug == "minimal-project"
            assert result.autonomy_level == AutonomyLevel.MILESTONE  # Default
            assert result.status == ProjectStatus.ACTIVE  # Default


class TestProjectRead:
    """Tests for project read operations."""

    @pytest.mark.asyncio
    async def test_get_project_by_id(self, async_test_db, test_project_crud):
        """Test getting project by ID."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await project_crud.get(session, id=str(test_project_crud.id))

            assert result is not None
            assert result.id == test_project_crud.id
            assert result.name == test_project_crud.name

    @pytest.mark.asyncio
    async def test_get_project_by_id_not_found(self, async_test_db):
        """Test getting non-existent project returns None."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await project_crud.get(session, id=str(uuid.uuid4()))
            assert result is None

    @pytest.mark.asyncio
    async def test_get_project_by_slug(self, async_test_db, test_project_crud):
        """Test getting project by slug."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await project_crud.get_by_slug(session, slug=test_project_crud.slug)

            assert result is not None
            assert result.slug == test_project_crud.slug

    @pytest.mark.asyncio
    async def test_get_project_by_slug_not_found(self, async_test_db):
        """Test getting non-existent slug returns None."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await project_crud.get_by_slug(session, slug="non-existent-slug")
            assert result is None


class TestProjectUpdate:
    """Tests for project update operations."""

    @pytest.mark.asyncio
    async def test_update_project_basic_fields(self, async_test_db, test_project_crud):
        """Test updating basic project fields."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            project = await project_crud.get(session, id=str(test_project_crud.id))

            update_data = ProjectUpdate(
                name="Updated Project Name",
                description="Updated description",
            )
            result = await project_crud.update(session, db_obj=project, obj_in=update_data)

            assert result.name == "Updated Project Name"
            assert result.description == "Updated description"

    @pytest.mark.asyncio
    async def test_update_project_status(self, async_test_db, test_project_crud):
        """Test updating project status."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            project = await project_crud.get(session, id=str(test_project_crud.id))

            update_data = ProjectUpdate(status=ProjectStatus.PAUSED)
            result = await project_crud.update(session, db_obj=project, obj_in=update_data)

            assert result.status == ProjectStatus.PAUSED

    @pytest.mark.asyncio
    async def test_update_project_autonomy_level(self, async_test_db, test_project_crud):
        """Test updating project autonomy level."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            project = await project_crud.get(session, id=str(test_project_crud.id))

            update_data = ProjectUpdate(autonomy_level=AutonomyLevel.AUTONOMOUS)
            result = await project_crud.update(session, db_obj=project, obj_in=update_data)

            assert result.autonomy_level == AutonomyLevel.AUTONOMOUS

    @pytest.mark.asyncio
    async def test_update_project_settings(self, async_test_db, test_project_crud):
        """Test updating project settings."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            project = await project_crud.get(session, id=str(test_project_crud.id))

            new_settings = {"mcp_servers": ["gitea", "slack"], "webhook_url": "https://example.com"}
            update_data = ProjectUpdate(settings=new_settings)
            result = await project_crud.update(session, db_obj=project, obj_in=update_data)

            assert result.settings == new_settings


class TestProjectDelete:
    """Tests for project delete operations."""

    @pytest.mark.asyncio
    async def test_delete_project(self, async_test_db, test_owner_crud):
        """Test deleting a project."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create a project to delete
        async with AsyncTestingSessionLocal() as session:
            project_data = ProjectCreate(
                name="Delete Me",
                slug="delete-me-project",
                owner_id=test_owner_crud.id,
            )
            created = await project_crud.create(session, obj_in=project_data)
            project_id = created.id

        # Delete the project
        async with AsyncTestingSessionLocal() as session:
            result = await project_crud.remove(session, id=str(project_id))
            assert result is not None
            assert result.id == project_id

        # Verify deletion
        async with AsyncTestingSessionLocal() as session:
            deleted = await project_crud.get(session, id=str(project_id))
            assert deleted is None

    @pytest.mark.asyncio
    async def test_delete_nonexistent_project(self, async_test_db):
        """Test deleting non-existent project returns None."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await project_crud.remove(session, id=str(uuid.uuid4()))
            assert result is None


class TestProjectFilters:
    """Tests for project filtering and search."""

    @pytest.mark.asyncio
    async def test_get_multi_with_filters_status(self, async_test_db, test_owner_crud):
        """Test filtering projects by status."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create multiple projects with different statuses
        async with AsyncTestingSessionLocal() as session:
            for i, status in enumerate(ProjectStatus):
                project_data = ProjectCreate(
                    name=f"Project {status.value}",
                    slug=f"project-filter-{status.value}-{i}",
                    status=status,
                    owner_id=test_owner_crud.id,
                )
                await project_crud.create(session, obj_in=project_data)

        # Filter by ACTIVE status
        async with AsyncTestingSessionLocal() as session:
            projects, _total = await project_crud.get_multi_with_filters(
                session,
                status=ProjectStatus.ACTIVE,
            )

            assert all(p.status == ProjectStatus.ACTIVE for p in projects)

    @pytest.mark.asyncio
    async def test_get_multi_with_filters_search(self, async_test_db, test_owner_crud):
        """Test searching projects by name/slug."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            project_data = ProjectCreate(
                name="Searchable Project",
                slug="searchable-unique-slug",
                description="This project is searchable",
                owner_id=test_owner_crud.id,
            )
            await project_crud.create(session, obj_in=project_data)

        async with AsyncTestingSessionLocal() as session:
            projects, total = await project_crud.get_multi_with_filters(
                session,
                search="Searchable",
            )

            assert total >= 1
            assert any(p.name == "Searchable Project" for p in projects)

    @pytest.mark.asyncio
    async def test_get_multi_with_filters_owner(self, async_test_db, test_owner_crud, test_project_crud):
        """Test filtering projects by owner."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            projects, total = await project_crud.get_multi_with_filters(
                session,
                owner_id=test_owner_crud.id,
            )

            assert total >= 1
            assert all(p.owner_id == test_owner_crud.id for p in projects)

    @pytest.mark.asyncio
    async def test_get_multi_with_filters_pagination(self, async_test_db, test_owner_crud):
        """Test pagination of project results."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create multiple projects
        async with AsyncTestingSessionLocal() as session:
            for i in range(5):
                project_data = ProjectCreate(
                    name=f"Page Project {i}",
                    slug=f"page-project-{i}",
                    owner_id=test_owner_crud.id,
                )
                await project_crud.create(session, obj_in=project_data)

        # Get first page
        async with AsyncTestingSessionLocal() as session:
            page1, total = await project_crud.get_multi_with_filters(
                session,
                skip=0,
                limit=2,
                owner_id=test_owner_crud.id,
            )

            assert len(page1) <= 2
            assert total >= 5

    @pytest.mark.asyncio
    async def test_get_multi_with_filters_sorting(self, async_test_db, test_owner_crud):
        """Test sorting project results."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            for _i, name in enumerate(["Charlie", "Alice", "Bob"]):
                project_data = ProjectCreate(
                    name=name,
                    slug=f"sort-project-{name.lower()}",
                    owner_id=test_owner_crud.id,
                )
                await project_crud.create(session, obj_in=project_data)

        async with AsyncTestingSessionLocal() as session:
            projects, _ = await project_crud.get_multi_with_filters(
                session,
                sort_by="name",
                sort_order="asc",
                owner_id=test_owner_crud.id,
            )

            names = [p.name for p in projects if p.name in ["Alice", "Bob", "Charlie"]]
            assert names == sorted(names)


class TestProjectSpecialMethods:
    """Tests for special project CRUD methods."""

    @pytest.mark.asyncio
    async def test_archive_project(self, async_test_db, test_project_crud):
        """Test archiving a project."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await project_crud.archive_project(session, project_id=test_project_crud.id)

            assert result is not None
            assert result.status == ProjectStatus.ARCHIVED

    @pytest.mark.asyncio
    async def test_archive_nonexistent_project(self, async_test_db):
        """Test archiving non-existent project returns None."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await project_crud.archive_project(session, project_id=uuid.uuid4())
            assert result is None

    @pytest.mark.asyncio
    async def test_get_projects_by_owner(self, async_test_db, test_owner_crud, test_project_crud):
        """Test getting all projects by owner."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            projects = await project_crud.get_projects_by_owner(
                session,
                owner_id=test_owner_crud.id,
            )

            assert len(projects) >= 1
            assert all(p.owner_id == test_owner_crud.id for p in projects)

    @pytest.mark.asyncio
    async def test_get_projects_by_owner_with_status(self, async_test_db, test_owner_crud):
        """Test getting projects by owner filtered by status."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create projects with different statuses
        async with AsyncTestingSessionLocal() as session:
            active_project = ProjectCreate(
                name="Active Owner Project",
                slug="active-owner-project",
                status=ProjectStatus.ACTIVE,
                owner_id=test_owner_crud.id,
            )
            await project_crud.create(session, obj_in=active_project)

            paused_project = ProjectCreate(
                name="Paused Owner Project",
                slug="paused-owner-project",
                status=ProjectStatus.PAUSED,
                owner_id=test_owner_crud.id,
            )
            await project_crud.create(session, obj_in=paused_project)

        async with AsyncTestingSessionLocal() as session:
            projects = await project_crud.get_projects_by_owner(
                session,
                owner_id=test_owner_crud.id,
                status=ProjectStatus.ACTIVE,
            )

            assert all(p.status == ProjectStatus.ACTIVE for p in projects)
