"""
Unit tests for next_api_poll_time calculation logic.

Tests the precise minute/second offset calculation for Timer #1 scheduling,
ensuring accurate prediction of when the next API poll will occur.
"""

from __future__ import annotations

from datetime import datetime
from unittest.mock import Mock
from zoneinfo import ZoneInfo

import pytest

from custom_components.tibber_prices.coordinator.constants import UPDATE_INTERVAL
from custom_components.tibber_prices.sensor.calculators.lifecycle import TibberPricesLifecycleCalculator


@pytest.mark.unit
def test_next_api_poll_before_13_with_timer_offset() -> None:
    """
    Test next_api_poll before 13:00 with known timer offset.

    Scenario: Timer runs at X:04:37 (4 minutes 37 seconds past quarter-hour)
    Current time: 10:19:37 (before 13:00)
    Expected: Next poll at 13:04:37 (first timer execution at or after 13:00)
    """
    # Mock coordinator with timer history
    coordinator = Mock()
    coordinator.time = Mock()

    # Current time: 10:19:37 (Timer just ran)
    current_time = datetime(2025, 11, 22, 10, 19, 37, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.now.return_value = current_time
    coordinator.time.as_local.side_effect = lambda dt: dt

    # Mock get_day_boundaries (needed to determine if tomorrow data is missing)
    today_midnight = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    tomorrow_midnight = datetime(2025, 11, 23, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.get_day_boundaries.return_value = (today_midnight, tomorrow_midnight)

    # Timer last ran at 10:19:37 (offset: 4 min 37 sec past quarter)
    coordinator._last_coordinator_update = current_time  # noqa: SLF001

    # Mock coordinator.data (no tomorrow data yet)
    coordinator.data = {"priceInfo": {"today": [1, 2, 3], "tomorrow": []}}

    # Mock _needs_tomorrow_data (not relevant for this case)
    coordinator._needs_tomorrow_data.return_value = False  # noqa: SLF001

    # Create calculator
    calculator = TibberPricesLifecycleCalculator(coordinator)

    # Calculate next poll
    next_poll = calculator.get_next_api_poll_time()

    # Should be 13:04:37 (first timer at or after 13:00 with same offset)
    assert next_poll is not None
    assert next_poll.hour == 13
    assert next_poll.minute == 4
    assert next_poll.second == 37


@pytest.mark.unit
def test_next_api_poll_before_13_different_offset() -> None:
    """
    Test next_api_poll with different timer offset.

    Scenario: Timer runs at X:11:22 (11 minutes 22 seconds past quarter-hour)
    Current time: 09:26:22
    Expected: Next poll at 13:11:22
    """
    coordinator = Mock()
    coordinator.time = Mock()

    current_time = datetime(2025, 11, 22, 9, 26, 22, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.now.return_value = current_time
    coordinator.time.as_local.side_effect = lambda dt: dt

    # Mock get_day_boundaries
    today_midnight = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    tomorrow_midnight = datetime(2025, 11, 23, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.get_day_boundaries.return_value = (today_midnight, tomorrow_midnight)

    coordinator._last_coordinator_update = current_time  # noqa: SLF001
    coordinator.data = {"priceInfo": {"today": [1, 2, 3], "tomorrow": []}}
    coordinator._needs_tomorrow_data.return_value = False  # noqa: SLF001

    calculator = TibberPricesLifecycleCalculator(coordinator)
    next_poll = calculator.get_next_api_poll_time()

    assert next_poll is not None
    assert next_poll.hour == 13
    assert next_poll.minute == 11
    assert next_poll.second == 22


@pytest.mark.unit
def test_next_api_poll_before_13_offset_requires_14xx() -> None:
    """
    Test next_api_poll when timer offset doesn't fit in 13:xx hour.

    Scenario: Timer runs at X:58:15 (58 minutes past hour, 13 min past 45-min mark)
    Current time: 11:58:15
    Expected: Next poll at 13:13:15 (13:00+13min, 13:15+13min, 13:30+13min, 13:45+13min)
    Note: Even extreme offsets fit in 13:xx hour, 14:xx overflow is theoretical edge case
    """
    coordinator = Mock()
    coordinator.time = Mock()

    current_time = datetime(2025, 11, 22, 11, 58, 15, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.now.return_value = current_time
    coordinator.time.as_local.side_effect = lambda dt: dt

    # Mock get_day_boundaries
    today_midnight = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    tomorrow_midnight = datetime(2025, 11, 23, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.get_day_boundaries.return_value = (today_midnight, tomorrow_midnight)

    # Timer offset: 58 % 15 = 13 minutes past quarter-hour
    coordinator._last_coordinator_update = current_time  # noqa: SLF001
    coordinator.data = {"priceInfo": {"today": [1, 2, 3], "tomorrow": []}}
    coordinator._needs_tomorrow_data.return_value = False  # noqa: SLF001

    calculator = TibberPricesLifecycleCalculator(coordinator)
    next_poll = calculator.get_next_api_poll_time()

    # Even with 13-minute offset, first valid is 13:13:15
    assert next_poll is not None
    assert next_poll.hour == 13
    assert next_poll.minute == 13
    assert next_poll.second == 15


@pytest.mark.unit
def test_next_api_poll_before_13_no_timer_history() -> None:
    """
    Test next_api_poll fallback when no timer history exists.

    Scenario: Integration just started, no _last_coordinator_update yet
    Current time: 10:30:00
    Expected: Fallback to 13:00:00
    """
    coordinator = Mock()
    coordinator.time = Mock()

    current_time = datetime(2025, 11, 22, 10, 30, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.now.return_value = current_time
    coordinator.time.as_local.side_effect = lambda dt: dt

    # Mock get_day_boundaries
    today_midnight = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    tomorrow_midnight = datetime(2025, 11, 23, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.get_day_boundaries.return_value = (today_midnight, tomorrow_midnight)

    # No timer history
    coordinator._last_coordinator_update = None  # noqa: SLF001
    coordinator.data = {"priceInfo": {"today": [1, 2, 3], "tomorrow": []}}
    coordinator._needs_tomorrow_data.return_value = False  # noqa: SLF001

    calculator = TibberPricesLifecycleCalculator(coordinator)
    next_poll = calculator.get_next_api_poll_time()

    # Should fallback to 13:00:00
    assert next_poll is not None
    assert next_poll.hour == 13
    assert next_poll.minute == 0
    assert next_poll.second == 0


@pytest.mark.unit
def test_next_api_poll_after_13_tomorrow_missing() -> None:
    """
    Test next_api_poll after 13:00 when tomorrow data is missing.

    Scenario: After 13:00, actively polling for tomorrow data
    Current time: 14:30:00
    Last update: 14:15:45
    Expected: Last update + UPDATE_INTERVAL (15 minutes) = 14:30:45
    """
    coordinator = Mock()
    coordinator.time = Mock()

    current_time = datetime(2025, 11, 22, 14, 30, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    last_update = datetime(2025, 11, 22, 14, 15, 45, tzinfo=ZoneInfo("Europe/Oslo"))

    coordinator.time.now.return_value = current_time
    coordinator.time.as_local.side_effect = lambda dt: dt

    # Mock get_day_boundaries
    today_midnight = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    tomorrow_midnight = datetime(2025, 11, 23, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.get_day_boundaries.return_value = (today_midnight, tomorrow_midnight)

    coordinator._last_coordinator_update = last_update  # noqa: SLF001
    coordinator.data = {"priceInfo": {"today": [1, 2, 3], "tomorrow": []}}  # Tomorrow missing!
    coordinator._needs_tomorrow_data.return_value = True  # noqa: SLF001 - Tomorrow missing!

    calculator = TibberPricesLifecycleCalculator(coordinator)
    next_poll = calculator.get_next_api_poll_time()

    # Should be last_update + 15 minutes
    expected = last_update + UPDATE_INTERVAL
    assert next_poll is not None
    assert next_poll == expected
    assert next_poll.minute == 30
    assert next_poll.second == 45


@pytest.mark.unit
def test_next_api_poll_after_13_tomorrow_present() -> None:
    """
    Test next_api_poll after 13:00 when tomorrow data is present.

    Scenario: After 13:00, tomorrow data fetched, predicting tomorrow's first poll
    Current time: 15:34:12
    Timer offset: 4 minutes 12 seconds past quarter (from 15:34:12)
    Expected: Tomorrow at 13:04:12
    """
    coordinator = Mock()
    coordinator.time = Mock()

    current_time = datetime(2025, 11, 22, 15, 34, 12, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.now.return_value = current_time
    coordinator.time.as_local.side_effect = lambda dt: dt

    # Mock get_day_boundaries
    today_midnight = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    tomorrow_midnight = datetime(2025, 11, 23, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.get_day_boundaries.return_value = (today_midnight, tomorrow_midnight)

    # Timer offset: 34 % 15 = 4 minutes past quarter-hour
    coordinator._last_coordinator_update = current_time  # noqa: SLF001
    coordinator.data = {"priceInfo": {"today": [1, 2, 3], "tomorrow": [4, 5, 6]}}  # Tomorrow present!
    coordinator._needs_tomorrow_data.return_value = False  # noqa: SLF001 - Tomorrow present!

    calculator = TibberPricesLifecycleCalculator(coordinator)
    next_poll = calculator.get_next_api_poll_time()

    # Should be tomorrow at 13:04:12
    assert next_poll is not None
    assert next_poll.day == 23  # Tomorrow
    assert next_poll.hour == 13
    assert next_poll.minute == 4
    assert next_poll.second == 12


@pytest.mark.unit
def test_next_api_poll_exact_13_00_boundary() -> None:
    """
    Test next_api_poll exactly at 13:00:00 boundary.

    Scenario: Timer runs exactly at 13:00:00 (offset: 0 min 0 sec)
    Current time: 13:00:00
    Expected: 13:00:00 (current time matches first valid slot)
    """
    coordinator = Mock()
    coordinator.time = Mock()

    current_time = datetime(2025, 11, 22, 13, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.now.return_value = current_time
    coordinator.time.as_local.side_effect = lambda dt: dt

    # Mock get_day_boundaries
    today_midnight = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    tomorrow_midnight = datetime(2025, 11, 23, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.get_day_boundaries.return_value = (today_midnight, tomorrow_midnight)

    # Timer runs at exact quarter-hour boundaries
    coordinator._last_coordinator_update = current_time  # noqa: SLF001
    coordinator.data = {"priceInfo": {"today": [1, 2, 3], "tomorrow": []}}
    coordinator._needs_tomorrow_data.return_value = False  # noqa: SLF001

    calculator = TibberPricesLifecycleCalculator(coordinator)
    next_poll = calculator.get_next_api_poll_time()

    # Should be 13:00:00 (first valid slot)
    assert next_poll is not None
    assert next_poll.hour == 13
    assert next_poll.minute == 0
    assert next_poll.second == 0


@pytest.mark.unit
def test_next_api_poll_offset_spans_multiple_quarters() -> None:
    """
    Test timer offset calculation across different quarter-hour marks.

    Scenario: Timer at 12:47:33 (offset: 2 min 33 sec past 45-min mark)
    Expected: 13:02:33, 13:17:33, 13:32:33, or 13:47:33 depending on >= 13:00
    Result: First valid is 13:02:33
    """
    coordinator = Mock()
    coordinator.time = Mock()

    current_time = datetime(2025, 11, 22, 12, 47, 33, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.now.return_value = current_time
    coordinator.time.as_local.side_effect = lambda dt: dt

    # Mock get_day_boundaries
    today_midnight = datetime(2025, 11, 22, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    tomorrow_midnight = datetime(2025, 11, 23, 0, 0, 0, tzinfo=ZoneInfo("Europe/Oslo"))
    coordinator.time.get_day_boundaries.return_value = (today_midnight, tomorrow_midnight)

    # Timer offset: 47 % 15 = 2 minutes past quarter
    coordinator._last_coordinator_update = current_time  # noqa: SLF001
    coordinator.data = {"priceInfo": {"today": [1, 2, 3], "tomorrow": []}}
    coordinator._needs_tomorrow_data.return_value = False  # noqa: SLF001

    calculator = TibberPricesLifecycleCalculator(coordinator)
    next_poll = calculator.get_next_api_poll_time()

    # Should be 13:02:33 (first quarter-hour slot >= 13:00 with offset 2:33)
    assert next_poll is not None
    assert next_poll.hour == 13
    assert next_poll.minute == 2
    assert next_poll.second == 33
