"""Tests for therminal.ml — WeatherFeatures transformer."""

from datetime import UTC, datetime
from unittest.mock import MagicMock

import numpy as np
import pytest

from therminal.ml.cache import _cache_key
from therminal.ml.features import WeatherFeatures, _calendar_features, _to_dates

# ── Fixtures ────────────────────────────────────────────────────────────────

SAMPLE_OMO = [
    {"observed_at": "2024-06-15T11:00:00Z", "station_code": "ATL",
     "temp_c": 28, "dewpoint_c": 20, "pressure1_inhg": 29.92},
    {"observed_at": "2024-06-15T11:01:00Z", "station_code": "ATL",
     "temp_c": 29, "dewpoint_c": 20, "pressure1_inhg": 29.91},
    {"observed_at": "2024-06-15T11:02:00Z", "station_code": "ATL",
     "temp_c": 27, "dewpoint_c": 19, "pressure1_inhg": 29.93},
    {"observed_at": "2024-06-15T11:30:00Z", "station_code": "ATL",
     "temp_c": 30, "dewpoint_c": 21, "pressure1_inhg": 29.90},
    {"observed_at": "2024-06-15T11:59:00Z", "station_code": "ATL",
     "temp_c": 31, "dewpoint_c": 22, "pressure1_inhg": 29.89},
]

SAMPLE_METAR = [
    {
        "observed_at": "2024-06-15T11:00:00Z", "station_code": "ATL",
        "observation_type": "METAR", "source": "iem",
        "temp_f": 82.0, "dewpoint_f": 68.0, "relative_humidity": 62.0,
        "wind_speed_kt": 8, "wind_dir_degrees": 210, "wind_gust_kt": None,
        "altimeter_inhg": 29.92, "visibility_miles": 10.0,
    },
]


def _mock_client(omo_data=None, metar_data=None):
    """Create a mock TherminalClient that returns test data."""
    client = MagicMock()

    def mock_observations(**kwargs):
        if kwargs.get("resolution") == "1min":
            return omo_data or SAMPLE_OMO
        return metar_data or SAMPLE_METAR

    client.observations = MagicMock(side_effect=mock_observations)
    return client


# ── Date parsing ────────────────────────────────────────────────────────────

class TestToDate:
    def test_string_dates(self):
        dates = _to_dates(["2024-06-15", "2024-06-16"])
        assert len(dates) == 2
        assert dates[0].day == 15
        assert dates[0].tzinfo is not None

    def test_rfc3339_dates(self):
        dates = _to_dates(["2024-06-15T12:00:00Z"])
        assert dates[0].hour == 12

    def test_datetime_objects(self):
        dt = datetime(2024, 6, 15, 12, 0, tzinfo=UTC)
        dates = _to_dates([dt])
        assert dates[0] == dt

    def test_numpy_array(self):
        arr = np.array(["2024-06-15", "2024-06-16"])
        dates = _to_dates(arr)
        assert len(dates) == 2


# ── Calendar features ───────────────────────────────────────────────────────

class TestCalendarFeatures:
    def test_basic(self):
        dt = datetime(2024, 6, 15, 12, 0, tzinfo=UTC)
        feat = _calendar_features(dt)
        assert feat["month"] == 6.0
        assert feat["hour"] == 12.0
        assert feat["weekday"] == 5.0  # Saturday
        assert -1 <= feat["day_of_year_sin"] <= 1
        assert -1 <= feat["month_cos"] <= 1


# ── WeatherFeatures transformer ────────────────────────────────────────────

