"""Tests for WakeAlarm — beep loop phases, run, update timer, and flash challenge."""

from __future__ import annotations

import tkinter as tk
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import pytest

if TYPE_CHECKING:
    from collections.abc import Generator

from python_pkg.wake_alarm._alarm import (
    WakeAlarm,
)
from python_pkg.wake_alarm._constants import (
    PHASE_MEDIUM_END,
    PHASE_SOFT_END,
)


def _make_mock_tk() -> MagicMock:
    """Build a MagicMock that stands in for the tkinter module."""
    mock = MagicMock()
    mock_root = MagicMock()
    mock_root.winfo_screenwidth.return_value = 1920
    mock_root.winfo_screenheight.return_value = 1080
    mock.Tk.return_value = mock_root
    mock.Frame.return_value = MagicMock()
    mock.Label.return_value = MagicMock()
    mock.Entry.return_value = MagicMock()
    mock.TclError = tk.TclError
    mock.END = tk.END
    return mock


@pytest.fixture(autouse=True)
def _block_real_tk() -> Generator[MagicMock]:
    """Prevent any real Tk windows in tests."""
    mock = _make_mock_tk()
    with patch("python_pkg.wake_alarm._alarm.tk", mock):
        yield mock


@pytest.fixture(autouse=True)
def _block_extra_devices() -> Generator[MagicMock]:
    """Prevent real subprocess calls for extra ALSA devices and hardware."""
    with (
        patch("python_pkg.wake_alarm._alarm._play_on_extra_devices") as mock,
        patch("python_pkg.wake_alarm._alarm._max_fans", return_value=False),
        patch("python_pkg.wake_alarm._alarm._restore_fans"),
        patch("python_pkg.wake_alarm._alarm._set_max_brightness"),
        patch("python_pkg.wake_alarm._alarm._wake_display"),
        patch("python_pkg.wake_alarm._alarm._warn_if_no_real_sink"),
        patch("python_pkg.wake_alarm._alarm._activate_alarm_audio", return_value=None),
        patch("python_pkg.wake_alarm._alarm._restore_alarm_audio"),
        patch("python_pkg.wake_alarm._alarm.turn_on_plug"),
        patch("python_pkg.wake_alarm._alarm.turn_off_plug"),
    ):
        yield mock


@pytest.fixture
def mock_tk_module() -> Generator[MagicMock]:
    """Provide explicit access to the mocked tk module."""
    mock = _make_mock_tk()
    with patch("python_pkg.wake_alarm._alarm.tk", mock):
        yield mock


