"""
Tests for Celery embedding tasks.

Tasks are tested with CELERY_TASK_ALWAYS_EAGER=True for synchronous execution.
"""

from unittest.mock import MagicMock, patch

from django.test import TestCase, override_settings


@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
class EmbedItemTaskTests(TestCase):
    """Tests for the embed_item task."""

    @patch("library.services.pipeline.EmbeddingPipeline")
    def test_embed_item_success(self, mock_pipeline_cls):
        from library.tasks import embed_item

        mock_pipeline = MagicMock()
        mock_pipeline.process_item.return_value = {
            "chunks_created": 10,
            "images_stored": 2,
            "model_name": "test-model",
        }
        mock_pipeline_cls.return_value = mock_pipeline

        result = embed_item("test-uid-123")

        self.assertTrue(result["success"])
        self.assertEqual(result["item_uid"], "test-uid-123")
        mock_pipeline.process_item.assert_called_once()

    @patch("library.services.pipeline.EmbeddingPipeline")
    def test_embed_item_failure(self, mock_pipeline_cls):
        from library.tasks import embed_item

        mock_pipeline = MagicMock()
        mock_pipeline.process_item.side_effect = ValueError("Item not found")
        mock_pipeline_cls.return_value = mock_pipeline

        result = embed_item("nonexistent-uid")

        self.assertFalse(result["success"])
        self.assertIn("error", result)


@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
class ReembedItemTaskTests(TestCase):
    """Tests for the reembed_item task."""

    @patch("library.services.pipeline.EmbeddingPipeline")
    def test_reembed_item_success(self, mock_pipeline_cls):
        from library.tasks import reembed_item

        mock_pipeline = MagicMock()
        mock_pipeline.reprocess_item.return_value = {
            "chunks_created": 5,
            "images_stored": 1,
            "model_name": "test-model",
        }
        mock_pipeline_cls.return_value = mock_pipeline

        result = reembed_item("test-uid-123")

        self.assertTrue(result["success"])
        mock_pipeline.reprocess_item.assert_called_once()


@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
class IngestFromDaedalusFailureClassificationTests(TestCase):
    """A deterministic parse failure is terminal; a transient error retries.

    Neo4j and S3 are mocked at the task's boundaries so the test exercises the
    exception-classification branch (parsers.UnsupportedFileTypeError vs. any
    other Exception) without a live graph or object store.
    """

    def setUp(self):
        from library.tasks import ingest_from_daedalus

        # Calling .run() bypasses Celery's request setup, but the task
        # persists self.request.id into the NOT NULL celery_task_id column
        # and reads self.request.retries — push a real request context.
        ingest_from_daedalus.push_request(id="test-task-id", retries=0)
        self.addCleanup(ingest_from_daedalus.pop_request)

    def _make_job(self):
        from library.models import IngestJob

        return IngestJob.objects.create(
            id="job_test_unsupported",
            library_uid="lib-uid-123",
            source="daedalus",
            s3_key="incoming/bad.drawio",
            file_type="vnd.jgraph.mxfile",
            title="bad.drawio",
            content_hash="abc123",
        )

    def _patched_boundaries(self, pipeline_side_effect):
        """Patch every boundary the task hits before the pipeline runs.

        Returns a context-manager list; the pipeline's ``process_item`` is set
        to raise ``pipeline_side_effect``.
        """
        from library.services.parsers import UnsupportedFileTypeError  # noqa: F401

        patchers = [
            patch("library.tasks.db"),
            patch("library.models.Library"),
            patch("library.models.Item"),
            patch("library.services.source_s3.fetch_from_source", return_value=b"data"),
            patch("library.services.source_s3.copy_into_mnemosyne"),
            patch("library.tasks._resolve_or_create_default_collection"),
            patch("library.services.pipeline.EmbeddingPipeline"),
        ]
        mocks = [p.start() for p in patchers]
        self.addCleanup(lambda: [p.stop() for p in patchers])

        # db.cypher_query returns (rows, meta); no prior item to supersede.
        mocks[0].cypher_query.return_value = ([], None)
        # Library.nodes.get returns a stand-in library node.
        mocks[1].nodes.get.return_value = MagicMock(uid="lib-uid-123")
        # Item() instances carry a uid used for the S3 key.
        item_instance = MagicMock(uid="item-uid-abc")
        mocks[2].return_value = item_instance
        # The pipeline raises the classification-relevant error.
        pipeline_instance = MagicMock()
        pipeline_instance.process_item.side_effect = pipeline_side_effect
        mocks[6].return_value = pipeline_instance
        return pipeline_instance

    def test_unsupported_file_type_is_terminal_and_not_retried(self):
        from library.models import IngestJob
        from library.services.parsers import UnsupportedFileTypeError
        from library.tasks import ingest_from_daedalus

        job = self._make_job()
        self._patched_boundaries(
            UnsupportedFileTypeError("Unsupported file type 'vnd.jgraph.mxfile'.")
        )

        with patch.object(ingest_from_daedalus, "retry") as mock_retry:
            result = ingest_from_daedalus.run(job.id)

        # Never retried.
        mock_retry.assert_not_called()
        # Terminal failure with a machine-readable reason.
        self.assertFalse(result["success"])
        self.assertEqual(result["reason"], "unsupported_file_type")

        job.refresh_from_db()
        self.assertEqual(job.status, "failed")
        self.assertEqual(job.retry_count, 0)
        self.assertIsNotNone(job.completed_at)
        self.assertIn("Unsupported file type", job.error)

    def test_transient_error_takes_the_retry_path(self):
        from library.tasks import ingest_from_daedalus

        job = self._make_job()
        self._patched_boundaries(ConnectionError("S3 hiccup"))

        # self.retry raises Retry in real Celery; simulate that so the task
        # doesn't fall through to the terminal branch.
        from celery.exceptions import Retry

        with patch.object(ingest_from_daedalus, "retry", side_effect=Retry()) as mock_retry:
            with self.assertRaises(Retry):
                ingest_from_daedalus.run(job.id)

        mock_retry.assert_called_once()
        job.refresh_from_db()
        self.assertEqual(job.retry_count, 1)


class ResolveUserTests(TestCase):
    """Tests for the _resolve_user helper."""

    def test_none_user_id(self):
        from library.tasks import _resolve_user

        self.assertIsNone(_resolve_user(None))

    def test_zero_user_id(self):
        from library.tasks import _resolve_user

        self.assertIsNone(_resolve_user(0))

    def test_invalid_user_id(self):
        from library.tasks import _resolve_user

        self.assertIsNone(_resolve_user(999999))
