"""
Test sensor-to-timer assignment correctness.

This tests the CRITICAL mapping between sensor entities and update timers:
- TIME_SENSITIVE sensors → Timer #2 (quarter-hour: :00, :15, :30, :45)
- MINUTE_UPDATE sensors → Timer #3 (minute: :00, :30)
- All other sensors → No timer (only update on API data arrival)

Ensures:
1. Each sensor is assigned to the correct timer
2. Timer constants match sensor definitions
3. No sensors are missing from or incorrectly added to timer groups
"""

from custom_components.tibber_prices.binary_sensor.definitions import (
    ENTITY_DESCRIPTIONS as BINARY_SENSOR_ENTITY_DESCRIPTIONS,
)
from custom_components.tibber_prices.coordinator.constants import MINUTE_UPDATE_ENTITY_KEYS, TIME_SENSITIVE_ENTITY_KEYS
from custom_components.tibber_prices.sensor.definitions import ENTITY_DESCRIPTIONS


def test_time_sensitive_sensors_are_valid() -> None:
    """
    Test that all TIME_SENSITIVE_ENTITY_KEYS correspond to actual sensors.

    Timer #2 (quarter-hour) should only trigger for sensors that exist.
    """
    all_sensor_keys = {desc.key for desc in ENTITY_DESCRIPTIONS}
    all_binary_sensor_keys = {desc.key for desc in BINARY_SENSOR_ENTITY_DESCRIPTIONS}
    all_entity_keys = all_sensor_keys | all_binary_sensor_keys

    for entity_key in TIME_SENSITIVE_ENTITY_KEYS:
        assert entity_key in all_entity_keys, (
            f"TIME_SENSITIVE key '{entity_key}' not found in sensor/binary_sensor definitions"
        )


def test_minute_update_sensors_are_valid() -> None:
    """
    Test that all MINUTE_UPDATE_ENTITY_KEYS correspond to actual sensors.

    Timer #3 (minute) should only trigger for sensors that exist.
    """
    all_sensor_keys = {desc.key for desc in ENTITY_DESCRIPTIONS}
    all_binary_sensor_keys = {desc.key for desc in BINARY_SENSOR_ENTITY_DESCRIPTIONS}
    all_entity_keys = all_sensor_keys | all_binary_sensor_keys

    for entity_key in MINUTE_UPDATE_ENTITY_KEYS:
        assert entity_key in all_entity_keys, (
            f"MINUTE_UPDATE key '{entity_key}' not found in sensor/binary_sensor definitions"
        )


def test_no_overlap_between_timer_groups() -> None:
    """
    Test that TIME_SENSITIVE and MINUTE_UPDATE groups are mutually exclusive.

    A sensor should never be in both timer groups simultaneously.
    This would cause duplicate updates and wasted resources.
    """
    overlap = TIME_SENSITIVE_ENTITY_KEYS & MINUTE_UPDATE_ENTITY_KEYS

    assert not overlap, (
        f"Sensors should not be in both TIME_SENSITIVE and MINUTE_UPDATE: {overlap}\n"
        "Each sensor should use only ONE timer for updates."
    )


def test_interval_sensors_use_quarter_hour_timer() -> None:
    """
    Test that interval-based sensors (current/next/previous) use Timer #2.

    These sensors need updates every 15 minutes because they reference
    specific 15-minute intervals that change at quarter-hour boundaries.
    """
    interval_sensors = [
        "current_interval_price",
        "current_interval_price_base",  # Energy Dashboard version (€/kWh)
        "next_interval_price",
        "previous_interval_price",
        "current_interval_price_level",
        "next_interval_price_level",
        "previous_interval_price_level",
        "current_interval_price_rating",
        "next_interval_price_rating",
        "previous_interval_price_rating",
    ]

    for sensor_key in interval_sensors:
        assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Interval sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
        )


