"""Tests for TAB Affiliates API client."""

import httpx
import pytest
import respx

from packages.tab_client.client import (
    TABClient,
    TABClientError,
    TABRateLimitError,
)
from packages.tab_client.mock_client import MockTABClient


class TestTABClient:
    """Tests for TABClient class."""

    @pytest.mark.asyncio
    async def test_client_initialization(self):
        """Test TAB client can be initialized with default settings."""
        async with TABClient() as client:
            assert client.tab_config.base_url == "https://api.tab.co.nz/affiliates/v1"
            assert client.tab_config.timeout == 30.0
            assert client.tab_config.max_retries == 3

    @pytest.mark.asyncio
    async def test_client_reads_from_settings(self):
        """Test TAB client reads configuration from settings."""
        async with TABClient() as client:
            # Client should have settings from environment/config
            assert client.settings is not None
            assert client.tab_config is not None
            assert hasattr(client.tab_config, "base_url")
            assert hasattr(client.tab_config, "timeout")
            assert hasattr(client.tab_config, "max_retries")

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_meetings_success(self):
        """Test successful meetings retrieval."""
        mock_response = {
            "meetings": [
                {
                    "meeting": "MEET123",
                    "name": "Cambridge",
                    "date": "2024-01-15",
                    "category": "H",
                    "country": "NZ",
                }
            ]
        }

        respx.get("https://api.tab.co.nz/affiliates/v1/racing/meetings").mock(
            return_value=httpx.Response(200, json=mock_response)
        )

        async with TABClient() as client:
            meetings = await client.get_meetings("2024-01-15", "2024-01-15")

        assert len(meetings) == 1
        assert meetings[0]["meeting"] == "MEET123"
        assert meetings[0]["name"] == "Cambridge"

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_meetings_with_filters(self):
        """Test meetings retrieval with category and country filters."""
        mock_response = {"meetings": []}

        route = respx.get(
            "https://api.tab.co.nz/affiliates/v1/racing/meetings",
            params={
                "date_from": "2024-01-15",
                "date_to": "2024-01-15",
                "category": "T",
                "country": "NZ",
            },
        ).mock(return_value=httpx.Response(200, json=mock_response))

        async with TABClient() as client:
            await client.get_meetings(
                "2024-01-15", "2024-01-15", category="T", country="NZ"
            )

        assert route.called

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_meeting_success(self):
        """Test successful single meeting retrieval."""
        # TAB API wraps single meeting in meetings array
        mock_response = {
            "meetings": [
                {
                    "meeting": "MEET123",
                    "name": "Cambridge",
                    "date": "2024-01-15",
                    "category": "H",
                }
            ]
        }

        respx.get("https://api.tab.co.nz/affiliates/v1/racing/meetings/MEET123").mock(
            return_value=httpx.Response(200, json=mock_response)
        )

        async with TABClient() as client:
            meeting = await client.get_meeting("MEET123")

        assert meeting["meeting"] == "MEET123"
        assert meeting["name"] == "Cambridge"

    @pytest.mark.asyncio
    @respx.mock
    async def test_get_event_success(self):
        """Test successful event retrieval."""
        # TAB API wraps event data in "data" key, which client unwraps
        mock_response = {
            "data": {
                "race": {"event_id": "EVT456", "race_number": 1, "distance": 2000},
                "runners": [],
                "results": [],
            }
        }

        respx.get("https://api.tab.co.nz/affiliates/v1/racing/events/EVT456").mock(
            return_value=httpx.Response(200, json=mock_response)
        )

        async with TABClient() as client:
            event = await client.get_event("EVT456")

        # Client unwraps the "data" key, so we get race/runners/results directly
        assert event["race"]["event_id"] == "EVT456"
        assert event["race"]["race_number"] == 1

    @pytest.mark.asyncio
    @respx.mock
    async def test_retry_on_server_error(self):
        """Test client retries on 500 server errors."""
        # First request fails with 500, second succeeds
        respx.get("https://api.tab.co.nz/affiliates/v1/racing/meetings").mock(
            side_effect=[
                httpx.Response(500, json={"error": "Server error"}),
                httpx.Response(200, json={"meetings": []}),
            ]
        )

        async with TABClient() as client:
            # Client will retry automatically based on settings (default max_retries=3)
            meetings = await client.get_meetings("2024-01-15", "2024-01-15")

        assert meetings == []

    @pytest.mark.asyncio
    @respx.mock
    async def test_rate_limit_handling(self):
        """Test client handles 429 rate limit responses."""
        respx.get("https://api.tab.co.nz/affiliates/v1/racing/meetings").mock(
            return_value=httpx.Response(429, json={"error": "Rate limit exceeded"})
        )

        async with TABClient() as client:
            with pytest.raises(TABRateLimitError):
                await client.get_meetings("2024-01-15", "2024-01-15")

    @pytest.mark.asyncio
    @respx.mock
    async def test_client_error_on_400(self):
        """Test client raises TABClientError on 400 bad request."""
        respx.get("https://api.tab.co.nz/affiliates/v1/racing/meetings").mock(
            return_value=httpx.Response(400, json={"error": "Bad request"})
        )

        async with TABClient() as client:
            with pytest.raises(TABClientError, match="Bad request"):
                await client.get_meetings("2024-01-15", "2024-01-15")

    @pytest.mark.asyncio
    @respx.mock
    async def test_network_timeout(self):
        """Test client handles network timeouts."""
        respx.get("https://api.tab.co.nz/affiliates/v1/racing/meetings").mock(
            side_effect=httpx.TimeoutException("Request timeout")
        )

        async with TABClient() as client:
            with pytest.raises(TABClientError, match="Request timeout"):
                await client.get_meetings("2024-01-15", "2024-01-15")

    @pytest.mark.asyncio
    async def test_context_manager(self):
        """Test TABClient works as async context manager."""
        client = TABClient()
        async with client:
            assert client._client is not None
            assert not client._client.is_closed

        # Client should be closed after context exit
        assert client._client is None  # Client sets to None after close


