"""Tests for indexing API methods."""

from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest
from pydantic import BaseModel

from railtown.engine.client import Railengine
from railtown.engine.indexing import search_index


@pytest.fixture
def mock_client(sample_pat, sample_engine_id):
    """Create a mock Railengine client."""
    with patch("railtown.engine.indexing.httpx.AsyncClient") as mock_client_class:
        mock_http_client = AsyncMock()
        mock_client_class.return_value = mock_http_client

        client = Railengine(pat=sample_pat, engine_id=sample_engine_id)
        client._client = mock_http_client
        yield client, mock_http_client


@pytest.mark.asyncio
async def test_search_index_success(mock_client):
    """Test searching index successfully."""
    client, mock_http_client = mock_client

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "value": [
            {"eventId": "1", "body": "apple pie"},
            {"eventId": "2", "body": "banana bread"},
        ],
    }
    mock_http_client.post = AsyncMock(return_value=mock_response)

    results = []
    async for item in search_index(
        client,
        engine_id=client.engine_id,
        query={"search": "apple"},
    ):
        results.append(item)

    assert len(results) == 2
    assert results[0]["eventId"] == "1"
    mock_http_client.post.assert_called_once()


@pytest.mark.asyncio
async def test_search_index_with_filter(mock_client):
    """Test searching index with filter function."""
    client, mock_http_client = mock_client

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "value": [
            {"eventId": "1", "body": "apple"},
            {"eventId": "2", "body": "banana"},
        ],
    }
    mock_http_client.post = AsyncMock(return_value=mock_response)

    def filter_fn(item):
        return "apple" in item.get("body", "")

    results = []
    async for item in search_index(
        client,
        engine_id=client.engine_id,
        query={"search": "fruit"},
        filter_fn=filter_fn,
    ):
        results.append(item)

    assert len(results) == 1
    assert results[0]["eventId"] == "1"


@pytest.mark.asyncio
async def test_search_index_empty_results(mock_client):
    """Test searching index with empty results."""
    client, mock_http_client = mock_client

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"value": []}
    mock_http_client.post = AsyncMock(return_value=mock_response)

    results = []
    async for item in search_index(
        client,
        engine_id=client.engine_id,
        query={"search": "nonexistent"},
    ):
        results.append(item)

    assert len(results) == 0


@pytest.mark.asyncio
async def test_search_index_handles_errors_gracefully(mock_client):
    """Test search_index handles errors gracefully."""
    client, mock_http_client = mock_client

    error = httpx.RequestError("Connection error")
    mock_http_client.post = AsyncMock(side_effect=error)

    results = []
    async for item in search_index(
        client,
        engine_id=client.engine_id,
        query={"search": "test"},
    ):
        results.append(item)

    # Should return empty iterable on error
    assert len(results) == 0


@pytest.mark.asyncio
async def test_search_index_handles_http_status_error(mock_client):
    """Test search_index handles HTTPStatusError (e.g. 500, 404)."""
    client, mock_http_client = mock_client

    error = httpx.HTTPStatusError("Server Error", request=MagicMock(), response=MagicMock())
    mock_http_client.post = AsyncMock(side_effect=error)

    results = [
        item
        async for item in search_index(
            client,
            engine_id=client.engine_id,
            query={"search": "test"},
        )
    ]

    assert len(results) == 0


@pytest.mark.asyncio
async def test_search_index_non_dict_response_returns_empty(mock_client):
    """Test that non-dict API response (e.g. list) yields empty results."""
    client, mock_http_client = mock_client

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = [{"eventId": "1"}]  # list, not dict
    mock_http_client.post = AsyncMock(return_value=mock_response)

    results = [
        item
        async for item in search_index(
            client,
            engine_id=client.engine_id,
            query={"search": "test"},
        )
    ]

    assert len(results) == 0


@pytest.mark.asyncio
async def test_search_index_with_model_deserialization(mock_client):
    """Test search_index deserializes to Pydantic model when model provided."""
    client, mock_http_client = mock_client

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "value": [
            {"content": '{"food_name": "Apple", "calories": 95}'},
        ],
    }
    mock_http_client.post = AsyncMock(return_value=mock_response)

    class FoodItem(BaseModel):
        food_name: str
        calories: int

    results = [
        item
        async for item in search_index(
            client,
            engine_id=client.engine_id,
            query={"search": "apple"},
            model=FoodItem,
        )
    ]

    assert len(results) == 1
    assert isinstance(results[0], FoodItem)
    assert results[0].food_name == "Apple"
    assert results[0].calories == 95


@pytest.mark.asyncio
async def test_search_index_sends_correct_payload(mock_client, sample_engine_id):
    """Test search_index sends correct endpoint and JSON body."""
    client, mock_http_client = mock_client

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"value": []}
    mock_http_client.post = AsyncMock(return_value=mock_response)

    async for _ in search_index(
        client,
        engine_id=sample_engine_id,
        query={"search": "banana", "filter": "calories gt 50"},
    ):
        pass

    call_args = mock_http_client.post.call_args
    assert "/api/Engine/Indexing/Search" in call_args[0][0]
    assert call_args.kwargs["json"] == {
        "EngineId": sample_engine_id,
        "Query": {"search": "banana", "filter": "calories gt 50"},
    }


@pytest.mark.asyncio
async def test_search_index_uses_explicit_engine_id(mock_client):
    """Test that explicit engine_id overrides client default."""
    client, mock_http_client = mock_client

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"value": []}
    mock_http_client.post = AsyncMock(return_value=mock_response)

    custom_engine_id = "custom-engine-456"
    async for _ in search_index(
        client,
        engine_id=custom_engine_id,
        query={"search": "test"},
    ):
        pass

    call_args = mock_http_client.post.call_args
    assert custom_engine_id in call_args.kwargs["json"]["EngineId"]


@pytest.mark.asyncio
async def test_search_index_query_none_sends_empty_dict(mock_client):
    """Test that query=None sends empty dict in request body."""
    client, mock_http_client = mock_client

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {"value": []}
    mock_http_client.post = AsyncMock(return_value=mock_response)

    async for _ in search_index(
        client,
        engine_id=client.engine_id,
        query=None,
    ):
        pass

    call_args = mock_http_client.post.call_args
    assert call_args.kwargs["json"]["Query"] == {}
