# tests/crud/syndarix/test_issue_crud.py
"""
Tests for Issue CRUD operations.
"""

import uuid
from datetime import UTC, datetime

import pytest

from app.crud.syndarix import issue as issue_crud
from app.models.syndarix import IssuePriority, IssueStatus, SyncStatus
from app.schemas.syndarix import IssueCreate, IssueUpdate


class TestIssueCreate:
    """Tests for issue creation."""

    @pytest.mark.asyncio
    async def test_create_issue_success(self, async_test_db, test_project_crud):
        """Test successfully creating an issue."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            issue_data = IssueCreate(
                project_id=test_project_crud.id,
                title="Test Issue",
                body="This is a test issue body",
                status=IssueStatus.OPEN,
                priority=IssuePriority.HIGH,
                labels=["bug", "security"],
                story_points=5,
            )
            result = await issue_crud.create(session, obj_in=issue_data)

            assert result.id is not None
            assert result.title == "Test Issue"
            assert result.body == "This is a test issue body"
            assert result.status == IssueStatus.OPEN
            assert result.priority == IssuePriority.HIGH
            assert result.labels == ["bug", "security"]
            assert result.story_points == 5

    @pytest.mark.asyncio
    async def test_create_issue_with_external_tracker(self, async_test_db, test_project_crud):
        """Test creating issue with external tracker info."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            issue_data = IssueCreate(
                project_id=test_project_crud.id,
                title="External Issue",
                external_tracker_type="gitea",
                external_issue_id="gitea-123",
                remote_url="https://gitea.example.com/issues/123",
                external_issue_number=123,
            )
            result = await issue_crud.create(session, obj_in=issue_data)

            assert result.external_tracker_type == "gitea"
            assert result.external_issue_id == "gitea-123"
            assert result.external_issue_number == 123

    @pytest.mark.asyncio
    async def test_create_issue_minimal(self, async_test_db, test_project_crud):
        """Test creating issue with minimal fields."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            issue_data = IssueCreate(
                project_id=test_project_crud.id,
                title="Minimal Issue",
            )
            result = await issue_crud.create(session, obj_in=issue_data)

            assert result.title == "Minimal Issue"
            assert result.body == ""  # Default
            assert result.status == IssueStatus.OPEN  # Default
            assert result.priority == IssuePriority.MEDIUM  # Default


class TestIssueRead:
    """Tests for issue read operations."""

    @pytest.mark.asyncio
    async def test_get_issue_by_id(self, async_test_db, test_issue_crud):
        """Test getting issue by ID."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

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

            assert result is not None
            assert result.id == test_issue_crud.id

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

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

    @pytest.mark.asyncio
    async def test_get_with_details(self, async_test_db, test_issue_crud):
        """Test getting issue with related details."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.get_with_details(
                session,
                issue_id=test_issue_crud.id,
            )

            assert result is not None
            assert result["issue"].id == test_issue_crud.id
            assert result["project_name"] is not None


class TestIssueUpdate:
    """Tests for issue update operations."""

    @pytest.mark.asyncio
    async def test_update_issue_basic_fields(self, async_test_db, test_issue_crud):
        """Test updating basic issue fields."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            issue = await issue_crud.get(session, id=str(test_issue_crud.id))

            update_data = IssueUpdate(
                title="Updated Title",
                body="Updated body content",
            )
            result = await issue_crud.update(session, db_obj=issue, obj_in=update_data)

            assert result.title == "Updated Title"
            assert result.body == "Updated body content"

    @pytest.mark.asyncio
    async def test_update_issue_status(self, async_test_db, test_issue_crud):
        """Test updating issue status."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            issue = await issue_crud.get(session, id=str(test_issue_crud.id))

            update_data = IssueUpdate(status=IssueStatus.IN_PROGRESS)
            result = await issue_crud.update(session, db_obj=issue, obj_in=update_data)

            assert result.status == IssueStatus.IN_PROGRESS

    @pytest.mark.asyncio
    async def test_update_issue_priority(self, async_test_db, test_issue_crud):
        """Test updating issue priority."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            issue = await issue_crud.get(session, id=str(test_issue_crud.id))

            update_data = IssueUpdate(priority=IssuePriority.CRITICAL)
            result = await issue_crud.update(session, db_obj=issue, obj_in=update_data)

            assert result.priority == IssuePriority.CRITICAL

    @pytest.mark.asyncio
    async def test_update_issue_labels(self, async_test_db, test_issue_crud):
        """Test updating issue labels."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            issue = await issue_crud.get(session, id=str(test_issue_crud.id))

            update_data = IssueUpdate(labels=["new-label", "updated"])
            result = await issue_crud.update(session, db_obj=issue, obj_in=update_data)

            assert "new-label" in result.labels


class TestIssueAssignment:
    """Tests for issue assignment operations."""

    @pytest.mark.asyncio
    async def test_assign_to_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud):
        """Test assigning issue to an agent."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.assign_to_agent(
                session,
                issue_id=test_issue_crud.id,
                agent_id=test_agent_instance_crud.id,
            )

            assert result is not None
            assert result.assigned_agent_id == test_agent_instance_crud.id
            assert result.human_assignee is None

    @pytest.mark.asyncio
    async def test_unassign_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud):
        """Test unassigning agent from issue."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # First assign
        async with AsyncTestingSessionLocal() as session:
            await issue_crud.assign_to_agent(
                session,
                issue_id=test_issue_crud.id,
                agent_id=test_agent_instance_crud.id,
            )

        # Then unassign
        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.assign_to_agent(
                session,
                issue_id=test_issue_crud.id,
                agent_id=None,
            )

            assert result.assigned_agent_id is None

    @pytest.mark.asyncio
    async def test_assign_to_human(self, async_test_db, test_issue_crud):
        """Test assigning issue to a human."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.assign_to_human(
                session,
                issue_id=test_issue_crud.id,
                human_assignee="developer@example.com",
            )

            assert result is not None
            assert result.human_assignee == "developer@example.com"
            assert result.assigned_agent_id is None

    @pytest.mark.asyncio
    async def test_assign_to_human_clears_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud):
        """Test assigning to human clears agent assignment."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # First assign to agent
        async with AsyncTestingSessionLocal() as session:
            await issue_crud.assign_to_agent(
                session,
                issue_id=test_issue_crud.id,
                agent_id=test_agent_instance_crud.id,
            )

        # Then assign to human
        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.assign_to_human(
                session,
                issue_id=test_issue_crud.id,
                human_assignee="developer@example.com",
            )

            assert result.human_assignee == "developer@example.com"
            assert result.assigned_agent_id is None


class TestIssueLifecycle:
    """Tests for issue lifecycle operations."""

    @pytest.mark.asyncio
    async def test_close_issue(self, async_test_db, test_issue_crud):
        """Test closing an issue."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.close_issue(session, issue_id=test_issue_crud.id)

            assert result is not None
            assert result.status == IssueStatus.CLOSED
            assert result.closed_at is not None

    @pytest.mark.asyncio
    async def test_reopen_issue(self, async_test_db, test_project_crud):
        """Test reopening a closed issue."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create and close an issue
        async with AsyncTestingSessionLocal() as session:
            issue_data = IssueCreate(
                project_id=test_project_crud.id,
                title="Issue to Reopen",
            )
            created = await issue_crud.create(session, obj_in=issue_data)
            await issue_crud.close_issue(session, issue_id=created.id)
            issue_id = created.id

        # Reopen
        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.reopen_issue(session, issue_id=issue_id)

            assert result is not None
            assert result.status == IssueStatus.OPEN
            assert result.closed_at is None


class TestIssueByProject:
    """Tests for getting issues by project."""

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

        async with AsyncTestingSessionLocal() as session:
            issues, total = await issue_crud.get_by_project(
                session,
                project_id=test_project_crud.id,
            )

            assert total >= 1
            assert all(i.project_id == test_project_crud.id for i in issues)

    @pytest.mark.asyncio
    async def test_get_by_project_with_status(self, async_test_db, test_project_crud):
        """Test filtering issues by status."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create issues with different statuses
        async with AsyncTestingSessionLocal() as session:
            open_issue = IssueCreate(
                project_id=test_project_crud.id,
                title="Open Issue Filter",
                status=IssueStatus.OPEN,
            )
            await issue_crud.create(session, obj_in=open_issue)

            closed_issue = IssueCreate(
                project_id=test_project_crud.id,
                title="Closed Issue Filter",
                status=IssueStatus.CLOSED,
            )
            await issue_crud.create(session, obj_in=closed_issue)

        async with AsyncTestingSessionLocal() as session:
            issues, _ = await issue_crud.get_by_project(
                session,
                project_id=test_project_crud.id,
                status=IssueStatus.OPEN,
            )

            assert all(i.status == IssueStatus.OPEN for i in issues)

    @pytest.mark.asyncio
    async def test_get_by_project_with_priority(self, async_test_db, test_project_crud):
        """Test filtering issues by priority."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            high_issue = IssueCreate(
                project_id=test_project_crud.id,
                title="High Priority Issue",
                priority=IssuePriority.HIGH,
            )
            await issue_crud.create(session, obj_in=high_issue)

        async with AsyncTestingSessionLocal() as session:
            issues, _ = await issue_crud.get_by_project(
                session,
                project_id=test_project_crud.id,
                priority=IssuePriority.HIGH,
            )

            assert all(i.priority == IssuePriority.HIGH for i in issues)

    @pytest.mark.asyncio
    async def test_get_by_project_with_search(self, async_test_db, test_project_crud):
        """Test searching issues by title/body."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        async with AsyncTestingSessionLocal() as session:
            searchable_issue = IssueCreate(
                project_id=test_project_crud.id,
                title="Searchable Unique Title",
                body="This body contains searchable content",
            )
            await issue_crud.create(session, obj_in=searchable_issue)

        async with AsyncTestingSessionLocal() as session:
            issues, total = await issue_crud.get_by_project(
                session,
                project_id=test_project_crud.id,
                search="Searchable Unique",
            )

            assert total >= 1
            assert any(i.title == "Searchable Unique Title" for i in issues)


