"""app/weather/'s provider modules -- pure-logic, no HTTP/DB: monkeypatches
httpx.get with canned responses. Covers category normalization (Open-
Meteo's WMO codes, NWS's icon-URL/text shapes) landing on the same shared
category set, and each provider's fetch_current/fetch_hourly/fetch_daily
shape."""

from __future__ import annotations

import httpx
import pytest

from app.weather import WeatherFetchError, nws, open_meteo


class _FakeResponse:
    def __init__(self, data: dict):
        self._data = data

    def raise_for_status(self) -> None:
        pass

    def json(self) -> dict:
        return self._data


# --- open_meteo -----------------------------------------------------------

def test_open_meteo_fetch_current(monkeypatch):
    monkeypatch.setattr(httpx, "get", lambda url, params, timeout: _FakeResponse(
        {"current": {"temperature_2m": 72.5, "weathercode": 3}}
    ))
    result = open_meteo.fetch_current(45.5, -122.6, "fahrenheit")
    assert result == {"temp": 72.5, "category": "cloudy"}


def test_open_meteo_fetch_hourly_respects_hours_limit(monkeypatch):
    monkeypatch.setattr(httpx, "get", lambda url, params, timeout: _FakeResponse({
        "hourly": {
            "time": [f"2026-07-27T{h:02d}:00" for h in range(24)],
            "temperature_2m": list(range(24)),
            "weathercode": [0] * 24,
        }
    }))
    result = open_meteo.fetch_hourly(45.5, -122.6, "fahrenheit", hours=6)
    assert len(result) == 6
    assert result[0] == {"time": "2026-07-27T00:00", "temp": 0, "category": "clear"}


def test_open_meteo_fetch_daily_normalizes_category_and_clamps_days(monkeypatch):
    monkeypatch.setattr(httpx, "get", lambda url, params, timeout: _FakeResponse({
        "daily": {
            "time": ["2026-07-27", "2026-07-28", "2026-07-29"],
            "weathercode": [95, 71, 61],
            "temperature_2m_max": [80, 60, 65],
            "temperature_2m_min": [65, 45, 50],
        }
    }))
    result = open_meteo.fetch_daily(45.5, -122.6, "fahrenheit", days=2)
    assert list(result.keys()) == ["2026-07-27", "2026-07-28"]
    assert result["2026-07-27"] == {"high": 80, "low": 65, "category": "thunderstorm"}
    assert result["2026-07-28"]["category"] == "snow"


def test_open_meteo_weather_category_unknown_code_falls_back_to_cloudy():
    assert open_meteo.weather_category(9999) == "cloudy"


def test_open_meteo_raises_weather_fetch_error_on_http_failure(monkeypatch):
    def _raise(*a, **kw):
        raise httpx.ConnectError("boom")
    monkeypatch.setattr(httpx, "get", _raise)
    with pytest.raises(WeatherFetchError):
        open_meteo.fetch_current(45.5, -122.6, "fahrenheit")


# --- nws --------------------------------------------------------------------

@pytest.mark.parametrize("icon_url,expected", [
    ("https://api.weather.gov/icons/land/day/skc?size=medium", "clear"),
    ("https://api.weather.gov/icons/land/night/few?size=medium", "partly_cloudy"),
    ("https://api.weather.gov/icons/land/day/bkn?size=medium", "cloudy"),
    ("https://api.weather.gov/icons/land/day/tsra,40?size=medium", "thunderstorm"),
    ("https://api.weather.gov/icons/land/night/wind_skc?size=medium", "clear"),
    ("https://api.weather.gov/icons/land/day/snow,80?size=medium", "snow"),
    ("https://api.weather.gov/icons/land/day/rain_showers,60?size=medium", "rain"),
])
def test_nws_category_from_icon(icon_url, expected):
    assert nws._category_from_icon(icon_url) == expected


def test_nws_category_from_icon_unrecognized_code_falls_back_to_none():
    assert nws._category_from_icon("https://api.weather.gov/icons/land/day/mystery_code?size=medium") is None


@pytest.mark.parametrize("text,expected", [
    ("Chance Thunderstorms", "thunderstorm"),
    ("Snow likely", "snow"),
    ("Rain showers", "rain"),
    ("Patchy Fog", "fog"),
    ("Mostly Sunny", "clear"),
    ("Something Unrelated", "cloudy"),
])
def test_nws_category_from_text_fallback(text, expected):
    assert nws._category_from_text(text) == expected


