from bubblecam import state_lib
from conftest import load_bin_module

led_status = load_bin_module("led_status")


def cfg(mode="sbs_mjpeg"):
    return {"camera": {"mode": mode}}


def test_camera_service_names_single_combined_stream():
    assert led_status.camera_service_names(cfg("sbs_mjpeg")) == ["camera_primary"]


def test_camera_service_names_dual_node():
    assert led_status.camera_service_names(cfg("dual_node_h264")) == ["camera_left", "camera_right"]


def test_recording_confirmed_false_when_nothing_running(sim_env):
    assert led_status.recording_confirmed(cfg()) is False


def test_recording_confirmed_true_when_camera_and_sensors_running(sim_env):
    state_lib.write_state("camera_primary", "running")
    state_lib.write_state("sensors", "running")
    assert led_status.recording_confirmed(cfg()) is True


def test_recording_confirmed_false_if_only_camera_running(sim_env):
    state_lib.write_state("camera_primary", "running")
    assert led_status.recording_confirmed(cfg()) is False


def test_recording_confirmed_requires_both_camera_nodes_in_dual_mode(sim_env):
    state_lib.write_state("camera_left", "running")
    state_lib.write_state("sensors", "running")
    assert led_status.recording_confirmed(cfg("dual_node_mjpeg")) is False
    state_lib.write_state("camera_right", "running")
    assert led_status.recording_confirmed(cfg("dual_node_mjpeg")) is True


def test_any_mission_critical_error_false_when_healthy(sim_env):
    state_lib.write_state("camera_primary", "running")
    state_lib.write_state("sensors", "running")
    state_lib.write_state("watchdog", "running")
    assert led_status.any_mission_critical_error(cfg(), stale_after=30) is False


def test_any_mission_critical_error_true_on_sensor_fault(sim_env):
    state_lib.write_state("camera_primary", "running")
    state_lib.write_state("sensors", "error", detail="fault")
    assert led_status.any_mission_critical_error(cfg(), stale_after=30) is True


def test_epaper_or_lumen_faults_do_not_count_as_mission_critical(sim_env):
    # Deliberate design choice: a display/light glitch shouldn't trigger the
    # diver-facing error LED or a reboot -- only camera/sensors/watchdog do.
    state_lib.write_state("camera_primary", "running")
    state_lib.write_state("sensors", "running")
    state_lib.write_state("epaper", "error", detail="spi glitch")
    state_lib.write_state("lumen", "error", detail="pwm glitch")
    assert led_status.any_mission_critical_error(cfg(), stale_after=30) is False


def test_camera_fault_not_mission_critical_when_switch_says_stop(sim_env):
    # Camera stopped on purpose via the record switch: its stale/error state
    # must not trigger the diver-facing blue error blink.
    state_lib.write_state("record_switch", "running", recording_requested=False)
    state_lib.write_state("camera_primary", "error", detail="stopped by switch")
    state_lib.write_state("sensors", "running")
    state_lib.write_state("watchdog", "running")
    assert led_status.any_mission_critical_error(cfg(), stale_after=30) is False


def test_camera_fault_still_critical_while_switch_says_record(sim_env):
    state_lib.write_state("record_switch", "running", recording_requested=True)
    state_lib.write_state("camera_primary", "error", detail="real fault")
    state_lib.write_state("sensors", "running")
    state_lib.write_state("watchdog", "running")
    assert led_status.any_mission_critical_error(cfg(), stale_after=30) is True


def test_sensor_fault_critical_regardless_of_switch(sim_env):
    state_lib.write_state("record_switch", "running", recording_requested=False)
    state_lib.write_state("sensors", "error", detail="fault")
    assert led_status.any_mission_critical_error(cfg(), stale_after=30) is True


def test_swap_rg_for_rgb_order_first_pixel():
    # The Blue Robotics Indicator is RGB-order; the driver speaks GRB, so
    # its color must go in red/green-swapped to display correctly.
    from bubblecam.hardware.ws2812b import swap_rg

    assert swap_rg((255, 0, 0)) == (0, 255, 0)
    assert swap_rg((0, 255, 0)) == (255, 0, 0)
    assert swap_rg((0, 0, 255)) == (0, 0, 255)   # blue unaffected
    assert swap_rg((10, 20, 30)) == (20, 10, 30)


def led_cfg():
    return {
        "error_blink_on_ms": 500,
        "error_blink_off_ms": 5000,
        "idle_blink_on_ms": 500,
        "idle_blink_off_ms": 2500,
        "heartbeat_blink_on_ms": 200,
        "heartbeat_blink_off_ms": 9800,
    }


def test_green_window_restarts_each_recording_session():
    """Regression guard: confirmed_at must reset when recording stops, or
    a second session in the same boot inherits the first's expired window
    and never shows green."""
    # Session 1 confirms at t=100.
    at = led_status.next_confirmed_at(True, None, 100.0)
    assert at == 100.0
    # Still recording: window start is stable.
    assert led_status.next_confirmed_at(True, at, 200.0) == 100.0
    # Recording stops: window resets.
    at = led_status.next_confirmed_at(False, at, 300.0)
    assert at is None
    # Session 2 confirms at t=1000: fresh window, not the stale one.
    assert led_status.next_confirmed_at(True, at, 1000.0) == 1000.0