class TestIssueBySprint:
    """Tests for getting issues by sprint."""

    @pytest.mark.asyncio
    async def test_get_by_sprint(self, async_test_db, test_project_crud, test_sprint_crud):
        """Test getting issues by sprint."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create issue in sprint
        async with AsyncTestingSessionLocal() as session:
            issue_data = IssueCreate(
                project_id=test_project_crud.id,
                title="Sprint Issue",
                sprint_id=test_sprint_crud.id,
            )
            await issue_crud.create(session, obj_in=issue_data)

        async with AsyncTestingSessionLocal() as session:
            issues = await issue_crud.get_by_sprint(
                session,
                sprint_id=test_sprint_crud.id,
            )

            assert len(issues) >= 1
            assert all(i.sprint_id == test_sprint_crud.id for i in issues)


class TestIssueSyncStatus:
    """Tests for issue sync status operations."""

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

        # Create issue with external tracker
        async with AsyncTestingSessionLocal() as session:
            issue_data = IssueCreate(
                project_id=test_project_crud.id,
                title="Sync Status Issue",
                external_tracker_type="gitea",
                external_issue_id="gitea-456",
            )
            created = await issue_crud.create(session, obj_in=issue_data)
            issue_id = created.id

        # Update sync status
        now = datetime.now(UTC)
        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.update_sync_status(
                session,
                issue_id=issue_id,
                sync_status=SyncStatus.PENDING,
                last_synced_at=now,
            )

            assert result is not None
            assert result.sync_status == SyncStatus.PENDING
            assert result.last_synced_at is not None

    @pytest.mark.asyncio
    async def test_get_pending_sync(self, async_test_db, test_project_crud):
        """Test getting issues pending sync."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create issue with pending sync
        async with AsyncTestingSessionLocal() as session:
            issue_data = IssueCreate(
                project_id=test_project_crud.id,
                title="Pending Sync Issue",
                external_tracker_type="gitea",
                external_issue_id="gitea-789",
            )
            created = await issue_crud.create(session, obj_in=issue_data)

            # Set to pending
            await issue_crud.update_sync_status(
                session,
                issue_id=created.id,
                sync_status=SyncStatus.PENDING,
            )

        async with AsyncTestingSessionLocal() as session:
            issues = await issue_crud.get_pending_sync(session)

            assert any(i.sync_status == SyncStatus.PENDING for i in issues)


