use std::collections::BTreeSet;

use crate::a11y::{AccessNode, Role};
use crate::frame::{FrameCtx, InteractDeclaration};
use crate::hit_test::Sense;
use crate::input::{KeyCode, ModifierMask, NamedKey};
use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use crate::strings::StringKey;
use crate::theme::{Color, Step12};
use crate::widget_id::{WidgetId, WidgetKey};

use super::keys::{TakeKey, take_key};
use super::paint::{GlyphMark, HorizontalAlign, LabelText, WidgetPaint};
use super::text_input::{AlwaysValid, MemoryClipboard, TextInput, TextInputState, show_text_input};
use super::visuals::push_focus_ring;

#[derive(Clone, Debug, PartialEq)]
pub struct TreeNode {
    pub id: WidgetId,
    pub label: LabelText,
    pub children: Vec<TreeNode>,
    pub disabled: bool,
    pub glyph: Option<GlyphMark>,
}

impl TreeNode {
    #[must_use]
    pub fn leaf(id: WidgetId, label: StringKey) -> Self {
        Self::with_label(id, LabelText::Key(label), Vec::new())
    }

    #[must_use]
    pub fn leaf_owned(id: WidgetId, label: String) -> Self {
        Self::with_label(id, LabelText::Owned(label), Vec::new())
    }

    #[must_use]
    pub fn parent(id: WidgetId, label: StringKey, children: Vec<TreeNode>) -> Self {
        Self::with_label(id, LabelText::Key(label), children)
    }

    #[must_use]
    pub fn parent_owned(id: WidgetId, label: String, children: Vec<TreeNode>) -> Self {
        Self::with_label(id, LabelText::Owned(label), children)
    }

    #[must_use]
    fn with_label(id: WidgetId, label: LabelText, children: Vec<TreeNode>) -> Self {
        Self {
            id,
            label,
            children,
            disabled: false,
            glyph: None,
        }
    }

    #[must_use]
    pub fn disabled(mut self, disabled: bool) -> Self {
        self.disabled = disabled;
        self
    }

    #[must_use]
    pub fn with_glyph(mut self, glyph: GlyphMark) -> Self {
        self.glyph = Some(glyph);
        self
    }

