"""Tests for the full-text search API (app/api/search.py) and Meilisearch
client utilities (app/utils/meilisearch_client.py).
"""

from unittest.mock import MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# app/utils/meilisearch_client tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestMeilisearchClientDisabled:
    """Tests when search is disabled or Meilisearch is unavailable."""

    def test_get_client_returns_none_when_disabled(self):
        """get_meilisearch_client returns None when enable_search=False."""
        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=None):
            from app.utils.meilisearch_client import index_document, search_documents

            class _FakeRecord:
                id = 1
                original_filename = "test.pdf"
                mime_type = "application/pdf"
                file_size = 1024
                created_at = None

            result = index_document(_FakeRecord(), "some text", {})
            assert result is False

            result = search_documents("invoice")
            assert result["results"] == []
            assert result["total"] == 0

    def test_search_documents_import_error(self):
        """search_documents returns empty dict when meilisearch not installed."""
        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=None):
            from app.utils.meilisearch_client import search_documents

            result = search_documents("test")
            assert result["results"] == []
            assert result["total"] == 0
            assert result["page"] == 1
            assert result["query"] == "test"

    def test_delete_document_no_client(self):
        """delete_document returns False when client unavailable."""
        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=None):
            from app.utils.meilisearch_client import delete_document

            result = delete_document(99)
            assert result is False


@pytest.mark.unit
class TestMeilisearchIndexDocument:
    """Tests for index_document function."""

    def test_index_document_success(self):
        """index_document returns True when Meilisearch succeeds."""
        mock_client = MagicMock()
        mock_index = MagicMock()
        mock_task = MagicMock()
        mock_task.task_uid = 1

        mock_client.get_index.return_value = mock_index
        mock_index.add_documents.return_value = mock_task

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import index_document

            class _FakeRecord:
                id = 1
                original_filename = "invoice.pdf"
                mime_type = "application/pdf"
                file_size = 2048
                created_at = None

            result = index_document(
                _FakeRecord(),
                "This is an invoice for services rendered",
                {
                    "title": "Invoice January 2026",
                    "document_type": "Invoice",
                    "tags": ["invoice", "services"],
                    "absender": "ACME Corp",
                    "language": "en",
                },
            )
            assert result is True
            mock_index.add_documents.assert_called_once()
            call_docs = mock_index.add_documents.call_args[0][0]
            assert len(call_docs) == 1
            doc = call_docs[0]
            assert doc["file_id"] == 1
            assert doc["document_title"] == "Invoice January 2026"
            assert "invoice" in doc["tags"]
            assert doc["ocr_text_length"] == len("This is an invoice for services rendered")

    def test_index_document_meilisearch_error(self):
        """index_document returns False on Meilisearch exception."""
        mock_client = MagicMock()
        mock_index = MagicMock()
        mock_client.get_index.return_value = mock_index
        mock_index.add_documents.side_effect = RuntimeError("Meilisearch down")

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import index_document

            class _FakeRecord:
                id = 2
                original_filename = "test.pdf"
                mime_type = "application/pdf"
                file_size = 512
                created_at = None

            result = index_document(_FakeRecord(), "some text", {})
            assert result is False


