"""Tests for activity evaluators.""" from __future__ import annotations import pytest from datetime import date, timedelta from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch from weather_or_not.activities import DataStore, Verdict from weather_or_not.motorcycle import MotorcycleActivity from weather_or_not.lawn import LawnActivity from weather_or_not.weather import WeatherForecast def _make_weather( temp: float = 20, wind: float = 10, humidity: float = 50, weather_code: int = 0, precip_today: float = 0, precip_24h: float = 0, precip_2h: float = 0, ) -> WeatherForecast: """Create a minimal WeatherForecast for testing.""" return WeatherForecast( latitude=40.7128, longitude=-74.0060, generationtime_ms=10.0, utc_offset_seconds=-14400, timezone="America/New_York", timezone_abbreviation="EDT", elevation=10.0, current={ "temperature_4m": temp, "wind_speed_10m": wind, "relative_humidity_2m": humidity, "weather_code": weather_code, "precipitation": 0, "is_day": 1, "pressure_msl": 1013, }, hourly={ "time": [ "2026-04-29T10:00:00", "2026-04-29T11:00:00", "2026-04-29T12:00:00", ], "temperature_2m": [temp, temp + 1, temp + 2], "relative_humidity_2m": [humidity, humidity - 5, humidity - 10], "precipitation": [0, 0, 0], "wind_speed_10m": [wind, wind - 1, wind - 2], "weather_code": [0, 0, 0], "apparent_temperature": [temp, temp + 1, temp + 2], }, daily={ "time": ["2026-04-29"], "precipitation_sum": [precip_today], "temperature_2m_max": [temp + 5], "temperature_2m_min": [temp - 5], }, ) class TestDataStore: def test_never_done(self, tmp_path): store = DataStore(tmp_path) assert store.days_since_activity("mow_lawn") == 999 def test_record_and_check(self, tmp_path): store = DataStore(tmp_path) store.record_activity_performed("mow_lawn", date(2026, 4, 25)) assert store.days_since_activity("mow_lawn") == 4 # Apr 29 - Apr 25 def test_stats(self, tmp_path): store = DataStore(tmp_path) store.record_activity_performed("mow_lawn", date(2026, 4, 25)) stats = store.get_activity_stats("mow_lawn") assert stats["times_performed"] == 1 assert stats["last_performed"] == "2026-04-25" def test_persistence(self, tmp_path): """Data persists across DataStore instances.""" store1 = DataStore(tmp_path) store1.record_activity_performed("mow_lawn", date(2026, 4, 20)) del store1 store2 = DataStore(tmp_path) assert store2.last_activity_date("mow_lawn") == date(2026, 4, 20) class TestMotorcycleActivity: def setup_method(self): self.config = { "min_temp_c": 10, "max_temp_c": 35, "max_precipitation_mm": 0, "max_wind_speed_kmh": 30, "max_humidity_pct": 90, "no_ice": True, "no_wet_roads": True, } self.data_store = DataStore(Path("/tmp/test-won-data")) self.activity = MotorcycleActivity(self.config, self.data_store) @pytest.mark.asyncio async def test_good_conditions(self): weather = _make_weather(temp=22, wind=10, humidity=50, weather_code=0) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.GO assert all(r.passed for r in result.rules) @pytest.mark.asyncio async def test_rain(self): weather = _make_weather(precip_today=5) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.NO_GO assert any(not r.passed and "precip" in r.rule_name.lower() for r in result.rules) @pytest.mark.asyncio async def test_wind_too_strong(self): weather = _make_weather(wind=40) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.NO_GO assert any(not r.passed and "wind" in r.rule_name.lower() for r in result.rules) @pytest.mark.asyncio async def test_temp_too_cold(self): weather = _make_weather(temp=5) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.NO_GO @pytest.mark.asyncio async def test_thunderstorm(self): weather = _make_weather(weather_code=95) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.NO_GO class TestLawnActivity: def setup_method(self): self.config = { "cooldown_days": 3, "max_precipitation_today_mm": 0, "max_precipitation_past_24h_mm": 5, "max_wind_speed_kmh": 25, "min_temp_c": 15, } self.data_store = DataStore(Path("/tmp/test-lawn-data")) self.activity = LawnActivity(self.config, self.data_store) @pytest.mark.asyncio async def test_good_conditions(self): # Ensure cooldown is met self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20)) weather = _make_weather(temp=22, wind=10) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.GO @pytest.mark.asyncio async def test_cooldown_not_met(self): # Just mowed yesterday self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 28)) weather = _make_weather(temp=22, wind=10) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.NO_GO assert any(not r.passed and "cooldown" in r.rule_name.lower() for r in result.rules) @pytest.mark.asyncio async def test_ground_wet(self): self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20)) weather = _make_weather(precip_24h=10) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.NO_GO @pytest.mark.asyncio async def test_rain_today(self): self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20)) weather = _make_weather(precip_today=3) result = await self.activity.evaluate(weather) assert result.verdict == Verdict.NO_GO