"""Unit tests for document deletion service (Story 4.5)."""

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

import pytest

from src.core.exceptions import IndexingError
from src.extraction.models import ExtractedRecord
from src.services.document_deletion_service import DocumentDeletionService


@pytest.fixture
def mock_db_session():
    """Create a mock async database session."""
    session = AsyncMock()
    session.begin = MagicMock()
    session.begin.return_value.__aenter__ = AsyncMock()
    session.begin.return_value.__aexit__ = AsyncMock()
    return session


@pytest.fixture
def deletion_service(mock_db_session):
    """Create a DocumentDeletionService instance with mocked session."""
    return DocumentDeletionService(mock_db_session)


@pytest.fixture
def test_document_id():
    """Create a test document UUID."""
    return uuid4()


@pytest.fixture
def sample_records():
    """Create sample ExtractedRecord objects for testing."""
    return [
        ExtractedRecord(
            content={"Product": "Widget A", "Price": "100"},
            headers=["Product", "Price"],
            _source={
                "document_id": str(uuid4()),
                "filename": "test.xlsx",
                "sheet": "Sheet1",
                "table_id": 1,
                "row": 2,
                "col_range": "A-B",
            },
        ),
        ExtractedRecord(
            content={"Product": "Widget B", "Price": "200"},
            headers=["Product", "Price"],
            _source={
                "document_id": str(uuid4()),
                "filename": "test.xlsx",
                "sheet": "Sheet1",
                "table_id": 1,
                "row": 3,
                "col_range": "A-B",
            },
        ),
    ]


class TestDeleteDocument:
    """Test delete_document method."""

    @pytest.mark.asyncio
    @patch("src.services.document_deletion_service.delete_vectors_by_document_id")
    async def test_delete_document_success(
        self, mock_delete_vectors, deletion_service, test_document_id, mock_db_session
    ):
        """Should delete records and vectors atomically."""
        # Arrange
        mock_structured_store = AsyncMock()
        mock_structured_store.delete_records_by_document_id = AsyncMock(return_value=10)
        deletion_service.structured_store = mock_structured_store

        mock_delete_vectors.return_value = 10

        # Act
        result = await deletion_service.delete_document(test_document_id)

        # Assert
        assert result == {"records": 10, "vectors": 10}
        mock_structured_store.delete_records_by_document_id.assert_called_once_with(
            test_document_id
        )
        mock_delete_vectors.assert_called_once_with(test_document_id)

    @pytest.mark.asyncio
    @patch("src.services.document_deletion_service.delete_vectors_by_document_id")
    async def test_delete_document_rollback_on_milvus_failure(
        self, mock_delete_vectors, deletion_service, test_document_id, mock_db_session
    ):
        """Should rollback PostgreSQL transaction if Milvus deletion fails."""
        # Arrange
        mock_structured_store = AsyncMock()
        mock_structured_store.delete_records_by_document_id = AsyncMock(return_value=10)
        deletion_service.structured_store = mock_structured_store

        # Simulate Milvus failure
        mock_delete_vectors.side_effect = Exception("Milvus connection error")

        # Setup transaction context to propagate the exception
        async def mock_begin_context():
            yield

        mock_db_session.begin.return_value.__aenter__ = AsyncMock()
        mock_db_session.begin.return_value.__aexit__ = AsyncMock(return_value=False)

        # Act & Assert
        with pytest.raises(IndexingError) as exc_info:
            await deletion_service.delete_document(test_document_id)

        assert "Milvus deletion failed" in str(exc_info.value)

    @pytest.mark.asyncio
    @patch("src.services.document_deletion_service.delete_vectors_by_document_id")
    async def test_delete_document_handles_nonexistent_document(
        self, mock_delete_vectors, deletion_service, test_document_id
    ):
        """Should handle deletion of nonexistent document gracefully."""
        # Arrange
        mock_structured_store = AsyncMock()
        mock_structured_store.delete_records_by_document_id = AsyncMock(return_value=0)
        deletion_service.structured_store = mock_structured_store

        mock_delete_vectors.return_value = 0

        # Act
        result = await deletion_service.delete_document(test_document_id)

        # Assert
        assert result == {"records": 0, "vectors": 0}


class TestReindexDocument:
    """Test reindex_document method."""

    @pytest.mark.asyncio
    @patch("src.services.document_deletion_service.VectorStore")
    async def test_reindex_document_success(
        self, mock_vector_store_class, deletion_service, test_document_id, sample_records
    ):
        """Should delete old and index new records atomically."""
        # Arrange
        mock_structured_store = AsyncMock()
        mock_structured_store.delete_records_by_document_id = AsyncMock(return_value=5)
        mock_structured_store.store_records = AsyncMock(return_value=[1, 2])
        deletion_service.structured_store = mock_structured_store

        with patch(
            "src.services.document_deletion_service.delete_vectors_by_document_id"
        ) as mock_delete_vectors:
            mock_delete_vectors.return_value = 5

            mock_vector_store = AsyncMock()
            mock_vector_store.create_collection = AsyncMock()
            mock_vector_store.index_records = AsyncMock(return_value=2)
            mock_vector_store_class.return_value = mock_vector_store

            # Act
            result = await deletion_service.reindex_document(
                test_document_id, sample_records
            )

            # Assert
            assert result == {
                "deleted_records": 5,
                "deleted_vectors": 5,
                "indexed_records": 2,
                "indexed_vectors": 2,
            }
            mock_structured_store.store_records.assert_called_once_with(
                sample_records, test_document_id
            )
            mock_vector_store.index_records.assert_called_once()

    @pytest.mark.asyncio
    @patch("src.services.document_deletion_service.VectorStore")
    async def test_reindex_document_handles_new_document(
        self, mock_vector_store_class, deletion_service, test_document_id, sample_records
    ):
        """Should handle reindexing when document doesn't exist (first-time index)."""
        # Arrange
        mock_structured_store = AsyncMock()
        mock_structured_store.delete_records_by_document_id = AsyncMock(return_value=0)
        mock_structured_store.store_records = AsyncMock(return_value=[1, 2])
        deletion_service.structured_store = mock_structured_store

        with patch(
            "src.services.document_deletion_service.delete_vectors_by_document_id"
        ) as mock_delete_vectors:
            mock_delete_vectors.return_value = 0

            mock_vector_store = AsyncMock()
            mock_vector_store.create_collection = AsyncMock()
            mock_vector_store.index_records = AsyncMock(return_value=2)
            mock_vector_store_class.return_value = mock_vector_store

            # Act
            result = await deletion_service.reindex_document(
                test_document_id, sample_records
            )

            # Assert
            assert result["deleted_records"] == 0
            assert result["deleted_vectors"] == 0
            assert result["indexed_records"] == 2
            assert result["indexed_vectors"] == 2

    @pytest.mark.asyncio
    async def test_reindex_document_failure_handling(
        self, deletion_service, test_document_id, sample_records
    ):
        """Should raise IndexingError if reindexing fails."""
        # Arrange
        mock_structured_store = AsyncMock()
        mock_structured_store.delete_records_by_document_id = AsyncMock(
            side_effect=Exception("Database error")
        )
        deletion_service.structured_store = mock_structured_store

        # Act & Assert
        with pytest.raises(IndexingError) as exc_info:
            await deletion_service.reindex_document(test_document_id, sample_records)

        assert "Failed to reindex document" in str(exc_info.value)
