use crate::form_editor;
use crate::frame_snapshot::FrameSnapshot;
use crate::input::{ActiveTab, AppState, ArgValue, Focus, UiState};
use crate::layout::form as form_layout;
use crate::query::{
    form::{self, FieldWidget, OrderedArg},
    selectors,
    tree::TreeRow,
};
use crate::repeated_field::project_repeated_field;
use crate::spec::{CommandPath, SelectionError};
use std::collections::HashMap;

pub(crate) fn switch_tab(state: &mut AppState, tab: ActiveTab) {
    let tabs = UiState::visible_tabs();
    let target = tabs
        .into_iter()
        .find(|candidate| *candidate == tab)
        .unwrap_or(ActiveTab::Inputs);
    if target == state.ui.active_tab {
        return;
    }
    state.ui.active_tab = target;
    state.ui.last_non_help_tab = state.ui.active_tab;
    state.sync_visible_form_selection();
    state.ui.reset_transient_form_ui();
}

pub(crate) fn toggle_help_tab(state: &mut AppState) {
    state.ui.toggle_help();
}

pub(crate) fn move_sidebar_selection(
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
    delta: isize,
) {
    let items = selectors::visible_sidebar_items(
        &state.domain.root,
        &state.domain.expanded,
        &state.ui.search_query,
    );
    if items.is_empty() {
        return;
    }
    if state.domain.selected_path().is_empty() {
        if delta > 0 {
            select_command(state, items[0].path.as_slice());
        }
        return;
    }
    let current_index = match items
        .iter()
        .position(|item| item.path == *state.domain.selected_path())
    {
        Some(current_index) => current_index,
        None if delta > 0 => {
            select_command(state, items[0].path.as_slice());
            return;
        }
        None if delta < 0 => {
            select_command(state, items[items.len() - 1].path.as_slice());
            return;
        }
        None => return,
    };
    if delta < 0 && current_index == 0 {
        select_root(state);
        return;
    }
    let next_index = current_index
        .saturating_add_signed(delta)
        .min(items.len() - 1);
    if *state.domain.selected_path() != items[next_index].path {
        select_command(state, items[next_index].path.as_slice());
    }
    ensure_selected_sidebar_visible(state, frame_snapshot);
}

pub(crate) fn move_form_selection(
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
    delta: isize,
) {
    if state.ui.help_open {
        return;
    }
    let root = state.domain.root.clone();
    let selected_path = state.domain.selected_path().clone();
    let args = selectors::visible_form_args(&root, &selected_path, state.ui.active_tab);
    if args.is_empty() {
        return;
    }
    let current_pos = args
        .iter()
        .position(|item| item.order_index == state.ui.selected_arg_index)
        .unwrap_or(0);
    let next_pos = current_pos.saturating_add_signed(delta).min(args.len() - 1);
    state.ui.set_selected_arg_index(args[next_pos].order_index);
    ensure_form_visible(state, frame_snapshot);
}

pub(crate) fn select_sidebar(state: &mut AppState, frame_snapshot: &FrameSnapshot) {
    let items = selectors::visible_sidebar_items(
        &state.domain.root,
        &state.domain.expanded,
        &state.ui.search_query,
    );
    if let Some(item) = items
        .iter()
        .find(|item| item.path == *state.domain.selected_path())
        .filter(|item| item.has_children)
    {
        toggle_expand(state, &item.path, item.expanded);
    }
    ensure_selected_sidebar_visible(state, frame_snapshot);
}

pub(crate) fn collapse_selected(state: &mut AppState, frame_snapshot: &FrameSnapshot) {
    let items = selectors::visible_sidebar_items(
        &state.domain.root,
        &state.domain.expanded,
        &state.ui.search_query,
    );
    if let Some(item) = items
        .iter()
        .find(|item| item.path == *state.domain.selected_path())
        .filter(|item| item.has_children && item.expanded)
    {
        toggle_expand(state, &item.path, true);
    }
    ensure_selected_sidebar_visible(state, frame_snapshot);
}

pub(crate) fn expand_selected(state: &mut AppState, frame_snapshot: &FrameSnapshot) {
    let items = selectors::visible_sidebar_items(
        &state.domain.root,
        &state.domain.expanded,
        &state.ui.search_query,
    );
    if let Some(item) = items
        .iter()
        .find(|item| item.path == *state.domain.selected_path())
        .filter(|item| item.has_children && !item.expanded)
    {
        toggle_expand(state, &item.path, false);
    }
    ensure_selected_sidebar_visible(state, frame_snapshot);
}

pub(crate) fn sidebar_right(state: &mut AppState, frame_snapshot: &FrameSnapshot) {
    if state.domain.selected_path().is_empty() {
        state.ui.focus_form();
        return;
    }

    let items = selectors::visible_sidebar_items(
        &state.domain.root,
        &state.domain.expanded,
        &state.ui.search_query,
    );
    let Some(item) = items
        .iter()
        .find(|item| item.path == *state.domain.selected_path())
    else {
        state.ui.focus_form();
        return;
    };

    if item.has_children && !item.expanded {
        toggle_expand(state, &item.path, false);
        ensure_selected_sidebar_visible(state, frame_snapshot);
    } else {
        state.ui.focus_form();
    }
}

