"""
Unit tests for PD API client with mocked HTTP responses.

Tests the PdApiClient class that fetches position ratings from the PD API,
using mocked httpx responses to avoid external dependencies.
"""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
import httpx
from app.services.pd_api_client import PdApiClient


@pytest.fixture
def api_client():
    """Create PdApiClient instance for testing"""
    return PdApiClient(base_url="https://test.api.com")


@pytest.fixture
def mock_position_data():
    """Sample position rating data from API"""
    return {
        "position": "SS",
        "innings": 1452,
        "range": 4,
        "error": 12,
        "arm": 3,
        "pb": None,
        "overthrow": 2
    }


@pytest.fixture
def mock_multiple_positions():
    """Multiple position ratings for a player"""
    return [
        {
            "position": "2B",
            "innings": 800,
            "range": 3,
            "error": 8,
            "arm": 2,
            "pb": None,
            "overthrow": 1
        },
        {
            "position": "SS",
            "innings": 600,
            "range": 4,
            "error": 12,
            "arm": 3,
            "pb": None,
            "overthrow": 2
        },
        {
            "position": "3B",
            "innings": 150,
            "range": 3,
            "error": 15,
            "arm": 2,
            "pb": None,
            "overthrow": 1
        }
    ]


def setup_mock_http_client(mock_client_class, response_data=None, exception=None):
    """
    Helper to properly setup httpx.AsyncClient mock with async context manager.

    Args:
        mock_client_class: The mocked httpx.AsyncClient class
        response_data: Data to return from response.json(), or None
        exception: Exception to raise from client.get(), or None

    Returns:
        mock_client: The mock client instance for additional assertions
    """
    if exception:
        # Setup client that raises exception
        mock_client = MagicMock()
        mock_client.get = AsyncMock(side_effect=exception)
    else:
        # Setup successful response
        mock_response = MagicMock()
        mock_response.json.return_value = response_data or []
        mock_response.raise_for_status = MagicMock()

        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

    # Setup async context manager protocol
    mock_client_instance = MagicMock()
    mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
    mock_client_instance.__aexit__ = AsyncMock(return_value=None)

    mock_client_class.return_value = mock_client_instance

    return mock_client


class TestPdApiClientInitialization:
    """Tests for PdApiClient initialization"""

    def test_init_with_default_url(self):
        """Test initialization with default production URL"""
        client = PdApiClient()
        assert client.base_url == "https://pd.manticorum.com"
        assert client.timeout.connect == 5.0
        assert client.timeout.read == 10.0

    def test_init_with_custom_url(self):
        """Test initialization with custom base URL"""
        client = PdApiClient(base_url="https://custom.api.com")
        assert client.base_url == "https://custom.api.com"


class TestGetPositionRatingsSuccess:
    """Tests for successful position rating retrieval"""

    @pytest.mark.asyncio
    @patch('app.services.pd_api_client.httpx.AsyncClient')
    async def test_get_single_position(self, mock_client_class, api_client, mock_position_data):
        """Test fetching single position rating"""
        # Setup mock response
        mock_response = MagicMock()
        mock_response.json.return_value = [mock_position_data]
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        ratings = await api_client.get_position_ratings(8807)

        # Verify
        assert len(ratings) == 1
        assert ratings[0].position == "SS"
        assert ratings[0].range == 4
        assert ratings[0].error == 12
        assert ratings[0].innings == 1452

        # Verify API call
        mock_client.get.assert_called_once()
        call_args = mock_client.get.call_args
        assert "player_id" in call_args[1]['params']
        assert call_args[1]['params']['player_id'] == 8807

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_get_multiple_positions(self, mock_client_class, api_client, mock_multiple_positions):
        """Test fetching multiple position ratings"""
        # Setup mock response
        mock_response = MagicMock()
        mock_response.json.return_value = mock_multiple_positions
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        ratings = await api_client.get_position_ratings(8807)

        # Verify
        assert len(ratings) == 3
        assert ratings[0].position == "2B"
        assert ratings[1].position == "SS"
        assert ratings[2].position == "3B"
        assert all(r.range in range(1, 6) for r in ratings)

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_get_positions_with_filter(self, mock_client_class, api_client, mock_multiple_positions):
        """Test fetching positions with filter parameter"""
        # Setup mock response
        mock_response = MagicMock()
        mock_response.json.return_value = mock_multiple_positions[:2]  # Return filtered results
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        ratings = await api_client.get_position_ratings(8807, positions=['SS', '2B'])

        # Verify
        assert len(ratings) == 2

        # Verify filter was passed to API
        mock_client.get.assert_called_once()
        call_args = mock_client.get.call_args
        assert 'position' in call_args[1]['params']
        assert call_args[1]['params']['position'] == ['SS', '2B']

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_get_positions_wrapped_in_positions_key(self, mock_client_class, api_client, mock_multiple_positions):
        """Test handling API response wrapped in 'positions' key"""
        # Setup mock response - API returns dict with 'positions' key
        mock_response = MagicMock()
        mock_response.json.return_value = {'positions': mock_multiple_positions}
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        ratings = await api_client.get_position_ratings(8807)

        # Verify - should handle both response formats
        assert len(ratings) == 3

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_get_empty_positions_list(self, mock_client_class, api_client):
        """Test fetching positions when player has none (empty list)"""
        # Setup mock response
        mock_response = MagicMock()
        mock_response.json.return_value = []
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        ratings = await api_client.get_position_ratings(9999)

        # Verify
        assert len(ratings) == 0


