import pytest

from hef.selection import Coordinate
from player.alteration import (
    DEFAULT_CALIBRATION,
    AffectOverlay,
    AnalyticalOverlay,
    Calibration,
    ColorGrade,
    Dream,
    RenderPlan,
    plan_alteration,
    render_plan_to_dict,
)


def _coord(left=0, right=0, dark=0, light=0):
    return Coordinate(left=left, right=right, dark=dark, light=light)


def test_all_zero_knobs_is_the_unaltered_base():
    plan = plan_alteration(_coord())
    assert plan.is_identity
    assert plan.overlay.level == 0
    assert plan.overlay.intensity == 0.0
    assert plan.dream.strength == 0
    assert plan.dream.intensity == 0.0
    assert plan.grade.tone == 0.0
    assert plan.grade.is_identity


def test_left_drives_the_analytical_overlay_only():
    plan = plan_alteration(_coord(left=4))
    assert plan.overlay.level == 4
    assert plan.overlay.intensity == 1.0
    assert plan.dream.strength == 0  # Left does not touch the substrate
    assert plan.dream.intensity == 0.0
    assert plan.grade.tone == 0.0


def test_right_drives_the_deterministic_dream_only():
    # Right-axis dream reframe (session 0013): Right is a live deterministic
    # haze, carried as strength (0..4) + intensity (0..1), not a baked variant.
    plan = plan_alteration(_coord(right=2))
    assert plan.dream.strength == 2
    assert plan.dream.intensity == pytest.approx(0.5)
    assert plan.overlay.level == 0  # Right does not add overlay


def test_left_and_right_stack_not_cancel():
    # design §4.2: whole-brain corner = dream substrate WITH labels on top
    plan = plan_alteration(_coord(left=4, right=4))
    assert plan.overlay.level == 4
    assert plan.dream.strength == 4
    assert plan.dream.intensity == 1.0


def test_affect_is_driven_by_the_right_knob_alone():
    # Right-brain affect (session 0018): emotion-words are controlled by the Right
    # knob alone — Right at 0 means no affect regardless of Left, and Left no longer
    # gates the channel (full Right with Left at 0 still surfaces affect).
    assert plan_alteration(_coord(left=4, right=0)).affect.strength == 0
    assert plan_alteration(_coord(left=4, right=0)).affect.intensity == 0.0
    assert plan_alteration(_coord(left=0, right=4)).affect.strength == 4
    assert plan_alteration(_coord(left=0, right=4)).affect.intensity == 1.0


def test_affect_strength_is_the_right_knob():
    # strength = right — Left is irrelevant to the affect channel.
    assert plan_alteration(_coord(left=2, right=4)).affect.strength == 4
    assert plan_alteration(_coord(left=4, right=2)).affect.strength == 2
    assert plan_alteration(_coord(left=0, right=3)).affect.strength == 3
    assert plan_alteration(_coord(left=4, right=1)).affect.strength == 1


def test_affect_intensity_scales_with_right_over_full_scale():
    for r in range(5):
        plan = plan_alteration(_coord(left=0, right=r))
        assert plan.affect.intensity == pytest.approx(r / 4)
    assert plan_alteration(_coord(left=0, right=4)).affect.intensity == 1.0


def test_affect_intensity_honors_overlay_gain():
    # affect text rides the overlay-opacity gain, but is keyed off the Right knob.
    cal = Calibration(overlay_gain=0.5)
    assert plan_alteration(_coord(left=0, right=4), cal).affect.intensity == pytest.approx(0.5)
    clamp = Calibration(overlay_gain=10.0)
    assert plan_alteration(_coord(left=0, right=4), clamp).affect.intensity == 1.0


def test_affect_active_implies_non_identity():
    # affect.strength == right, and right > 0 makes dream.strength > 0, so an active
    # affect plan (even with Left at 0) is never identity.
    plan = plan_alteration(_coord(left=0, right=3))
    assert plan.affect.strength == 3
    assert not plan.is_identity


