"""Tests for phone workout verification, phone check, and unlock operations."""
# pylint: disable=protected-access,unused-argument

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

from python_pkg.screen_locker._constants import NO_PHONE_EXTRA_LOCKOUT_SECONDS
from python_pkg.screen_locker.screen_lock import (
    PHONE_PENALTY_DELAY_DEMO,
    PHONE_PENALTY_DELAY_PRODUCTION,
)
from python_pkg.screen_locker.tests.conftest import create_locker

if TYPE_CHECKING:
    from pathlib import Path


class TestVerifyPhoneWorkout:
    """Tests for _verify_phone_workout method."""

    def test_verified(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test workout verified on phone with sufficient duration."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(
            locker,
            "_is_phone_connected",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_pull_stronglifts_db",
            MagicMock(return_value=tmp_path / "sl.db"),
        )
        object.__setattr__(
            locker,
            "_count_today_workouts",
            MagicMock(return_value=2),
        )
        object.__setattr__(
            locker,
            "_is_workout_finish_recent",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_get_today_exercise_count",
            MagicMock(return_value=3),
        )
        object.__setattr__(
            locker,
            "_get_today_workout_duration_minutes",
            MagicMock(return_value=65.0),
        )

        with patch(
            "python_pkg.screen_locker._phone_verification.check_clock_skew",
            return_value=(True, "Clock OK"),
        ):
            status, message = locker._verify_phone_workout()

        assert status == "verified"
        assert "2 session" in message
        assert "65 min" in message
        assert "3 exercise" in message

    def test_not_verified(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test no workout found on phone."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(
            locker,
            "_is_phone_connected",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_pull_stronglifts_db",
            MagicMock(return_value=tmp_path / "sl.db"),
        )
        object.__setattr__(
            locker,
            "_count_today_workouts",
            MagicMock(return_value=0),
        )

        with patch(
            "python_pkg.screen_locker._phone_verification.check_clock_skew",
            return_value=(True, "Clock OK"),
        ):
            status, message = locker._verify_phone_workout()

        assert status == "not_verified"
        assert "No workout" in message

    def test_too_short(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test workout found but too short."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(
            locker,
            "_is_phone_connected",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_pull_stronglifts_db",
            MagicMock(return_value=tmp_path / "sl.db"),
        )
        object.__setattr__(
            locker,
            "_count_today_workouts",
            MagicMock(return_value=1),
        )
        object.__setattr__(
            locker,
            "_is_workout_finish_recent",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_get_today_exercise_count",
            MagicMock(return_value=3),
        )
        object.__setattr__(
            locker,
            "_get_today_workout_duration_minutes",
            MagicMock(return_value=25.0),
        )

        with patch(
            "python_pkg.screen_locker._phone_verification.check_clock_skew",
            return_value=(True, "Clock OK"),
        ):
            status, message = locker._verify_phone_workout()

        assert status == "too_short"
        assert "25 min" in message
        assert "50 min" in message

    def test_no_phone(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test no phone connected."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(
            locker,
            "_is_phone_connected",
            MagicMock(return_value=False),
        )

        with patch(
            "python_pkg.screen_locker._phone_verification.check_clock_skew",
            return_value=(True, "Clock OK"),
        ):
            status, _ = locker._verify_phone_workout()

        assert status == "no_phone"

    def test_error_no_db(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test error when StrongLifts DB cannot be pulled."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(
            locker,
            "_is_phone_connected",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_pull_stronglifts_db",
            MagicMock(return_value=None),
        )

        with patch(
            "python_pkg.screen_locker._phone_verification.check_clock_skew",
            return_value=(True, "Clock OK"),
        ):
            status, message = locker._verify_phone_workout()

        assert status == "error"
        assert "database" in message.lower()

    def test_clock_tampered(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test clock_tampered when NTP check fails."""
        locker = create_locker(mock_tk, tmp_path)

        with patch(
            "python_pkg.screen_locker._phone_verification.check_clock_skew",
            return_value=(False, "System clock is 600s ahead"),
        ):
            status, message = locker._verify_phone_workout()

        assert status == "clock_tampered"
        assert "600s" in message

    def test_stale_workout(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test stale status when workout finish is not recent."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(
            locker,
            "_is_phone_connected",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_pull_stronglifts_db",
            MagicMock(return_value=tmp_path / "sl.db"),
        )
        object.__setattr__(
            locker,
            "_count_today_workouts",
            MagicMock(return_value=1),
        )
        object.__setattr__(
            locker,
            "_is_workout_finish_recent",
            MagicMock(return_value=False),
        )

        with patch(
            "python_pkg.screen_locker._phone_verification.check_clock_skew",
            return_value=(True, "Clock OK"),
        ):
            status, message = locker._verify_phone_workout()

        assert status == "stale"
        assert "old" in message.lower()

    def test_no_exercises(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test no_exercises when workout has no exercise data."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(
            locker,
            "_is_phone_connected",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_pull_stronglifts_db",
            MagicMock(return_value=tmp_path / "sl.db"),
        )
        object.__setattr__(
            locker,
            "_count_today_workouts",
            MagicMock(return_value=1),
        )
        object.__setattr__(
            locker,
            "_is_workout_finish_recent",
            MagicMock(return_value=True),
        )
        object.__setattr__(
            locker,
            "_get_today_exercise_count",
            MagicMock(return_value=0),
        )

        with patch(
            "python_pkg.screen_locker._phone_verification.check_clock_skew",
            return_value=(True, "Clock OK"),
        ):
            status, message = locker._verify_phone_workout()

        assert status == "no_exercises"
        assert "exercise" in message.lower()


class TestStartPhoneCheck:
    """Tests for _start_phone_check and _handle_startup_phone_result."""

    def test_start_phone_check_shows_checking_screen(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test _start_phone_check shows checking message and starts check."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "clear_container", MagicMock())
        object.__setattr__(
            locker,
            "_verify_phone_workout",
            MagicMock(
                return_value=("no_phone", "No phone"),
            ),
        )
        object.__setattr__(locker, "_poll_phone_check", MagicMock())

        locker._start_phone_check()

        locker.clear_container.assert_called()
        locker._poll_phone_check.assert_called_once()
        assert locker._phone_future is not None

    def test_handle_startup_verified_unlocks_directly(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test verified result shows success screen then unlocks via after()."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "unlock_screen", MagicMock())
        object.__setattr__(locker.root, "after", MagicMock())

        locker._handle_startup_phone_result("verified", "Workout verified! (1 session)")

        # unlock_screen is deferred via root.after, not called directly
        locker.unlock_screen.assert_not_called()
        assert locker.workout_data["type"] == "phone_verified"
        locker.root.after.assert_called_once_with(1500, locker.unlock_screen)

    def test_handle_startup_not_verified_shows_retry_and_sick(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test not_verified result shows retry and sick buttons."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "_show_retry_and_sick", MagicMock())
        locker._handle_startup_phone_result(
            "not_verified", "No workout found on phone today"
        )

        locker._show_retry_and_sick.assert_called_once()

    def test_handle_startup_too_short_shows_retry_and_sick(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test too_short result shows retry and sick buttons."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "_show_retry_and_sick", MagicMock())
        locker._handle_startup_phone_result(
            "too_short", "Workout too short! 25 min logged, need at least 50 min."
        )

        locker._show_retry_and_sick.assert_called_once()
        call_args = locker._show_retry_and_sick.call_args[0][0]
        assert "too short" in call_args.lower()

    def test_handle_startup_stale_shows_retry_and_sick(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test stale result shows retry and sick buttons."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "_show_retry_and_sick", MagicMock())
        locker._handle_startup_phone_result("stale", "Workout too old")

        locker._show_retry_and_sick.assert_called_once()
        call_args = locker._show_retry_and_sick.call_args[0][0]
        assert "reason: stale" in call_args.lower()

    def test_handle_startup_no_exercises_shows_retry_and_sick(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test no_exercises result shows retry and sick buttons."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "_show_retry_and_sick", MagicMock())
        locker._handle_startup_phone_result("no_exercises", "No exercises found")

        locker._show_retry_and_sick.assert_called_once()
        call_args = locker._show_retry_and_sick.call_args[0][0]
        assert "reason: no_exercises" in call_args.lower()

    def test_handle_startup_clock_tampered_shows_retry_and_sick(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test clock_tampered result shows retry and sick buttons."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "_show_retry_and_sick", MagicMock())
        locker._handle_startup_phone_result(
            "clock_tampered",
            "System clock is 600s ahead",
        )

        locker._show_retry_and_sick.assert_called_once()
        call_args = locker._show_retry_and_sick.call_args[0][0]
        assert "clock" in call_args.lower()

    def test_handle_startup_no_phone_shows_penalty(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test no_phone result triggers penalty with default retry+sick callback."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "_show_phone_penalty", MagicMock())

        locker._handle_startup_phone_result("no_phone", "No phone")

        locker._show_phone_penalty.assert_called_once_with("No phone")

    def test_handle_startup_error_shows_penalty(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test error result triggers penalty with default retry+sick callback."""
        locker = create_locker(mock_tk, tmp_path)
        object.__setattr__(locker, "_show_phone_penalty", MagicMock())

        locker._handle_startup_phone_result("error", "DB not found")

        locker._show_phone_penalty.assert_called_once_with("DB not found")

    def test_poll_phone_check_schedules_retry_when_pending(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test _poll_phone_check reschedules itself when future is not done."""
        locker = create_locker(mock_tk, tmp_path)
        mock_future: MagicMock = MagicMock()
        mock_future.done.return_value = False
        object.__setattr__(locker, "_phone_future", mock_future)
        object.__setattr__(locker.root, "after", MagicMock())

        locker._poll_phone_check()

        locker.root.after.assert_called_once_with(500, locker._poll_phone_check)

    def test_poll_phone_check_routes_when_done(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test _poll_phone_check calls result handler when future is done."""
        locker = create_locker(mock_tk, tmp_path)
        mock_future: MagicMock = MagicMock()
        mock_future.done.return_value = True
        mock_future.result.return_value = ("no_phone", "No phone")
        object.__setattr__(locker, "_phone_future", mock_future)
        object.__setattr__(locker, "_handle_startup_phone_result", MagicMock())

        locker._poll_phone_check()

        locker._handle_startup_phone_result.assert_called_once_with(
            "no_phone", "No phone"
        )


class TestShowPhonePenalty:
    """Tests for _show_phone_penalty and _update_phone_penalty methods."""

    def test_show_phone_penalty_demo_delay(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test demo mode uses short penalty delay."""
        locker = create_locker(mock_tk, tmp_path, demo_mode=True)
        object.__setattr__(locker, "clear_container", MagicMock())

        locker._show_phone_penalty("test message")

        # _update_phone_penalty is called once, decrementing by 1
        assert locker.phone_penalty_remaining == PHONE_PENALTY_DELAY_DEMO - 1

    def test_show_phone_penalty_production_delay(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test production mode uses long penalty delay (base + no-phone bump)."""
        locker = create_locker(mock_tk, tmp_path, demo_mode=False)
        object.__setattr__(locker, "clear_container", MagicMock())

        locker._show_phone_penalty("test message")

        expected = PHONE_PENALTY_DELAY_PRODUCTION + NO_PHONE_EXTRA_LOCKOUT_SECONDS - 1
        assert locker.phone_penalty_remaining == expected

    def test_update_phone_penalty_countdown(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test phone penalty countdown decrements."""
        locker = create_locker(mock_tk, tmp_path)
        locker.phone_penalty_remaining = 5
        locker.phone_penalty_label = MagicMock()

        locker._update_phone_penalty()

        assert locker.phone_penalty_remaining == 4
        locker.phone_penalty_label.config.assert_called_once_with(text="5")
        locker.root.after.assert_called()

    def test_update_phone_penalty_at_zero(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test phone penalty calls done function when timer reaches zero."""
        locker = create_locker(mock_tk, tmp_path)
        locker.phone_penalty_remaining = 0
        locker.phone_penalty_label = MagicMock()
        mock_done = MagicMock()
        locker._phone_penalty_done_fn = mock_done

        locker._update_phone_penalty()

        mock_done.assert_called_once()

    def test_show_phone_penalty_default_callback_shows_retry(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test default phone penalty callback shows retry+sick screen."""
        locker = create_locker(mock_tk, tmp_path, demo_mode=True)
        object.__setattr__(locker, "clear_container", MagicMock())
        object.__setattr__(locker, "_show_retry_and_sick", MagicMock())

        locker._show_phone_penalty("No phone connected")

        # Simulate timer reaching zero by calling the done function
        locker._phone_penalty_done_fn()
        locker._show_retry_and_sick.assert_called_once_with("No phone connected")


class TestUnlockScreenShutdownAdjustment:
    """Tests for unlock_screen shutdown time adjustment."""

    def test_unlock_screen_adjusts_for_phone_verified(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test unlock_screen adjusts shutdown for phone-verified workout."""
        locker = create_locker(mock_tk, tmp_path)
        locker.log_file = tmp_path / "workout_log.json"
        locker.workout_data = {"type": "phone_verified"}
        object.__setattr__(
            locker, "_adjust_shutdown_time_later", MagicMock(return_value=True)
        )

        locker.unlock_screen()

        locker._adjust_shutdown_time_later.assert_called_once()

    def test_unlock_screen_skips_adjustment_for_sick_day(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test unlock_screen does not adjust for sick day."""
        locker = create_locker(mock_tk, tmp_path)
        locker.log_file = tmp_path / "workout_log.json"
        locker.workout_data = {"type": "sick_day"}
        object.__setattr__(
            locker, "_adjust_shutdown_time_later", MagicMock(return_value=True)
        )

        locker.unlock_screen()

        locker._adjust_shutdown_time_later.assert_not_called()

    def test_unlock_screen_skips_adjustment_no_type(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test unlock_screen does not adjust when no workout type."""
        locker = create_locker(mock_tk, tmp_path)
        locker.log_file = tmp_path / "workout_log.json"
        locker.workout_data = {}
        object.__setattr__(
            locker, "_adjust_shutdown_time_later", MagicMock(return_value=True)
        )

        locker.unlock_screen()

        locker._adjust_shutdown_time_later.assert_not_called()

    def test_unlock_screen_handles_adjustment_failure(
        self,
        mock_tk: MagicMock,
        mock_sys_exit: MagicMock,
        tmp_path: Path,
    ) -> None:
        """Test unlock_screen continues when adjustment fails."""
        locker = create_locker(mock_tk, tmp_path)
        locker.log_file = tmp_path / "workout_log.json"
        locker.workout_data = {"type": "phone_verified"}
        object.__setattr__(
            locker, "_adjust_shutdown_time_later", MagicMock(return_value=False)
        )

        # Should not raise, should continue with unlock
        locker.unlock_screen()

        locker._adjust_shutdown_time_later.assert_called_once()
        locker.root.after.assert_called()