@pytest.mark.unit
class TestMeilisearchSearchDocuments:
    """Tests for search_documents function."""

    def _make_mock_client(self, hits=None, total=None):
        mock_client = MagicMock()
        mock_index = MagicMock()
        mock_client.get_index.return_value = mock_index
        mock_index.search.return_value = {
            "hits": hits or [],
            "estimatedTotalHits": total if total is not None else len(hits or []),
        }
        return mock_client, mock_index

    def test_search_returns_results(self):
        """search_documents returns hits from Meilisearch."""
        hits = [
            {
                "file_id": 42,
                "original_filename": "2026-01-15_Invoice_Amazon.pdf",
                "document_title": "Amazon Invoice",
                "document_type": "Invoice",
                "tags": ["amazon", "invoice"],
                "ocr_text": "Amazon invoice content here",
                "_formatted": {
                    "document_title": "Amazon <mark>Invoice</mark>",
                    "ocr_text": "…Amazon <mark>invoice</mark> content here…",
                },
            }
        ]
        mock_client, mock_index = self._make_mock_client(hits=hits, total=1)

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            result = search_documents("invoice", page=1, per_page=20)

        assert result["total"] == 1
        assert result["pages"] == 1
        assert result["query"] == "invoice"
        assert len(result["results"]) == 1
        # Raw ocr_text should be stripped from result (only _formatted snippet kept)
        assert "ocr_text" not in result["results"][0]
        assert result["results"][0]["file_id"] == 42

    def test_search_with_filters(self):
        """search_documents passes filter expressions to Meilisearch."""
        mock_client, mock_index = self._make_mock_client(hits=[], total=0)

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            search_documents("contract", mime_type="application/pdf", language="de", page=1, per_page=10)

        call_kwargs = mock_index.search.call_args
        search_params = call_kwargs[0][1]
        assert "filter" in search_params
        assert 'mime_type = "application/pdf"' in search_params["filter"]
        assert 'language = "de"' in search_params["filter"]

    def test_search_with_tags_filter(self):
        """search_documents passes tags filter to Meilisearch."""
        mock_client, mock_index = self._make_mock_client(hits=[], total=0)

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            search_documents("test", tags="invoice", page=1, per_page=10)

        call_kwargs = mock_index.search.call_args
        search_params = call_kwargs[0][1]
        assert "filter" in search_params
        assert 'tags = "invoice"' in search_params["filter"]

    def test_search_with_sender_filter(self):
        """search_documents passes sender filter to Meilisearch."""
        mock_client, mock_index = self._make_mock_client(hits=[], total=0)

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            search_documents("test", sender="ACME Corp", page=1, per_page=10)

        call_kwargs = mock_index.search.call_args
        search_params = call_kwargs[0][1]
        assert "filter" in search_params
        assert 'sender = "ACME Corp"' in search_params["filter"]

    def test_search_with_text_quality_high(self):
        """search_documents translates text_quality=high to ocr_text_length filter."""
        mock_client, mock_index = self._make_mock_client(hits=[], total=0)

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            search_documents("test", text_quality="high", page=1, per_page=10)

        call_kwargs = mock_index.search.call_args
        search_params = call_kwargs[0][1]
        assert "filter" in search_params
        assert "ocr_text_length >= 2000" in search_params["filter"]

    def test_search_with_text_quality_no_text(self):
        """search_documents translates text_quality=no_text to ocr_text_length filter."""
        mock_client, mock_index = self._make_mock_client(hits=[], total=0)

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            search_documents("test", text_quality="no_text", page=1, per_page=10)

        call_kwargs = mock_index.search.call_args
        search_params = call_kwargs[0][1]
        assert "filter" in search_params
        assert "ocr_text_length = 0" in search_params["filter"]

    def test_search_pagination(self):
        """search_documents applies correct offset for page 2."""
        mock_client, mock_index = self._make_mock_client(hits=[], total=50)

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            result = search_documents("test", page=3, per_page=10)

        call_kwargs = mock_index.search.call_args
        search_params = call_kwargs[0][1]
        assert search_params["offset"] == 20  # (3-1) * 10
        assert search_params["limit"] == 10
        assert result["pages"] == 5

    def test_search_empty_results(self):
        """search_documents returns proper empty structure."""
        mock_client, mock_index = self._make_mock_client(hits=[], total=0)

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            result = search_documents("nothing")

        assert result["results"] == []
        assert result["total"] == 0
        assert result["pages"] == 0

    def test_search_exception_returns_empty(self):
        """search_documents returns empty dict on Meilisearch exception."""
        mock_client = MagicMock()
        mock_index = MagicMock()
        mock_client.get_index.side_effect = RuntimeError("Meilisearch unavailable")

        with patch("app.utils.meilisearch_client.get_meilisearch_client", return_value=mock_client):
            from app.utils.meilisearch_client import search_documents

            result = search_documents("invoice")

        assert result["results"] == []
        assert result["total"] == 0