class TestIssueExternalTracker:
    """Tests for external tracker operations."""

    @pytest.mark.asyncio
    async def test_get_by_external_id(self, async_test_db, test_project_crud):
        """Test getting issue by external tracker ID."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create issue with external ID
        async with AsyncTestingSessionLocal() as session:
            issue_data = IssueCreate(
                project_id=test_project_crud.id,
                title="External ID Issue",
                external_tracker_type="github",
                external_issue_id="github-unique-123",
            )
            await issue_crud.create(session, obj_in=issue_data)

        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.get_by_external_id(
                session,
                external_tracker_type="github",
                external_issue_id="github-unique-123",
            )

            assert result is not None
            assert result.external_issue_id == "github-unique-123"

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

        async with AsyncTestingSessionLocal() as session:
            result = await issue_crud.get_by_external_id(
                session,
                external_tracker_type="gitea",
                external_issue_id="non-existent",
            )
            assert result is None


class TestIssueStats:
    """Tests for issue statistics."""

    @pytest.mark.asyncio
    async def test_get_project_stats(self, async_test_db, test_project_crud):
        """Test getting issue statistics for a project."""
        _test_engine, AsyncTestingSessionLocal = async_test_db

        # Create issues with various statuses and priorities
        async with AsyncTestingSessionLocal() as session:
            for status in [IssueStatus.OPEN, IssueStatus.IN_PROGRESS, IssueStatus.CLOSED]:
                issue_data = IssueCreate(
                    project_id=test_project_crud.id,
                    title=f"Stats Issue {status.value}",
                    status=status,
                    story_points=3,
                )
                await issue_crud.create(session, obj_in=issue_data)

        async with AsyncTestingSessionLocal() as session:
            stats = await issue_crud.get_project_stats(
                session,
                project_id=test_project_crud.id,
            )

            assert "total" in stats
            assert "open" in stats
            assert "in_progress" in stats
            assert "closed" in stats
            assert "by_priority" in stats
            assert "total_story_points" in stats