pub(crate) fn handle_escape(state: &mut AppState) {
    if state.ui.help_open {
        state.ui.toggle_help();
        return;
    }
    if state.ui.dropdown_open.is_some() {
        state.ui.close_dropdown();
        return;
    }

    match state.ui.focus {
        Focus::Search | Focus::Form => state.ui.focus_sidebar(),
        Focus::Sidebar if !state.domain.selected_path().is_empty() => select_root(state),
        Focus::Sidebar => {}
    }
}

pub(crate) fn activate_form_field(state: &mut AppState, frame_snapshot: &FrameSnapshot) {
    if state.ui.help_open {
        return;
    }
    let root = state.domain.root.clone();
    let selected_path = state.domain.selected_path().clone();
    let args = selectors::visible_form_args(&root, &selected_path, state.ui.active_tab);
    let Some(item) = selectors::active_form_field(&args, state.ui.selected_arg_index) else {
        return;
    };
    let arg = item.arg;
    if !state.field_can_edit(arg) {
        return;
    }
    if matches!(item.widget, FieldWidget::Toggle) {
        state.domain.toggle_flag_touched(&arg.id);
    } else if matches!(
        item.widget,
        FieldWidget::SingleChoice | FieldWidget::MultiChoice
    ) {
        if arg.choices.is_empty() || state.ui.dropdown_open.as_deref() == Some(arg.id.as_str()) {
            state.ui.close_dropdown();
        } else {
            open_enum_dropdown(state, frame_snapshot, &arg.id, arg.choices.len());
        }
    } else if matches!(item.widget, FieldWidget::Counter) {
        state.domain.increment_counter(&arg.id);
    } else if matches!(item.widget, FieldWidget::OptionalValue) {
        let owner_key = state.domain.command_path_key_for(arg.owner_path());
        let state_kind = state
            .domain
            .forms
            .get(&owner_key)
            .and_then(|form| form.input(&arg.id))
            .map(|input| match &input.value {
                crate::input::ArgInput::Flag { present: true, .. } => "flag",
                crate::input::ArgInput::Values { occurrences }
                    if input.touched
                        && occurrences.iter().any(|occurrence| {
                            occurrence.values.iter().any(|value| !value.is_empty())
                        }) =>
                {
                    "value"
                }
                _ => "empty",
            });
        match state_kind {
            Some("flag") => state.domain.clear_value_and_untouch(&arg.id),
            Some("value") => state.ui.focus_form(),
            _ => state.domain.toggle_optional_value_flag(&arg.id, true),
        }
    } else {
        state.ui.focus_form();
    }
}

pub(crate) fn open_enum_dropdown(
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
    arg_id: &str,
    total: usize,
) {
    if total == 0 {
        state.ui.close_dropdown();
        state.ui.set_dropdown_scroll(0);
        return;
    }
    if let Some(arg) = state.domain.arg_for_input(arg_id).cloned()
        && !state.field_can_edit(&arg)
    {
        state.ui.close_dropdown();
        state.ui.set_dropdown_scroll(0);
        return;
    }

    let (clear_offset, augmented_total) =
        state
            .domain
            .arg_for_input(arg_id)
            .cloned()
            .map_or((0, total), |arg| {
                let widget = form::widget_for(&arg);
                let required = state.field_required(&arg);
                let has_clear_row = form::single_choice_has_clear_row(&arg, widget, required);
                let offset = usize::from(has_clear_row);
                (offset, total.saturating_add(offset))
            });

    state.ui.open_dropdown(arg_id.to_string(), 0, 0);
    let current = state
        .domain
        .arg_for_input(arg_id)
        .and_then(|arg| {
            state
                .domain
                .current_form()
                .and_then(|inputs| inputs.selected_values(arg).first().cloned())
                .and_then(|selected| arg.choices.iter().position(|choice| *choice == selected))
                .or_else(|| {
                    state
                        .domain
                        .current_form()
                        .and_then(|inputs| inputs.compatibility_value(arg))
                        .and_then(|value| match value {
                            ArgValue::Choice(selected) => {
                                arg.choices.iter().position(|choice| *choice == selected)
                            }
                            _ => None,
                        })
                })
                .map(|position| position + clear_offset)
        })
        .unwrap_or(0);
    let visible_rows = frame_snapshot
        .dropdown_geometry_for_input(arg_id, augmented_total)
        .map_or(
            augmented_total.min(usize::from(crate::frame_snapshot::MAX_DROPDOWN_ROWS)),
            |layout| layout.visible_rows,
        );
    let max_scroll = augmented_total.saturating_sub(visible_rows);
    state
        .ui
        .set_dropdown_scroll(current.saturating_sub(visible_rows / 2).min(max_scroll));
    state.ui.set_dropdown_cursor(current);
}

pub(crate) fn ensure_form_visible(state: &mut AppState, frame_snapshot: &FrameSnapshot) {
    if state.ui.help_open {
        return;
    }
    let Some(form_area) = frame_snapshot.form_view_rect() else {
        return;
    };
    let root = state.domain.root.clone();
    let selected_path = state.domain.selected_path().clone();
    let args = selectors::visible_form_args(&root, &selected_path, state.ui.active_tab);
    let derived = state.derived().clone();
    let Some((input_top, input_bottom)) =
        form_layout::field_content_bounds_with_layout_overrides_and_semantics(
            &args,
            state.ui.selected_arg_index,
            &derived.validation.field_errors,
            &HashMap::new(),
            &HashMap::new(),
            &derived.field_semantics,
        )
    else {
        return;
    };
    let visible_top = state.ui.form_scroll(frame_snapshot);
    let visible_bottom = visible_top.saturating_add(form_area.height);

    if input_top < visible_top {
        state.ui.set_form_scroll(input_top);
    } else if input_bottom > visible_bottom {
        state.ui.set_form_scroll(
            state
                .ui
                .form_scroll
                .saturating_add(input_bottom.saturating_sub(visible_bottom)),
        );
    }
    state.ui.clamp_form_scroll(frame_snapshot);
}