    #[must_use]
    pub fn has_children(&self) -> bool {
        !self.children.is_empty()
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TreeSelectionMode {
    Single,
    Multi,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DropPlacement {
    Before,
    After,
    Into,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct DropTarget {
    pub anchor: WidgetId,
    pub placement: DropPlacement,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct TreeViewState {
    pub expanded: BTreeSet<WidgetId>,
    pub selection: BTreeSet<WidgetId>,
    pub focused: Option<WidgetId>,
    pub renaming: Option<WidgetId>,
    pub rename_buffer: TextInputState,
    pub clipboard: MemoryClipboard,
    pub drag_source: Option<WidgetId>,
    pub drop_target: Option<DropTarget>,
    pub pending_rename: Option<PendingRename>,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct PendingRename {
    pub id: WidgetId,
    pub at: crate::input::FrameInstant,
}

#[derive(Debug, PartialEq)]
pub struct TreeView<'a, 'state> {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub label: StringKey,
    pub roots: &'a [TreeNode],
    pub state: &'state mut TreeViewState,
    pub mode: TreeSelectionMode,
    pub row_height: LayoutPx,
    pub indent_step: LayoutPx,
    pub renamable: &'a [WidgetId],
}

impl<'a, 'state> TreeView<'a, 'state> {
    #[must_use]
    pub const fn new(
        id: WidgetId,
        rect: LayoutRect,
        label: StringKey,
        roots: &'a [TreeNode],
        state: &'state mut TreeViewState,
    ) -> Self {
        Self {
            id,
            rect,
            label,
            roots,
            state,
            mode: TreeSelectionMode::Single,
            row_height: LayoutPx::new(22.0),
            indent_step: LayoutPx::new(16.0),
            renamable: &[],
        }
    }

    #[must_use]
    pub const fn mode(self, mode: TreeSelectionMode) -> Self {
        Self { mode, ..self }
    }

    #[must_use]
    pub const fn renamable(self, renamable: &'a [WidgetId]) -> Self {
        Self { renamable, ..self }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RenameCommit {
    pub id: WidgetId,
    pub text: String,
}

#[derive(Clone, Debug, PartialEq)]
pub struct TreeViewResponse {
    pub activated: Option<WidgetId>,
    pub double_activated: Option<WidgetId>,
    pub rename_committed: Option<RenameCommit>,
    pub rename_cancelled: Option<WidgetId>,
    pub drop_committed: Option<(WidgetId, DropTarget)>,
    pub paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_tree_view(ctx: &mut FrameCtx<'_>, view: TreeView<'_, '_>) -> TreeViewResponse {
    let TreeView {
        id,
        rect,
        label,
        roots,
        state,
        mode,
        row_height,
        indent_step,
        renamable,
    } = view;
    ctx.a11y
        .push(id, rect, AccessNode::new(Role::Tree).with_label(label));
    let mut paint = vec![WidgetPaint::Surface {
        rect,
        fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0),
        border: None,
        radius: ctx.theme().radius.none,
        elevation: None,
    }];
    let visible: Vec<VisibleRow> = flatten(roots, &state.expanded, 0);
    let entry_id = state
        .focused
        .filter(|f| visible.iter().any(|r| r.id == *f))
        .or_else(|| visible.first().map(|r| r.id));
    if let Some(id) = entry_id {
        ctx.focus.register_tab_stop(id);
    }
    let mut activated: Option<WidgetId> = None;
    let mut double_activated: Option<WidgetId> = None;
    let mut drop_committed: Option<(WidgetId, DropTarget)> = None;
    let row_paint = visible
        .iter()
        .enumerate()
        .map(|(idx, row)| {
            let row_rect = row_rect_at(rect, idx, row_height);
            draw_row(
                ctx,
                RowDrawArgs {
                    row,
                    row_rect,
                    indent_step,
                    state,
                    mode,
                    renamable,
                },
                &mut activated,
                &mut double_activated,
                &mut drop_committed,
            )
        })
        .fold(Vec::new(), |mut acc, mut p| {
            acc.append(&mut p);
            acc
        });
    paint.extend(row_paint);
    commit_pending_rename(ctx, &visible, state, renamable);
    let rename_committed = if let Some(id) = state.renaming
        && take_key(ctx.input, &[TakeKey::named(NamedKey::Enter)]).is_some()
    {
        let text = state.rename_buffer.text.clone();
        state.renaming = None;
        state.rename_buffer = TextInputState::default();
        Some(RenameCommit { id, text })
    } else {
        None
    };
    let rename_cancelled = if let Some(id) = state.renaming
        && take_key(ctx.input, &[TakeKey::named(NamedKey::Escape)]).is_some()
    {
        state.renaming = None;
        state.rename_buffer = TextInputState::default();
        Some(id)
    } else {
        None
    };
    if state.renaming.is_none() {
        handle_keyboard(ctx, &visible, state, renamable, &mut activated);
    }
    TreeViewResponse {
        activated,
        double_activated,
        rename_committed,
        rename_cancelled,
        drop_committed,
        paint,
    }
}

#[derive(Clone, Debug, PartialEq)]
struct VisibleRow {
    id: WidgetId,
    label: LabelText,
    depth: usize,
    has_children: bool,
    disabled: bool,
    glyph: Option<GlyphMark>,
}

fn flatten(roots: &[TreeNode], expanded: &BTreeSet<WidgetId>, depth: usize) -> Vec<VisibleRow> {
    roots
        .iter()
        .flat_map(|node| {
            let row = VisibleRow {
                id: node.id,
                label: node.label.clone(),
                depth,
                has_children: node.has_children(),
                disabled: node.disabled,
                glyph: node.glyph,
            };
            let children = if expanded.contains(&node.id) {
                flatten(&node.children, expanded, depth + 1)
            } else {
                Vec::new()
            };
            std::iter::once(row).chain(children)
        })
        .collect()
}

fn row_rect_at(view: LayoutRect, idx: usize, row_height: LayoutPx) -> LayoutRect {
    #[allow(
        clippy::cast_precision_loss,
        reason = "tree row index fits f32 mantissa"
    )]
    let i = idx as f32;
    LayoutRect::new(
        LayoutPos::new(
            view.origin.x,
            LayoutPx::new(view.origin.y.value() + i * row_height.value()),
        ),
        LayoutSize::new(view.size.width, row_height),
    )
}

struct RowDrawArgs<'a> {
    row: &'a VisibleRow,
    row_rect: LayoutRect,
    indent_step: LayoutPx,
    state: &'a mut TreeViewState,
    mode: TreeSelectionMode,
    renamable: &'a [WidgetId],
}

fn draw_row(
    ctx: &mut FrameCtx<'_>,
    args: RowDrawArgs<'_>,
    activated: &mut Option<WidgetId>,
    double_activated: &mut Option<WidgetId>,
    drop_committed: &mut Option<(WidgetId, DropTarget)>,
) -> Vec<WidgetPaint> {
    let RowDrawArgs {
        row,
        row_rect,
        indent_step,
        state,
        mode,
        renamable,
    } = args;
    #[allow(clippy::cast_precision_loss, reason = "tree depth fits f32 mantissa")]
    let indent = LayoutPx::new(row.depth as f32 * indent_step.value());
    let disclosure_icon_rect = disclosure_icon_rect_at(row_rect, indent);
    let disclosure_hit_rect = disclosure_hit_rect_at(row_rect, indent);
    let label_rect = label_rect_at(row_rect, indent);
    let selected = state.selection.contains(&row.id);
    let expanded = state.expanded.contains(&row.id);
    let interaction = ctx.interact(
        InteractDeclaration::new(row.id, row_rect, Sense::DRAGGABLE)
            .focusable(false)
            .active(selected)
            .a11y({
                let node = AccessNode::new(Role::TreeItem)
                    .with_label_text(row.label.clone())
                    .with_selected(selected);
                if row.has_children {
                    node.with_expanded(expanded)
                } else {
                    node
                }
            }),
    );
    let live_focused = ctx.is_focused(row.id);
    apply_row_interaction(
        ctx,
        RowInteractionArgs {
            row,
            row_rect,
            interaction: &interaction,
            state,
            mode,
            renamable,
            outcomes: RowOutcomes {
                activated,
                double_activated,
                drop_committed,
            },
        },
    );
    let mut paint = vec![WidgetPaint::Surface {
        rect: row_rect,
        fill: row_fill(ctx, &interaction, state.selection.contains(&row.id)),
        border: None,
        radius: ctx.theme().radius.none,
        elevation: None,
    }];
    if row.has_children {
        paint.extend(draw_disclosure(
            ctx,
            row,
            disclosure_icon_rect,
            disclosure_hit_rect,
            state,
            row.disabled,
        ));
    }
    if state.renaming == Some(row.id) {
        paint.extend(draw_rename_editor(
            ctx, row.id, &row.label, label_rect, state,
        ));
    } else {
        paint.extend(label_with_glyph_paint(ctx, row, label_rect));
    }
    push_focus_ring(
        ctx,
        &mut paint,
        row_rect,
        ctx.theme().radius.none,
        live_focused,
    );
    if let Some(target) = state.drop_target
        && target.anchor == row.id
    {
        paint.push(WidgetPaint::Surface {
            rect: drop_indicator_rect(row_rect, target.placement),
            fill: ctx.theme().colors.accent.step(Step12::SOLID),
            border: None,
            radius: ctx.theme().radius.none,
            elevation: None,
        });
    }
    paint
}

struct RowOutcomes<'a> {
    activated: &'a mut Option<WidgetId>,
    double_activated: &'a mut Option<WidgetId>,
    drop_committed: &'a mut Option<(WidgetId, DropTarget)>,
}

struct RowInteractionArgs<'a> {
    row: &'a VisibleRow,
    row_rect: LayoutRect,
    interaction: &'a crate::hit_test::Interaction,
    state: &'a mut TreeViewState,
    mode: TreeSelectionMode,
    renamable: &'a [WidgetId],
    outcomes: RowOutcomes<'a>,
}

fn commit_pending_rename(
    ctx: &mut FrameCtx<'_>,
    visible: &[VisibleRow],
    state: &mut TreeViewState,
    renamable: &[WidgetId],
) {
    let Some(pending) = state.pending_rename else {
        return;
    };
    if state.renaming.is_some() || !renamable.contains(&pending.id) {
        state.pending_rename = None;
        return;
    }
    let Some(row) = visible.iter().find(|r| r.id == pending.id) else {
        state.pending_rename = None;
        return;
    };
    let elapsed = ctx.input.frame.since(pending.at);
    if elapsed < ctx.input.double_click_window.duration() {
        return;
    }
    let text = match &row.label {
        LabelText::Key(k) => ctx.strings.resolve(*k).to_owned(),
        LabelText::Owned(s) => s.clone(),
    };
    state.renaming = Some(pending.id);
    state.rename_buffer = TextInputState::from_text(&text);
    state.pending_rename = None;
}

fn apply_row_interaction(ctx: &mut FrameCtx<'_>, args: RowInteractionArgs<'_>) {
    let RowInteractionArgs {
        row,
        row_rect,
        interaction,
        state,
        mode,
        renamable,
        outcomes,
    } = args;
    let RowOutcomes {
        activated,
        double_activated,
        drop_committed,
    } = outcomes;
    if row.disabled {
        return;
    }
    if interaction.click() {
        let was_selected = state.selection.contains(&row.id);
        let is_double = interaction.double_click();
        update_selection(state, row.id, ctx.input.modifiers, mode);
        if activated.is_none() {
            *activated = Some(row.id);
        }
        state.focused = Some(row.id);
        ctx.focus.request_focus(row.id);
        let row_pending = state.pending_rename.is_some_and(|p| p.id == row.id);
        if !is_double && was_selected && state.renaming.is_none() && renamable.contains(&row.id) {
            if !row_pending {
                state.pending_rename = Some(PendingRename {
                    id: row.id,
                    at: ctx.input.frame,
                });
            }
        } else if is_double || (!was_selected && row_pending) {
            state.pending_rename = None;
        }
    }
    if interaction.double_click() {
        if state.pending_rename.is_some_and(|p| p.id == row.id) {
            state.pending_rename = None;
        }
        if double_activated.is_none() {
            *double_activated = Some(row.id);
        }
    }
    if interaction.drag_start() {
        state.drag_source = Some(row.id);
    }
    if interaction.drag_release()
        && let Some(src) = state.drag_source
        && let Some(target) = state.drop_target
    {
        if drop_committed.is_none() && src != target.anchor {
            *drop_committed = Some((src, target));
        }
        state.drag_source = None;
        state.drop_target = None;
    }
    if state.drag_source.is_some() && interaction.hover() {
        let placement = ctx.input.pointer.map_or(DropPlacement::Into, |p| {
            placement_from_pointer(p.position, row_rect)
        });
        state.drop_target = Some(DropTarget {
            anchor: row.id,
            placement,
        });
    }
}

fn draw_disclosure(
    ctx: &mut FrameCtx<'_>,
    row: &VisibleRow,
    icon_rect: LayoutRect,
    hit_rect: LayoutRect,
    state: &mut TreeViewState,
    disabled: bool,
) -> Vec<WidgetPaint> {
    let disclosure_id = row.id.child(WidgetKey::new("disclosure"));
    let sense = if disabled {
        Sense::HOVER
    } else {
        Sense::INTERACTIVE
    };
    let disclosure_interaction = ctx.interact(
        InteractDeclaration::new(disclosure_id, hit_rect, sense)
            .disabled(disabled)
            .a11y(
                AccessNode::new(Role::DisclosureTriangle)
                    .with_label_text(row.label.clone())
                    .with_expanded(state.expanded.contains(&row.id))
                    .with_disabled(disabled),
            ),
    );
    if !disabled && disclosure_interaction.click() {
        toggle_expanded(state, row.id);
    }
    vec![WidgetPaint::Mark {
        rect: icon_rect,
        kind: if state.expanded.contains(&row.id) {
            GlyphMark::DisclosureOpen
        } else {
            GlyphMark::DisclosureClosed
        },
        color: ctx.theme().colors.text_secondary(),
    }]
}

const TREE_GLYPH_COLUMN_PX: f32 = 18.0;

fn label_with_glyph_paint(
    ctx: &FrameCtx<'_>,
    row: &VisibleRow,
    label_rect: LayoutRect,
) -> Vec<WidgetPaint> {
    let color = if row.disabled {
        ctx.theme().colors.text_disabled()
    } else {
        ctx.theme().colors.text_primary()
    };
    let secondary = if row.disabled {
        ctx.theme().colors.text_disabled()
    } else {
        ctx.theme().colors.text_secondary()
    };
    let glyph_paint = row.glyph.map(|kind| WidgetPaint::Mark {
        rect: glyph_slot_rect(label_rect),
        kind,
        color: secondary,
    });
    let text_rect = if row.glyph.is_some() {
        text_after_glyph_rect(label_rect)
    } else {
        label_rect
    };
    glyph_paint
        .into_iter()
        .chain(core::iter::once(WidgetPaint::AlignedLabel {
            rect: text_rect,
            text: row.label.clone(),
            color,
            role: ctx.theme().typography.label,
            align: HorizontalAlign::Start,
        }))
        .collect()
}

fn glyph_slot_rect(label_rect: LayoutRect) -> LayoutRect {
    LayoutRect::new(
        label_rect.origin,
        LayoutSize::new(LayoutPx::new(TREE_GLYPH_COLUMN_PX), label_rect.size.height),
    )
}

fn text_after_glyph_rect(label_rect: LayoutRect) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(label_rect.origin.x.value() + TREE_GLYPH_COLUMN_PX),
            label_rect.origin.y,
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(label_rect.size.width.value() - TREE_GLYPH_COLUMN_PX),
            label_rect.size.height,
        ),
    )
}

