from unittest.mock import MagicMock, patch

from styles.blink import (
    STATE_BLINKING,
    STATE_EVALUATING,
    STATE_WAIT_BEFORE_BLINK,
    STATE_WAIT_BEFORE_RETRY,
    STATE_WAIT_FOR_INPUT,
    BlinkStyle,
)


def _make_blink(music_file="music.mp3"):
    """Return (style, set_led calls) for a BlinkStyle ready to update."""
    calls = []
    style = BlinkStyle(lambda on: calls.append(on), music_file)
    return style, calls


def test_start_plays_music_and_led_off():
    style, calls = _make_blink("track.mp3")
    with patch("styles.blink.pygame", MagicMock()) as pg:
        style.start()
    pg.mixer.music.load.assert_called_with("track.mp3")
    pg.mixer.music.set_volume.assert_called_with(1.0)
    pg.mixer.music.play.assert_called_with(-1)
    assert calls == [False]  # LED off while music plays until a press starts the puzzle


def test_press_starts_puzzle():
    style, _ = _make_blink()
    assert style.update(100.0, True) is True
    assert style.state == STATE_WAIT_BEFORE_BLINK


def test_blink_correct_sequence_stops(monkeypatch):
    style, _ = _make_blink()
    style.update(100.0, True)  # press -> start puzzle
    assert style.state == STATE_WAIT_BEFORE_BLINK

    monkeypatch.setattr("styles.blink.random.randint", lambda a, b: 3)
    style.update(103.1, False)  # 3s passed -> blinking, 3 blinks
    assert style.state == STATE_BLINKING
    assert style.target_blinks == 3

    style.update(105.0, False)  # run through the blink sequence
    assert style.state == STATE_WAIT_FOR_INPUT

    # Press exactly 3 times
    for t in (105.5, 106.0, 106.5):
        style.update(t, True)
        style.update(t + 0.05, False)
    assert style.user_presses == 3

    style.update(109.7, False)  # 3s idle -> evaluating
    assert style.state == STATE_EVALUATING
    assert style.update(109.8, False) is False  # correct -> stop


def test_blink_incorrect_retries():
    style, _ = _make_blink()
    style.state = STATE_WAIT_FOR_INPUT
    style.target_blinks = 3

    style.update(100.0, True)  # one press
    style.update(100.1, False)

    style.last_interaction_time = 100.1
    style.update(103.2, False)  # -> evaluating
    keep_running = style.update(103.3, False)  # 1 != 3 -> retry

    assert keep_running is True
    assert style.state == STATE_WAIT_BEFORE_RETRY


def test_blinking_non_blocking():
    style, calls = _make_blink()
    style.set_led = calls.append
    style.state = STATE_BLINKING
    style.target_blinks = 2

    style.update(100.0, False)  # start of blinking: LED on
    assert style.state == STATE_BLINKING
    assert calls[-1] is True

    style.update(100.3, False)  # mid-blink toggles by elapsed time
    assert calls[-1] is False

    # 2 blinks * 2 phases * 0.3s = 1.2s
    style.update(101.2, False)
    assert style.state == STATE_WAIT_FOR_INPUT
    assert calls[-1] is False


def test_blinking_advances_time():
    style, _ = _make_blink()
    style.state = STATE_BLINKING
    style.target_blinks = 4

    # 4 blinks * 2 phases * 0.3s = 2.4s
    style.update(100.0, False)
    style.update(102.4, False)

    assert style.state == STATE_WAIT_FOR_INPUT
    assert style.last_interaction_time == 102.4
