"""
Integration tests for document_indexing_sync_task using testcontainers.

This module validates SQL-backed behavior for document sync flows:
- Notion sync precondition checks
- Segment cleanup and document state updates
- Credential and indexing error handling
"""

import json
from unittest.mock import Mock, patch
from uuid import uuid4

import pytest
from sqlalchemy import delete, func, select, update
from sqlalchemy.orm import Session

from core.indexing_runner import DocumentIsPausedError, IndexingRunner
from core.rag.index_processor.constant.index_type import IndexStructureType, IndexTechniqueType
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
from models.dataset import Dataset, Document, DocumentSegment
from models.enums import DataSourceType, DocumentCreatedFrom, IndexingStatus, SegmentStatus
from tasks.document_indexing_sync_task import document_indexing_sync_task


class DocumentIndexingSyncTaskTestDataFactory:
    """Create real DB entities for document indexing sync integration tests."""

    @staticmethod
    def create_account_with_tenant(db_session_with_containers) -> tuple[Account, Tenant]:
        account = Account(
            email=f"{uuid4()}@example.com",
            name=f"user-{uuid4()}",
            interface_language="en-US",
            status=AccountStatus.ACTIVE,
        )
        db_session_with_containers.add(account)
        db_session_with_containers.flush()

        tenant = Tenant(name=f"tenant-{account.id}", status=TenantStatus.NORMAL)
        db_session_with_containers.add(tenant)
        db_session_with_containers.flush()

        join = TenantAccountJoin(
            tenant_id=tenant.id,
            account_id=account.id,
            role=TenantAccountRole.OWNER,
            current=True,
        )
        db_session_with_containers.add(join)
        db_session_with_containers.commit()

        return account, tenant

    @staticmethod
    def create_dataset(db_session_with_containers, tenant_id: str, created_by: str) -> Dataset:
        dataset = Dataset(
            tenant_id=tenant_id,
            name=f"dataset-{uuid4()}",
            description="sync test dataset",
            data_source_type=DataSourceType.NOTION_IMPORT,
            indexing_technique=IndexTechniqueType.HIGH_QUALITY,
            created_by=created_by,
        )
        db_session_with_containers.add(dataset)
        db_session_with_containers.commit()
        return dataset

    @staticmethod
    def create_document(
        db_session_with_containers,
        *,
        tenant_id: str,
        dataset_id: str,
        created_by: str,
        data_source_info: dict | None,
        indexing_status: str = "completed",
    ) -> Document:
        document = Document(
            tenant_id=tenant_id,
            dataset_id=dataset_id,
            position=0,
            data_source_type=DataSourceType.NOTION_IMPORT,
            data_source_info=json.dumps(data_source_info) if data_source_info is not None else None,
            batch="test-batch",
            name=f"doc-{uuid4()}",
            created_from=DocumentCreatedFrom.WEB,
            created_by=created_by,
            indexing_status=indexing_status,
            enabled=True,
            doc_form=IndexStructureType.PARAGRAPH_INDEX,
            doc_language="en",
        )
        db_session_with_containers.add(document)
        db_session_with_containers.commit()
        return document

    @staticmethod
    def create_segments(
        db_session_with_containers,
        *,
        tenant_id: str,
        dataset_id: str,
        document_id: str,
        created_by: str,
        count: int = 3,
    ) -> list[DocumentSegment]:
        segments: list[DocumentSegment] = []
        for i in range(count):
            segment = DocumentSegment(
                tenant_id=tenant_id,
                dataset_id=dataset_id,
                document_id=document_id,
                position=i,
                content=f"segment-{i}",
                answer=None,
                word_count=10,
                tokens=5,
                index_node_id=f"node-{document_id}-{i}",
                status=SegmentStatus.COMPLETED,
                created_by=created_by,
            )
            db_session_with_containers.add(segment)
            segments.append(segment)
        db_session_with_containers.commit()
        return segments


