from __future__ import annotations

from typing import Any

import pytest

from src.normals_service import NormalsService, NormalsValidationError


class FakeResponse:
    def __init__(self, payload: dict[str, Any], status_code: int = 200) -> None:
        self._payload = payload
        self.status_code = status_code

    def json(self) -> dict[str, Any]:
        return self._payload

    def raise_for_status(self) -> None:
        if self.status_code >= 400:
            raise RuntimeError("upstream error")


class FakeNormalsClient:
    def __init__(self, responses: dict[tuple[str, str], dict[str, Any]]) -> None:
        self.responses = responses
        self.calls = 0

    async def get(self, _url: str, params: dict[str, Any]) -> FakeResponse:
        self.calls += 1
        key = (params["start_date"], params["end_date"])
        payload = self.responses.get(key)
        if payload is None:
            return FakeResponse({}, status_code=500)
        return FakeResponse(payload)

    async def aclose(self) -> None:
        return None


def make_payload(
    year: int,
    temp_mean: float,
    temp_max: float,
    temp_min: float,
    precip: float,
    elevation: float = 608.0,
) -> dict[str, Any]:
    return {
        "elevation": elevation,
        "daily": {
            "time": [f"{year}-03-05", f"{year}-03-06"],
            "temperature_2m_mean": [temp_mean, temp_mean + 1],
            "temperature_2m_max": [temp_max, temp_max + 1],
            "temperature_2m_min": [temp_min, temp_min + 1],
            "precipitation_sum": [precip, precip + 1],
        },
    }


@pytest.mark.asyncio
async def test_normals_computation_basic() -> None:
    responses = {
        ("1991-01-01", "2000-12-31"): make_payload(1992, 10.0, 14.0, 6.0, 2.0),
        ("2001-01-01", "2010-12-31"): make_payload(2004, 11.0, 15.0, 7.0, 3.0),
        ("2011-01-01", "2020-12-31"): make_payload(2016, 12.0, 16.0, 8.0, 4.0),
    }
    service = NormalsService(client=FakeNormalsClient(responses))

    result = await service.get_normals(44.09, 6.24, "2026-03-05", "2026-03-05")

    assert result is not None
    assert result["daily_normals"][0]["temp_avg"] == 11.0
    assert result["daily_normals"][0]["temp_max"] == 15.0
    assert result["daily_normals"][0]["temp_min"] == 7.0
    assert result["daily_normals"][0]["precipitation"] == 3.0


@pytest.mark.asyncio
async def test_normals_handles_missing_data() -> None:
    responses = {
        ("1991-01-01", "2000-12-31"): {
            "elevation": 500.0,
            "daily": {
                "time": ["1992-03-05", "1992-03-06"],
                "temperature_2m_mean": [10.0],
                "temperature_2m_max": [14.0, 15.0],
                "temperature_2m_min": [6.0, 7.0],
                "precipitation_sum": [2.0, 1.0],
            },
        },
        ("2001-01-01", "2010-12-31"): make_payload(2004, 11.0, 15.0, 7.0, 3.0),
        ("2011-01-01", "2020-12-31"): make_payload(2016, 12.0, 16.0, 8.0, 4.0),
    }
    service = NormalsService(client=FakeNormalsClient(responses))

    result = await service.get_normals(44.09, 6.24, "2026-03-05", "2026-03-06")

    assert result is not None
    assert len(result["daily_normals"]) == 2
    assert result["period_normals"]["temp_avg"] is not None


@pytest.mark.asyncio
async def test_normals_caching() -> None:
    responses = {
        ("1991-01-01", "2000-12-31"): make_payload(1992, 10.0, 14.0, 6.0, 2.0),
        ("2001-01-01", "2010-12-31"): make_payload(2004, 11.0, 15.0, 7.0, 3.0),
        ("2011-01-01", "2020-12-31"): make_payload(2016, 12.0, 16.0, 8.0, 4.0),
    }
    client = FakeNormalsClient(responses)
    service = NormalsService(client=client)

    first = await service.get_normals(44.09, 6.24, "2026-03-05", "2026-03-06")
    second = await service.get_normals(44.09, 6.24, "2026-03-06", "2026-03-06")

    assert first is not None
    assert second is not None
    assert client.calls == 3