const RENAME_FALLBACK_PLACEHOLDER: StringKey = StringKey::new("tree.rename.placeholder");

fn draw_rename_editor(
    ctx: &mut FrameCtx<'_>,
    row_id: WidgetId,
    label: &LabelText,
    label_rect: LayoutRect,
    state: &mut TreeViewState,
) -> Vec<WidgetPaint> {
    let placeholder = match label {
        LabelText::Key(k) => *k,
        LabelText::Owned(_) => RENAME_FALLBACK_PLACEHOLDER,
    };
    let rename_id = row_id.child(WidgetKey::new("rename"));
    let response = show_text_input(
        ctx,
        TextInput {
            id: rename_id,
            rect: label_rect,
            placeholder,
            state: &mut state.rename_buffer,
            disabled: false,
            validator: AlwaysValid,
        },
        &mut state.clipboard,
    );
    if !ctx.is_focused(rename_id) {
        ctx.focus.request_focus(rename_id);
    }
    response.paint
}

fn placement_from_pointer(pointer: LayoutPos, row: LayoutRect) -> DropPlacement {
    let third = row.size.height.value() / 3.0;
    let local = pointer.y.value() - row.origin.y.value();
    if local < third {
        DropPlacement::Before
    } else if local > 2.0 * third {
        DropPlacement::After
    } else {
        DropPlacement::Into
    }
}