def test_rolling_hour_sensors_use_quarter_hour_timer() -> None:
    """
    Test that rolling hour sensors (5-interval windows) use Timer #2.

    Rolling hour calculations depend on current interval position,
    which changes every 15 minutes.
    """
    rolling_hour_sensors = [
        "current_hour_average_price",
        "next_hour_average_price",
        "current_hour_price_level",
        "next_hour_price_level",
        "current_hour_price_rating",
        "next_hour_price_rating",
    ]

    for sensor_key in rolling_hour_sensors:
        assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Rolling hour sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
        )


def test_future_mean_sensors_use_quarter_hour_timer() -> None:
    """
    Test that future N-hour mean sensors use Timer #2.

    Future means calculate rolling windows starting from "next interval",
    which changes every 15 minutes.
    """
    future_mean_sensors = [
        "next_avg_1h",
        "next_avg_2h",
        "next_avg_3h",
        "next_avg_4h",
        "next_avg_5h",
        "next_avg_6h",
        "next_avg_8h",
        "next_avg_12h",
    ]

    for sensor_key in future_mean_sensors:
        assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Future mean sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
        )


def test_trend_sensors_use_quarter_hour_timer() -> None:
    """
    Test that price outlook/trajectory sensors use Timer #2.

    Trend analysis depends on current interval position and
    needs updates at quarter-hour boundaries.
    """
    trend_sensors = [
        "current_price_trend",
        "next_price_trend_change",
        "price_outlook_1h",
        "price_outlook_2h",
        "price_outlook_3h",
        "price_outlook_4h",
        "price_outlook_5h",
        "price_outlook_6h",
        "price_outlook_8h",
        "price_outlook_12h",
        "price_trajectory_2h",
        "price_trajectory_3h",
        "price_trajectory_4h",
        "price_trajectory_5h",
        "price_trajectory_6h",
        "price_trajectory_8h",
        "price_trajectory_12h",
    ]

    for sensor_key in trend_sensors:
        assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Trend sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
        )


def test_window_24h_sensors_use_quarter_hour_timer() -> None:
    """
    Test that trailing/leading 24h window sensors use Timer #2.

    24h windows are calculated relative to current interval,
    which changes every 15 minutes.
    """
    window_24h_sensors = [
        "trailing_price_average",
        "leading_price_average",
        "trailing_price_min",
        "trailing_price_max",
        "leading_price_min",
        "leading_price_max",
    ]

    for sensor_key in window_24h_sensors:
        assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
            f"24h window sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
        )


def test_period_binary_sensors_use_quarter_hour_timer() -> None:
    """
    Test that best/peak price period binary sensors use Timer #2.

    Binary sensors check if current time is within a period.
    Periods can only change at quarter-hour interval boundaries.
    """
    period_binary_sensors = [
        "best_price_period",
        "peak_price_period",
    ]

    for sensor_key in period_binary_sensors:
        assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Period binary sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
        )


def test_period_timestamp_sensors_use_quarter_hour_timer() -> None:
    """
    Test that period timestamp sensors (end_time, next_start_time) use Timer #2.

    Timestamp sensors report when periods end/start. Since periods can only
    change at quarter-hour boundaries (intervals), they only need quarter-hour updates.
    """
    timestamp_sensors = [
        "best_price_end_time",
        "best_price_next_start_time",
        "peak_price_end_time",
        "peak_price_next_start_time",
    ]

    for sensor_key in timestamp_sensors:
        assert sensor_key in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Timestamp sensor '{sensor_key}' should be TIME_SENSITIVE (Timer #2)"
        )


def test_timing_sensors_use_minute_timer() -> None:
    """
    Test that countdown/progress timing sensors use Timer #3.

    These sensors track time remaining and progress percentage within periods.
    They need minute-by-minute updates for accurate countdown displays.

    IMPORTANT: Timestamp sensors (end_time, next_start_time) do NOT use Timer #3
    because periods can only change at quarter-hour boundaries.
    """
    timing_sensors = [
        "best_price_remaining_minutes",
        "best_price_progress",
        "best_price_next_in_minutes",  # Corrected from best_price_next_start_minutes
        "peak_price_remaining_minutes",
        "peak_price_progress",
        "peak_price_next_in_minutes",  # Corrected from peak_price_next_start_minutes
    ]

    for sensor_key in timing_sensors:
        assert sensor_key in MINUTE_UPDATE_ENTITY_KEYS, (
            f"Timing sensor '{sensor_key}' should be MINUTE_UPDATE (Timer #3)"
        )

        # Also verify it's NOT in TIME_SENSITIVE (no double updates)
        assert sensor_key not in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Timing sensor '{sensor_key}' should NOT be in TIME_SENSITIVE\n"
            "Minute updates are sufficient for countdown/progress tracking."
        )