class TestDocumentIndexingSyncTask:
    """Integration tests for document_indexing_sync_task with real database assertions."""

    @pytest.fixture
    def mock_external_dependencies(self):
        """Patch only external collaborators; keep DB access real."""
        with (
            patch("tasks.document_indexing_sync_task.DatasourceProviderService") as mock_datasource_service_class,
            patch("tasks.document_indexing_sync_task.NotionExtractor") as mock_notion_extractor_class,
            patch("tasks.document_indexing_sync_task.IndexProcessorFactory") as mock_index_processor_factory,
            patch("tasks.document_indexing_sync_task.IndexingRunner") as mock_indexing_runner_class,
        ):
            datasource_service = Mock()
            datasource_service.get_datasource_credentials.return_value = {"integration_secret": "test_token"}
            mock_datasource_service_class.return_value = datasource_service

            notion_extractor = Mock()
            notion_extractor.get_notion_last_edited_time.return_value = "2024-01-02T00:00:00Z"
            mock_notion_extractor_class.return_value = notion_extractor

            index_processor = Mock()
            index_processor.clean = Mock()
            mock_index_processor_factory.return_value.init_index_processor.return_value = index_processor

            indexing_runner = Mock(spec=IndexingRunner)
            indexing_runner.run = Mock()
            mock_indexing_runner_class.return_value = indexing_runner

            yield {
                "datasource_service": datasource_service,
                "notion_extractor": notion_extractor,
                "notion_extractor_class": mock_notion_extractor_class,
                "index_processor": index_processor,
                "index_processor_factory": mock_index_processor_factory,
                "indexing_runner": indexing_runner,
            }

    def _create_notion_sync_context(self, db_session_with_containers: Session, *, data_source_info: dict | None = None):
        account, tenant = DocumentIndexingSyncTaskTestDataFactory.create_account_with_tenant(db_session_with_containers)
        dataset = DocumentIndexingSyncTaskTestDataFactory.create_dataset(
            db_session_with_containers,
            tenant_id=tenant.id,
            created_by=account.id,
        )

        notion_info = data_source_info or {
            "notion_workspace_id": str(uuid4()),
            "notion_page_id": str(uuid4()),
            "type": "page",
            "last_edited_time": "2024-01-01T00:00:00Z",
            "credential_id": str(uuid4()),
        }

        document = DocumentIndexingSyncTaskTestDataFactory.create_document(
            db_session_with_containers,
            tenant_id=tenant.id,
            dataset_id=dataset.id,
            created_by=account.id,
            data_source_info=notion_info,
            indexing_status=IndexingStatus.COMPLETED,
        )

        segments = DocumentIndexingSyncTaskTestDataFactory.create_segments(
            db_session_with_containers,
            tenant_id=tenant.id,
            dataset_id=dataset.id,
            document_id=document.id,
            created_by=account.id,
            count=3,
        )

        return {
            "account": account,
            "tenant": tenant,
            "dataset": dataset,
            "document": document,
            "segments": segments,
            "node_ids": [segment.index_node_id for segment in segments],
            "notion_info": notion_info,
        }

    def test_document_not_found(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test that task handles missing document gracefully."""
        # Arrange
        dataset_id = str(uuid4())
        document_id = str(uuid4())

        # Act
        document_indexing_sync_task(dataset_id, document_id)

        # Assert
        mock_external_dependencies["datasource_service"].get_datasource_credentials.assert_not_called()
        mock_external_dependencies["indexing_runner"].run.assert_not_called()

    def test_missing_notion_workspace_id(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test that task raises error when notion_workspace_id is missing."""
        # Arrange
        context = self._create_notion_sync_context(
            db_session_with_containers,
            data_source_info={
                "notion_page_id": str(uuid4()),
                "type": "page",
                "last_edited_time": "2024-01-01T00:00:00Z",
            },
        )

        # Act & Assert
        with pytest.raises(ValueError, match="no notion page found"):
            document_indexing_sync_task(context["dataset"].id, context["document"].id)

    def test_missing_notion_page_id(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test that task raises error when notion_page_id is missing."""
        # Arrange
        context = self._create_notion_sync_context(
            db_session_with_containers,
            data_source_info={
                "notion_workspace_id": str(uuid4()),
                "type": "page",
                "last_edited_time": "2024-01-01T00:00:00Z",
            },
        )

        # Act & Assert
        with pytest.raises(ValueError, match="no notion page found"):
            document_indexing_sync_task(context["dataset"].id, context["document"].id)

    def test_empty_data_source_info(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test that task raises error when data_source_info is empty."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers, data_source_info=None)
        db_session_with_containers.execute(
            update(Document).where(Document.id == context["document"].id).values(data_source_info=None)
        )
        db_session_with_containers.commit()

        # Act & Assert
        with pytest.raises(ValueError, match="no notion page found"):
            document_indexing_sync_task(context["dataset"].id, context["document"].id)

    def test_credential_not_found(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test that task sets document error state when credential is missing."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers)
        mock_external_dependencies["datasource_service"].get_datasource_credentials.return_value = None

        # Act
        document_indexing_sync_task(context["dataset"].id, context["document"].id)

        # Assert
        db_session_with_containers.expire_all()
        updated_document = db_session_with_containers.scalar(
            select(Document).where(Document.id == context["document"].id).limit(1)
        )
        assert updated_document is not None
        assert updated_document.indexing_status == IndexingStatus.ERROR
        assert "Datasource credential not found" in updated_document.error
        assert updated_document.stopped_at is not None
        mock_external_dependencies["indexing_runner"].run.assert_not_called()

    def test_page_not_updated(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test that task exits early when notion page is unchanged."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers)
        mock_external_dependencies["notion_extractor"].get_notion_last_edited_time.return_value = "2024-01-01T00:00:00Z"

        # Act
        document_indexing_sync_task(context["dataset"].id, context["document"].id)

        # Assert
        db_session_with_containers.expire_all()
        updated_document = db_session_with_containers.scalar(
            select(Document).where(Document.id == context["document"].id).limit(1)
        )
        remaining_segments = db_session_with_containers.scalar(
            select(func.count())
            .select_from(DocumentSegment)
            .where(DocumentSegment.document_id == context["document"].id)
        )
        assert updated_document is not None
        assert updated_document.indexing_status == IndexingStatus.COMPLETED
        assert updated_document.processing_started_at is None
        assert remaining_segments == 3
        mock_external_dependencies["index_processor"].clean.assert_not_called()
        mock_external_dependencies["indexing_runner"].run.assert_not_called()

    def test_successful_sync_when_page_updated(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test full successful sync flow with SQL state updates and side effects."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers)

        # Act
        document_indexing_sync_task(context["dataset"].id, context["document"].id)

        # Assert
        db_session_with_containers.expire_all()
        updated_document = db_session_with_containers.scalar(
            select(Document).where(Document.id == context["document"].id).limit(1)
        )
        remaining_segments = db_session_with_containers.scalar(
            select(func.count())
            .select_from(DocumentSegment)
            .where(DocumentSegment.document_id == context["document"].id)
        )

        assert updated_document is not None
        assert updated_document.indexing_status == IndexingStatus.PARSING
        assert updated_document.processing_started_at is not None
        assert updated_document.data_source_info_dict.get("last_edited_time") == "2024-01-02T00:00:00Z"
        assert remaining_segments == 0

        clean_call_args = mock_external_dependencies["index_processor"].clean.call_args
        assert clean_call_args is not None
        clean_args, clean_kwargs = clean_call_args
        assert getattr(clean_args[0], "id", None) == context["dataset"].id
        assert set(clean_args[1]) == set(context["node_ids"])
        assert clean_kwargs.get("with_keywords") is True
        assert clean_kwargs.get("delete_child_chunks") is True

        run_call_args = mock_external_dependencies["indexing_runner"].run.call_args
        assert run_call_args is not None
        run_documents = run_call_args[0][0]
        assert len(run_documents) == 1
        assert getattr(run_documents[0], "id", None) == context["document"].id

    def test_dataset_not_found_during_cleaning(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test that task still updates document and reindexes if dataset vanishes before clean."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers)

        def _delete_dataset_before_clean() -> str:
            db_session_with_containers.execute(delete(Dataset).where(Dataset.id == context["dataset"].id))
            db_session_with_containers.commit()
            return "2024-01-02T00:00:00Z"

        mock_external_dependencies[
            "notion_extractor"
        ].get_notion_last_edited_time.side_effect = _delete_dataset_before_clean

        # Act
        document_indexing_sync_task(context["dataset"].id, context["document"].id)

        # Assert
        db_session_with_containers.expire_all()
        updated_document = db_session_with_containers.scalar(
            select(Document).where(Document.id == context["document"].id).limit(1)
        )
        assert updated_document is not None
        assert updated_document.indexing_status == IndexingStatus.PARSING
        mock_external_dependencies["index_processor"].clean.assert_not_called()
        mock_external_dependencies["indexing_runner"].run.assert_called_once()

    def test_cleaning_error_continues_to_indexing(
        self, db_session_with_containers: Session, mock_external_dependencies
    ):
        """Test that indexing continues when index cleanup fails."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers)
        mock_external_dependencies["index_processor"].clean.side_effect = Exception("Cleaning error")

        # Act
        document_indexing_sync_task(context["dataset"].id, context["document"].id)

        # Assert
        db_session_with_containers.expire_all()
        updated_document = db_session_with_containers.scalar(
            select(Document).where(Document.id == context["document"].id).limit(1)
        )
        remaining_segments = db_session_with_containers.scalar(
            select(func.count())
            .select_from(DocumentSegment)
            .where(DocumentSegment.document_id == context["document"].id)
        )
        assert updated_document is not None
        assert updated_document.indexing_status == IndexingStatus.PARSING
        assert remaining_segments == 0
        mock_external_dependencies["indexing_runner"].run.assert_called_once()

    def test_indexing_runner_document_paused_error(
        self, db_session_with_containers: Session, mock_external_dependencies
    ):
        """Test that DocumentIsPausedError does not flip document into error state."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers)
        mock_external_dependencies["indexing_runner"].run.side_effect = DocumentIsPausedError("Document paused")

        # Act
        document_indexing_sync_task(context["dataset"].id, context["document"].id)

        # Assert
        db_session_with_containers.expire_all()
        updated_document = db_session_with_containers.scalar(
            select(Document).where(Document.id == context["document"].id).limit(1)
        )
        assert updated_document is not None
        assert updated_document.indexing_status == IndexingStatus.PARSING
        assert updated_document.error is None

    def test_indexing_runner_general_error(self, db_session_with_containers: Session, mock_external_dependencies):
        """Test that indexing errors are persisted to document state."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers)
        mock_external_dependencies["indexing_runner"].run.side_effect = Exception("Indexing error")

        # Act
        document_indexing_sync_task(context["dataset"].id, context["document"].id)

        # Assert
        db_session_with_containers.expire_all()
        updated_document = db_session_with_containers.scalar(
            select(Document).where(Document.id == context["document"].id).limit(1)
        )
        assert updated_document is not None
        assert updated_document.indexing_status == IndexingStatus.ERROR
        assert "Indexing error" in updated_document.error
        assert updated_document.stopped_at is not None

    def test_index_processor_clean_called_with_correct_params(
        self,
        db_session_with_containers,
        mock_external_dependencies,
    ):
        """Test that clean is called with dataset instance and collected node ids."""
        # Arrange
        context = self._create_notion_sync_context(db_session_with_containers)

        # Act
        document_indexing_sync_task(context["dataset"].id, context["document"].id)

        # Assert
        clean_call_args = mock_external_dependencies["index_processor"].clean.call_args
        assert clean_call_args is not None
        clean_args, clean_kwargs = clean_call_args
        assert getattr(clean_args[0], "id", None) == context["dataset"].id
        assert set(clean_args[1]) == set(context["node_ids"])
        assert clean_kwargs.get("with_keywords") is True
        assert clean_kwargs.get("delete_child_chunks") is True