fn drop_indicator_rect(row: LayoutRect, placement: DropPlacement) -> LayoutRect {
    let stripe = LayoutPx::new(2.0);
    match placement {
        DropPlacement::Before => {
            LayoutRect::new(row.origin, LayoutSize::new(row.size.width, stripe))
        }
        DropPlacement::After => LayoutRect::new(
            LayoutPos::new(
                row.origin.x,
                LayoutPx::new(row.origin.y.value() + row.size.height.value() - stripe.value()),
            ),
            LayoutSize::new(row.size.width, stripe),
        ),
        DropPlacement::Into => row,
    }
}

fn row_fill(
    ctx: &FrameCtx<'_>,
    interaction: &crate::hit_test::Interaction,
    selected: bool,
) -> Color {
    let neutral = ctx.theme().colors.neutral;
    if selected {
        ctx.theme().colors.accent.step(Step12::HOVER_BG)
    } else if interaction.hover() {
        neutral.step(Step12::HOVER_BG)
    } else {
        Color::TRANSPARENT
    }
}

const DISCLOSURE_ICON_SIZE_PX: f32 = 14.0;
const DISCLOSURE_COLUMN_WIDTH_PX: f32 = 20.0;

fn disclosure_icon_rect_at(row: LayoutRect, indent: LayoutPx) -> LayoutRect {
    let pad_y = (row.size.height.value() - DISCLOSURE_ICON_SIZE_PX) / 2.0;
    let pad_x = (DISCLOSURE_COLUMN_WIDTH_PX - DISCLOSURE_ICON_SIZE_PX) / 2.0;
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(row.origin.x.value() + indent.value() + pad_x),
            LayoutPx::new(row.origin.y.value() + pad_y),
        ),
        LayoutSize::new(
            LayoutPx::new(DISCLOSURE_ICON_SIZE_PX),
            LayoutPx::new(DISCLOSURE_ICON_SIZE_PX),
        ),
    )
}