def test_lifecycle_sensor_uses_quarter_hour_timer_with_state_filter() -> None:
    """
    Test that data lifecycle status sensor uses Timer #2 WITH state-change filtering.

    The lifecycle sensor needs quarter-hour precision for detecting:
    - 23:45: turnover_pending (last interval before midnight)
    - 00:00: turnover complete (after midnight API update)
    - 13:00: searching_tomorrow (when tomorrow data search begins)

    To prevent recorder spam, it uses state-change filtering in both:
    - _handle_coordinator_update() (Timer #1)
    - _handle_time_sensitive_update() (Timer #2)

    State is only written to recorder if it actually changed.
    This reduces recorder entries from ~96/day to ~10-15/day.
    """
    # Lifecycle sensor MUST be in TIME_SENSITIVE_ENTITY_KEYS for quarter-hour precision
    assert "data_lifecycle_status" in TIME_SENSITIVE_ENTITY_KEYS, (
        "Lifecycle sensor needs quarter-hour updates for precise state transitions\n"
        "at 23:45 (turnover_pending), 00:00 (turnover complete), 13:00 (searching_tomorrow).\n"
        "State-change filter in _handle_time_sensitive_update() prevents recorder spam."
    )


def test_daily_stat_sensors_not_in_timers() -> None:
    """
    Test that daily statistic sensors (min/max/avg) do NOT use timers.

    Daily stats don't depend on current time - they represent full-day aggregates.
    They only need updates when new API data arrives (not time-dependent).
    """
    daily_stat_sensors = [
        # Today/tomorrow min prices
        "daily_min_price_today",
        "daily_min_price_tomorrow",
        # Today/tomorrow max prices
        "daily_max_price_today",
        "daily_max_price_tomorrow",
        # Today/tomorrow averages
        "daily_average_price_today",
        "daily_average_price_tomorrow",
        # Daily price levels
        "daily_price_level_today",
        "daily_price_level_tomorrow",
        # Daily price ratings
        "daily_price_rating_today",
        "daily_price_rating_tomorrow",
    ]

    for sensor_key in daily_stat_sensors:
        assert sensor_key not in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Daily stat sensor '{sensor_key}' should NOT use Timer #2\n"
            "Daily statistics don't depend on current time - only on API data arrival."
        )
        assert sensor_key not in MINUTE_UPDATE_ENTITY_KEYS, (
            f"Daily stat sensor '{sensor_key}' should NOT use Timer #3\n"
            "Daily statistics don't need minute-by-minute updates."
        )


def test_volatility_sensors_not_in_timers() -> None:
    """
    Test that volatility sensors do NOT use timers.

    Volatility analyzes price variation over fixed time windows.
    Values only change when new API data arrives (not time-dependent).
    """
    volatility_sensors = [
        "today_volatility_level",
        "tomorrow_volatility_level",
        "yesterday_volatility_level",
        "next_24h_volatility_level",
    ]

    for sensor_key in volatility_sensors:
        assert sensor_key not in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Volatility sensor '{sensor_key}' should NOT use Timer #2\n"
            "Volatility calculates over fixed time windows - not time-dependent."
        )
        assert sensor_key not in MINUTE_UPDATE_ENTITY_KEYS, (
            f"Volatility sensor '{sensor_key}' should NOT use Timer #3\n"
            "Volatility doesn't need minute-by-minute updates."
        )


