from __future__ import annotations

from django.apps import apps
from django.test import SimpleTestCase, TestCase, override_settings
from unittest.mock import Mock, patch

from location_data.models import SoilLocation
from weather.adapters import MockWeatherAdapter, OpenMeteoWeatherAdapter, OpenWeatherOneCallAdapter
from weather.models import WeatherForecast
from weather.services import fetch_weather_from_api, update_weather_for_location


class MockWeatherAdapterTests(SimpleTestCase):
    def setUp(self):
        self.adapter = MockWeatherAdapter(delay_seconds=0)

    def test_same_coordinate_returns_same_forecast(self):
        first = self.adapter.fetch_forecast(35.71, 51.4)
        second = self.adapter.fetch_forecast(35.71, 51.4)

        self.assertEqual(first, second)

    def test_nearby_coordinates_produce_nearby_forecast(self):
        first = self.adapter.fetch_forecast(35.71, 51.4)
        second = self.adapter.fetch_forecast(35.715, 51.405)

        first_daily = first["daily"]
        second_daily = second["daily"]
        self.assertLess(
            abs(first_daily["temperature_2m_mean"][0] - second_daily["temperature_2m_mean"][0]),
            2.5,
        )
        self.assertLess(
            abs(first_daily["relative_humidity_2m_mean"][0] - second_daily["relative_humidity_2m_mean"][0]),
            8.0,
        )
        self.assertLess(
            abs(first_daily["wind_speed_10m_max"][0] - second_daily["wind_speed_10m_max"][0]),
            6.0,
        )

    def test_shape_matches_open_meteo_daily_contract(self):
        forecast = self.adapter.fetch_forecast(35.71, 51.4)
        daily = forecast["daily"]

        self.assertEqual(len(daily["time"]), 7)
        self.assertEqual(len(daily["temperature_2m_max"]), 7)
        self.assertEqual(len(daily["weather_code"]), 7)


class WeatherAdapterSelectionTests(SimpleTestCase):
    def tearDown(self):
        apps.get_app_config("weather").__dict__.pop("weather_data_adapter", None)

    @override_settings(WEATHER_DATA_PROVIDER="mock", WEATHER_MOCK_DELAY_SECONDS=0)
    def test_app_config_returns_mock_adapter(self):
        config = apps.get_app_config("weather")
        config.__dict__.pop("weather_data_adapter", None)

        adapter = config.get_weather_data_adapter()

        self.assertIsInstance(adapter, MockWeatherAdapter)

    @override_settings(WEATHER_DATA_PROVIDER="open-meteo", WEATHER_TIMEOUT_SECONDS=12)
    def test_app_config_returns_live_adapter(self):
        config = apps.get_app_config("weather")
        config.__dict__.pop("weather_data_adapter", None)

        adapter = config.get_weather_data_adapter()

        self.assertIsInstance(adapter, OpenMeteoWeatherAdapter)
        self.assertEqual(adapter.timeout, 12)

    @override_settings(
        WEATHER_DATA_PROVIDER="openweather",
        WEATHER_API_BASE_URL="https://api.openweathermap.org/data/3.0/onecall",
        WEATHER_API_KEY="test-key",
        WEATHER_TIMEOUT_SECONDS=18,
    )
    def test_app_config_returns_openweather_adapter(self):
        config = apps.get_app_config("weather")
        config.__dict__.pop("weather_data_adapter", None)

        adapter = config.get_weather_data_adapter()

        self.assertIsInstance(adapter, OpenWeatherOneCallAdapter)
        self.assertEqual(adapter.timeout, 18)


class OpenWeatherOneCallAdapterTests(SimpleTestCase):
    @patch("weather.adapters.requests.get")
    def test_adapter_maps_openweather_daily_payload_to_internal_shape(self, mock_get):
        response = Mock()
        response.json.return_value = {
            "lat": 35.71,
            "lon": 51.4,
            "timezone": "Asia/Tehran",
            "daily": [
                {
                    "dt": 1775001600,
                    "temp": {"min": 12.0, "max": 24.0, "day": 19.0},
                    "humidity": 44,
                    "wind_speed": 5.0,
                    "pop": 0.35,
                    "rain": 2.4,
                    "clouds": 25,
                    "uvi": 7.0,
                    "weather": [{"id": 500, "main": "Rain"}],
                }
            ],
        }
        mock_get.return_value = response

        adapter = OpenWeatherOneCallAdapter(
            base_url="https://api.openweathermap.org/data/3.0/onecall",
            api_key="test-key",
            timeout=10,
        )
        payload = adapter.fetch_forecast(35.71, 51.4, days=1)

        self.assertEqual(payload["daily"]["time"], ["2026-04-01"])
        self.assertEqual(payload["daily"]["temperature_2m_mean"], [19.0])
        self.assertEqual(payload["daily"]["precipitation_sum"], [2.4])
        self.assertEqual(payload["daily"]["precipitation_probability_max"], [35.0])
        self.assertEqual(payload["daily"]["wind_speed_10m_max"], [18.0])
        self.assertEqual(payload["daily"]["weather_code"], [61])


@override_settings(WEATHER_DATA_PROVIDER="mock", WEATHER_MOCK_DELAY_SECONDS=0)
class WeatherServiceTests(TestCase):
    def setUp(self):
        self.location = SoilLocation.objects.create(
            latitude="35.710000",
            longitude="51.400000",
        )

    def test_fetch_weather_from_api_uses_mock_provider(self):
        payload = fetch_weather_from_api(35.71, 51.4)

        self.assertIn("daily", payload)
        self.assertEqual(len(payload["daily"]["time"]), 7)

    def test_update_weather_for_location_persists_seven_days(self):
        result = update_weather_for_location(self.location)

        self.assertEqual(result["status"], "success")
        self.assertEqual(result["days_updated"], 7)
        self.assertEqual(
            WeatherForecast.objects.filter(location=self.location).count(),
            7,
        )
        self.assertTrue(
            WeatherForecast.objects.filter(
                location=self.location,
                precipitation__isnull=False,
                weather_code__isnull=False,
            ).exists()
        )