class TestWeatherFeatures:
    def test_fit_transform_shape(self):
        client = _mock_client()
        wf = WeatherFeatures(station="ATL", sources=["omo"], client=client, cache=False)
        dates = ["2024-06-15T12:00:00Z", "2024-06-16T12:00:00Z"]
        X = wf.fit_transform(dates)

        assert isinstance(X, np.ndarray)
        assert X.shape[0] == 2
        assert X.shape[1] == wf.n_features_

    def test_feature_names_out(self):
        client = _mock_client()
        wf = WeatherFeatures(station="ATL", sources=["omo"], client=client, cache=False)
        wf.fit(["2024-06-15T12:00:00Z"])

        names = wf.get_feature_names_out()
        assert isinstance(names, np.ndarray)
        assert len(names) == wf.n_features_
        assert all(isinstance(n, str) for n in names)

    def test_omo_feature_names_contain_aggs(self):
        client = _mock_client()
        wf = WeatherFeatures(
            station="ATL", sources=["omo"],
            omo_aggs=("min", "max", "mean"),
            include_calendar=False, client=client, cache=False,
        )
        wf.fit(["2024-06-15T12:00:00Z"])

        names = list(wf.get_feature_names_out())
        assert any("omo_temp_c_min" in n for n in names)
        assert any("omo_temp_c_max" in n for n in names)
        assert any("omo_temp_c_mean" in n for n in names)

    def test_metar_features(self):
        client = _mock_client()
        wf = WeatherFeatures(
            station="ATL", sources=["metar"],
            include_calendar=False, client=client, cache=False,
        )
        wf.fit_transform(["2024-06-15T12:00:00Z"])

        names = list(wf.get_feature_names_out())
        assert any("metar_temp_f" in n for n in names)
        assert any("metar_wind_speed_kt" in n for n in names)

    def test_both_sources(self):
        client = _mock_client()
        wf = WeatherFeatures(
            station="ATL", sources=["omo", "metar"],
            include_calendar=True, client=client, cache=False,
        )
        wf.fit_transform(["2024-06-15T12:00:00Z"])

        names = list(wf.get_feature_names_out())
        has_omo = any("omo_" in n for n in names)
        has_metar = any("metar_" in n for n in names)
        has_calendar = any("day_of_year" in n for n in names)
        assert has_omo and has_metar and has_calendar

    def test_no_calendar(self):
        client = _mock_client()
        wf = WeatherFeatures(
            station="ATL", sources=["omo"],
            include_calendar=False, client=client, cache=False,
        )
        wf.fit(["2024-06-15T12:00:00Z"])

        names = list(wf.get_feature_names_out())
        assert not any("day_of_year" in n for n in names)

    def test_custom_aggs(self):
        client = _mock_client()
        wf = WeatherFeatures(
            station="ATL", sources=["omo"],
            omo_aggs=("min", "max", "range", "last"),
            include_calendar=False, client=client, cache=False,
        )
        wf.fit(["2024-06-15T12:00:00Z"])

        names = list(wf.get_feature_names_out())
        assert any("range" in n for n in names)
        assert any("last" in n for n in names)
        assert not any("std" in n for n in names)

    def test_sklearn_clone(self):
        """Verify estimator follows sklearn conventions (get_params/set_params)."""
        from sklearn.base import clone

        wf = WeatherFeatures(station="ATL", lookback_hours=48)
        wf2 = clone(wf)
        assert wf2.station == "ATL"
        assert wf2.lookback_hours == 48
        assert wf2 is not wf

    def test_pipeline_integration(self):
        """Verify WeatherFeatures works in a sklearn Pipeline."""
        from sklearn.impute import SimpleImputer
        from sklearn.pipeline import make_pipeline

        client = _mock_client()
        pipe = make_pipeline(
            WeatherFeatures(station="ATL", sources=["omo"], client=client, cache=False),
            SimpleImputer(strategy="constant", fill_value=0.0),
        )

        dates = ["2024-06-15T12:00:00Z", "2024-06-16T12:00:00Z"]
        X = pipe.fit_transform(dates)
        assert X.shape[0] == 2
        assert not np.any(np.isnan(X))

    def test_not_fitted_raises(self):
        wf = WeatherFeatures(station="ATL")
        with pytest.raises(Exception):  # NotFittedError
            wf.transform(["2024-06-15T12:00:00Z"])

    def test_empty_dates_raises(self):
        wf = WeatherFeatures(station="ATL")
        with pytest.raises(ValueError, match="at least one date"):
            wf.fit([])


# ── Cache ───────────────────────────────────────────────────────────────────

class TestCache:
    def test_cache_key_deterministic(self):
        k1 = _cache_key("ATL", "omo", {"from": "2024-01-01"})
        k2 = _cache_key("ATL", "omo", {"from": "2024-01-01"})
        assert k1 == k2

    def test_cache_key_varies(self):
        k1 = _cache_key("ATL", "omo", {"from": "2024-01-01"})
        k2 = _cache_key("ATL", "omo", {"from": "2024-01-02"})
        assert k1 != k2