def test_diagnostic_sensors_not_in_timers() -> None:
    """
    Test that diagnostic/metadata sensors do NOT use timers.

    Diagnostic sensors report static metadata or system state.
    They only update when configuration changes or new API data arrives.
    """
    diagnostic_sensors = [
        "data_last_updated",
        "home_id",
        "currency_code",
        "price_unit",
        "grid_company",
        "price_level",
        "address_line1",
        "address_line2",
        "address_line3",
        "zip_code",
        "city",
        "country",
        "latitude",
        "longitude",
        "time_zone",
        "estimated_annual_consumption",
        "subscription_status",
        "chart_data_export",
    ]

    for sensor_key in diagnostic_sensors:
        # Skip data_lifecycle_status - it needs quarter-hour updates
        if sensor_key == "data_lifecycle_status":
            continue

        assert sensor_key not in TIME_SENSITIVE_ENTITY_KEYS, (
            f"Diagnostic sensor '{sensor_key}' should NOT use Timer #2\nDiagnostic data doesn't depend on current time."
        )
        assert sensor_key not in MINUTE_UPDATE_ENTITY_KEYS, (
            f"Diagnostic sensor '{sensor_key}' should NOT use Timer #3\n"
            "Diagnostic data doesn't need minute-by-minute updates."
        )


def test_timer_constants_are_comprehensive() -> None:
    """
    Test that timer constants account for all time-dependent sensors.

    Verifies no time-dependent sensors are missing from timer groups.
    This is a safety check to catch sensors that need timers but don't have them.
    """
    all_sensor_keys = {desc.key for desc in ENTITY_DESCRIPTIONS}
    all_binary_sensor_keys = {desc.key for desc in BINARY_SENSOR_ENTITY_DESCRIPTIONS}
    all_entity_keys = all_sensor_keys | all_binary_sensor_keys
    sensors_with_timers = TIME_SENSITIVE_ENTITY_KEYS | MINUTE_UPDATE_ENTITY_KEYS

    # Expected time-dependent sensor patterns
    time_dependent_patterns = [
        "current_",
        "next_",
        "previous_",
        "trailing_",
        "leading_",
        "_remaining_",
        "_progress",
        "_next_in_",  # Corrected from _next_start_
        "_end_time",
        "_period",  # Binary sensors checking if NOW is in period
        "price_trend_",
        "next_avg_",
    ]

    # Known exceptions that look time-dependent but aren't
    known_exceptions = {
        "data_last_updated",  # Timestamp of last update, not time-dependent
        "next_24h_volatility",  # Uses fixed 24h window from current time, updated on API data
        "best_price_period_duration",  # Duration state in hours (static per period), no minute-by-minute timer
        "peak_price_period_duration",  # Duration state in hours (static per period), no minute-by-minute timer
    }

    potentially_missing = [
        sensor_key
        for sensor_key in all_entity_keys
        if (
            any(pattern in sensor_key for pattern in time_dependent_patterns)
            and sensor_key not in sensors_with_timers
            and sensor_key not in known_exceptions
        )
    ]

    assert not potentially_missing, (
        f"These sensors appear time-dependent but aren't in any timer group:\n"
        f"{potentially_missing}\n\n"
        "If they truly need time-based updates, add them to TIME_SENSITIVE_ENTITY_KEYS\n"
        "or MINUTE_UPDATE_ENTITY_KEYS in coordinator/constants.py"
    )


def test_timer_group_sizes() -> None:
    """
    Test timer group sizes as documentation/regression detection.

    This isn't a strict requirement, but significant changes in group sizes
    might indicate accidental additions/removals.
    """
    # As of Apr 2026 (added 5 price-phase countdown sensors)
    expected_time_sensitive_min = 40  # At least 40 sensors
    expected_minute_update = 12  # 7 original + 5 price-phase countdown sensors

    assert len(TIME_SENSITIVE_ENTITY_KEYS) >= expected_time_sensitive_min, (
        f"Expected at least {expected_time_sensitive_min} TIME_SENSITIVE sensors, got {len(TIME_SENSITIVE_ENTITY_KEYS)}"
    )

    assert len(MINUTE_UPDATE_ENTITY_KEYS) == expected_minute_update, (
        f"Expected exactly {expected_minute_update} MINUTE_UPDATE sensors, got {len(MINUTE_UPDATE_ENTITY_KEYS)}"
    )