def test_pattern_for_each_mode():
    assert led_status.pattern_for("error", led_cfg()) == (led_status.BLUE, 0.5, 5.0)
    assert led_status.pattern_for("red", led_cfg()) == (led_status.RED, 0.5, 2.5)
    color, on_s, off_s = led_status.pattern_for("green", led_cfg())
    assert color == led_status.GREEN and off_s == 0.0  # solid
    assert led_status.pattern_for("heartbeat", led_cfg()) == (led_status.GREEN, 0.2, 9.8)


def test_ramp_toward_rises_and_falls_bounded():
    # 255-units/sec rate, 0.1s steps: +25.5 per step, clamped at target.
    level = led_status.ramp_toward(0.0, 128.0, 255.0, 0.1)
    assert level == 25.5
    level = led_status.ramp_toward(120.0, 128.0, 255.0, 0.1)
    assert level == 128.0  # clamped, no overshoot
    level = led_status.ramp_toward(128.0, 0.0, 255.0, 0.1)
    assert level == 102.5  # ramping down
    assert led_status.ramp_toward(10.0, 0.0, 255.0, 0.1) == 0.0


def test_ramp_toward_stable_at_target():
    assert led_status.ramp_toward(128.0, 128.0, 255.0, 0.1) == 128.0


def test_lumen_says_dark_reads_live_flag(sim_env):
    state_lib.write_state("lumen", "running", dark=True, brightness_pct=30.0)
    assert led_status.lumen_says_dark() is True
    state_lib.write_state("lumen", "running", dark=False, brightness_pct=0.0)
    assert led_status.lumen_says_dark() is False


def test_lumen_says_dark_defaults_dark_when_no_data(sim_env):
    # Missing lumen state: fail toward lighting the camera, not toward
    # dark footage.
    assert led_status.lumen_says_dark() is True


def test_lumen_says_dark_tolerates_lumen_60s_poll(sim_env):
    import json

    # A healthy lumen service updates only every ~60s; a 90s-old flag must
    # still be trusted (the 30s default staleness would wrongly reject it).
    state_lib.write_state("lumen", "running", dark=False)
    state = state_lib.read_state("lumen")
    state["last_update"] -= 90
    (sim_env["state_dir"] / "lumen.json").write_text(json.dumps(state))
    assert led_status.lumen_says_dark() is False


def test_ring_mode_policies(sim_env):
    state_lib.write_state("lumen", "running", dark=True)
    assert led_status.ring_should_light("night", confirmed=False) is True
    assert led_status.ring_should_light("night_and_recording", confirmed=False) is False
    assert led_status.ring_should_light("night_and_recording", confirmed=True) is True

    state_lib.write_state("lumen", "running", dark=False)
    assert led_status.ring_should_light("night", confirmed=True) is False
    assert led_status.ring_should_light("recording", confirmed=True) is True
    assert led_status.ring_should_light("recording", confirmed=False) is False

    assert led_status.ring_should_light("always", confirmed=False) is True
    assert led_status.ring_should_light("off", confirmed=True) is False


def test_rpi_ws281x_channel_by_pin():
    from bubblecam.hardware.ws2812b import rpi_ws281x_channel_for

    assert rpi_ws281x_channel_for(21) == 0  # PCM -- the deployed pin
    assert rpi_ws281x_channel_for(13) == 1  # PWM1 (conflicts with Lumen)
    assert rpi_ws281x_channel_for(19) == 1
    assert rpi_ws281x_channel_for(18) == 0


def test_indicator_index_by_position():
    from bubblecam.hardware.ws2812b import StatusLED
    import os

    os.environ["BUBBLECAM_SIM"] = "1"
    assert StatusLED(led_count=41, indicator_position="first")._indicator_index == 0
    assert StatusLED(led_count=41, indicator_position="last")._indicator_index == 40
    # Deployment case: lone indicator, both positions are pixel 0.
    assert StatusLED(led_count=1, indicator_position="first")._indicator_index == 0
    assert StatusLED(led_count=1, indicator_position="last")._indicator_index == 0


def test_status_led_zones_are_independent_and_deduped(sim_env, capsys):
    from bubblecam.hardware.ws2812b import StatusLED

    led = StatusLED(led_count=41)
    led.set_status_color((255, 0, 0))
    led.set_status_color((255, 0, 0))   # duplicate: no re-print
    led.set_ring_color((128, 128, 128))
    led.set_ring_color((128, 128, 128))  # duplicate: no re-print
    out = capsys.readouterr().out
    assert out.count("status -> (255, 0, 0)") == 1
    assert out.count("ring -> (128, 128, 128)") == 1


def test_recover_confirmed_at_none_when_no_prior_state(sim_env):
    assert led_status.recover_confirmed_at() is None


def test_recover_confirmed_at_reads_back_own_last_state(sim_env):
    state_lib.write_state("led", "green", confirmed_at=12345.0)
    assert led_status.recover_confirmed_at() == 12345.0