class TestMockTABClient:
    """Tests for MockTABClient class."""

    @pytest.mark.asyncio
    async def test_mock_client_initialization(self):
        """Test mock client can be initialized."""
        async with MockTABClient() as client:
            assert client is not None
            # Mock client doesn't expose base_url directly

    @pytest.mark.asyncio
    async def test_mock_get_meetings(self):
        """Test mock client returns meetings data."""
        async with MockTABClient() as client:
            meetings = await client.get_meetings("2024-12-26", "2024-12-26")

        assert len(meetings) >= 1
        assert "meeting" in meetings[0]
        assert "name" in meetings[0]
        assert "date" in meetings[0]
        assert "category" in meetings[0]

    @pytest.mark.asyncio
    async def test_mock_get_meetings_deterministic(self):
        """Test mock client returns same data for same date."""
        async with MockTABClient() as client:
            meetings1 = await client.get_meetings("2024-12-26", "2024-12-26")
            meetings2 = await client.get_meetings("2024-12-26", "2024-12-26")

        assert len(meetings1) == len(meetings2)
        assert meetings1[0]["meeting"] == meetings2[0]["meeting"]

    @pytest.mark.asyncio
    async def test_mock_get_meetings_different_dates(self):
        """Test mock client returns different data for different dates."""
        async with MockTABClient() as client:
            meetings1 = await client.get_meetings("2024-12-26", "2024-12-26")
            meetings2 = await client.get_meetings("2024-12-27", "2024-12-27")

        # Different dates should have different meeting IDs
        assert meetings1[0]["meeting"] != meetings2[0]["meeting"]

    @pytest.mark.asyncio
    async def test_mock_get_meetings_with_category_filter(self):
        """Test mock client respects category filter."""
        async with MockTABClient() as client:
            meetings = await client.get_meetings(
                "2024-12-26", "2024-12-26", category="T"
            )

        # All meetings should have category "T"
        for meeting in meetings:
            assert meeting["category"] == "T"

    @pytest.mark.asyncio
    async def test_mock_get_meeting(self):
        """Test mock client returns single meeting data."""
        async with MockTABClient() as client:
            # First get a meeting ID
            meetings = await client.get_meetings("2024-12-26", "2024-12-26")
            meeting_id = meetings[0]["meeting"]

            # Then fetch that specific meeting
            meeting = await client.get_meeting(meeting_id)

        assert meeting["meeting"] == meeting_id
        assert "name" in meeting
        assert "date" in meeting
        assert "races" in meeting  # TAB API uses "races" not "events"

    @pytest.mark.asyncio
    async def test_mock_get_meeting_has_races(self):
        """Test mock meeting has race IDs."""
        async with MockTABClient() as client:
            meetings = await client.get_meetings("2024-12-26", "2024-12-26")
            meeting = await client.get_meeting(meetings[0]["meeting"])

        assert len(meeting["races"]) >= 1
        assert "id" in meeting["races"][0]  # Race/event ID is in "id" field

    @pytest.mark.asyncio
    async def test_mock_get_event(self):
        """Test mock client returns event data with race, runners, and results."""
        async with MockTABClient() as client:
            # Get a meeting and its first race/event
            meetings = await client.get_meetings("2024-12-26", "2024-12-26")
            meeting = await client.get_meeting(meetings[0]["meeting"])
            event_id = meeting["races"][0]["id"]  # Get event ID from races array

            # Fetch the event
            event = await client.get_event(event_id)

        assert "race" in event
        assert "runners" in event
        assert "results" in event

        # Validate race structure
        race = event["race"]
        assert "event_id" in race
        assert race["event_id"] == event_id
        assert "race_number" in race
        assert "distance" in race
        assert "advertised_start_string" in race

    @pytest.mark.asyncio
    async def test_mock_event_has_runners(self):
        """Test mock event has runners with proper structure."""
        async with MockTABClient() as client:
            meetings = await client.get_meetings("2024-12-26", "2024-12-26")
            meeting = await client.get_meeting(meetings[0]["meeting"])
            event = await client.get_event(meeting["races"][0]["id"])

        runners = event["runners"]
        assert len(runners) >= 8  # Should have at least 8 runners

        # Check first runner structure (TAB API structure)
        runner = runners[0]
        assert "entrant_id" in runner
        assert "runner_number" in runner
        assert "barrier" in runner
        assert "name" in runner  # Horse name is directly on runner
        assert "horse_id" in runner  # Horse ID is directly on runner
        assert "jockey" in runner  # Driver/jockey name
        assert "trainer_name" in runner  # Trainer name directly on runner

    @pytest.mark.asyncio
    async def test_mock_event_has_results(self):
        """Test mock event has results matching runners."""
        async with MockTABClient() as client:
            meetings = await client.get_meetings("2024-12-26", "2024-12-26")
            meeting = await client.get_meeting(meetings[0]["meeting"])
            event = await client.get_event(meeting["races"][0]["id"])

        results = event["results"]
        runners = event["runners"]

        # Mock generates results for some runners (not all might finish)
        assert len(results) > 0
        assert len(results) <= len(runners)

        # Check result structure
        result = results[0]
        assert "entrant_id" in result
        assert "position" in result

        # Verify all entrant_ids in results are from runners
        runner_ids = {r["entrant_id"] for r in runners}
        result_ids = {r["entrant_id"] for r in results}
        assert result_ids.issubset(runner_ids)  # All result IDs must be in runners

    @pytest.mark.asyncio
    async def test_mock_id_generation_deterministic(self):
        """Test mock ID generation is deterministic."""
        client = MockTABClient()

        # Generate IDs with same seed multiple times
        id1 = client._generate_deterministic_id("test_seed")
        id2 = client._generate_deterministic_id("test_seed")

        assert id1 == id2

        # Different seeds should produce different IDs
        id3 = client._generate_deterministic_id("different_seed")
        assert id1 != id3

    @pytest.mark.asyncio
    async def test_mock_multiple_races_in_meeting(self):
        """Test mock meeting has multiple races."""
        async with MockTABClient() as client:
            meetings = await client.get_meetings("2024-12-26", "2024-12-26")
            meeting = await client.get_meeting(meetings[0]["meeting"])

        # Should have 8 races
        assert len(meeting["races"]) == 8

        # Verify race numbers are sequential
        race_numbers = [r["race_number"] for r in meeting["races"]]
        assert race_numbers == list(range(1, 9))