@pytest.mark.asyncio
async def test_normals_includes_elevation() -> None:
    responses = {
        ("1991-01-01", "2000-12-31"): make_payload(1992, 10.0, 14.0, 6.0, 2.0, 608.0),
        ("2001-01-01", "2010-12-31"): make_payload(2004, 11.0, 15.0, 7.0, 3.0, 608.0),
        ("2011-01-01", "2020-12-31"): make_payload(2016, 12.0, 16.0, 8.0, 4.0, 608.0),
    }
    service = NormalsService(client=FakeNormalsClient(responses))

    result = await service.get_normals(44.09, 6.24, "2026-03-05", "2026-03-05")

    assert result is not None
    assert result["elevation"] == 608.0


@pytest.mark.asyncio
async def test_normals_period_aggregation() -> None:
    service = NormalsService(client=FakeNormalsClient({}))
    service.cache.set(
        "normals:44.09:6.24",
        {
            "elevation": 600.0,
            "days": {
                "03-05": {
                    "temp_avg": 10.0,
                    "temp_max": 14.0,
                    "temp_min": 6.0,
                    "precipitation": 1.0,
                },
                "03-06": {
                    "temp_avg": 12.0,
                    "temp_max": 16.0,
                    "temp_min": 8.0,
                    "precipitation": 3.0,
                },
            },
        },
    )

    result = await service.get_normals(44.09, 6.24, "2026-03-05", "2026-03-06")

    assert result is not None
    assert len(result["daily_normals"]) == 2
    assert result["period_normals"]["temp_avg"] == 11.0
    assert result["period_normals"]["precipitation_total"] == 4.0


@pytest.mark.asyncio
async def test_month_normals_basic() -> None:
    service = NormalsService(client=FakeNormalsClient({}))
    march_days: dict[str, dict[str, float]] = {}
    for day in range(1, 32):
        key = f"03-{day:02d}"
        march_days[key] = {
            "temp_avg": 10.0,
            "temp_max": 14.0,
            "temp_min": 6.0,
            "precipitation": 2.0,
        }

    service.cache.set(
        "normals:44.09:6.24",
        {
            "elevation": 600.0,
            "days": march_days,
        },
    )

    result = await service.get_normals(44.09, 6.24, "2026-03-05", "2026-03-11")

    assert result is not None
    assert result["month_normals"] is not None
    assert result["month_normals"]["month"] == 3
    assert result["month_normals"]["month_name"] == "mars"
    assert result["month_normals"]["temp_avg"] == 10.0
    assert result["month_normals"]["temp_max_avg"] == 14.0
    assert result["month_normals"]["temp_min_avg"] == 6.0
    assert result["month_normals"]["precipitation_total"] == 62.0


@pytest.mark.asyncio
async def test_month_normals_december() -> None:
    service = NormalsService(client=FakeNormalsClient({}))
    service.cache.set(
        "normals:44.09:6.24",
        {
            "elevation": 600.0,
            "days": {
                "12-01": {
                    "temp_avg": 5.0,
                    "temp_max": 8.0,
                    "temp_min": 2.0,
                    "precipitation": 1.0,
                },
                "12-02": {
                    "temp_avg": 6.0,
                    "temp_max": 9.0,
                    "temp_min": 3.0,
                    "precipitation": 2.0,
                },
            },
        },
    )

    result = await service.get_normals(44.09, 6.24, "2026-12-01", "2026-12-02")

    assert result is not None
    assert result["month_normals"] is not None
    assert result["month_normals"]["month"] == 12
    assert result["month_normals"]["month_name"] == "décembre"


