"""Integration tests for TMDB API resilience and error handling.

This module tests TMDB API resilience scenarios including unavailability,
partial/invalid responses, network timeouts, and fallback behavior.
"""
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

import aiohttp
import pytest

from src.core.services.tmdb_client import TMDBAPIError, TMDBClient


def _make_ctx(response):
    """Create an async context manager mock wrapping a response."""
    ctx = AsyncMock()
    ctx.__aenter__.return_value = response
    ctx.__aexit__.return_value = None
    return ctx


def _make_session():
    """Create a properly configured mock session for TMDB tests.

    Returns a MagicMock (not AsyncMock) so that session.get() returns
    a value directly instead of a coroutine, which is needed because
    the real aiohttp session.get() returns a context manager, not a
    coroutine.
    """
    session = MagicMock()
    session.closed = False
    session.close = AsyncMock()
    return session


class TestTMDBAPIUnavailability:
    """Test handling of TMDB API unavailability."""

    @pytest.mark.asyncio
    async def test_503_service_unavailable(self):
        """Test handling of 503 Service Unavailable response."""
        client = TMDBClient(api_key="test_key")

        mock_response = AsyncMock()
        mock_response.status = 503
        mock_response.raise_for_status = MagicMock(
            side_effect=aiohttp.ClientResponseError(
                request_info=MagicMock(),
                history=(),
                status=503,
                message="Service Unavailable",
            )
        )

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response)
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError):
                await client._request("tv/123", max_retries=2)

        await client.close()

    @pytest.mark.asyncio
    async def test_connection_refused_error(self):
        """Test handling of connection refused error."""
        client = TMDBClient(api_key="test_key")

        session = _make_session()
        session.get.side_effect = aiohttp.ClientConnectorError(
            connection_key=MagicMock(),
            os_error=ConnectionRefusedError("Connection refused"),
        )
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError):
                await client._request("tv/123", max_retries=2)

        await client.close()

    @pytest.mark.asyncio
    async def test_dns_resolution_failure(self):
        """Test handling of DNS resolution failure."""
        client = TMDBClient(api_key="test_key")

        session = _make_session()
        session.get.side_effect = aiohttp.ClientConnectorError(
            connection_key=MagicMock(),
            os_error=OSError("Name or service not known"),
        )
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError):
                await client._request(
                    "search/tv", {"query": "test"}, max_retries=2
                )

        await client.close()


class TestTMDBPartialDataResponse:
    """Test handling of partial or incomplete data responses."""

    @pytest.mark.asyncio
    async def test_missing_required_fields(self):
        """Test handling of response missing required fields."""
        client = TMDBClient(api_key="test_key")

        incomplete_data = {"page": 1, "total_pages": 0}

        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json = AsyncMock(return_value=incomplete_data)
        mock_response.raise_for_status = MagicMock()

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response)
        client.session = session

        result = await client.search_tv_show("test query")
        assert "page" in result
        assert "results" not in result

        await client.close()

    @pytest.mark.asyncio
    async def test_empty_results_list(self):
        """Test handling of search with no results."""
        client = TMDBClient(api_key="test_key")

        empty_results = {
            "page": 1,
            "results": [],
            "total_pages": 0,
            "total_results": 0,
        }

        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json = AsyncMock(return_value=empty_results)
        mock_response.raise_for_status = MagicMock()

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response)
        client.session = session

        result = await client.search_tv_show("nonexistent show 12345")
        assert result["results"] == []
        assert result["total_results"] == 0

        await client.close()

    @pytest.mark.asyncio
    async def test_null_values_in_response(self):
        """Test handling of null values in response data."""
        client = TMDBClient(api_key="test_key")

        data_with_nulls = {
            "id": 123,
            "name": "Test Show",
            "overview": None,
            "poster_path": None,
            "backdrop_path": None,
            "first_air_date": None,
        }

        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json = AsyncMock(return_value=data_with_nulls)
        mock_response.raise_for_status = MagicMock()

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response)
        client.session = session

        result = await client.get_tv_show_details(123)
        assert result["id"] == 123
        assert result["overview"] is None
        assert result["poster_path"] is None

        await client.close()


class TestTMDBInvalidResponseFormat:
    """Test handling of invalid response formats."""

    @pytest.mark.asyncio
    async def test_malformed_json_response(self):
        """Test handling of malformed JSON response."""
        client = TMDBClient(api_key="test_key")

        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json.side_effect = aiohttp.ContentTypeError(
            request_info=MagicMock(),
            history=(),
            message="Invalid JSON",
        )
        mock_response.raise_for_status = MagicMock()

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response)
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError):
                await client._request("tv/123", max_retries=2)

        await client.close()

    @pytest.mark.asyncio
    async def test_non_dict_json_response(self):
        """Test handling of JSON response that isn't a dictionary."""
        client = TMDBClient(api_key="test_key")

        invalid_structure = ["unexpected", "list", "format"]

        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json = AsyncMock(return_value=invalid_structure)
        mock_response.raise_for_status = MagicMock()

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response)
        client.session = session

        result = await client._request("tv/123")
        assert isinstance(result, list)

        await client.close()

    @pytest.mark.asyncio
    async def test_html_error_page_response(self):
        """Test handling of HTML error page instead of JSON."""
        client = TMDBClient(api_key="test_key")

        mock_response = AsyncMock()
        mock_response.status = 200
        mock_response.json.side_effect = aiohttp.ContentTypeError(
            request_info=MagicMock(),
            history=(),
            message="Expecting JSON, got HTML",
        )
        mock_response.raise_for_status = MagicMock()

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response)
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError):
                await client._request(
                    "search/tv", {"query": "test"}, max_retries=2
                )

        await client.close()


