"""
Tests for the search API endpoints.

All search service calls are mocked — tests focus on request validation,
response format, and authentication.
"""

from unittest.mock import MagicMock, patch

from django.contrib.auth import get_user_model
from django.test import TestCase
from rest_framework.test import APIClient

from library.services.fusion import ImageSearchResult, SearchCandidate
from library.services.search import SearchResponse

User = get_user_model()


class SearchAPIAuthTest(TestCase):
    """Tests for search API authentication."""

    def setUp(self):
        self.client = APIClient()

    def test_search_requires_auth(self):
        """POST /api/v1/library/search/ requires authentication."""
        response = self.client.post(
            "/library/api/search/",
            {"query": "test"},
            format="json",
        )
        # Should be 403 or 401 (not authenticated)
        self.assertIn(response.status_code, [401, 403])


class SearchAPIValidationTest(TestCase):
    """Tests for search API request validation."""

    def setUp(self):
        self.user = User.objects.create_user(
            username="testuser", password="testpass123"
        )
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    def test_missing_query_returns_400(self):
        """Request without query field returns 400."""
        response = self.client.post(
            "/library/api/search/",
            {},
            format="json",
        )
        self.assertEqual(response.status_code, 400)

    def test_empty_query_returns_400(self):
        """Empty query string returns 400."""
        response = self.client.post(
            "/library/api/search/",
            {"query": ""},
            format="json",
        )
        self.assertEqual(response.status_code, 400)

    def test_invalid_library_type_returns_400(self):
        """Invalid library_type returns 400."""
        response = self.client.post(
            "/library/api/search/",
            {"query": "test", "library_type": "invalid_type"},
            format="json",
        )
        self.assertEqual(response.status_code, 400)

    def test_invalid_search_type_returns_400(self):
        """Invalid search type returns 400."""
        response = self.client.post(
            "/library/api/search/",
            {"query": "test", "search_types": ["invalid"]},
            format="json",
        )
        self.assertEqual(response.status_code, 400)

    def test_limit_above_max_returns_400(self):
        """Limit > 100 returns 400."""
        response = self.client.post(
            "/library/api/search/",
            {"query": "test", "limit": 200},
            format="json",
        )
        self.assertEqual(response.status_code, 400)


class SearchAPIResponseTest(TestCase):
    """Tests for search API response format."""

    def setUp(self):
        self.user = User.objects.create_user(
            username="testuser", password="testpass123"
        )
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    @patch("library.services.search.SearchService")
    def test_successful_search_response_format(self, MockService):
        """Successful search returns expected JSON structure."""
        mock_response = SearchResponse(
            query="neural networks",
            candidates=[
                SearchCandidate(
                    chunk_uid="c1", item_uid="i1", item_title="Deep Learning",
                    library_type="technical", text_preview="Neural networks are...",
                    chunk_s3_key="s3/chunk.txt", chunk_index=0,
                    score=0.95, source="vector+fulltext",
                )
            ],
            images=[
                ImageSearchResult(
                    image_uid="img1", item_uid="i1", item_title="Deep Learning",
                    image_type="diagram", description="Neural network architecture",
                    s3_key="s3/img.png", score=0.8, source="vector",
                )
            ],
            total_candidates=42,
            search_time_ms=156.7,
            reranker_used=True,
            reranker_model="qwen3-vl-reranker-2b",
            search_types_used=["vector", "fulltext"],
        )

        mock_instance = MockService.return_value
        mock_instance.search.return_value = mock_response

        response = self.client.post(
            "/library/api/search/",
            {"query": "neural networks"},
            format="json",
        )

        self.assertEqual(response.status_code, 200)
        data = response.json()

        # Verify top-level fields
        self.assertEqual(data["query"], "neural networks")
        self.assertEqual(data["total_candidates"], 42)
        self.assertTrue(data["reranker_used"])
        self.assertEqual(data["reranker_model"], "qwen3-vl-reranker-2b")
        self.assertIn("vector", data["search_types_used"])

        # Verify candidate structure
        self.assertEqual(len(data["candidates"]), 1)
        candidate = data["candidates"][0]
        self.assertEqual(candidate["chunk_uid"], "c1")
        self.assertEqual(candidate["item_title"], "Deep Learning")
        self.assertAlmostEqual(candidate["score"], 0.95)

        # Verify image structure
        self.assertEqual(len(data["images"]), 1)
        image = data["images"][0]
        self.assertEqual(image["image_uid"], "img1")
        self.assertEqual(image["image_type"], "diagram")

    @patch("library.services.search.SearchService")
    def test_vector_only_endpoint(self, MockService):
        """Vector-only endpoint sets correct search types."""
        mock_response = SearchResponse(
            query="test", candidates=[], images=[],
            total_candidates=0, search_time_ms=10,
            reranker_used=False, reranker_model=None,
            search_types_used=[],
        )
        mock_instance = MockService.return_value
        mock_instance.search.return_value = mock_response

        response = self.client.post(
            "/library/api/search/vector/",
            {"query": "test"},
            format="json",
        )

        self.assertEqual(response.status_code, 200)

        # Verify search was called with vector only
        call_args = mock_instance.search.call_args[0][0]
        self.assertEqual(call_args.search_types, ["vector"])
        self.assertFalse(call_args.rerank)

    @patch("library.services.search.SearchService")
    def test_fulltext_only_endpoint(self, MockService):
        """Fulltext-only endpoint sets correct search types."""
        mock_response = SearchResponse(
            query="test", candidates=[], images=[],
            total_candidates=0, search_time_ms=10,
            reranker_used=False, reranker_model=None,
            search_types_used=[],
        )
        mock_instance = MockService.return_value
        mock_instance.search.return_value = mock_response

        response = self.client.post(
            "/library/api/search/fulltext/",
            {"query": "test"},
            format="json",
        )

        self.assertEqual(response.status_code, 200)

        call_args = mock_instance.search.call_args[0][0]
        self.assertEqual(call_args.search_types, ["fulltext"])
        self.assertFalse(call_args.rerank)

    @patch("library.services.search.SearchService")
    def test_reranker_skip_reason_surfaced_in_json(self, MockService):
        """``reranker_skip_reason`` propagates through the JSON API."""
        mock_response = SearchResponse(
            query="neural networks",
            candidates=[],
            images=[],
            total_candidates=0,
            search_time_ms=10.0,
            reranker_used=False,
            reranker_model=None,
            search_types_used=[],
            reranker_skip_reason=(
                "api_error: 404 Client Error: Not Found for url: "
                "http://pan.helu.ca:8400/v1/v1/rerank"
            ),
        )
        mock_instance = MockService.return_value
        mock_instance.search.return_value = mock_response

        response = self.client.post(
            "/library/api/search/",
            {"query": "neural networks"},
            format="json",
        )

        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertFalse(data["reranker_used"])
        self.assertIn("reranker_skip_reason", data)
        self.assertTrue(
            data["reranker_skip_reason"].startswith("api_error:"),
            f"got {data['reranker_skip_reason']!r}",
        )


class ConceptAPITest(TestCase):
    """Tests for concept API endpoints."""

    def setUp(self):
        self.user = User.objects.create_user(
            username="testuser", password="testpass123"
        )
        self.client = APIClient()
        self.client.force_authenticate(user=self.user)

    def test_concept_list_requires_auth(self):
        """GET /api/v1/library/concepts/ requires authentication."""
        client = APIClient()  # Unauthenticated
        response = client.get("/library/api/concepts/")
        self.assertIn(response.status_code, [401, 403])