# ---------------------------------------------------------------------------
# Search API endpoint tests (GET /api/search)
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestSearchAPIEndpoint:
    """Tests for GET /api/search endpoint."""

    def test_search_endpoint_success(self, client):
        """GET /api/search?q=... returns search results."""
        mock_result = {
            "results": [{"file_id": 1, "document_title": "Test Invoice", "document_type": "Invoice"}],
            "total": 1,
            "page": 1,
            "pages": 1,
            "query": "invoice",
        }
        with patch("app.api.search.search_documents", return_value=mock_result):
            response = client.get("/api/search?q=invoice")

        assert response.status_code == 200
        data = response.json()
        assert data["total"] == 1
        assert data["query"] == "invoice"
        assert len(data["results"]) == 1

    def test_search_endpoint_missing_query(self, client):
        """GET /api/search without q returns 422."""
        response = client.get("/api/search")
        assert response.status_code == 422

    def test_search_endpoint_empty_query(self, client):
        """GET /api/search?q= (empty) returns 422 due to min_length=1."""
        response = client.get("/api/search?q=")
        assert response.status_code == 422

    def test_search_endpoint_with_filters(self, client):
        """GET /api/search with optional filters passes them to search_documents."""
        mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "invoice"}
        with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
            response = client.get("/api/search?q=invoice&mime_type=application/pdf&language=en&page=2&per_page=10")

        assert response.status_code == 200
        mock_search.assert_called_once_with(
            "invoice",
            mime_type="application/pdf",
            document_type=None,
            language="en",
            tags=None,
            sender=None,
            text_quality=None,
            date_from=None,
            date_to=None,
            page=2,
            per_page=10,
        )

    def test_search_endpoint_per_page_max(self, client):
        """GET /api/search with per_page > 100 returns 422."""
        response = client.get("/api/search?q=test&per_page=200")
        assert response.status_code == 422

    def test_search_endpoint_pagination_defaults(self, client):
        """GET /api/search uses default page=1 per_page=20."""
        mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "test"}
        with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
            response = client.get("/api/search?q=test")

        assert response.status_code == 200
        mock_search.assert_called_once_with(
            "test",
            mime_type=None,
            document_type=None,
            language=None,
            tags=None,
            sender=None,
            text_quality=None,
            date_from=None,
            date_to=None,
            page=1,
            per_page=20,
        )

    def test_search_endpoint_date_filters(self, client):
        """GET /api/search with date_from and date_to passes them as int."""
        mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "contract"}
        with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
            response = client.get("/api/search?q=contract&date_from=1704067200&date_to=1735689600")

        assert response.status_code == 200
        call_kwargs = mock_search.call_args
        assert call_kwargs[1]["date_from"] == 1704067200
        assert call_kwargs[1]["date_to"] == 1735689600

    def test_search_endpoint_tags_filter(self, client):
        """GET /api/search?q=...&tags=invoice passes tags to search_documents."""
        mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "test"}
        with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
            response = client.get("/api/search?q=test&tags=invoice")

        assert response.status_code == 200
        assert mock_search.call_args[1]["tags"] == "invoice"

    def test_search_endpoint_sender_filter(self, client):
        """GET /api/search?q=...&sender=ACME passes sender to search_documents."""
        mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "test"}
        with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
            response = client.get("/api/search?q=test&sender=ACME")

        assert response.status_code == 200
        assert mock_search.call_args[1]["sender"] == "ACME"

    def test_search_endpoint_text_quality_filter(self, client):
        """GET /api/search?q=...&text_quality=high passes text_quality to search_documents."""
        mock_result = {"results": [], "total": 0, "page": 1, "pages": 0, "query": "test"}
        with patch("app.api.search.search_documents", return_value=mock_result) as mock_search:
            response = client.get("/api/search?q=test&text_quality=high")

        assert response.status_code == 200
        assert mock_search.call_args[1]["text_quality"] == "high"
