"""Tests for retrieval client."""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from pydantic import BaseModel

from railtown.engine.client import RailEngine
from railtown.engine.exceptions import RailtownBadRequestError


class FoodDiaryItem(BaseModel):
    """Test Pydantic model."""

    food_name: str
    calories: int


@pytest.fixture
def mock_httpx_client():
    """Mock httpx.AsyncClient."""
    with patch("railtown.engine.client.httpx.AsyncClient") as mock_client_class:
        mock_client = AsyncMock()
        mock_client_class.return_value = mock_client
        # Yield both class (for assertions) and instance (for method setup)
        yield type(
            "MockClient",
            (),
            {
                "class": mock_client_class,
                "instance": mock_client,
                "post": mock_client.post,
                "get": mock_client.get,
                "aclose": mock_client.aclose,
                "assert_called_once": mock_client_class.assert_called_once,
            },
        )()


@pytest.mark.asyncio
async def test_client_initialization_with_params(sample_pat, sample_engine_id, mock_httpx_client):
    """Test client initialization with parameters."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id)

    assert client.pat == sample_pat
    assert client.engine_id == sample_engine_id
    assert client.api_url == "https://cndr.railtown.ai"  # Normalized (no /api)
    mock_httpx_client.assert_called_once()


@pytest.mark.asyncio
async def test_client_initialization_with_env(
    monkeypatch, sample_pat, sample_engine_id, mock_httpx_client
):
    """Test client initialization with environment variables."""
    monkeypatch.setenv("ENGINE_PAT", sample_pat)
    monkeypatch.setenv("ENGINE_ID", sample_engine_id)

    client = RailEngine()

    assert client.pat == sample_pat
    assert client.engine_id == sample_engine_id


@pytest.mark.asyncio
async def test_client_initialization_missing_pat(mock_httpx_client):
    """Test client initialization without PAT."""
    with pytest.raises(RailtownBadRequestError) as exc_info:
        RailEngine(engine_id="test-engine-id")

    assert "PAT is required" in str(exc_info.value)


@pytest.mark.asyncio
async def test_client_initialization_missing_engine_id(monkeypatch, sample_pat, mock_httpx_client):
    """Test client initialization without engine ID."""
    monkeypatch.delenv("ENGINE_ID", raising=False)

    with pytest.raises(RailtownBadRequestError) as exc_info:
        RailEngine(pat=sample_pat)

    assert "engine_id is required" in str(exc_info.value)


@pytest.mark.asyncio
async def test_client_initialization_with_model(sample_pat, sample_engine_id, mock_httpx_client):
    """Test client initialization with Pydantic model."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id, model=FoodDiaryItem)
    assert client.model == FoodDiaryItem


@pytest.mark.asyncio
async def test_client_initialization_with_custom_api_url(
    sample_pat, sample_engine_id, mock_httpx_client
):
    """Test client initialization with custom API URL."""
    custom_url = "https://custom.api.url/api"
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id, api_url=custom_url)

    assert client.api_url == "https://custom.api.url"  # Normalized


@pytest.mark.asyncio
async def test_client_get_headers(sample_pat, sample_engine_id, mock_httpx_client):
    """Test _get_headers method."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id)

    headers = client._get_headers()

    assert "Authorization" in headers
    assert headers["Authorization"] == sample_pat
    assert "Content-Type" in headers
    assert "charset=utf-8" in headers["Content-Type"]


@pytest.mark.asyncio
async def test_client_context_manager(sample_pat, sample_engine_id, mock_httpx_client):
    """Test client as async context manager."""
    async with RailEngine(pat=sample_pat, engine_id=sample_engine_id) as client:
        assert client.engine_id == sample_engine_id

    # Client should be closed after context exit
    mock_httpx_client.instance.aclose.assert_called_once()


@pytest.mark.asyncio
async def test_client_close(sample_pat, sample_engine_id, mock_httpx_client):
    """Test client close method."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id)
    await client.close()

    mock_httpx_client.instance.aclose.assert_called_once()


@pytest.mark.asyncio
async def test_get_storage_document_by_event_id_success(
    sample_pat, sample_engine_id, mock_httpx_client
):
    """Test getting storage document by EventId successfully."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id)

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "EngineDocumentId": "doc-123",
        "EventId": "event-123",
        "Content": '{"food_name": "Apple"}',
    }
    mock_httpx_client.instance.get = AsyncMock(return_value=mock_response)

    result = await client.get_storage_document_by_event_id(event_id="event-123")

    assert result is not None
    assert result["EventId"] == "event-123"
    mock_httpx_client.instance.get.assert_called_once()
    # Verify EventId parameter was used
    call_args = mock_httpx_client.instance.get.call_args
    assert "EventId" in call_args.kwargs["params"]
    assert call_args.kwargs["params"]["EventId"] == "event-123"


@pytest.mark.asyncio
async def test_get_storage_document_by_event_id_not_found(
    sample_pat, sample_engine_id, mock_httpx_client
):
    """Test getting storage document by EventId when not found."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id)

    mock_response = MagicMock()
    mock_response.status_code = 404
    mock_httpx_client.instance.get = AsyncMock(return_value=mock_response)

    result = await client.get_storage_document_by_event_id(event_id="event-123")

    assert result is None


@pytest.mark.asyncio
async def test_get_storage_document_by_event_id_with_filter(
    sample_pat, sample_engine_id, mock_httpx_client
):
    """Test getting storage document by EventId with filter."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id)

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "EngineDocumentId": "doc-123",
        "EventId": "event-123",
        "Content": '{"food_name": "Apple", "calories": 95}',
    }
    mock_httpx_client.instance.get = AsyncMock(return_value=mock_response)

    def filter_fn(doc):
        return doc.get("Content", "").find("Apple") != -1

    result = await client.get_storage_document_by_event_id(
        event_id="event-123",
        filter_fn=filter_fn,
    )

    assert result is not None


@pytest.mark.asyncio
async def test_get_storage_document_by_event_id_with_model(
    sample_pat, sample_engine_id, mock_httpx_client
):
    """Test getting storage document by EventId with model override."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id)

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "EngineDocumentId": "doc-123",
        "EventId": "event-123",
        "food_name": "Apple",
        "calories": 95,
    }
    mock_httpx_client.instance.get = AsyncMock(return_value=mock_response)

    result = await client.get_storage_document_by_event_id(
        event_id="event-123",
        model=FoodDiaryItem,
    )

    assert result is not None
    assert isinstance(result, FoodDiaryItem)
    assert result.food_name == "Apple"
    assert result.calories == 95


@pytest.mark.asyncio
async def test_get_storage_document_by_event_id_filter_rejects(
    sample_pat, sample_engine_id, mock_httpx_client
):
    """Test get_storage_document_by_event_id with filter function that returns False."""
    client = RailEngine(pat=sample_pat, engine_id=sample_engine_id)

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.json.return_value = {
        "EngineDocumentId": "doc-123",
        "EventId": "event-123",
        "Content": '{"food_name": "Apple", "calories": 95}',
    }
    mock_httpx_client.instance.get = AsyncMock(return_value=mock_response)

    def filter_fn(doc):
        # Filter out documents with calories < 100
        return doc.get("Content", "").find('"calories": 200') != -1

    result = await client.get_storage_document_by_event_id(
        event_id="event-123",
        filter_fn=filter_fn,
    )

    # Filter should reject this document (calories is 95, not 200)
    assert result is None
