"""Tests for Bug #7: Min/Max functions returning 0.0 instead of None.""" from datetime import UTC, datetime, timedelta import pytest from custom_components.tibber_prices.coordinator.time_service import ( TibberPricesTimeService, ) from custom_components.tibber_prices.utils.average import ( calculate_leading_24h_max, calculate_leading_24h_min, calculate_trailing_24h_max, calculate_trailing_24h_min, ) @pytest.fixture def time_service() -> TibberPricesTimeService: """Create a TibberPricesTimeService instance for testing.""" return TibberPricesTimeService() def test_trailing_24h_min_with_empty_window(time_service: TibberPricesTimeService) -> None: """ Test that trailing min returns None when no data in window. Bug #7: Previously returned 0.0, which could be misinterpreted as a maximum value with negative prices. """ # Data exists, but outside the 24h window old_data = [ { "startsAt": datetime(2025, 11, 1, 10, 0, tzinfo=UTC), # 20 days ago "total": -15.0, # Negative price } ] interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) # Today # Should return None (no data in window), not 0.0 result = calculate_trailing_24h_min(old_data, interval_start, time=time_service) assert result is None def test_trailing_24h_max_with_empty_window(time_service: TibberPricesTimeService) -> None: """Test that trailing max returns None when no data in window.""" old_data = [ { "startsAt": datetime(2025, 11, 1, 10, 0, tzinfo=UTC), # 20 days ago "total": -15.0, } ] interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) result = calculate_trailing_24h_max(old_data, interval_start, time=time_service) assert result is None def test_leading_24h_min_with_empty_window(time_service: TibberPricesTimeService) -> None: """Test that leading min returns None when no data in window.""" old_data = [ { "startsAt": datetime(2025, 11, 1, 10, 0, tzinfo=UTC), # Past data "total": -15.0, } ] interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) # Today - no future data result = calculate_leading_24h_min(old_data, interval_start, time=time_service) assert result is None def test_leading_24h_max_with_empty_window(time_service: TibberPricesTimeService) -> None: """Test that leading max returns None when no data in window.""" old_data = [ { "startsAt": datetime(2025, 11, 1, 10, 0, tzinfo=UTC), # Past data "total": -15.0, } ] interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) result = calculate_leading_24h_max(old_data, interval_start, time=time_service) assert result is None def test_trailing_24h_min_with_negative_prices(time_service: TibberPricesTimeService) -> None: """ Test trailing min with negative prices returns actual minimum, not 0.0. This demonstrates why Bug #7 was critical: with negative prices, 0.0 would appear to be a maximum, not an error indicator. """ interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) window_start = interval_start - timedelta(hours=24) # All negative prices data = [{"startsAt": window_start + timedelta(hours=i), "total": -10.0 - i} for i in range(24)] result = calculate_trailing_24h_min(data, interval_start, time=time_service) # Should return actual minimum (-33.0), not 0.0 assert result == -33.0 assert result != 0.0 # Emphasize this was the bug def test_trailing_24h_max_with_negative_prices(time_service: TibberPricesTimeService) -> None: """Test trailing max with negative prices.""" interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) window_start = interval_start - timedelta(hours=24) # All negative prices data = [{"startsAt": window_start + timedelta(hours=i), "total": -10.0 - i} for i in range(24)] result = calculate_trailing_24h_max(data, interval_start, time=time_service) # Maximum of negative numbers is least negative assert result == -10.0 def test_trailing_24h_min_distinguishes_zero_from_none( time_service: TibberPricesTimeService, ) -> None: """ Test that function distinguishes between 0.0 price and no data. Bug #7: Previously, both cases returned 0.0, making them indistinguishable. """ interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) window_start = interval_start - timedelta(hours=24) # Case 1: Price is actually 0.0 data_with_zero = [{"startsAt": window_start + timedelta(hours=i), "total": 0.0 + i} for i in range(24)] result_with_zero = calculate_trailing_24h_min(data_with_zero, interval_start, time=time_service) assert result_with_zero == 0.0 # Actual price # Case 2: No data in window empty_data: list[dict] = [] result_no_data = calculate_trailing_24h_min(empty_data, interval_start, time=time_service) assert result_no_data is None # No data # CRITICAL: These must be distinguishable! assert result_with_zero != result_no_data def test_trailing_24h_functions_with_partial_window( time_service: TibberPricesTimeService, ) -> None: """ Test that functions work correctly with partial 24h window. This tests the edge case where data exists but doesn't cover full 24h. """ interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) window_start = interval_start - timedelta(hours=24) # Only 12 hours of data (half the window) data = [{"startsAt": window_start + timedelta(hours=i), "total": float(i)} for i in range(12)] result_min = calculate_trailing_24h_min(data, interval_start, time=time_service) result_max = calculate_trailing_24h_max(data, interval_start, time=time_service) # Should calculate based on available data assert result_min == 0.0 assert result_max == 11.0 def test_leading_24h_functions_with_negative_and_positive_mix( time_service: TibberPricesTimeService, ) -> None: """Test leading functions with mix of negative and positive prices.""" interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) # Mix of negative and positive prices data = [{"startsAt": interval_start + timedelta(hours=i), "total": -10.0 + i} for i in range(24)] result_min = calculate_leading_24h_min(data, interval_start, time=time_service) result_max = calculate_leading_24h_max(data, interval_start, time=time_service) assert result_min == -10.0 # Most negative assert result_max == 13.0 # Most positive def test_empty_price_list_returns_none(time_service: TibberPricesTimeService) -> None: """Test that all functions return None with completely empty price list.""" interval_start = datetime(2025, 11, 21, 14, 0, tzinfo=UTC) empty_data: list[dict] = [] assert calculate_trailing_24h_min(empty_data, interval_start, time=time_service) is None assert calculate_trailing_24h_max(empty_data, interval_start, time=time_service) is None assert calculate_leading_24h_min(empty_data, interval_start, time=time_service) is None assert calculate_leading_24h_max(empty_data, interval_start, time=time_service) is None