fn disclosure_hit_rect_at(row: LayoutRect, indent: LayoutPx) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(row.origin.x.value() + indent.value()),
            row.origin.y,
        ),
        LayoutSize::new(LayoutPx::new(DISCLOSURE_COLUMN_WIDTH_PX), row.size.height),
    )
}

fn label_rect_at(row: LayoutRect, indent: LayoutPx) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(row.origin.x.value() + indent.value() + DISCLOSURE_COLUMN_WIDTH_PX),
            row.origin.y,
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(
                row.size.width.value() - indent.value() - DISCLOSURE_COLUMN_WIDTH_PX,
            ),
            row.size.height,
        ),
    )
}

fn update_selection(
    state: &mut TreeViewState,
    id: WidgetId,
    modifiers: ModifierMask,
    mode: TreeSelectionMode,
) {
    match mode {
        TreeSelectionMode::Single => {
            state.selection = BTreeSet::from([id]);
        }
        TreeSelectionMode::Multi => {
            if modifiers.contains(ModifierMask::CTRL) {
                if !state.selection.insert(id) {
                    state.selection.remove(&id);
                }
            } else {
                state.selection = BTreeSet::from([id]);
            }
        }
    }
}

fn toggle_expanded(state: &mut TreeViewState, id: WidgetId) {
    if !state.expanded.insert(id) {
        state.expanded.remove(&id);
    }
}

