from dataclasses import dataclass

import pytest

from player.controls import Controls
from player.state import Player, Playback, Transition, TransitionKind


@dataclass(frozen=True)
class FakeClip:
    id: str


LIB = [FakeClip("base-a"), FakeClip("base-b")]


def _controls(visual="on", audio="off", left=0, right=0, dark=0, light=0,
              volume=2, brightness=2):
    return Controls(visual, audio, left, right, dark, light, volume, brightness)


def test_first_update_to_video_fades_in_from_black():
    p = Player(LIB)
    t = p.update(_controls(visual="on"))
    assert t.kind == TransitionKind.FADE_FROM_BLACK
    assert t.playback.clip_id == "base-a"
    assert t.playback.video is True


def test_visual_off_from_video_fades_to_black():
    p = Player(LIB)
    p.update(_controls(visual="on"))
    t = p.update(_controls(visual="off"))
    assert t.kind == TransitionKind.FADE_TO_BLACK
    assert t.playback.clip_id is None
    assert t.playback.video is False
    assert t.playback.audio_source == "off"


def test_audio_survives_visual_off():
    # soundtrack + black is reachable (a gap the bundled dial omitted)
    p = Player(LIB)
    t = p.update(_controls(visual="off", audio="soundtrack"))
    assert t.playback.video is False
    assert t.playback.audio_source == "soundtrack"


def test_no_change_yields_none_transition():
    p = Player(LIB)
    p.update(_controls(visual="on", left=1))
    t = p.update(_controls(visual="on", left=1))
    assert t.kind == TransitionKind.NONE


def test_grade_change_is_a_live_update_not_a_crossfade():
    # design §4.3: the Dark/Light grade is a continuous runtime op
    p = Player(LIB)
    p.update(_controls(visual="on", dark=0, light=0))
    t = p.update(_controls(visual="on", dark=4, light=0))
    assert t.kind == TransitionKind.LIVE_UPDATE
    assert t.playback.plan.grade.tone == -1.0


def test_overlay_change_is_a_live_update():
    p = Player(LIB)
    p.update(_controls(visual="on", left=0))
    t = p.update(_controls(visual="on", left=4))
    assert t.kind == TransitionKind.LIVE_UPDATE
    assert t.playback.plan.overlay.level == 4


def test_dream_change_is_a_live_update_not_a_crossfade():
    # Right-axis dream reframe (session 0013): the Right dream is a deterministic
    # LIVE filter now, so changing it no longer crossfades — only a clip swap does.
    p = Player(LIB)
    p.update(_controls(visual="on", right=0))
    t = p.update(_controls(visual="on", right=4))
    assert t.kind == TransitionKind.LIVE_UPDATE
    assert t.playback.plan.dream.strength == 4
    assert t.playback.plan.dream.intensity == 1.0


def test_volume_only_change_is_a_live_update():
    p = Player(LIB)
    p.update(_controls(visual="on", volume=1))
    t = p.update(_controls(visual="on", volume=4))
    assert t.kind == TransitionKind.LIVE_UPDATE
    assert t.playback.volume == 4


def test_audio_source_change_while_black_is_a_live_update():
    p = Player(LIB)
    p.update(_controls(visual="off", audio="off"))
    t = p.update(_controls(visual="off", audio="soundtrack"))
    assert t.kind == TransitionKind.LIVE_UPDATE
    assert t.playback.audio_source == "soundtrack"


def test_injected_base_chooser_is_used():
    p = Player(LIB, choose_base=lambda lib: lib[1])
    t = p.update(_controls(visual="on"))
    assert t.playback.clip_id == "base-b"


def test_empty_library_with_video_raises():
    p = Player([])
    with pytest.raises(ValueError):
        p.update(_controls(visual="on"))


def test_off_with_empty_library_is_fine():
    p = Player([])
    # levels at 0 match the initial black state, so this is a no-op transition
    t = p.update(_controls(visual="off", volume=0, brightness=0))
    assert t.kind == TransitionKind.NONE  # already black at init
    assert t.playback.clip_id is None


def test_off_with_levels_from_black_is_a_live_update():
    p = Player([])
    t = p.update(_controls(visual="off", volume=3, brightness=2))
    assert t.kind == TransitionKind.LIVE_UPDATE  # black->black, levels set
    assert t.playback.clip_id is None
    assert t.playback.volume == 3
