use core::num::NonZeroU32;
use std::sync::Arc;

use bone_ui::{
    AccessTreeBuilder, ActionId, FocusManager, FrameCtx, FrameInstant, HitFrame, HitState,
    HotkeyBinding, HotkeyScope, HotkeyScopes, HotkeyTable, InputSnapshot, KeyChar, KeyChord,
    KeyCode, KeyEvent, ModifierMask, StringKey, StringTable, Theme, WidgetId, WidgetKey,
    widgets::{
        AlwaysValid, HotkeyCapture, HotkeyCaptureState, MemoryClipboard, TextInput, TextInputState,
        show_hotkey_capture, show_text_input,
    },
};

const FIELD_LABEL: StringKey = StringKey::new("ordering.field");

fn field_id() -> WidgetId {
    WidgetId::ROOT.child(WidgetKey::new("ordered_field"))
}

fn rect() -> bone_ui::layout::LayoutRect {
    use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    LayoutRect::new(
        LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
        LayoutSize::new(LayoutPx::new(120.0), LayoutPx::new(24.0)),
    )
}

fn action(n: u32) -> ActionId {
    let Some(nz) = NonZeroU32::new(n) else {
        panic!("test action id must be non-zero");
    };
    ActionId::new(nz)
}

fn focused_field() -> FocusManager {
    let mut focus = FocusManager::new();
    focus.register_focusable(field_id());
    focus.request_focus(field_id());
    focus.end_frame();
    focus
}

fn ctrl(c: char) -> KeyEvent {
    KeyEvent::new(KeyCode::Char(KeyChar::from_char(c)), ModifierMask::CTRL)
}

#[test]
fn text_input_before_dispatch_eats_overlapping_editing_chord() {
    let theme = Arc::new(Theme::light());
    let select_all_global = action(1);
    let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new(
        KeyChord::new(KeyCode::Char(KeyChar::from_char('a')), ModifierMask::CTRL),
        HotkeyScope::Global,
        select_all_global,
    )]) else {
        panic!("registration must succeed");
    };
    let mut focus = focused_field();
    let mut hits = HitFrame::new();
    let prev = HitState::new();
    let mut input = InputSnapshot::idle(FrameInstant::ZERO);
    input.keys_pressed.push(ctrl('a'));
    let mut state = TextInputState::from_text("hello");
    let mut clipboard = MemoryClipboard::default();
    let scopes = HotkeyScopes::from_outer_to_inner([HotkeyScope::Global]);

    let actions = {
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        let widget = TextInput {
            id: field_id(),
            rect: rect(),
            placeholder: FIELD_LABEL,
            state: &mut state,
            disabled: false,
            validator: AlwaysValid,
        };
        let _ = show_text_input(&mut ctx, widget, &mut clipboard);
        ctx.dispatch_hotkeys(&scopes)
    };

    assert!(
        actions.is_empty(),
        "TextInput must claim Ctrl+A before dispatch sees it",
    );
    assert_eq!(state.selection.min().value(), 0);
    assert_eq!(state.selection.max().value(), 5);
}

#[test]
fn dispatch_after_text_input_still_routes_unowned_chord() {
    let theme = Arc::new(Theme::light());
    let save_global = action(7);
    let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new(
        KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL),
        HotkeyScope::Global,
        save_global,
    )]) else {
        panic!("registration must succeed");
    };
    let mut focus = focused_field();
    let mut hits = HitFrame::new();
    let prev = HitState::new();
    let mut input = InputSnapshot::idle(FrameInstant::ZERO);
    input.keys_pressed.push(ctrl('s'));
    let mut state = TextInputState::from_text("hello");
    let mut clipboard = MemoryClipboard::default();
    let scopes = HotkeyScopes::from_outer_to_inner([HotkeyScope::Global]);

    let actions = {
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        let widget = TextInput {
            id: field_id(),
            rect: rect(),
            placeholder: FIELD_LABEL,
            state: &mut state,
            disabled: false,
            validator: AlwaysValid,
        };
        let _ = show_text_input(&mut ctx, widget, &mut clipboard);
        ctx.dispatch_hotkeys(&scopes)
    };

    assert_eq!(actions, vec![save_global]);
    assert_eq!(state.text, "hello");
}