fn handle_keyboard(
    ctx: &mut FrameCtx<'_>,
    visible: &[VisibleRow],
    state: &mut TreeViewState,
    renamable: &[WidgetId],
    activated: &mut Option<WidgetId>,
) {
    let in_tree_focus = ctx
        .focus
        .focused()
        .is_some_and(|f| visible.iter().any(|r| r.id == f));
    if !in_tree_focus {
        return;
    }
    let focused = ctx.focus.focused();
    let event = take_key(
        ctx.input,
        &[
            TakeKey::named(NamedKey::ArrowUp),
            TakeKey::named(NamedKey::ArrowDown),
            TakeKey::named(NamedKey::ArrowLeft),
            TakeKey::named(NamedKey::ArrowRight),
            TakeKey::named(NamedKey::Home),
            TakeKey::named(NamedKey::End),
            TakeKey::named(NamedKey::Enter),
            TakeKey::named(NamedKey::Space),
            TakeKey::named(NamedKey::F2),
        ],
    );
    let Some(event) = event else { return };
    let current = focused.and_then(|f| visible.iter().position(|r| r.id == f));
    match event.code {
        KeyCode::Named(NamedKey::ArrowDown) => {
            if let Some(idx) = current
                && idx + 1 < visible.len()
            {
                ctx.focus.request_focus(visible[idx + 1].id);
                state.focused = Some(visible[idx + 1].id);
            }
        }
        KeyCode::Named(NamedKey::ArrowUp) => {
            if let Some(idx) = current
                && idx > 0
            {
                ctx.focus.request_focus(visible[idx - 1].id);
                state.focused = Some(visible[idx - 1].id);
            }
        }
        KeyCode::Named(NamedKey::ArrowRight) => {
            if let Some(idx) = current
                && visible[idx].has_children
                && !state.expanded.contains(&visible[idx].id)
            {
                state.expanded.insert(visible[idx].id);
            }
        }
        KeyCode::Named(NamedKey::ArrowLeft) => {
            if let Some(idx) = current
                && state.expanded.contains(&visible[idx].id)
            {
                state.expanded.remove(&visible[idx].id);
            }
        }
        KeyCode::Named(NamedKey::Home) => {
            if let Some(first) = visible.first() {
                ctx.focus.request_focus(first.id);
                state.focused = Some(first.id);
            }
        }
        KeyCode::Named(NamedKey::End) => {
            if let Some(last) = visible.last() {
                ctx.focus.request_focus(last.id);
                state.focused = Some(last.id);
            }
        }
        KeyCode::Named(NamedKey::Enter | NamedKey::Space) => {
            if let Some(idx) = current {
                let id = visible[idx].id;
                state.selection = BTreeSet::from([id]);
                if activated.is_none() {
                    *activated = Some(id);
                }
            }
        }
        KeyCode::Named(NamedKey::F2) => {
            if let Some(idx) = current {
                let row = &visible[idx];
                if !row.disabled && renamable.contains(&row.id) {
                    let text = match &row.label {
                        LabelText::Key(k) => ctx.strings.resolve(*k).to_owned(),
                        LabelText::Owned(s) => s.clone(),
                    };
                    state.renaming = Some(row.id);
                    state.rename_buffer = TextInputState::from_text(&text);
                }
            }
        }
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;
    use std::sync::Arc;

    use super::{
        PendingRename, TreeNode, TreeSelectionMode, TreeView, TreeViewState, show_tree_view,
    };
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState, resolve};
    use crate::hotkey::HotkeyTable;
    use crate::input::{
        DoubleClickWindow, FrameInstant, InputSnapshot, KeyCode, KeyEvent, ModifierMask, NamedKey,
        PointerButton, PointerButtonMask, PointerSample,
    };
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::{StringKey, StringTable};
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};

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

    fn sample_tree() -> Vec<TreeNode> {
        vec![
            TreeNode::parent(
                root_id("origin"),
                StringKey::new("tree.origin"),
                vec![
                    TreeNode::leaf(root_id("plane_xy"), StringKey::new("tree.xy")),
                    TreeNode::leaf(root_id("plane_yz"), StringKey::new("tree.yz")),
                ],
            ),
            TreeNode::leaf(root_id("sketch"), StringKey::new("tree.sketch")),
        ]
    }

    fn render(
        roots: &[TreeNode],
        state: &mut TreeViewState,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
    ) -> (super::TreeViewResponse, HitState) {
        render_with(roots, state, focus, snap, prev, &[])
    }

    fn render_with(
        roots: &[TreeNode],
        state: &mut TreeViewState,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
        renamable: &[WidgetId],
    ) -> (super::TreeViewResponse, HitState) {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(400.0)),
        );
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                snap,
                focus,
                &table,
                StringTable::empty(),
                &mut hits,
                prev,
                &mut a11y,
                &mut shaper,
            );
            show_tree_view(
                &mut ctx,
                TreeView::new(
                    WidgetId::ROOT.child(WidgetKey::new("tree")),
                    rect,
                    StringKey::new("test.tree"),
                    roots,
                    state,
                )
                .renamable(renamable),
            )
        };
        let next = resolve(prev, &hits, snap, focus.focused());
        (response, next)
    }

    fn press(pos: LayoutPos) -> InputSnapshot {
        press_at(pos, FrameInstant::ZERO)
    }

    fn release(pos: LayoutPos) -> InputSnapshot {
        release_at(pos, FrameInstant::ZERO)
    }

    fn idle(pos: LayoutPos) -> InputSnapshot {
        idle_at(pos, FrameInstant::ZERO)
    }

    fn press_at(pos: LayoutPos, at: FrameInstant) -> InputSnapshot {
        let mut s = InputSnapshot::idle(at);
        s.pointer = Some(PointerSample::new(pos));
        s.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn release_at(pos: LayoutPos, at: FrameInstant) -> InputSnapshot {
        let mut s = InputSnapshot::idle(at);
        s.pointer = Some(PointerSample::new(pos));
        s.buttons_released = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn idle_at(pos: LayoutPos, at: FrameInstant) -> InputSnapshot {
        let mut s = InputSnapshot::idle(at);
        s.pointer = Some(PointerSample::new(pos));
        s
    }

    #[test]
    fn click_disclosure_toggles_expansion() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let pos = LayoutPos::new(LayoutPx::new(8.0), LayoutPx::new(11.0));
        [press(pos), release(pos), idle(pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (_, next) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
                prev = next;
            });
        assert!(state.expanded.contains(&roots[0].id));
    }

    #[test]
    fn click_indent_column_outside_old_icon_still_toggles() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let pos = LayoutPos::new(LayoutPx::new(18.0), LayoutPx::new(11.0));
        [press(pos), release(pos), idle(pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (_, next) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
                prev = next;
            });
        assert!(
            state.expanded.contains(&roots[0].id),
            "indent column click toggles even outside the 14px icon",
        );
    }

    #[test]
    fn click_disclosure_toggles_back_after_collapse() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        state.expanded.insert(roots[0].id);
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let pos = LayoutPos::new(LayoutPx::new(8.0), LayoutPx::new(11.0));
        [press(pos), release(pos), idle(pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (_, next) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
                prev = next;
            });
        assert!(!state.expanded.contains(&roots[0].id), "first click closes");
        [press(pos), release(pos), idle(pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (_, next) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
                prev = next;
            });
        assert!(
            state.expanded.contains(&roots[0].id),
            "second click reopens",
        );
    }

    #[test]
    fn click_label_selects_node_and_marks_active() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(80.0), LayoutPx::new(11.0));
        let mut last: Option<super::TreeViewResponse> = None;
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (response, next) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
                last = Some(response);
                prev = next;
            });
        let Some(response) = last else {
            panic!("response missing")
        };
        assert_eq!(response.activated, Some(roots[0].id));
        assert!(state.selection.contains(&roots[0].id));
    }

    #[test]
    fn arrow_down_moves_focus_to_next_visible() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        state.expanded.insert(roots[0].id);
        let mut focus = FocusManager::new();
        focus.register_focusable(roots[0].id);
        focus.request_focus(roots[0].id);
        focus.end_frame();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::ArrowDown),
            ModifierMask::NONE,
        ));
        let _ = render(&roots, &mut state, &mut focus, &mut snap, &prev);
        assert_eq!(state.focused, Some(roots[0].children[0].id));
    }

    #[test]
    fn arrow_right_expands_collapsed_parent() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        focus.register_focusable(roots[0].id);
        focus.request_focus(roots[0].id);
        focus.end_frame();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::ArrowRight),
            ModifierMask::NONE,
        ));
        let _ = render(&roots, &mut state, &mut focus, &mut snap, &prev);
        assert!(state.expanded.contains(&roots[0].id));
    }

    #[test]
    fn ctrl_click_in_multi_mode_toggles_selection_membership() {
        let roots = sample_tree();
        let mut state = TreeViewState {
            selection: BTreeSet::from([roots[1].id]),
            ..TreeViewState::default()
        };
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(80.0), LayoutPx::new(11.0));
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(400.0)),
        );
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                snap.modifiers = ModifierMask::CTRL;
                let mut hits = HitFrame::new();
                {
                    let mut shaper = bone_text::Shaper::new();
                    let mut a11y = crate::a11y::AccessTreeBuilder::new();
                    let mut ctx = FrameCtx::new(
                        theme.clone(),
                        &mut snap,
                        &mut focus,
                        &table,
                        StringTable::empty(),
                        &mut hits,
                        &prev,
                        &mut a11y,
                        &mut shaper,
                    );
                    let _ = show_tree_view(
                        &mut ctx,
                        TreeView::new(
                            WidgetId::ROOT.child(WidgetKey::new("tree")),
                            rect,
                            StringKey::new("test.tree"),
                            &roots,
                            &mut state,
                        )
                        .mode(TreeSelectionMode::Multi),
                    );
                }
                prev = resolve(&prev, &hits, &snap, focus.focused());
            });
        assert!(state.selection.contains(&roots[0].id));
        assert!(state.selection.contains(&roots[1].id));
    }

    #[test]
    fn tree_registers_single_tab_stop_so_tab_does_not_walk_every_row() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        state.expanded.insert(roots[0].id);
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let _ = render(&roots, &mut state, &mut focus, &mut snap, &prev);
        let stops: Vec<WidgetId> = focus.tab_stops().iter().map(|(id, _)| *id).collect();
        assert_eq!(stops, vec![roots[0].id]);
    }

    #[test]
    fn click_row_focuses_it_so_arrow_keys_engage() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(80.0), LayoutPx::new(11.0));
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (_, next) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
                prev = next;
            });
        assert_eq!(focus.focused(), Some(roots[0].id));
    }

    #[test]
    fn enter_with_renaming_clears_state_and_emits_committed() {
        use super::RenameCommit;
        use crate::widgets::text_input::TextInputState;
        let roots = sample_tree();
        let mut state = TreeViewState {
            renaming: Some(roots[0].id),
            rename_buffer: TextInputState::from_text("renamed"),
            ..TreeViewState::default()
        };
        let mut focus = FocusManager::new();
        focus.register_focusable(roots[0].id);
        focus.request_focus(roots[0].id);
        focus.end_frame();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Enter),
            ModifierMask::NONE,
        ));
        let (response, _) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
        assert_eq!(
            response.rename_committed,
            Some(RenameCommit {
                id: roots[0].id,
                text: "renamed".into(),
            }),
        );
        assert!(state.renaming.is_none());
    }

    #[test]
    fn keyboard_enter_on_focused_row_emits_activated() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        focus.register_focusable(roots[0].id);
        focus.request_focus(roots[0].id);
        focus.end_frame();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Enter),
            ModifierMask::NONE,
        ));
        let (response, _) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
        assert_eq!(response.activated, Some(roots[0].id));
    }

    #[test]
    fn f2_on_renamable_focused_row_starts_rename_prefilled_with_label() {
        let sketch_id = root_id("sketch");
        let roots = vec![TreeNode::leaf_owned(sketch_id, "Profile".to_owned())];
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        focus.register_focusable(sketch_id);
        focus.request_focus(sketch_id);
        focus.end_frame();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::F2),
            ModifierMask::NONE,
        ));
        let (_, _) = render_with(
            &roots,
            &mut state,
            &mut focus,
            &mut snap,
            &prev,
            &[sketch_id],
        );
        assert_eq!(state.renaming, Some(sketch_id));
        assert_eq!(state.rename_buffer.text, "Profile");
    }

    #[test]
    fn slow_click_on_already_selected_renamable_row_starts_rename() {
        let sketch_id = root_id("sketch");
        let roots = vec![TreeNode::leaf_owned(sketch_id, "Profile".to_owned())];
        let mut state = TreeViewState {
            selection: BTreeSet::from([sketch_id]),
            ..TreeViewState::default()
        };
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(80.0), LayoutPx::new(11.0));
        let window = DoubleClickWindow::DEFAULT.duration();
        let after_click = FrameInstant::from_duration(core::time::Duration::from_millis(10));
        let after_window = FrameInstant::from_duration(
            after_click.duration() + window + core::time::Duration::from_millis(5),
        );
        let frames = [
            press_at(click_pos, FrameInstant::ZERO),
            release_at(click_pos, FrameInstant::ZERO),
            idle_at(click_pos, after_click),
            idle_at(click_pos, after_window),
        ];
        frames.into_iter().for_each(|mut snap| {
            let (_, next) = render_with(
                &roots,
                &mut state,
                &mut focus,
                &mut snap,
                &prev,
                &[sketch_id],
            );
            prev = next;
        });
        assert_eq!(state.renaming, Some(sketch_id));
        assert_eq!(state.rename_buffer.text, "Profile");
    }

    #[test]
    fn extra_slow_click_on_already_pending_row_does_not_reset_pending_at() {
        let sketch_id = root_id("sketch");
        let roots = vec![TreeNode::leaf_owned(sketch_id, "Profile".to_owned())];
        let original_at = FrameInstant::from_duration(core::time::Duration::from_millis(5));
        let mut state = TreeViewState {
            selection: BTreeSet::from([sketch_id]),
            pending_rename: Some(PendingRename {
                id: sketch_id,
                at: original_at,
            }),
            ..TreeViewState::default()
        };
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(80.0), LayoutPx::new(11.0));
        let new_click = FrameInstant::from_duration(core::time::Duration::from_millis(120));
        let new_visible = FrameInstant::from_duration(
            new_click.duration() + core::time::Duration::from_millis(10),
        );
        let frames = [
            press_at(click_pos, new_click),
            release_at(click_pos, new_click),
            idle_at(click_pos, new_visible),
        ];
        frames.into_iter().for_each(|mut snap| {
            let (_, next) = render_with(
                &roots,
                &mut state,
                &mut focus,
                &mut snap,
                &prev,
                &[sketch_id],
            );
            prev = next;
        });
        let Some(pending) = state.pending_rename else {
            panic!("pending rename must persist across slow extra clicks");
        };
        assert_eq!(
            pending.at, original_at,
            "subsequent slow click on already-pending row must keep the original at",
        );
    }

    #[test]
    fn double_click_on_already_selected_renamable_row_does_not_start_rename() {
        let sketch_id = root_id("sketch");
        let roots = vec![TreeNode::leaf_owned(sketch_id, "Profile".to_owned())];
        let mut state = TreeViewState {
            selection: BTreeSet::from([sketch_id]),
            ..TreeViewState::default()
        };
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(80.0), LayoutPx::new(11.0));
        let window = DoubleClickWindow::DEFAULT.duration();
        let fast = FrameInstant::from_duration(core::time::Duration::from_millis(60));
        let after_window = FrameInstant::from_duration(
            fast.duration() + window + core::time::Duration::from_millis(5),
        );
        let frames = [
            press_at(click_pos, FrameInstant::ZERO),
            release_at(click_pos, FrameInstant::ZERO),
            press_at(click_pos, fast),
            release_at(click_pos, fast),
            idle_at(click_pos, after_window),
        ];
        frames.into_iter().for_each(|mut snap| {
            let (_, next) = render_with(
                &roots,
                &mut state,
                &mut focus,
                &mut snap,
                &prev,
                &[sketch_id],
            );
            prev = next;
        });
        assert_eq!(
            state.renaming, None,
            "fast double-click on already-selected row must not start rename",
        );
        assert_eq!(
            state.pending_rename, None,
            "double-click must clear any deferred rename",
        );
    }

    #[test]
    fn double_click_on_renamable_row_does_not_start_rename() {
        let sketch_id = root_id("sketch");
        let roots = vec![TreeNode::leaf_owned(sketch_id, "Profile".to_owned())];
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(80.0), LayoutPx::new(11.0));
        let frames = [
            press(click_pos),
            release(click_pos),
            idle(click_pos),
            press(click_pos),
            release(click_pos),
            idle(click_pos),
        ];
        let mut last: Option<super::TreeViewResponse> = None;
        frames.into_iter().for_each(|mut snap| {
            let (response, next) = render_with(
                &roots,
                &mut state,
                &mut focus,
                &mut snap,
                &prev,
                &[sketch_id],
            );
            last = Some(response);
            prev = next;
        });
        assert!(
            state.renaming.is_none(),
            "fast double-click must enter via double_activated, not rename",
        );
        let Some(response) = last else {
            panic!("response captured");
        };
        assert_eq!(response.double_activated, Some(sketch_id));
    }

    #[test]
    fn f2_on_non_renamable_row_is_ignored() {
        let roots = sample_tree();
        let mut state = TreeViewState::default();
        let mut focus = FocusManager::new();
        focus.register_focusable(roots[1].id);
        focus.request_focus(roots[1].id);
        focus.end_frame();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::F2),
            ModifierMask::NONE,
        ));
        let (_, _) = render(&roots, &mut state, &mut focus, &mut snap, &prev);
        assert!(state.renaming.is_none());
    }
}