pub(crate) fn ensure_active_repeated_row_visible(
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
    arg_id: &str,
    active_row_index: Option<usize>,
) {
    if state.ui.help_open {
        return;
    }
    let Some(form_view) = frame_snapshot.form_view_rect() else {
        return;
    };
    let Some(active_row_index) = active_row_index else {
        return;
    };

    let root = state.domain.root.clone();
    let selected_path = state.domain.selected_path().clone();
    let args = selectors::visible_form_args(&root, &selected_path, state.ui.active_tab);
    let derived = state.derived().clone();
    let input_height_overrides = repeated_input_height_overrides(state, &args);
    let label_height_overrides = HashMap::new();
    let content_height = form_layout::measure_fields_height_with_layout_overrides_and_semantics(
        &args,
        &derived.validation.field_errors,
        &input_height_overrides,
        &label_height_overrides,
        &derived.field_semantics,
    );
    let scroll_max = content_height.saturating_sub(form_view.height);
    state
        .ui
        .set_form_scroll(state.ui.form_scroll.min(scroll_max));

    let Some(item) = args.iter().find(|item| item.arg.id == arg_id) else {
        return;
    };
    let arg = item.arg;
    if !matches!(form::widget_for(arg), FieldWidget::RepeatedText) {
        return;
    }

    let Some(field_top) = form_field_content_top(
        &args,
        &derived.validation.field_errors,
        &label_height_overrides,
        &input_height_overrides,
        &derived.field_semantics,
        arg_id,
    ) else {
        return;
    };
    let preferred_label_width =
        form_layout::preferred_label_column_width_with_semantics(&args, &derived.field_semantics);
    let (_, _, input_x, input_width) = form_layout::field_content_geometry(
        frame_snapshot.layout.form.unwrap_or(form_view),
        form::field_is_in_section(item),
        preferred_label_width,
    );
    let semantic_reason = derived
        .field_semantics
        .get(&crate::pipeline::FieldInstanceId::from_arg(arg))
        .and_then(|semantics| semantics.reason.as_deref());
    let show_description = form::field_has_description(
        arg,
        derived
            .validation
            .field_errors
            .get(arg_id)
            .map(String::as_str)
            .or(semantic_reason),
    );
    let displayed = form_editor::displayed_text(state, arg);
    let projection = project_repeated_field(
        &state.ui,
        arg,
        &displayed,
        field_top,
        input_x,
        input_width,
        show_description,
        1,
    );
    let Some(row) = projection.row(active_row_index) else {
        return;
    };
    let preferred = projection
        .description
        .map(|description| vertical_target(row.y, description.y.saturating_add(description.height)))
        .filter(|target| target.height() <= form_view.height);
    let fallback = vertical_target(row.y, row.y.saturating_add(row.height));
    let next_scroll = repeated_row_scroll_target(
        state.ui.form_scroll,
        scroll_max,
        form_view.height,
        preferred,
        fallback,
    );
    if next_scroll != state.ui.form_scroll {
        state.ui.set_form_scroll(next_scroll);
    }
}