#[test]
fn dispatch_first_breaks_text_editing_when_overlap_exists() {
    let theme = Arc::new(Theme::light());
    let select_all_global = action(1);
    let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new(
        KeyChord::new(KeyCode::Char(KeyChar::from_char('a')), ModifierMask::CTRL),
        HotkeyScope::Global,
        select_all_global,
    )]) else {
        panic!("registration must succeed");
    };
    let mut focus = focused_field();
    let mut hits = HitFrame::new();
    let prev = HitState::new();
    let mut input = InputSnapshot::idle(FrameInstant::ZERO);
    input.keys_pressed.push(ctrl('a'));
    let mut state = TextInputState::from_text("hello");
    let mut clipboard = MemoryClipboard::default();
    let scopes = HotkeyScopes::from_outer_to_inner([HotkeyScope::Global]);

    let (actions, selection_min, selection_max) = {
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        let actions = ctx.dispatch_hotkeys(&scopes);
        let widget = TextInput {
            id: field_id(),
            rect: rect(),
            placeholder: FIELD_LABEL,
            state: &mut state,
            disabled: false,
            validator: AlwaysValid,
        };
        let _ = show_text_input(&mut ctx, widget, &mut clipboard);
        (
            actions,
            state.selection.min().value(),
            state.selection.max().value(),
        )
    };

    assert_eq!(
        actions,
        vec![select_all_global],
        "dispatch-first absorbs the chord, intentional opposite ordering",
    );
    assert_eq!(
        selection_min, selection_max,
        "TextInput cannot select-all because dispatch already drained Ctrl+A",
    );
}

#[test]
fn hotkey_capture_before_dispatch_claims_recordable_chord() {
    let theme = Arc::new(Theme::light());
    let save_global = action(11);
    let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new(
        KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL),
        HotkeyScope::Global,
        save_global,
    )]) else {
        panic!("registration must succeed");
    };
    let mut focus = focused_field();
    let mut hits = HitFrame::new();
    let prev = HitState::new();
    let mut input = InputSnapshot::idle(FrameInstant::ZERO);
    input.keys_pressed.push(ctrl('s'));
    let mut state = HotkeyCaptureState {
        recording: true,
        chord: None,
    };
    let scopes = HotkeyScopes::from_outer_to_inner([HotkeyScope::Global]);

    let (actions, captured) = {
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        let widget = HotkeyCapture::new(field_id(), rect(), FIELD_LABEL, &mut state);
        let response = show_hotkey_capture(&mut ctx, widget);
        let actions = ctx.dispatch_hotkeys(&scopes);
        (actions, response.captured)
    };

    assert_eq!(
        captured,
        Some(KeyChord::new(
            KeyCode::Char(KeyChar::from_char('s')),
            ModifierMask::CTRL,
        )),
    );
    assert!(
        actions.is_empty(),
        "HotkeyCapture must claim Ctrl+S before dispatch sees it",
    );
}

#[test]
fn dispatch_before_hotkey_capture_steals_chord() {
    let theme = Arc::new(Theme::light());
    let save_global = action(11);
    let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new(
        KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL),
        HotkeyScope::Global,
        save_global,
    )]) else {
        panic!("registration must succeed");
    };
    let mut focus = focused_field();
    let mut hits = HitFrame::new();
    let prev = HitState::new();
    let mut input = InputSnapshot::idle(FrameInstant::ZERO);
    input.keys_pressed.push(ctrl('s'));
    let mut state = HotkeyCaptureState {
        recording: true,
        chord: None,
    };
    let scopes = HotkeyScopes::from_outer_to_inner([HotkeyScope::Global]);

    let (actions, captured) = {
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        let actions = ctx.dispatch_hotkeys(&scopes);
        let widget = HotkeyCapture::new(field_id(), rect(), FIELD_LABEL, &mut state);
        let response = show_hotkey_capture(&mut ctx, widget);
        (actions, response.captured)
    };

    assert_eq!(
        actions,
        vec![save_global],
        "dispatch-first routes the chord to its action, intentional opposite ordering",
    );
    assert!(
        captured.is_none(),
        "capture cannot record because dispatch already drained Ctrl+S",
    );
}