@pytest.mark.asyncio
async def test_normals_feb29() -> None:
    responses = {
        ("1991-01-01", "2000-12-31"): {
            "elevation": 500.0,
            "daily": {
                "time": [
                    "1992-02-29",
                    "1996-02-29",
                    "2000-02-29",
                    "1991-02-28",
                    "1993-02-28",
                    "1995-02-28",
                ],
                "temperature_2m_mean": [10.0, 12.0, 14.0, 1.0, 2.0, 3.0],
                "temperature_2m_max": [15.0, 17.0, 19.0, 5.0, 6.0, 7.0],
                "temperature_2m_min": [6.0, 8.0, 10.0, -1.0, 0.0, 1.0],
                "precipitation_sum": [1.0, 2.0, 3.0, 9.0, 9.0, 9.0],
            },
        },
        ("2001-01-01", "2010-12-31"): {
            "elevation": 500.0,
            "daily": {
                "time": ["2004-02-29", "2008-02-29", "2001-02-28", "2003-02-28"],
                "temperature_2m_mean": [16.0, 18.0, 4.0, 5.0],
                "temperature_2m_max": [21.0, 23.0, 8.0, 9.0],
                "temperature_2m_min": [12.0, 14.0, 2.0, 3.0],
                "precipitation_sum": [4.0, 5.0, 9.0, 9.0],
            },
        },
        ("2011-01-01", "2020-12-31"): {
            "elevation": 500.0,
            "daily": {
                "time": ["2012-02-29", "2016-02-29", "2020-02-29", "2011-02-28"],
                "temperature_2m_mean": [20.0, 22.0, 24.0, 6.0],
                "temperature_2m_max": [25.0, 27.0, 29.0, 10.0],
                "temperature_2m_min": [16.0, 18.0, 20.0, 4.0],
                "precipitation_sum": [6.0, 7.0, 8.0, 9.0],
            },
        },
    }
    service = NormalsService(client=FakeNormalsClient(responses))

    result = await service.get_normals(44.09, 6.24, "2024-02-29", "2024-02-29")

    assert result is not None
    assert result["daily_normals"][0]["month_day"] == "02-29"
    assert result["daily_normals"][0]["temp_avg"] == 17.0
    assert result["daily_normals"][0]["temp_max"] == 22.0
    assert result["daily_normals"][0]["temp_min"] == 13.0
    assert result["daily_normals"][0]["precipitation"] == 4.5


@pytest.mark.asyncio
async def test_normals_no_matching_days() -> None:
    service = NormalsService(client=FakeNormalsClient({}))
    service.cache.set(
        "normals:44.09:6.24",
        {
            "elevation": 600.0,
            "days": {
                "01-01": {
                    "temp_avg": 1.0,
                    "temp_max": 4.0,
                    "temp_min": -1.0,
                    "precipitation": 1.0,
                },
                "01-02": {
                    "temp_avg": 2.0,
                    "temp_max": 5.0,
                    "temp_min": 0.0,
                    "precipitation": 1.5,
                },
            },
        },
    )

    result = await service.get_normals(44.09, 6.24, "2026-07-01", "2026-07-02")

    assert result is None


@pytest.mark.asyncio
async def test_annual_normals_returns_12_months() -> None:
  service = NormalsService(client=FakeNormalsClient({}))
  days = {}
  for month in range(1, 13):
      key = f"{month:02d}-01"
      days[key] = {
          "temp_avg": 10.0 + month,
          "temp_max": 14.0 + month,
          "temp_min": 6.0 + month,
          "precipitation": float(month),
      }

  service.cache.set(
      "normals:44.09:6.24",
      {
          "elevation": 600.0,
          "days": days,
      },
  )

  result = await service.get_annual_normals(44.09, 6.24)

  assert result is not None
  assert len(result["months"]) == 12
  assert result["months"][0]["month"] == 1
  assert result["months"][0]["month_name"] == "janvier"
  assert "temp_avg" in result["months"][0]
  assert "precipitation_total" in result["months"][0]


@pytest.mark.asyncio
async def test_annual_normals_reuses_cache() -> None:
    responses = {
        ("1991-01-01", "2000-12-31"): make_payload(1992, 10.0, 14.0, 6.0, 2.0),
        ("2001-01-01", "2010-12-31"): make_payload(2004, 11.0, 15.0, 7.0, 3.0),
        ("2011-01-01", "2020-12-31"): make_payload(2016, 12.0, 16.0, 8.0, 4.0),
    }
    client = FakeNormalsClient(responses)
    service = NormalsService(client=client)

    first = await service.get_normals(44.09, 6.24, "2026-03-05", "2026-03-06")
    second = await service.get_annual_normals(44.09, 6.24)

    assert first is not None
    assert second is not None
    assert client.calls == 3


@pytest.mark.asyncio
async def test_annual_normals_invalid_coordinates() -> None:
    service = NormalsService(client=FakeNormalsClient({}))

    with pytest.raises(NormalsValidationError):
        await service.get_annual_normals(120.0, 2.35)