def test_nws_convert_temp_fahrenheit_to_celsius():
    assert round(nws._convert_temp(32, "F", "celsius"), 1) == 0.0


def test_nws_convert_temp_no_op_when_units_already_match():
    assert nws._convert_temp(75, "F", "fahrenheit") == 75


def _points_response():
    return _FakeResponse({"properties": {
        "forecast": "https://api.weather.gov/gridpoints/PQR/1,1/forecast",
        "forecastHourly": "https://api.weather.gov/gridpoints/PQR/1,1/forecast/hourly",
    }})


def _hourly_periods_response(n=6):
    return _FakeResponse({"properties": {"periods": [
        {"startTime": f"2026-07-27T{h:02d}:00:00-07:00", "temperature": 60 + h, "temperatureUnit": "F",
         "shortForecast": "Sunny", "icon": "https://api.weather.gov/icons/land/day/skc?size=medium"}
        for h in range(n)
    ]}})


def test_nws_fetch_current_uses_first_hourly_period(monkeypatch):
    def _get(url, headers, timeout):
        if "/points/" in url:
            return _points_response()
        return _hourly_periods_response()
    monkeypatch.setattr(httpx, "get", _get)

    result = nws.fetch_current(45.5, -122.6, "fahrenheit")
    assert result == {"temp": 60, "category": "clear"}


def test_nws_fetch_hourly_respects_hours_limit(monkeypatch):
    def _get(url, headers, timeout):
        if "/points/" in url:
            return _points_response()
        return _hourly_periods_response(n=24)
    monkeypatch.setattr(httpx, "get", _get)

    result = nws.fetch_hourly(45.5, -122.6, "fahrenheit", hours=4)
    assert len(result) == 4
    assert result[0]["category"] == "clear"


def test_nws_fetch_daily_pairs_day_night_periods_by_date(monkeypatch):
    def _get(url, headers, timeout):
        if "/points/" in url:
            return _points_response()
        return _FakeResponse({"properties": {"periods": [
            {"startTime": "2026-07-27T06:00:00-07:00", "isDaytime": True, "temperature": 80,
             "temperatureUnit": "F", "shortForecast": "Sunny",
             "icon": "https://api.weather.gov/icons/land/day/skc?size=medium"},
            {"startTime": "2026-07-27T18:00:00-07:00", "isDaytime": False, "temperature": 60,
             "temperatureUnit": "F", "shortForecast": "Clear",
             "icon": "https://api.weather.gov/icons/land/night/skc?size=medium"},
            {"startTime": "2026-07-28T06:00:00-07:00", "isDaytime": True, "temperature": 75,
             "temperatureUnit": "F", "shortForecast": "Rain",
             "icon": "https://api.weather.gov/icons/land/day/rain,80?size=medium"},
            {"startTime": "2026-07-28T18:00:00-07:00", "isDaytime": False, "temperature": 55,
             "temperatureUnit": "F", "shortForecast": "Rain",
             "icon": "https://api.weather.gov/icons/land/night/rain,80?size=medium"},
        ]}})
    monkeypatch.setattr(httpx, "get", _get)

    result = nws.fetch_daily(45.5, -122.6, "fahrenheit", days=2)
    assert result == {
        "2026-07-27": {"high": 80, "low": 60, "category": "clear"},
        "2026-07-28": {"high": 75, "low": 55, "category": "rain"},
    }


def test_nws_fetch_daily_clamps_to_however_many_dates_came_back(monkeypatch):
    """Asking for more days than NWS actually returned degrades gracefully
    (fewer days), not an error -- same idiom as Open-Meteo's own clamp."""
    def _get(url, headers, timeout):
        if "/points/" in url:
            return _points_response()
        return _FakeResponse({"properties": {"periods": [
            {"startTime": "2026-07-27T06:00:00-07:00", "isDaytime": True, "temperature": 80,
             "temperatureUnit": "F", "shortForecast": "Sunny",
             "icon": "https://api.weather.gov/icons/land/day/skc?size=medium"},
        ]}})
    monkeypatch.setattr(httpx, "get", _get)

    result = nws.fetch_daily(45.5, -122.6, "fahrenheit", days=7)
    assert len(result) == 1


def test_nws_raises_weather_fetch_error_when_points_lookup_fails(monkeypatch):
    def _raise(*a, **kw):
        raise httpx.ConnectError("boom")
    monkeypatch.setattr(httpx, "get", _raise)
    with pytest.raises(WeatherFetchError):
        nws.fetch_current(45.5, -122.6, "fahrenheit")
