from __future__ import annotations

import time
from pathlib import Path

import pytest

from src.cache import FileCache
from src import prefetch_service
from src.prefetch_service import cache_key_month, cache_key_period, cache_key_town


@pytest.fixture
def isolated_file_cache(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> FileCache:
    cache = FileCache(tmp_path / "weather_cache", ttl_seconds=0)
    monkeypatch.setattr(prefetch_service, "_file_cache", cache)
    return cache


def test_file_cache_set_and_get(tmp_path: Path) -> None:
    cache = FileCache(tmp_path / "cache")
    payload = {"commune": {"nom": "Gap"}, "weather": {"data": []}}

    cache.set("period_gap-05_2026-03-01_2026-03-07", payload)

    assert cache.get("period_gap-05_2026-03-01_2026-03-07") == payload


def test_file_cache_miss(tmp_path: Path) -> None:
    cache = FileCache(tmp_path / "cache")

    assert cache.get("missing") is None


def test_file_cache_ttl_expired(tmp_path: Path) -> None:
    cache = FileCache(tmp_path / "cache", ttl_seconds=1)
    cache.set("expiring", {"weather": {"data": []}})

    time.sleep(1.05)

    assert cache.get("expiring") is None


def test_file_cache_atomic_write(tmp_path: Path) -> None:
    cache = FileCache(tmp_path / "cache")
    cache.set("atomic", {"weather": {"data": []}})

    tmp_files = list((tmp_path / "cache").glob("*.tmp"))
    assert tmp_files == []


def test_file_cache_key_sanitization(tmp_path: Path) -> None:
    cache = FileCache(tmp_path / "cache")

    path = cache._key_to_path("period_../gap-05_2026/03/01_2026:03:07")

    assert ".." not in path.name
    assert "/" not in path.name
    assert "\\" not in path.name
    assert path.name.endswith(".json")


@pytest.mark.asyncio
async def test_prefetch_period_cache_hit(
    isolated_file_cache: FileCache,
) -> None:
    key = prefetch_service.cache_key_period("gap-05", "2026-03-01", "2026-03-07")
    expected = {
        "commune": {"nom": "Gap", "latitude": 44.56, "longitude": 6.08},
        "weather": {"data": [{"time": "2026-03-01T00:00"}], "daily_summary": []},
        "normals": {"reference_period": "1991-2020"},
    }
    isolated_file_cache.set(key, expected)

    class FailingService:
        async def resolve_slug(self, _slug: str):
            raise AssertionError("resolve_slug should not be called on cache hit")

    result = await prefetch_service.prefetch_period(
        FailingService(),
        object(),
        object(),
        "gap-05",
        "2026-03-01",
        "2026-03-07",
    )

    assert result == expected


@pytest.mark.asyncio
async def test_prefetch_period_cache_miss(isolated_file_cache: FileCache) -> None:
    class CommuneService:
        async def resolve_slug(self, _slug: str):
            return {"nom": "Gap", "latitude": 44.56, "longitude": 6.08}

    class WeatherService:
        async def get_weather_for_prefetch(
            self,
            _lat: float,
            _lon: float,
            _start: str,
            _end: str,
        ):
            return {"data": [{"time": "2026-03-01T00:00"}], "daily_summary": []}

    class NormalsService:
        async def get_normals(self, _lat: float, _lon: float, _start: str, _end: str):
            return {"reference_period": "1991-2020"}

    result = await prefetch_service.prefetch_period(
        CommuneService(),
        WeatherService(),
        NormalsService(),
        "gap-05",
        "2026-03-01",
        "2026-03-07",
    )

    assert result is not None
    assert result["commune"]["nom"] == "Gap"
    assert result["weather"]["data"]

    key = prefetch_service.cache_key_period("gap-05", "2026-03-01", "2026-03-07")
    cached = isolated_file_cache.get(key)
    assert cached == result


@pytest.mark.asyncio
async def test_prefetch_period_service_failure(isolated_file_cache: FileCache) -> None:
    class CommuneService:
        async def resolve_slug(self, _slug: str):
            return {"nom": "Gap", "latitude": 44.56, "longitude": 6.08}

    class WeatherService:
        async def get_weather_for_prefetch(
            self,
            _lat: float,
            _lon: float,
            _start: str,
            _end: str,
        ):
            raise RuntimeError("upstream down")

    class NormalsService:
        async def get_normals(self, _lat: float, _lon: float, _start: str, _end: str):
            return {"reference_period": "1991-2020"}

    result = await prefetch_service.prefetch_period(
        CommuneService(),
        WeatherService(),
        NormalsService(),
        "gap-05",
        "2026-03-01",
        "2026-03-07",
    )

    assert result is None


@pytest.mark.asyncio
async def test_prefetch_period_normals_failure(isolated_file_cache: FileCache) -> None:
    class CommuneService:
        async def resolve_slug(self, _slug: str):
            return {"nom": "Gap", "latitude": 44.56, "longitude": 6.08}

    class WeatherService:
        async def get_weather_for_prefetch(
            self,
            _lat: float,
            _lon: float,
            _start: str,
            _end: str,
        ):
            return {"data": [{"time": "2026-03-01T00:00"}], "daily_summary": []}

    class NormalsService:
        async def get_normals(self, _lat: float, _lon: float, _start: str, _end: str):
            raise RuntimeError("normals unavailable")

    result = await prefetch_service.prefetch_period(
        CommuneService(),
        WeatherService(),
        NormalsService(),
        "gap-05",
        "2026-03-01",
        "2026-03-07",
    )

    assert result is not None
    assert result["normals"] is None


@pytest.mark.asyncio
async def test_prefetch_town_ok(isolated_file_cache: FileCache) -> None:
    class CommuneService:
        async def resolve_slug(self, _slug: str):
            return {"nom": "Gap", "latitude": 44.56, "longitude": 6.08}

    class NormalsService:
        async def get_annual_normals(self, _lat: float, _lon: float):
            return {"annual_avg_temp": 10.0, "months": []}

    result = await prefetch_service.prefetch_town(
        CommuneService(),
        NormalsService(),
        "gap-05",
    )

    assert result is not None
    assert result["commune"]["nom"] == "Gap"
    assert result["annual_climate"]["annual_avg_temp"] == 10.0


@pytest.mark.asyncio
async def test_prefetch_month_ok(isolated_file_cache: FileCache) -> None:
    class CommuneService:
        async def resolve_slug(self, _slug: str):
            return {"nom": "Gap", "latitude": 44.56, "longitude": 6.08}

    class WeatherService:
        async def get_weather_for_prefetch(
            self,
            _lat: float,
            _lon: float,
            _start: str,
            _end: str,
        ):
            return {"data": [{"time": "2026-03-01T00:00"}], "daily_summary": []}

    class NormalsService:
        async def get_normals(self, _lat: float, _lon: float, _start: str, _end: str):
            return {"reference_period": "1991-2020"}

    result = await prefetch_service.prefetch_month(
        CommuneService(),
        WeatherService(),
        NormalsService(),
        "gap-05",
        "2026-03-01",
        "2026-03-31",
        2026,
        3,
    )

    assert result is not None
    assert result["weather"]["data"]
    assert result["normals"]["reference_period"] == "1991-2020"


def test_cache_key_no_collision() -> None:
    """Verifie que des slugs et periodes differents produisent des cles distinctes."""
    keys = {
        cache_key_period("gap-05", "2026-03-01", "2026-03-07"),
        cache_key_period("gap-05", "2026-03-01", "2026-03-08"),
        cache_key_period("gap-06", "2026-03-01", "2026-03-07"),
        cache_key_month("gap-05", 2026, 3),
        cache_key_month("gap-05", 2026, 4),
        cache_key_town("gap-05"),
        cache_key_town("gap-06"),
        cache_key_period("saint-veran-05", "2026-03-01", "2026-03-07"),
        cache_key_period("saint-véran-05", "2026-03-01", "2026-03-07"),
    }
    assert len(keys) == 9
