"""Tests for the CarbonIntensityClient."""

from unittest.mock import AsyncMock, MagicMock, patch

import aiohttp
import pytest

from aioukcarbon import (
    CarbonIntensityClient,
    CarbonIntensityConnectionError,
    CarbonIntensityError,
    CarbonIntensityNoDataError,
    CarbonIntensityTimeoutError,
)


@pytest.fixture
def mock_session():
    """Create a mock aiohttp session."""
    return MagicMock(spec=aiohttp.ClientSession)


def _mock_response(data: dict, status: int = 200):
    """Create a mock aiohttp response."""
    resp = AsyncMock()
    resp.status = status
    resp.json = AsyncMock(return_value=data)
    resp.__aenter__ = AsyncMock(return_value=resp)
    resp.__aexit__ = AsyncMock(return_value=False)
    return resp


class TestRegionalIntensity:
    """Tests for get_regional_intensity."""

    @pytest.mark.asyncio
    async def test_success(self, mock_session, regional_intensity_response):
        """Test successful regional intensity fetch."""
        mock_session.get = MagicMock(
            return_value=_mock_response(regional_intensity_response)
        )
        client = CarbonIntensityClient(session=mock_session)
        result = await client.get_regional_intensity("DE45")

        assert result.regionid == 9
        assert result.dnoregion == "WPD East Midlands"
        assert result.shortname == "East Midlands"
        assert result.postcode == "DE45"
        assert len(result.periods) == 1
        assert result.periods[0].intensity.forecast == 261
        assert result.periods[0].intensity.index == "very high"
        assert result.periods[0].intensity.actual is None
        assert len(result.periods[0].generationmix) == 9

    @pytest.mark.asyncio
    async def test_no_data(self, mock_session):
        """Test empty data response."""
        mock_session.get = MagicMock(return_value=_mock_response({"data": []}))
        client = CarbonIntensityClient(session=mock_session)
        with pytest.raises(CarbonIntensityNoDataError):
            await client.get_regional_intensity("ZZ99")

    @pytest.mark.asyncio
    async def test_no_periods(self, mock_session):
        """Test response with region but no periods."""
        mock_session.get = MagicMock(
            return_value=_mock_response(
                {
                    "data": [
                        {
                            "regionid": 9,
                            "dnoregion": "WPD East Midlands",
                            "shortname": "East Midlands",
                            "postcode": "DE45",
                            "data": [],
                        }
                    ]
                }
            )
        )
        client = CarbonIntensityClient(session=mock_session)
        with pytest.raises(CarbonIntensityNoDataError):
            await client.get_regional_intensity("DE45")


class TestRegionalForecast:
    """Tests for get_regional_forecast."""

    @pytest.mark.asyncio
    async def test_success(self, mock_session, regional_forecast_response):
        """Test successful regional forecast fetch."""
        mock_session.get = MagicMock(
            return_value=_mock_response(regional_forecast_response)
        )
        client = CarbonIntensityClient(session=mock_session)
        result = await client.get_regional_forecast("DE45")

        assert result.regionid == 9
        assert result.shortname == "East Midlands"
        assert len(result.periods) == 3
        assert result.periods[0].intensity.forecast == 261
        assert result.periods[2].intensity.forecast == 180

    @pytest.mark.asyncio
    async def test_no_data(self, mock_session):
        """Test empty forecast response."""
        mock_session.get = MagicMock(return_value=_mock_response({"data": {}}))
        client = CarbonIntensityClient(session=mock_session)
        with pytest.raises(CarbonIntensityNoDataError):
            await client.get_regional_forecast("ZZ99")


class TestNationalIntensity:
    """Tests for get_national_intensity."""

    @pytest.mark.asyncio
    async def test_success(self, mock_session, national_intensity_response):
        """Test successful national intensity fetch."""
        mock_session.get = MagicMock(
            return_value=_mock_response(national_intensity_response)
        )
        client = CarbonIntensityClient(session=mock_session)
        result = await client.get_national_intensity()

        assert result.intensity.forecast == 136
        assert result.intensity.actual == 136
        assert result.intensity.index == "moderate"

    @pytest.mark.asyncio
    async def test_no_data(self, mock_session):
        """Test empty national intensity response."""
        mock_session.get = MagicMock(return_value=_mock_response({"data": []}))
        client = CarbonIntensityClient(session=mock_session)
        with pytest.raises(CarbonIntensityNoDataError):
            await client.get_national_intensity()


class TestGenerationMix:
    """Tests for get_generation_mix."""

    @pytest.mark.asyncio
    async def test_success(self, mock_session, generation_mix_response):
        """Test successful generation mix fetch."""
        mock_session.get = MagicMock(
            return_value=_mock_response(generation_mix_response)
        )
        client = CarbonIntensityClient(session=mock_session)
        result = await client.get_generation_mix()

        assert len(result.generationmix) == 9
        fuels = {f.fuel: f.perc for f in result.generationmix}
        assert fuels["wind"] == 37.9
        assert fuels["gas"] == 29.6
        assert fuels["nuclear"] == 9.8

    @pytest.mark.asyncio
    async def test_no_data(self, mock_session):
        """Test empty generation mix response."""
        mock_session.get = MagicMock(return_value=_mock_response({"data": {}}))
        client = CarbonIntensityClient(session=mock_session)
        with pytest.raises(CarbonIntensityNoDataError):
            await client.get_generation_mix()


class TestErrorHandling:
    """Tests for error handling."""

    @pytest.mark.asyncio
    async def test_timeout(self, mock_session):
        """Test timeout error."""
        mock_session.get = MagicMock(side_effect=TimeoutError("timed out"))
        client = CarbonIntensityClient(session=mock_session)
        with pytest.raises(CarbonIntensityTimeoutError):
            await client.get_national_intensity()

    @pytest.mark.asyncio
    async def test_connection_error(self, mock_session):
        """Test connection error."""
        mock_session.get = MagicMock(
            side_effect=aiohttp.ClientError("connection failed")
        )
        client = CarbonIntensityClient(session=mock_session)
        with pytest.raises(CarbonIntensityConnectionError):
            await client.get_national_intensity()

    @pytest.mark.asyncio
    async def test_http_error(self, mock_session):
        """Test non-200 HTTP status."""
        mock_session.get = MagicMock(return_value=_mock_response({}, status=500))
        client = CarbonIntensityClient(session=mock_session)
        with pytest.raises(CarbonIntensityError, match="status 500"):
            await client.get_national_intensity()

    @pytest.mark.asyncio
    async def test_no_session(self):
        """Test calling without session."""
        client = CarbonIntensityClient()
        with pytest.raises(CarbonIntensityError, match="Session not initialized"):
            await client.get_national_intensity()


class TestContextManager:
    """Tests for context manager behaviour."""

    @pytest.mark.asyncio
    async def test_creates_and_closes_session(self):
        """Test that context manager creates and closes its own session."""
        with patch("aioukcarbon.client.aiohttp.ClientSession") as mock_cls:
            mock_sess = AsyncMock()
            mock_cls.return_value = mock_sess

            async with CarbonIntensityClient() as client:
                assert client._session is mock_sess

            mock_sess.close.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_does_not_close_external_session(self, mock_session):
        """Test that external session is not closed."""
        mock_session.close = AsyncMock()
        client = CarbonIntensityClient(session=mock_session)
        await client.close()
        mock_session.close.assert_not_awaited()