def test_light_pole_grades_warm_positive_tone():
    plan = plan_alteration(_coord(light=4))
    assert plan.grade.tone == 1.0
    assert not plan.grade.is_identity


def test_dark_pole_grades_cool_negative_tone():
    assert plan_alteration(_coord(dark=4)).grade.tone == -1.0


def test_equal_dark_and_light_is_identity_grade():
    # design §5: the mood center is the raw, ungraded footage
    assert plan_alteration(_coord(dark=3, light=3)).grade.is_identity
    assert plan_alteration(_coord(dark=2, light=2)).grade.is_identity


def test_dark_minus_light_sets_intermediate_tone():
    assert plan_alteration(_coord(dark=4, light=2)).grade.tone == pytest.approx(-0.5)
    assert plan_alteration(_coord(dark=1, light=3)).grade.tone == pytest.approx(0.5)


def test_whole_brain_dark_corner_stacks_grade_substrate_and_overlay():
    plan = plan_alteration(_coord(left=4, right=2, dark=4, light=0))
    assert plan.overlay.level == 4
    assert plan.dream.strength == 2
    assert plan.dream.intensity == pytest.approx(0.5)
    assert plan.grade.tone == -1.0
    assert not plan.is_identity


def test_default_calibration_is_locked():
    # Session 0010: the knob->strength calibration is LOCKED to these values,
    # settled by eye in the simulator (closes the open session-0006 decision).
    # This pins the literal constants so they can't drift silently; changing the
    # locked feel is a deliberate edit here + in alteration.py.
    assert DEFAULT_CALIBRATION.mood_gain == 1.0
    assert DEFAULT_CALIBRATION.overlay_gain == 1.0
    assert DEFAULT_CALIBRATION.dream_gain == 1.0


def test_default_calibration_is_behavior_preserving():
    # DEFAULT_CALIBRATION reproduces the per-axis helpers exactly.
    for left in range(5):
        assert plan_alteration(_coord(left=left)).overlay.intensity == pytest.approx(left / 4)
    for right in range(5):
        assert plan_alteration(_coord(right=right)).dream.strength == right
        assert plan_alteration(_coord(right=right)).dream.intensity == pytest.approx(right / 4)
    for dark in range(5):
        for light in range(5):
            expected = (light - dark) / 4
            assert plan_alteration(_coord(dark=dark, light=light)).grade.tone == pytest.approx(expected)


def test_custom_calibration_scales_mood_overlay_and_dream():
    cal = Calibration(mood_gain=0.5, overlay_gain=0.5, dream_gain=0.5)
    assert plan_alteration(_coord(light=4), cal).grade.tone == pytest.approx(0.5)
    assert plan_alteration(_coord(left=4), cal).overlay.intensity == pytest.approx(0.5)
    assert plan_alteration(_coord(right=4), cal).dream.intensity == pytest.approx(0.5)


def test_calibration_gain_is_clamped_to_unit_range():
    cal = Calibration(mood_gain=10.0, overlay_gain=10.0, dream_gain=10.0)
    assert plan_alteration(_coord(light=4), cal).grade.tone == 1.0  # clamped, not 10
    assert plan_alteration(_coord(left=4), cal).overlay.intensity == 1.0
    assert plan_alteration(_coord(right=4), cal).dream.intensity == 1.0


def test_render_plan_to_dict_round_trips_the_numbers():
    d = render_plan_to_dict(plan_alteration(_coord(left=4, right=2, dark=4, light=0)))
    assert d == {
        "grade": {"tone": -1.0},
        "overlay": {"level": 4, "intensity": 1.0},
        "affect": {"strength": 2, "intensity": 0.5},
        "dream": {"strength": 2, "intensity": 0.5},
        "is_identity": False,
    }


def test_render_plan_is_frozen():
    plan = plan_alteration(_coord())
    with pytest.raises(Exception):
        plan.grade.tone = 0.5  # type: ignore[misc]