class TestGetPositionRatingsErrors:
    """Tests for error handling in position rating retrieval"""

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_http_404_error(self, mock_client_class, api_client):
        """Test handling 404 Not Found error"""
        # Setup mock response to raise 404
        mock_response = MagicMock()
        mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
            "404 Not Found",
            request=MagicMock(),
            response=MagicMock(status_code=404)
        )

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute and verify exception
        with pytest.raises(httpx.HTTPStatusError):
            await api_client.get_position_ratings(9999)

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_http_500_error(self, mock_client_class, api_client):
        """Test handling 500 Internal Server Error"""
        # Setup mock response to raise 500
        mock_response = MagicMock()
        mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
            "500 Internal Server Error",
            request=MagicMock(),
            response=MagicMock(status_code=500)
        )

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute and verify exception
        with pytest.raises(httpx.HTTPStatusError):
            await api_client.get_position_ratings(8807)

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_timeout_error(self, mock_client_class, api_client):
        """Test handling timeout"""
        # Setup mock client to raise timeout
        mock_client = MagicMock()
        mock_client.get = AsyncMock(side_effect=httpx.TimeoutException("Request timeout"))

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute and verify exception
        with pytest.raises(httpx.TimeoutException):
            await api_client.get_position_ratings(8807)

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_connection_error(self, mock_client_class, api_client):
        """Test handling connection error"""
        # Setup mock client to raise connection error
        mock_client = MagicMock()
        mock_client.get = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute and verify exception
        with pytest.raises(httpx.ConnectError):
            await api_client.get_position_ratings(8807)

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_malformed_json_response(self, mock_client_class, api_client):
        """Test handling malformed JSON in response"""
        # Setup mock response to raise JSON decode error
        mock_response = MagicMock()
        mock_response.json.side_effect = ValueError("Invalid JSON")
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute and verify exception
        with pytest.raises(Exception):  # Will raise ValueError
            await api_client.get_position_ratings(8807)


class TestAPIRequestConstruction:
    """Tests for proper API request construction"""

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_correct_url_construction(self, mock_client_class, api_client, mock_position_data):
        """Test that correct URL is constructed"""
        # Setup mock response
        mock_response = MagicMock()
        mock_response.json.return_value = [mock_position_data]
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        await api_client.get_position_ratings(8807)

        # Verify URL
        mock_client.get.assert_called_once()
        call_args = mock_client.get.call_args
        assert call_args[0][0] == "https://test.api.com/api/v2/cardpositions"

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_timeout_configuration(self, mock_client_class, api_client, mock_position_data):
        """Test that timeout is configured correctly"""
        # Setup mock response
        mock_response = MagicMock()
        mock_response.json.return_value = [mock_position_data]
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        await api_client.get_position_ratings(8807)

        # Verify timeout was passed to AsyncClient
        mock_client_class.assert_called_once()
        call_kwargs = mock_client_class.call_args[1]
        assert 'timeout' in call_kwargs
        assert call_kwargs['timeout'] == api_client.timeout


class TestPositionRatingModelParsing:
    """Tests for parsing API response into PositionRating models"""

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_all_fields_parsed(self, mock_client_class, api_client):
        """Test that all PositionRating fields are parsed correctly"""
        # Setup mock with all fields
        full_data = {
            "position": "C",
            "innings": 1200,
            "range": 2,
            "error": 5,
            "arm": 1,
            "pb": 3,
            "overthrow": 1
        }
        mock_response = MagicMock()
        mock_response.json.return_value = [full_data]
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        ratings = await api_client.get_position_ratings(8807)

        # Verify all fields
        assert len(ratings) == 1
        rating = ratings[0]
        assert rating.position == "C"
        assert rating.innings == 1200
        assert rating.range == 2
        assert rating.error == 5
        assert rating.arm == 1
        assert rating.pb == 3
        assert rating.overthrow == 1

    @pytest.mark.asyncio
    @patch('httpx.AsyncClient')
    async def test_optional_fields_none(self, mock_client_class, api_client):
        """Test that optional fields can be None"""
        # Setup mock with minimal fields
        minimal_data = {
            "position": "LF",
            "innings": 800,
            "range": 3,
            "error": 10,
            "arm": None,
            "pb": None,
            "overthrow": None
        }
        mock_response = MagicMock()
        mock_response.json.return_value = [minimal_data]
        mock_response.raise_for_status = MagicMock()

        # Setup mock client
        mock_client = MagicMock()
        mock_client.get = AsyncMock(return_value=mock_response)

        # Setup async context manager
        mock_client_instance = MagicMock()
        mock_client_instance.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client_instance.__aexit__ = AsyncMock(return_value=None)

        mock_client_class.return_value = mock_client_instance

        # Execute
        ratings = await api_client.get_position_ratings(8807)

        # Verify
        assert len(ratings) == 1
        rating = ratings[0]
        assert rating.position == "LF"
        assert rating.arm is None
        assert rating.pb is None
        assert rating.overthrow is None
