use std::sync::Arc;

use bone_ui::hit_test::resolve;
use bone_ui::layout::{LayoutOffset, LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use bone_ui::widgets::{
    Button, ButtonVariant, Slider, SliderRange, SliderStep, show_button, show_slider,
};
use bone_ui::{
    AccessTreeBuilder, FocusManager, FrameCtx, FrameInstant, HitFrame, HitState, HotkeyTable,
    InputSnapshot, NamedKey, PointerButton, Script, StringKey, StringTable, Theme, WidgetId,
    WidgetKey,
};

const LABEL: StringKey = StringKey::new("script_drive.label");

fn child(name: &'static str) -> WidgetId {
    WidgetId::ROOT.child(WidgetKey::new(name))
}

fn rect(w: f32, h: f32) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
        LayoutSize::new(LayoutPx::new(w), LayoutPx::new(h)),
    )
}

fn run<R>(
    focus: &mut FocusManager,
    prev: &HitState,
    snap: &mut InputSnapshot,
    render: impl FnOnce(&mut FrameCtx<'_>) -> R,
) -> (R, HitState) {
    let theme = Arc::new(Theme::light());
    let table = HotkeyTable::new();
    let mut hits = HitFrame::new();
    let mut shaper = bone_text::Shaper::new();
    let mut a11y = AccessTreeBuilder::new();
    let response = {
        let mut ctx = FrameCtx::new(
            theme,
            snap,
            focus,
            &table,
            StringTable::empty(),
            &mut hits,
            prev,
            &mut a11y,
            &mut shaper,
        );
        render(&mut ctx)
    };
    let next = resolve(prev, &hits, snap, focus.focused());
    (response, next)
}

fn drive_button(
    focus: &mut FocusManager,
    prev: &HitState,
    snap: &mut InputSnapshot,
) -> (bool, HitState) {
    let id = child("button");
    let (response, next) = run(focus, prev, snap, |ctx| {
        show_button(
            ctx,
            Button::new(id, rect(80.0, 24.0), LABEL, ButtonVariant::Primary),
        )
    });
    (response.activated, next)
}

#[test]
fn scripted_pointer_click_activates_button() {
    let center = LayoutPos::new(LayoutPx::new(40.0), LayoutPx::new(12.0));
    let frames = Script::new()
        .hover(center)
        .click(PointerButton::Primary)
        .idle()
        .frames();
    let mut focus = FocusManager::new();
    let mut prev = HitState::new();
    let mut activations: Vec<bool> = Vec::new();
    frames.into_iter().for_each(|mut frame| {
        let (activated, next) = drive_button(&mut focus, &prev, &mut frame);
        activations.push(activated);
        prev = next;
    });
    assert!(
        activations.iter().any(|a| *a),
        "scripted click must produce at least one activation, got {activations:?}",
    );
}

#[test]
fn scripted_keyboard_activation_after_focus() {
    let id = child("button");
    let mut focus = FocusManager::new();
    let prev = HitState::new();
    focus.request_focus(id);
    let mut warm = InputSnapshot::idle(FrameInstant::ZERO);
    let _ = drive_button(&mut focus, &prev, &mut warm);
    assert_eq!(focus.focused(), Some(id));

    let frames = Script::new().named(NamedKey::Enter).frames();
    let Some(mut snap) = frames.into_iter().next() else {
        panic!("Script must yield one frame for a single key step");
    };
    let (activated, _) = drive_button(&mut focus, &prev, &mut snap);
    assert!(activated, "Enter on focused button must activate it");
}

#[test]
fn scripted_pointer_drag_moves_slider_value() {
    let slider_rect = rect(200.0, 18.0);
    let slider_id = child("slider");
    let Ok(range) = SliderRange::try_new(0.0_f64, 100.0_f64) else {
        unreachable!("slider range valid");
    };
    let Ok(step) = SliderStep::try_new(1.0_f64) else {
        unreachable!("slider step valid");
    };
    let start = LayoutPos::new(LayoutPx::new(20.0), LayoutPx::new(9.0));
    let frames = Script::new()
        .hover(start)
        .press(PointerButton::Primary)
        .drag(LayoutOffset::new(LayoutPx::new(140.0), LayoutPx::ZERO))
        .release(PointerButton::Primary)
        .frames();
    let mut focus = FocusManager::new();
    let mut prev = HitState::new();
    let mut value = 0.0_f64;
    frames.into_iter().for_each(|mut frame| {
        let v = value;
        let (response, next) = run(&mut focus, &prev, &mut frame, |ctx| {
            show_slider(
                ctx,
                Slider::new(slider_id, slider_rect, LABEL, v, range, step),
            )
        });
        value = response.value;
        prev = next;
    });
    assert!(
        value > 50.0,
        "scripted drag from x=20 to x=160 across [0,100] range must move value past midpoint, got {value}",
    );
}