fn repeated_input_height_overrides(
    state: &AppState,
    args: &[OrderedArg<'_>],
) -> HashMap<String, u16> {
    args.iter()
        .filter(|item| matches!(item.widget, FieldWidget::RepeatedText))
        .map(|item| {
            let value = form_editor::displayed_text(state, item.arg);
            (
                item.arg.id.clone(),
                crate::repeated_field::repeated_input_height(&state.ui, item.arg, &value),
            )
        })
        .collect()
}

fn form_field_content_top(
    args: &[OrderedArg<'_>],
    field_errors: &std::collections::BTreeMap<String, String>,
    label_height_overrides: &HashMap<String, u16>,
    input_height_overrides: &HashMap<String, u16>,
    field_semantics: &std::collections::BTreeMap<
        crate::pipeline::FieldInstanceId,
        crate::pipeline::FieldSemantics,
    >,
    arg_id: &str,
) -> Option<u16> {
    let mut y: u16 = 0;
    let mut previous_heading = None;
    for item in args {
        if form::field_heading(previous_heading, item).is_some() {
            y = y.saturating_add(1);
        }
        if item.arg.id == arg_id {
            return Some(y);
        }
        let semantic_reason = field_semantics
            .get(&crate::pipeline::FieldInstanceId::from_arg(item.arg))
            .and_then(|semantics| semantics.reason.as_deref());
        let show_description = form::field_has_description(
            item.arg,
            field_errors
                .get(&item.arg.id)
                .map(String::as_str)
                .or(semantic_reason),
        );
        let metrics = form_layout::field_metrics_with_description_and_layout_overrides(
            item.arg,
            show_description,
            input_height_overrides.get(&item.arg.id).copied(),
            label_height_overrides.get(&item.arg.id).copied(),
        );
        y = y.saturating_add(metrics.total_height);
        previous_heading = item.section_heading.as_deref();
    }
    None
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct VerticalTarget {
    top: u16,
    bottom: u16,
}

impl VerticalTarget {
    fn height(self) -> u16 {
        self.bottom.saturating_sub(self.top)
    }

    fn fully_visible(self, viewport_top: u16, viewport_bottom: u16) -> bool {
        self.top >= viewport_top && self.bottom <= viewport_bottom
    }
}

fn vertical_target(top: u16, bottom: u16) -> VerticalTarget {
    VerticalTarget { top, bottom }
}

fn repeated_row_scroll_target(
    current_scroll: u16,
    scroll_max: u16,
    viewport_height: u16,
    preferred: Option<VerticalTarget>,
    fallback: VerticalTarget,
) -> u16 {
    let viewport_top = current_scroll;
    let viewport_bottom = viewport_top.saturating_add(viewport_height);
    let target = preferred.unwrap_or(fallback);
    if target.fully_visible(viewport_top, viewport_bottom) {
        return current_scroll;
    }
    if fallback.fully_visible(viewport_top, viewport_bottom) {
        return current_scroll;
    }
    if let Some(preferred) = preferred
        && !preferred.fully_visible(viewport_top, viewport_bottom)
    {
        return reveal_target_with_padding(current_scroll, scroll_max, viewport_height, preferred);
    }
    reveal_target_with_padding(current_scroll, scroll_max, viewport_height, fallback)
}

fn reveal_target_with_padding(
    current_scroll: u16,
    scroll_max: u16,
    viewport_height: u16,
    target: VerticalTarget,
) -> u16 {
    let viewport_bottom = current_scroll.saturating_add(viewport_height);
    if target.top < current_scroll {
        target.top
    } else if target.bottom > viewport_bottom {
        target
            .bottom
            .saturating_sub(viewport_height)
            .min(scroll_max)
    } else {
        current_scroll
    }
}

pub(crate) fn focus_first_invalid_field(
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
    validation: &crate::pipeline::ValidationState,
) {
    let root = state.domain.root.clone();
    let selected_path = state.domain.selected_path().clone();
    let args = selectors::visible_form_args(&root, &selected_path, state.ui.active_tab);
    let Some(item) = selectors::first_invalid_visible_field(&args, &validation.field_errors) else {
        return;
    };
    state.ui.set_selected_arg_index(item.order_index);
    state.ui.focus_form();
    ensure_form_visible(state, frame_snapshot);
}

pub(crate) fn scroll_form(state: &mut AppState, frame_snapshot: &FrameSnapshot, delta: i16) {
    if state.ui.help_open {
        state.ui.adjust_help_scroll(delta);
        state.ui.clamp_help_scroll(frame_snapshot);
        return;
    }
    state.ui.adjust_form_scroll(delta);
    state.ui.clamp_form_scroll(frame_snapshot);
}

pub(crate) fn scroll_enum(state: &mut AppState, frame_snapshot: &FrameSnapshot, delta: i16) {
    let Some(arg_id) = state.ui.dropdown_open.as_deref() else {
        return;
    };
    let total = state
        .domain
        .arg_for_input(arg_id)
        .map_or(0, |arg| arg.choices.len());
    let Some(visible) = frame_snapshot.dropdown_visible_rows() else {
        return;
    };
    state.ui.adjust_dropdown_scroll(delta, total, visible);
}

pub(crate) fn scroll_sidebar(state: &mut AppState, frame_snapshot: &FrameSnapshot, delta: i16) {
    let rows = selectors::visible_sidebar_rows(
        &state.domain.root,
        &state.domain.expanded,
        &state.ui.search_query,
    );
    let Some(visible_rows) = frame_snapshot.sidebar_visible_rows() else {
        return;
    };
    if visible_rows == 0 {
        return;
    }
    state
        .ui
        .adjust_sidebar_scroll(delta, rows.len(), visible_rows);
}

pub(crate) fn ensure_enum_visible(
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
    index: usize,
    total: usize,
) {
    let Some(visible) = frame_snapshot.dropdown_visible_rows() else {
        return;
    };
    if visible == 0 {
        return;
    }
    let max_scroll = total.saturating_sub(visible);
    if index < state.ui.dropdown_scroll {
        state.ui.set_dropdown_scroll(index);
    } else if index >= state.ui.dropdown_scroll + visible {
        state
            .ui
            .set_dropdown_scroll(index.saturating_sub(visible - 1));
    }
    state
        .ui
        .set_dropdown_scroll(state.ui.dropdown_scroll.min(max_scroll));
}

pub(crate) fn clamp_sidebar_selection_to_search(
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
) {
    let items = selectors::visible_sidebar_items(
        &state.domain.root,
        &state.domain.expanded,
        &state.ui.search_query,
    );
    if items.is_empty() {
        if !state.domain.selected_path().is_empty() {
            select_root(state);
        }
        state.ui.set_sidebar_scroll(0);
        return;
    }

    if !items
        .iter()
        .any(|item| item.path == *state.domain.selected_path())
    {
        select_command(state, items[0].path.as_slice());
    }

    ensure_selected_sidebar_visible(state, frame_snapshot);
}

pub(crate) fn ensure_selected_sidebar_visible(
    state: &mut AppState,
    frame_snapshot: &FrameSnapshot,
) {
    let rows = selectors::visible_sidebar_rows(
        &state.domain.root,
        &state.domain.expanded,
        &state.ui.search_query,
    );
    let Some(visible_rows) = frame_snapshot.sidebar_visible_rows() else {
        return;
    };
    let Some(selected_row) = selected_sidebar_row(&rows, state.domain.selected_path()) else {
        state.ui.set_sidebar_scroll(0);
        return;
    };
    if visible_rows == 0 {
        state.ui.set_sidebar_scroll(0);
        return;
    }

    let max_scroll = rows.len().saturating_sub(visible_rows.min(rows.len()));
    let mut scroll = state.ui.sidebar_scroll(rows.len(), visible_rows);
    if selected_row < scroll {
        scroll = selected_row;
    } else if selected_row >= scroll.saturating_add(visible_rows) {
        scroll = selected_row.saturating_add(1).saturating_sub(visible_rows);
    }
    state.ui.set_sidebar_scroll(scroll.min(max_scroll));
}

pub(crate) fn apply_start_command(state: &mut AppState, start: &str) {
    match state.select_command_by_search_path(start) {
        Ok(()) => {
            let root = state.domain.root.clone();
            let selected_path = state.domain.selected_path().clone();
            let args = selectors::visible_form_args(&root, &selected_path, state.ui.active_tab);
            state
                .ui
                .focus_first_tab(&selectors::visible_form_arg_pairs(&args));
        }
        Err(SelectionError::UnknownPath) => {
            state.notifications.show_toast(
                format!("Unknown start command: {start}"),
                std::time::Duration::from_secs(2),
                false,
            );
        }
    }
}

pub(crate) fn select_root(state: &mut AppState) {
    select_command(state, &[]);
}

fn selected_sidebar_row(rows: &[TreeRow], selected_path: &CommandPath) -> Option<usize> {
    rows.iter().position(|row| match row {
        TreeRow::Item(item) => item.path == *selected_path,
        TreeRow::Heading { .. } => false,
    })
}

fn select_command(state: &mut AppState, path: &[String]) {
    if state.select_command_path(path).is_ok() {
        let root = state.domain.root.clone();
        let selected_path = state.domain.selected_path().clone();
        let args = selectors::visible_form_args(&root, &selected_path, state.ui.active_tab);
        state
            .ui
            .focus_first_tab(&selectors::visible_form_arg_pairs(&args));
    }
}

fn toggle_expand(state: &mut AppState, path: &CommandPath, expanded: bool) {
    let key = path.to_key(&state.domain.root.name);
    if expanded {
        state.domain.expanded.remove(&key);
    } else {
        state.domain.expanded.insert(key);
    }
}

#[cfg(test)]
mod tests {
    use clap::{Arg, Command};
    use ratatui::layout::Rect;

    use crate::frame_snapshot::FrameSnapshot;
    use crate::input::{AppState, Focus};
    use crate::spec::{ArgKind, ArgSpec, CommandSpec, ValueCardinality};

    use super::{
        activate_form_field, apply_start_command, clamp_sidebar_selection_to_search,
        collapse_selected, ensure_active_repeated_row_visible, ensure_enum_visible,
        expand_selected, handle_escape, move_form_selection, move_sidebar_selection,
        open_enum_dropdown, repeated_row_scroll_target, scroll_sidebar, sidebar_right,
        vertical_target,
    };

    fn sidebar_snapshot() -> FrameSnapshot {
        let mut snapshot = FrameSnapshot::default();
        snapshot.layout.sidebar_list = Some(Rect::new(0, 0, 20, 3));
        snapshot
    }

    fn arg(id: &str, name: &str, kind: ArgKind) -> ArgSpec {
        ArgSpec {
            id: id.to_string(),
            display_name: name.to_string(),
            help: None,
            required: false,
            kind,
            default_values: Vec::new(),
            choices: Vec::new(),
            position: None,
            value_cardinality: ValueCardinality::One,
            value_hint: None,
            ..ArgSpec::default()
        }
    }

    fn command(name: &str, args: Vec<ArgSpec>, subcommands: Vec<CommandSpec>) -> CommandSpec {
        CommandSpec {
            name: name.to_string(),
            version: None,
            about: None,
            help: String::new(),
            args,
            subcommands,
            ..CommandSpec::default()
        }
    }

    #[test]
    fn valid_start_command_selects_resolved_path() {
        let root = command(
            "tool",
            vec![arg("verbose", "--verbose", ArgKind::Flag)],
            vec![command(
                "build",
                Vec::new(),
                vec![command("release", Vec::new(), Vec::new())],
            )],
        );
        let mut state = AppState::new(root);

        apply_start_command(&mut state, "build::release");

        assert_eq!(
            state.domain.selected_path().as_slice(),
            &["build".to_string(), "release".to_string()]
        );
        assert_eq!(state.domain.current_command().name, "release");
        assert_eq!(state.domain.command_path_key(), "tool::build::release");
    }

    #[test]
    fn invalid_start_command_keeps_root_selected_and_does_not_create_orphan_form_state() {
        let root = command("tool", Vec::new(), Vec::new());
        let mut state = AppState::new(root);

        apply_start_command(&mut state, "missing");

        assert!(state.domain.selected_path().is_empty());
        assert_eq!(state.domain.current_command().name, "tool");
        assert!(state.domain.current_form().is_none());
        assert_eq!(state.domain.forms.len(), 0);
        let toast = state.notifications.toast.as_ref().expect("toast");
        assert_eq!(toast.message, "Unknown start command: missing");
        assert!(!toast.is_error);
    }

    #[test]
    fn sidebar_selection_always_resolves_to_valid_command() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command("build", Vec::new(), Vec::new())],
        );
        let mut state = AppState::new(root);

        move_sidebar_selection(&mut state, &sidebar_snapshot(), 1);

        assert_eq!(state.domain.current_command().name, "build");
        assert!(
            state
                .domain
                .root
                .resolve_path(state.domain.selected_path().as_slice())
                .is_some()
        );
    }

    #[test]
    fn moving_sidebar_up_from_first_child_reselects_root() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command("build", Vec::new(), Vec::new())],
        );
        let mut state = AppState::new(root);

        let snapshot = sidebar_snapshot();
        move_sidebar_selection(&mut state, &snapshot, 1);
        move_sidebar_selection(&mut state, &snapshot, -1);

        assert!(state.domain.selected_path().is_empty());
        assert_eq!(state.domain.current_command().name, "tool");
    }

    #[test]
    fn filtered_sidebar_navigation_recovers_with_first_visible_match() {
        let root = command(
            "tool",
            Vec::new(),
            vec![
                command("build", Vec::new(), Vec::new()),
                command("deploy", Vec::new(), Vec::new()),
                command("debug", Vec::new(), Vec::new()),
            ],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");
        state.ui.search_query = "de".to_string();

        move_sidebar_selection(&mut state, &sidebar_snapshot(), 1);

        assert_eq!(state.domain.current_command().name, "deploy");
        assert_eq!(
            state.domain.selected_path().as_slice(),
            &["deploy".to_string()]
        );
    }

    #[test]
    fn filtered_sidebar_navigation_recovers_with_last_visible_match() {
        let root = command(
            "tool",
            Vec::new(),
            vec![
                command("build", Vec::new(), Vec::new()),
                command("deploy", Vec::new(), Vec::new()),
                command("debug", Vec::new(), Vec::new()),
            ],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");
        state.ui.search_query = "de".to_string();

        move_sidebar_selection(&mut state, &sidebar_snapshot(), -1);

        assert_eq!(state.domain.current_command().name, "debug");
        assert_eq!(
            state.domain.selected_path().as_slice(),
            &["debug".to_string()]
        );
    }

    #[test]
    fn selecting_invalid_command_path_is_rejected() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command("build", Vec::new(), Vec::new())],
        );
        let mut state = AppState::new(root);

        let result = state.select_command_path(&["missing".to_string()]);

        assert!(result.is_err());
        assert!(state.domain.selected_path().is_empty());
        assert_eq!(state.domain.current_command().name, "tool");
    }

    #[test]
    fn expand_and_collapse_selected_updates_expanded_keys() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command(
                "build",
                Vec::new(),
                vec![command("release", Vec::new(), Vec::new())],
            )],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");
        state.domain.expanded.remove("tool::build");

        let snapshot = sidebar_snapshot();
        expand_selected(&mut state, &snapshot);
        assert!(state.domain.expanded.contains("tool::build"));

        collapse_selected(&mut state, &snapshot);
        assert!(!state.domain.expanded.contains("tool::build"));
    }

    #[test]
    fn sidebar_right_expands_collapsed_branch_and_keeps_sidebar_focus() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command(
                "build",
                Vec::new(),
                vec![command("release", Vec::new(), Vec::new())],
            )],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");
        state.ui.focus = Focus::Sidebar;

        sidebar_right(&mut state, &sidebar_snapshot());

        assert!(state.domain.expanded.contains("tool::build"));
        assert!(matches!(state.ui.focus, Focus::Sidebar));
        assert_eq!(
            state.domain.selected_path().as_slice(),
            &["build".to_string()]
        );
    }

    #[test]
    fn sidebar_right_moves_focus_to_form_for_expanded_branch() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command(
                "build",
                Vec::new(),
                vec![command("release", Vec::new(), Vec::new())],
            )],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");
        state.domain.expanded.insert("tool::build".to_string());
        state.ui.focus = Focus::Sidebar;

        sidebar_right(&mut state, &sidebar_snapshot());

        assert!(matches!(state.ui.focus, Focus::Form));
        assert!(state.domain.expanded.contains("tool::build"));
    }

    #[test]
    fn sidebar_right_moves_focus_to_form_for_leaf_and_root() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command("build", Vec::new(), Vec::new())],
        );
        let mut state = AppState::new(root);
        state.ui.focus = Focus::Sidebar;

        sidebar_right(&mut state, &sidebar_snapshot());
        assert!(matches!(state.ui.focus, Focus::Form));

        state.ui.focus = Focus::Sidebar;
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");

        sidebar_right(&mut state, &sidebar_snapshot());
        assert!(matches!(state.ui.focus, Focus::Form));
    }

    #[test]
    fn move_sidebar_selection_does_not_auto_expand_selected_branch() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command(
                "build",
                Vec::new(),
                vec![command("release", Vec::new(), Vec::new())],
            )],
        );
        let mut state = AppState::new(root);

        move_sidebar_selection(&mut state, &sidebar_snapshot(), 1);

        assert_eq!(
            state.domain.selected_path().as_slice(),
            &["build".to_string()]
        );
        assert!(!state.domain.expanded.contains("tool::build"));
    }

    #[test]
    fn moving_sidebar_selection_scrolls_window_to_keep_selected_row_visible() {
        let root = command(
            "tool",
            Vec::new(),
            vec![
                command("one", Vec::new(), Vec::new()),
                command("two", Vec::new(), Vec::new()),
                command("three", Vec::new(), Vec::new()),
                command("four", Vec::new(), Vec::new()),
                command("five", Vec::new(), Vec::new()),
            ],
        );
        let mut state = AppState::new(root);
        let snapshot = sidebar_snapshot();

        for _ in 0..4 {
            move_sidebar_selection(&mut state, &snapshot, 1);
        }

        assert_eq!(state.domain.current_command().name, "four");
        assert!(state.ui.sidebar_scroll > 0);
    }

    #[test]
    fn search_clamp_reselects_first_visible_match_and_resets_sidebar_window() {
        let root = command(
            "tool",
            Vec::new(),
            vec![
                command("build", Vec::new(), Vec::new()),
                command("deploy", Vec::new(), Vec::new()),
                command("debug", Vec::new(), Vec::new()),
            ],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");
        state.ui.search_query = "de".to_string();
        state.ui.sidebar_scroll = 3;

        clamp_sidebar_selection_to_search(&mut state, &sidebar_snapshot());

        assert_eq!(state.domain.current_command().name, "deploy");
        assert_eq!(state.ui.sidebar_scroll, 0);
    }

    #[test]
    fn sidebar_wheel_scroll_clamps_to_available_range() {
        let root = command(
            "tool",
            Vec::new(),
            vec![
                command("one", Vec::new(), Vec::new()),
                command("two", Vec::new(), Vec::new()),
                command("three", Vec::new(), Vec::new()),
                command("four", Vec::new(), Vec::new()),
                command("five", Vec::new(), Vec::new()),
            ],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["three".to_string()])
            .expect("valid path");
        let snapshot = sidebar_snapshot();

        scroll_sidebar(&mut state, &snapshot, 3);

        assert_eq!(state.ui.sidebar_scroll, 2);
        assert_eq!(
            state.domain.selected_path().as_slice(),
            &["three".to_string()]
        );
    }

    #[test]
    fn sidebar_wheel_scroll_is_not_blocked_by_first_selected_entry() {
        let root = command(
            "tool",
            Vec::new(),
            vec![
                command("one", Vec::new(), Vec::new()),
                command("two", Vec::new(), Vec::new()),
                command("three", Vec::new(), Vec::new()),
                command("four", Vec::new(), Vec::new()),
                command("five", Vec::new(), Vec::new()),
                command("six", Vec::new(), Vec::new()),
            ],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["one".to_string()])
            .expect("valid path");

        scroll_sidebar(&mut state, &sidebar_snapshot(), 2);

        assert_eq!(state.ui.sidebar_scroll, 2);
        assert_eq!(
            state.domain.selected_path().as_slice(),
            &["one".to_string()]
        );
    }

    #[test]
    fn sidebar_wheel_scroll_is_not_blocked_by_last_selected_entry() {
        let root = command(
            "tool",
            Vec::new(),
            vec![
                command("one", Vec::new(), Vec::new()),
                command("two", Vec::new(), Vec::new()),
                command("three", Vec::new(), Vec::new()),
                command("four", Vec::new(), Vec::new()),
                command("five", Vec::new(), Vec::new()),
                command("six", Vec::new(), Vec::new()),
            ],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["six".to_string()])
            .expect("valid path");
        state.ui.sidebar_scroll = 2;

        scroll_sidebar(&mut state, &sidebar_snapshot(), -2);

        assert_eq!(state.ui.sidebar_scroll, 0);
        assert_eq!(
            state.domain.selected_path().as_slice(),
            &["six".to_string()]
        );
    }

    #[test]
    fn handle_escape_reselects_root_when_sidebar_has_non_root_selection() {
        let root = command(
            "tool",
            Vec::new(),
            vec![command("build", Vec::new(), Vec::new())],
        );
        let mut state = AppState::new(root);
        state
            .select_command_path(&["build".to_string()])
            .expect("valid path");
        state.ui.focus = Focus::Sidebar;

        handle_escape(&mut state);

        assert!(state.domain.selected_path().is_empty());
        assert_eq!(state.domain.current_command().name, "tool");
        assert!(matches!(state.ui.focus, Focus::Sidebar));
    }

    #[test]
    fn move_form_selection_clamps_and_scrolls_selected_field_into_view() {
        let root = command(
            "tool",
            vec![
                arg("alpha", "--alpha", ArgKind::Option),
                arg("beta", "--beta", ArgKind::Option),
                arg("gamma", "--gamma", ArgKind::Option),
            ],
            Vec::new(),
        );
        let mut state = AppState::new(root);
        let mut frame_snapshot = FrameSnapshot::default();
        frame_snapshot.layout.form_view = Some(Rect::new(0, 0, 30, 3));
        frame_snapshot.form_scroll_max = 20;

        move_form_selection(&mut state, &frame_snapshot, 2);

        assert_eq!(state.ui.selected_arg_index, 2);
        assert_eq!(state.ui.form_scroll, 8);
    }

    #[test]
    fn repeated_row_scroll_target_prefers_row_and_description_when_that_unit_fits() {
        let next = repeated_row_scroll_target(
            0,
            20,
            6,
            Some(vertical_target(3, 7)),
            vertical_target(3, 6),
        );

        assert_eq!(next, 0);
    }

    #[test]
    fn repeated_row_scroll_target_falls_back_to_the_row_when_description_unit_does_not_fit() {
        let next = repeated_row_scroll_target(0, 20, 5, None, vertical_target(2, 5));

        assert_eq!(next, 0);
    }

    #[test]
    fn repeated_row_scroll_target_does_not_chase_description_when_row_is_already_visible() {
        let next = repeated_row_scroll_target(
            10,
            20,
            6,
            Some(vertical_target(11, 17)),
            vertical_target(11, 14),
        );

        assert_eq!(next, 10);
    }

    #[test]
    fn repeated_row_visibility_clamps_stale_scroll_before_targeting_the_resolved_row() {
        let mut state = AppState::from_command(
            &Command::new("tool")
                .arg(Arg::new("prefix").long("prefix"))
                .arg(
                    Arg::new("include")
                        .long("include")
                        .help("Include path")
                        .action(clap::ArgAction::Append)
                        .num_args(1),
                ),
        );
        state.domain.set_text_value("include", "alpha");
        state.domain.mark_touched("include");
        state.ui.form_scroll = 20;

        let mut snapshot = FrameSnapshot::default();
        snapshot.layout.form = Some(Rect::new(0, 0, 60, 5));
        snapshot.layout.form_view = Some(Rect::new(0, 0, 60, 5));

        ensure_active_repeated_row_visible(&mut state, &snapshot, "include", Some(0));

        assert_eq!(state.ui.form_scroll, 4);
    }

    #[test]
    fn opening_dropdown_centers_current_choice_and_clamps_scroll() {
        let mut color = arg("color", "--color", ArgKind::Enum);
        color.required = true;
        color.choices = (0..10).map(|index| format!("choice-{index}")).collect();
        let mut state = AppState::new(command("tool", vec![color], Vec::new()));
        let mut frame_snapshot = FrameSnapshot::default();
        state
            .domain
            .set_choice_value("color", "choice-8".to_string());
        frame_snapshot.layout.form_view = Some(Rect::new(0, 0, 30, 10));
        frame_snapshot
            .layout
            .form_inputs
            .insert("color".to_string(), Rect::new(1, 1, 20, 1));

        open_enum_dropdown(&mut state, &frame_snapshot, "color", 10);

        assert_eq!(state.ui.dropdown_open.as_deref(), Some("color"));
        assert_eq!(state.ui.dropdown_scroll, 4);
    }

    #[test]
    fn ensure_enum_visible_uses_dropdown_height_to_adjust_scroll() {
        let root = command("tool", Vec::new(), Vec::new());
        let mut state = AppState::new(root);
        let mut frame_snapshot = FrameSnapshot::default();
        frame_snapshot.layout.dropdown = Some(Rect::new(0, 0, 20, 5));

        ensure_enum_visible(&mut state, &frame_snapshot, 4, 6);

        assert_eq!(state.ui.dropdown_scroll, 2);
    }

    #[test]
    fn activating_optional_value_with_existing_text_preserves_the_value() {
        let mut color = arg("color", "--color", ArgKind::Flag);
        color.metadata.action.value_arity = crate::spec::ArgValueArity::Optional;
        let mut state = AppState::new(command("tool", vec![color], Vec::new()));
        state.domain.set_text_value("color", "blue");
        state.domain.mark_touched("color");
        state.ui.focus_form();

        activate_form_field(&mut state, &FrameSnapshot::default());

        let arg = state
            .domain
            .current_command()
            .args
            .iter()
            .find(|arg| arg.id == "color")
            .expect("color arg");
        assert_eq!(
            state
                .domain
                .current_form()
                .and_then(|form| form.compatibility_value(arg)),
            Some(crate::input::ArgValue::Text("blue".to_string()))
        );
    }

    #[test]
    fn activating_optional_value_without_explicit_text_toggles_it_back_off() {
        let mut color = arg("color", "--color", ArgKind::Flag);
        color.metadata.action.value_arity = crate::spec::ArgValueArity::Optional;
        let mut state = AppState::new(command("tool", vec![color], Vec::new()));
        state.domain.toggle_optional_value_flag("color", true);
        state.ui.focus_form();

        activate_form_field(&mut state, &FrameSnapshot::default());

        assert!(state.domain.current_form().is_none());
    }

    #[test]
    fn activating_optional_value_with_default_value_enables_presence() {
        let root = CommandSpec::from_command(
            &Command::new("tool").arg(
                Arg::new("color")
                    .long("color")
                    .num_args(0..=1)
                    .require_equals(true)
                    .default_value("auto")
                    .default_missing_value("always"),
            ),
        );
        let mut state = AppState::new(root);
        state.ui.focus_form();

        activate_form_field(&mut state, &FrameSnapshot::default());

        let color = state
            .domain
            .current_command()
            .args
            .iter()
            .find(|arg| arg.id == "color")
            .expect("color arg");
        assert_eq!(
            state
                .domain
                .current_form()
                .and_then(|form| form.compatibility_value(color)),
            Some(crate::input::ArgValue::Bool(true))
        );
    }
}