class TestBeepLoopPhases:
    """Tests for different beep loop escalation phases."""

    def test_medium_phase(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """Beep loop enters medium phase after PHASE_SOFT_END minutes."""
        alarm = WakeAlarm(demo_mode=True)
        # Set alarm start to make elapsed > PHASE_SOFT_END minutes
        import time as time_mod

        alarm._alarm_start = time_mod.monotonic() - (PHASE_SOFT_END + 1) * 60

        call_count = 0

        def stop_after_one(*_args: object, **_kwargs: object) -> None:
            nonlocal call_count
            call_count += 1
            if call_count >= 1:
                alarm._stop_beep.set()

        with (
            patch(
                "python_pkg.wake_alarm._alarm._beep_medium",
                side_effect=stop_after_one,
            ) as mock_beep,
        ):
            alarm._beep_loop()

        mock_beep.assert_called()
        alarm._stop_beep.set()

    def test_loud_phase(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """Beep loop enters loud phase after PHASE_MEDIUM_END minutes."""
        alarm = WakeAlarm(demo_mode=True)
        import time as time_mod

        alarm._alarm_start = time_mod.monotonic() - (PHASE_MEDIUM_END + 1) * 60

        call_count = 0

        def stop_after_one(*_args: object, **_kwargs: object) -> None:
            nonlocal call_count
            call_count += 1
            if call_count >= 1:
                alarm._stop_beep.set()

        with (
            patch(
                "python_pkg.wake_alarm._alarm._beep_loud",
                side_effect=stop_after_one,
            ) as mock_beep,
        ):
            alarm._beep_loop()

        mock_beep.assert_called()
        alarm._stop_beep.set()


class TestRunMethod:
    """Tests for the run() method."""

    def test_run_calls_mainloop(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """run() calls root.mainloop()."""
        alarm = WakeAlarm(demo_mode=True)
        alarm.run()
        alarm.root.mainloop.assert_called_once()
        alarm._stop_beep.set()


class TestUpdateTimerActive:
    """Tests for timer update when alarm is active."""

    def test_update_timer_shows_skip_window(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """While the skip is earnable, the timer shows the skip-window count."""
        del mock_tk_module
        alarm = WakeAlarm(demo_mode=True)
        alarm._update_timer()
        text = alarm._timer_label.configure.call_args[1]["text"]
        assert text.startswith("Skip window:")
        alarm._stop_beep.set()

    def test_update_timer_shows_prompt_after_window(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """After the window the timer shows the silence prompt and keeps going."""
        import time as time_mod

        alarm = WakeAlarm(demo_mode=True)
        # Far in the past so remaining == 0 -> the else branch.
        alarm._alarm_start = time_mod.monotonic() - 60 * 60
        alarm.root.after.reset_mock()
        alarm._update_timer()
        text = alarm._timer_label.configure.call_args[1]["text"]
        assert "type the code" in text
        # The alarm keeps nagging: it always reschedules while active.
        alarm.root.after.assert_called_once()
        alarm._stop_beep.set()

    def test_update_timer_noop_when_not_active(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """Timer update is a no-op once the alarm is no longer active."""
        del mock_tk_module
        alarm = WakeAlarm(demo_mode=True)
        alarm._active = False
        alarm._timer_label.configure.reset_mock()
        alarm._update_timer()
        alarm._timer_label.configure.assert_not_called()
        alarm._stop_beep.set()


class TestFlashChallenge:
    """Tests for flash challenge countdown behaviour inside WakeAlarm."""

    def test_flash_tick_counts_down_and_hides(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """_flash_tick counts down per second and hides the code at zero."""
        from python_pkg.wake_alarm._alarm import _Challenge

        alarm = WakeAlarm(demo_mode=True)
        alarm._current_challenge = _Challenge(
            kind="flash",
            display="ABCDEFGH",
            answer="ABCDEFGH",
            hint="Memorise",
        )
        alarm._flash_remaining = 2
        alarm._status_label.configure.reset_mock()

        alarm._flash_tick()
        assert alarm._flash_remaining == 1
        alarm._status_label.configure.assert_called()

        alarm._flash_tick()
        assert alarm._flash_remaining == 0

        # Final tick hides the code.
        alarm._flash_tick()
        # _code_label and _status_label share the same mock; inspect all calls.
        all_texts = [
            c.kwargs.get("text", "") for c in alarm._code_label.configure.call_args_list
        ]
        assert any("?" in t for t in all_texts)
        alarm._stop_beep.set()

    def test_flash_tick_noop_when_inactive(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """_flash_tick returns immediately when the alarm is no longer active."""
        alarm = WakeAlarm(demo_mode=True)
        alarm._active = False
        alarm._flash_remaining = 3
        alarm._status_label.configure.reset_mock()

        alarm._flash_tick()

        alarm._status_label.configure.assert_not_called()
        alarm._stop_beep.set()

    def test_wrong_flash_answer_reshows_code(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """Wrong flash answer restores the code and restarts the countdown."""
        from python_pkg.wake_alarm._alarm import _Challenge

        alarm = WakeAlarm(demo_mode=True)
        alarm._current_challenge = _Challenge(
            kind="flash",
            display="TESTCODE",
            answer="TESTCODE",
            hint="Memorise",
        )
        mock_entry = mock_tk_module.Entry.return_value
        mock_entry.get.return_value = "WRONGCODE"
        alarm._code_label.configure.reset_mock()

        alarm._on_submit()

        assert alarm.dismissed is False
        # Code label should be reconfigured (code shown again + countdown restarted).
        alarm._code_label.configure.assert_called()
        alarm._stop_beep.set()

    def test_next_round_flash_starts_countdown(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """When the next-round challenge is flash, the countdown starts immediately."""
        from python_pkg.wake_alarm._alarm import _Challenge

        alarm = WakeAlarm(demo_mode=True)
        alarm._current_challenge = _Challenge(
            kind="math", display="2 + 2 = ?", answer="4", hint="test"
        )
        next_flash = _Challenge(
            kind="flash", display="ABCDEFGH", answer="ABCDEFGH", hint="Memorise"
        )
        mock_entry = mock_tk_module.Entry.return_value
        mock_entry.get.return_value = "4"

        with patch(
            "python_pkg.wake_alarm._alarm._make_challenge", return_value=next_flash
        ):
            alarm._on_submit()

        assert alarm._current_challenge.kind == "flash"
        assert alarm.dismissed is False
        alarm._stop_beep.set()


class TestDismissWithoutSkip:
    """Tests for alarm dismiss without earning skip."""

    def test_dismiss_without_skip_shows_no_skip_message(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """Dismissing with earned_skip=False shows appropriate message."""
        del mock_tk_module
        alarm = WakeAlarm(demo_mode=True)
        mock_widget = MagicMock()
        alarm._container.winfo_children.return_value = [mock_widget]

        with patch(
            "python_pkg.wake_alarm._alarm.save_wake_state",
        ) as mock_save:
            alarm._dismiss_alarm(earned_skip=False)

        assert alarm.dismissed is True
        mock_save.assert_called_once()
        assert mock_save.call_args[1]["skip_workout"] is False
        mock_widget.destroy.assert_called_once()
        alarm._stop_beep.set()


class TestSkipWindowExpiredMessage:
    """Tests for the on-screen message when the skip window expires."""

    def test_expired_updates_status_label(
        self,
        mock_tk_module: MagicMock,
    ) -> None:
        """Expiry updates the status label instead of closing the alarm."""
        del mock_tk_module
        alarm = WakeAlarm(demo_mode=True)

        alarm._on_skip_window_expired()

        alarm._status_label.configure.assert_called_with(
            text="No workout skip today.",
        )
        alarm._stop_beep.set()