class TestTMDBNetworkTimeout:
    """Test handling of network timeouts."""

    @pytest.mark.asyncio
    async def test_connect_timeout(self):
        """Test handling of connection timeout."""
        client = TMDBClient(api_key="test_key")

        session = _make_session()
        session.get.side_effect = asyncio.TimeoutError()
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError) as exc_info:
                await client._request("tv/123", max_retries=2)

            assert "failed after" in str(exc_info.value).lower()

        await client.close()

    @pytest.mark.asyncio
    async def test_read_timeout(self):
        """Test handling of read timeout during response."""
        client = TMDBClient(api_key="test_key")

        session = _make_session()
        session.get.side_effect = asyncio.TimeoutError()
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError):
                await client.search_tv_show("test query")

        await client.close()

    @pytest.mark.asyncio
    async def test_slow_response_recovery(self):
        """Test successful retry after slow response timeout."""
        client = TMDBClient(api_key="test_key")

        call_count = 0

        def mock_get_side_effect(*args, **kwargs):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise asyncio.TimeoutError()
            mock_response = AsyncMock()
            mock_response.status = 200
            mock_response.json = AsyncMock(return_value={"recovered": True})
            mock_response.raise_for_status = MagicMock()
            return _make_ctx(mock_response)

        session = _make_session()
        session.get.side_effect = mock_get_side_effect
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            result = await client._request("tv/123", max_retries=3)
            assert result == {"recovered": True}
            assert call_count == 2

        await client.close()


class TestTMDBFallbackBehavior:
    """Test fallback behavior when TMDB is unavailable."""

    @pytest.mark.asyncio
    async def test_graceful_degradation_on_search_failure(self):
        """Test that search failure can be handled gracefully."""
        client = TMDBClient(api_key="test_key")

        session = _make_session()
        session.get.side_effect = aiohttp.ClientError("Connection failed")
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError):
                await client.search_tv_show("test query")

        await client.close()

    @pytest.mark.asyncio
    async def test_details_request_failure_handling(self):
        """Test that details request failure can be handled gracefully."""
        client = TMDBClient(api_key="test_key")

        mock_response = AsyncMock()
        mock_response.status = 404

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response)
        client.session = session

        with pytest.raises(TMDBAPIError) as exc_info:
            await client.get_tv_show_details(999999)

        assert "Resource not found" in str(exc_info.value)

        await client.close()

    @pytest.mark.asyncio
    async def test_image_download_failure_handling(self):
        """Test that image download failure can be handled gracefully."""
        import tempfile
        from pathlib import Path

        client = TMDBClient(api_key="test_key")

        session = _make_session()
        session.get.side_effect = aiohttp.ClientError("Download failed")
        client.session = session

        with tempfile.TemporaryDirectory() as tmpdir:
            local_path = Path(tmpdir) / "poster.jpg"

            with pytest.raises(TMDBAPIError) as exc_info:
                await client.download_image("/path/to/image.jpg", local_path)

            assert "Failed to download image" in str(exc_info.value)

        await client.close()


class TestTMDBCacheResilience:
    """Test cache behavior during error scenarios."""

    @pytest.mark.asyncio
    async def test_cache_not_populated_on_error(self):
        """Test that cache is not populated when request fails."""
        client = TMDBClient(api_key="test_key")

        session = _make_session()
        session.get.side_effect = aiohttp.ClientError("Request failed")
        client.session = session

        with patch("asyncio.sleep", new_callable=AsyncMock):
            with pytest.raises(TMDBAPIError):
                await client._request("tv/123", max_retries=1)

        assert len(client._cache) == 0

        await client.close()

    @pytest.mark.asyncio
    async def test_cache_persists_across_retries(self):
        """Test that cache persists even when some requests fail."""
        client = TMDBClient(api_key="test_key")

        mock_response_success = AsyncMock()
        mock_response_success.status = 200
        mock_response_success.json = AsyncMock(return_value={"data": "cached"})
        mock_response_success.raise_for_status = MagicMock()

        session = _make_session()
        session.get.return_value = _make_ctx(mock_response_success)
        client.session = session

        result1 = await client._request("tv/123")
        assert result1 == {"data": "cached"}
        assert len(client._cache) == 1

        result2 = await client._request("tv/123")
        assert result2 == {"data": "cached"}

        assert session.get.call_count == 1

        await client.close()

    @pytest.mark.asyncio
    async def test_cache_isolation_between_clients(self):
        """Test that cache is isolated between different client instances."""
        client1 = TMDBClient(api_key="key1")
        client2 = TMDBClient(api_key="key2")

        mock_response1 = AsyncMock()
        mock_response1.status = 200
        mock_response1.json = AsyncMock(return_value={"client": "1"})
        mock_response1.raise_for_status = MagicMock()

        session1 = _make_session()
        session1.get.return_value = _make_ctx(mock_response1)
        client1.session = session1

        result1 = await client1._request("tv/123")
        assert result1 == {"client": "1"}

        assert len(client2._cache) == 0

        await client1.close()
        await client2.close()
