use std::collections::{BTreeMap, HashMap};

use ratatui::layout::Rect;

use crate::pipeline::{FieldInstanceId, FieldSemantics};
use crate::query::form::{self, FieldWidget, OrderedArg};
use crate::repeated_field::project_repeated_field_with_input_height;
use crate::spec::ArgSpec;

pub(crate) const LABEL_COLUMN_MIN_WIDTH: u16 = 12;
pub(crate) const LABEL_COLUMN_MAX_WIDTH: u16 = 24;
pub(crate) const COLUMN_GAP_WIDTH: u16 = 1;
pub(crate) const INPUT_RIGHT_PADDING: u16 = 2;
const INPUT_MIN_WIDTH_BEFORE_RIGHT_PADDING: u16 = 27;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::struct_field_names)]
pub(crate) struct FieldMetrics {
    pub(crate) label_height: u16,
    pub(crate) description_height: u16,
    pub(crate) input_height: u16,
    pub(crate) gap_height: u16,
    pub(crate) total_height: u16,
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct FormHit {
    pub(crate) order_index: usize,
    pub(crate) arg_id: String,
    pub(crate) widget: FieldWidget,
    pub(crate) in_input: bool,
    pub(crate) in_label: bool,
    pub(crate) in_description: bool,
}

#[derive(Debug, Clone)]
pub struct FormFieldLayout {
    pub arg_id: String,
    pub heading: Option<Rect>,
    pub label: Option<Rect>,
    pub input: Rect,
    pub input_clip_top: u16,
    pub full_input_height: u16,
    pub description: Option<Rect>,
}

pub(crate) struct FormProjectionInput<'a, 'arg> {
    pub(crate) area: Rect,
    pub(crate) content_area: Rect,
    pub(crate) form_scroll: u16,
    pub(crate) active_args: &'a [OrderedArg<'arg>],
    pub(crate) field_errors: &'a BTreeMap<String, String>,
    pub(crate) input_height_overrides: &'a HashMap<String, u16>,
    pub(crate) label_height_overrides: &'a HashMap<String, u16>,
    pub(crate) field_semantics: &'a BTreeMap<FieldInstanceId, FieldSemantics>,
}

pub(crate) fn preferred_label_column_width_with_semantics(
    args: &[OrderedArg<'_>],
    field_semantics: &BTreeMap<FieldInstanceId, FieldSemantics>,
) -> u16 {
    let widest_label = args
        .iter()
        .map(|item| {
            let required = field_semantics
                .get(&FieldInstanceId::from_arg(item.arg))
                .map_or(item.arg.required, |semantics| semantics.required_badge);
            let required_marker = u16::from(required) * 2;
            let label_width =
                u16::try_from(item.arg.display_label().chars().count()).unwrap_or(u16::MAX);
            label_width.saturating_add(required_marker)
        })
        .max()
        .unwrap_or(LABEL_COLUMN_MIN_WIDTH);

    widest_label.clamp(LABEL_COLUMN_MIN_WIDTH, LABEL_COLUMN_MAX_WIDTH)
}

#[cfg(test)]
pub(crate) fn field_metrics(arg: &ArgSpec) -> FieldMetrics {
    field_metrics_with_description(arg, form::field_has_description(arg, None))
}

#[cfg(test)]
pub(crate) fn field_metrics_with_description(
    arg: &ArgSpec,
    show_description: bool,
) -> FieldMetrics {
    field_metrics_with_description_and_input_height(arg, show_description, None)
}

#[cfg(test)]
pub(crate) fn field_metrics_with_description_and_input_height(
    arg: &ArgSpec,
    show_description: bool,
    input_height_override: Option<u16>,
) -> FieldMetrics {
    field_metrics_with_description_and_layout_overrides(
        arg,
        show_description,
        input_height_override,
        None,
    )
}

pub(crate) fn field_metrics_with_description_and_layout_overrides(
    arg: &ArgSpec,
    show_description: bool,
    input_height_override: Option<u16>,
    label_height_override: Option<u16>,
) -> FieldMetrics {
    let widget = form::widget_for(arg);
    let label_height = label_height_override.unwrap_or(1);
    let description_height = u16::from(show_description);
    let input_height = input_height_override.unwrap_or(match widget {
        FieldWidget::Toggle
        | FieldWidget::SingleChoice
        | FieldWidget::MultiChoice
        | FieldWidget::Counter
        | FieldWidget::SingleText
        | FieldWidget::OptionalValue => 3,
        FieldWidget::RepeatedText => 5,
    });
    let gap_height = 1;
    let content_height = label_height.max(input_height);
    FieldMetrics {
        label_height,
        description_height,
        input_height,
        gap_height,
        total_height: content_height + description_height + gap_height,
    }
}

#[cfg(test)]
pub(crate) fn field_input_offset(arg: &ArgSpec) -> u16 {
    field_input_offset_with_description(arg, form::field_has_description(arg, None))
}

pub(crate) fn field_input_offset_with_description(arg: &ArgSpec, show_description: bool) -> u16 {
    let _ = (arg, show_description);
    0
}

#[cfg(test)]
pub(crate) fn field_description_offset(arg: &ArgSpec) -> Option<u16> {
    field_description_offset_with_description(arg, form::field_has_description(arg, None))
}

#[cfg(test)]
pub(crate) fn field_description_offset_with_description(
    arg: &ArgSpec,
    show_description: bool,
) -> Option<u16> {
    field_description_offset_with_description_and_input_height(arg, show_description, None)
}

#[cfg(test)]
pub(crate) fn field_description_offset_with_description_and_input_height(
    arg: &ArgSpec,
    show_description: bool,
    input_height_override: Option<u16>,
) -> Option<u16> {
    field_description_offset_with_layout_overrides(
        arg,
        show_description,
        input_height_override,
        None,
    )
}

pub(crate) fn field_description_offset_with_layout_overrides(
    arg: &ArgSpec,
    show_description: bool,
    input_height_override: Option<u16>,
    label_height_override: Option<u16>,
) -> Option<u16> {
    let metrics = field_metrics_with_description_and_layout_overrides(
        arg,
        show_description,
        input_height_override,
        label_height_override,
    );
    if metrics.description_height == 0 {
        None
    } else {
        Some(metrics.label_height.max(metrics.input_height))
    }
}

#[cfg(test)]
pub(crate) fn measure_fields_height(args: &[OrderedArg<'_>]) -> u16 {
    measure_fields_height_with_errors(args, &BTreeMap::new())
}

#[cfg(test)]
pub(crate) fn measure_fields_height_with_errors(
    args: &[OrderedArg<'_>],
    field_errors: &BTreeMap<String, String>,
) -> u16 {
    measure_fields_height_with_overrides(args, field_errors, &HashMap::new())
}

#[cfg(test)]
pub(crate) fn measure_fields_height_with_overrides(
    args: &[OrderedArg<'_>],
    field_errors: &BTreeMap<String, String>,
    input_height_overrides: &HashMap<String, u16>,
) -> u16 {
    measure_fields_height_with_layout_overrides(
        args,
        field_errors,
        input_height_overrides,
        &HashMap::new(),
    )
}

#[cfg(test)]
pub(crate) fn measure_fields_height_with_layout_overrides(
    args: &[OrderedArg<'_>],
    field_errors: &BTreeMap<String, String>,
    input_height_overrides: &HashMap<String, u16>,
    label_height_overrides: &HashMap<String, u16>,
) -> u16 {
    measure_fields_height_with_layout_overrides_and_semantics(
        args,
        field_errors,
        input_height_overrides,
        label_height_overrides,
        &BTreeMap::new(),
    )
}

pub(crate) fn measure_fields_height_with_layout_overrides_and_semantics(
    args: &[OrderedArg<'_>],
    field_errors: &BTreeMap<String, String>,
    input_height_overrides: &HashMap<String, u16>,
    label_height_overrides: &HashMap<String, u16>,
    field_semantics: &BTreeMap<FieldInstanceId, FieldSemantics>,
) -> u16 {
    let mut total = 0;
    let mut previous_heading = None;
    for item in args {
        if form::field_heading(previous_heading, item).is_some() {
            total += 1;
        }
        let semantic_reason = field_semantics
            .get(&FieldInstanceId::from_arg(item.arg))
            .and_then(|semantics| semantics.reason.as_deref());
        total += field_metrics_with_description_and_layout_overrides(
            item.arg,
            form::field_has_description(
                item.arg,
                field_errors
                    .get(&item.arg.id)
                    .map(String::as_str)
                    .or(semantic_reason),
            ),
            input_height_overrides.get(&item.arg.id).copied(),
            label_height_overrides.get(&item.arg.id).copied(),
        )
        .total_height;
        previous_heading = item.section_heading.as_deref();
    }
    total
}

pub(crate) fn measure_help_height(help: &str) -> u16 {
    u16::try_from(help.lines().count()).unwrap_or(u16::MAX)
}

#[cfg(test)]
pub(crate) fn field_content_bounds(
    args: &[OrderedArg<'_>],
    selected_index: usize,
) -> Option<(u16, u16)> {
    field_content_bounds_with_errors(args, selected_index, &BTreeMap::new())
}

#[cfg(test)]
pub(crate) fn field_content_bounds_with_errors(
    args: &[OrderedArg<'_>],
    selected_index: usize,
    field_errors: &BTreeMap<String, String>,
) -> Option<(u16, u16)> {
    field_content_bounds_with_layout_overrides(
        args,
        selected_index,
        field_errors,
        &HashMap::new(),
        &HashMap::new(),
    )
}

#[cfg(test)]
pub(crate) fn field_content_bounds_with_layout_overrides(
    args: &[OrderedArg<'_>],
    selected_index: usize,
    field_errors: &BTreeMap<String, String>,
    input_height_overrides: &HashMap<String, u16>,
    label_height_overrides: &HashMap<String, u16>,
) -> Option<(u16, u16)> {
    field_content_bounds_with_layout_overrides_and_semantics(
        args,
        selected_index,
        field_errors,
        input_height_overrides,
        label_height_overrides,
        &BTreeMap::new(),
    )
}

pub(crate) fn field_content_bounds_with_layout_overrides_and_semantics(
    args: &[OrderedArg<'_>],
    selected_index: usize,
    field_errors: &BTreeMap<String, String>,
    input_height_overrides: &HashMap<String, u16>,
    label_height_overrides: &HashMap<String, u16>,
    field_semantics: &BTreeMap<FieldInstanceId, FieldSemantics>,
) -> Option<(u16, 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);
        }
        let semantic_reason = field_semantics
            .get(&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 = 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(),
        );
        let input_top = y.saturating_add(field_input_offset_with_description(
            item.arg,
            show_description,
        ));
        let input_bottom = input_top.saturating_add(metrics.input_height);
        if item.order_index == selected_index {
            return Some((input_top, input_bottom));
        }
        y = y.saturating_add(metrics.total_height);
        previous_heading = item.section_heading.as_deref();
    }
    None
}

#[cfg(test)]
pub(crate) fn hit_test_form_content(args: &[OrderedArg<'_>], content_y: u16) -> Option<FormHit> {
    hit_test_form_content_with_errors(args, content_y, &BTreeMap::new())
}

#[cfg(test)]
pub(crate) fn hit_test_form_content_with_errors(
    args: &[OrderedArg<'_>],
    content_y: u16,
    field_errors: &BTreeMap<String, String>,
) -> Option<FormHit> {
    hit_test_form_content_with_layout_overrides(
        args,
        content_y,
        field_errors,
        &HashMap::new(),
        &HashMap::new(),
    )
}

#[cfg(test)]
pub(crate) fn hit_test_form_content_with_layout_overrides(
    args: &[OrderedArg<'_>],
    content_y: u16,
    field_errors: &BTreeMap<String, String>,
    input_height_overrides: &HashMap<String, u16>,
    label_height_overrides: &HashMap<String, u16>,
) -> Option<FormHit> {
    hit_test_form_content_with_layout_overrides_and_semantics(
        args,
        content_y,
        field_errors,
        input_height_overrides,
        label_height_overrides,
        &BTreeMap::new(),
    )
}

pub(crate) fn hit_test_form_content_with_layout_overrides_and_semantics(
    args: &[OrderedArg<'_>],
    content_y: u16,
    field_errors: &BTreeMap<String, String>,
    input_height_overrides: &HashMap<String, u16>,
    label_height_overrides: &HashMap<String, u16>,
    field_semantics: &BTreeMap<FieldInstanceId, FieldSemantics>,
) -> Option<FormHit> {
    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);
        }
        let semantic_reason = field_semantics
            .get(&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 = 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(),
        );
        let input_top = y.saturating_add(field_input_offset_with_description(
            item.arg,
            show_description,
        ));
        let input_bottom = input_top.saturating_add(metrics.input_height);
        let description_top = field_description_offset_with_layout_overrides(
            item.arg,
            show_description,
            input_height_overrides.get(&item.arg.id).copied(),
            label_height_overrides.get(&item.arg.id).copied(),
        )
        .map(|offset| y.saturating_add(offset));
        let label_bottom = y.saturating_add(metrics.label_height);

        let in_label = metrics.label_height > 0 && content_y >= y && content_y < label_bottom;
        let in_description = description_top.is_some_and(|top| {
            content_y >= top && content_y < top.saturating_add(metrics.description_height)
        });
        let in_input = content_y >= input_top && content_y < input_bottom;
        if in_label || in_description || in_input {
            return Some(FormHit {
                order_index: item.order_index,
                arg_id: item.arg.id.clone(),
                widget: item.widget,
                in_input,
                in_label,
                in_description,
            });
        }
        y = y.saturating_add(metrics.total_height);
        previous_heading = item.section_heading.as_deref();
    }
    None
}

pub(crate) fn field_content_geometry(
    area: Rect,
    in_section: bool,
    preferred_label_width: u16,
) -> (u16, u16, u16, u16) {
    field_content_geometry_with_right_padding(area, in_section, preferred_label_width, true)
}

fn field_description_geometry(
    area: Rect,
    in_section: bool,
    preferred_label_width: u16,
) -> (u16, u16, u16, u16) {
    field_content_geometry_with_right_padding(area, in_section, preferred_label_width, false)
}

fn field_content_geometry_with_right_padding(
    area: Rect,
    _in_section: bool,
    preferred_label_width: u16,
    reserve_right_padding: bool,
) -> (u16, u16, u16, u16) {
    let content_x = area.x;
    let content_width = area.width;
    let gap = COLUMN_GAP_WIDTH.min(content_width.saturating_sub(1));
    let label_width = preferred_label_width
        .min(content_width.saturating_sub(gap).saturating_sub(8))
        .max(LABEL_COLUMN_MIN_WIDTH);
    let input_x = content_x.saturating_add(label_width).saturating_add(gap);
    let available_input_width = content_width
        .saturating_sub(label_width)
        .saturating_sub(gap);
    let right_padding = if reserve_right_padding {
        INPUT_RIGHT_PADDING
            .min(available_input_width.saturating_sub(INPUT_MIN_WIDTH_BEFORE_RIGHT_PADDING))
    } else {
        0
    };
    let input_width = available_input_width.saturating_sub(right_padding);
    (content_x, label_width, input_x, input_width)
}

#[allow(clippy::too_many_lines)]
pub(crate) fn project_visible_form_fields(
    input: &FormProjectionInput<'_, '_>,
) -> Vec<FormFieldLayout> {
    let preferred_label_width =
        preferred_label_column_width_with_semantics(input.active_args, input.field_semantics);
    let mut fields = Vec::new();
    let mut y = i32::from(input.content_area.y) - i32::from(input.form_scroll);
    let mut previous_heading = None;

    for item in input.active_args {
        let heading = form::field_heading(previous_heading, item);
        let semantic_reason = input
            .field_semantics
            .get(&FieldInstanceId::from_arg(item.arg))
            .and_then(|semantics| semantics.reason.as_deref());
        let show_description = form::field_has_description(
            item.arg,
            input
                .field_errors
                .get(&item.arg.id)
                .map(String::as_str)
                .or(semantic_reason),
        );
        let metrics = field_metrics_with_description_and_layout_overrides(
            item.arg,
            show_description,
            input.input_height_overrides.get(&item.arg.id).copied(),
            input.label_height_overrides.get(&item.arg.id).copied(),
        );
        let heading_height = i32::from(u16::from(heading.is_some()));
        let item_bottom = y + heading_height + i32::from(metrics.total_height);
        if y >= i32::from(input.content_area.y) + i32::from(input.content_area.height) {
            break;
        }
        if item_bottom <= i32::from(input.content_area.y) {
            y += heading_height + i32::from(metrics.total_height);
            previous_heading = item.section_heading.as_deref();
            continue;
        }

        let heading = if heading.is_some() && y >= i32::from(input.content_area.y) {
            let rect = clipped_rect(input.area.x, input.area.width, y, 1, input.content_area);
            y += 1;
            rect
        } else if heading.is_some() {
            y += 1;
            None
        } else {
            None
        };
        let (label_x, label_width, input_x, input_width) = field_content_geometry(
            input.area,
            form::field_is_in_section(item),
            preferred_label_width,
        );
        let label = if metrics.label_height > 0 {
            clipped_rect(
                label_x,
                label_width,
                y,
                metrics.label_height,
                input.content_area,
            )
        } else {
            None
        };
        let repeated_projection = matches!(item.widget, FieldWidget::RepeatedText).then(|| {
            let field_top = u16::try_from(
                (y - i32::from(input.content_area.y) + i32::from(input.form_scroll)).max(0),
            )
            .unwrap_or(u16::MAX);
            project_repeated_field_with_input_height(
                input
                    .input_height_overrides
                    .get(&item.arg.id)
                    .copied()
                    .unwrap_or(metrics.input_height),
                field_top,
                input_x,
                input_width,
                show_description,
                metrics.label_height,
            )
        });
        let (input_y, field_input, full_input_height, description) = if let Some(projection) =
            repeated_projection
        {
            let input_y = i32::from(input.content_area.y) + i32::from(projection.input.y)
                - i32::from(input.form_scroll);
            let description = projection.description.and_then(|description| {
                let description_y = i32::from(input.content_area.y) + i32::from(description.y)
                    - i32::from(input.form_scroll);
                let (_, _, description_x, description_width) = field_description_geometry(
                    input.area,
                    form::field_is_in_section(item),
                    preferred_label_width,
                );
                clipped_rect(
                    description_x,
                    description_width,
                    description_y,
                    description.height,
                    input.content_area,
                )
            });
            let field_input = clipped_rect(
                projection.input.x,
                projection.input.width,
                input_y,
                projection.input.height,
                input.content_area,
            )
            .or_else(|| {
                description.map(|_| {
                    hidden_input_rect(projection.input.x, input_width, input_y, input.content_area)
                })
            });
            let Some(field_input) = field_input else {
                y += i32::from(metrics.total_height);
                previous_heading = item.section_heading.as_deref();
                continue;
            };
            (input_y, field_input, projection.input.height, description)
        } else {
            let input_y = y + i32::from(field_input_offset_with_description(
                item.arg,
                show_description,
            ));
            let description = form_description_rect(
                item,
                y,
                input.area,
                input.content_area,
                show_description,
                preferred_label_width,
                input.input_height_overrides.get(&item.arg.id).copied(),
                input.label_height_overrides.get(&item.arg.id).copied(),
            );
            let field_input = clipped_rect(
                input_x,
                input_width,
                input_y,
                metrics.input_height,
                input.content_area,
            )
            .or_else(|| {
                description
                    .map(|_| hidden_input_rect(input_x, input_width, input_y, input.content_area))
            });
            let Some(field_input) = field_input else {
                y += i32::from(metrics.total_height);
                previous_heading = item.section_heading.as_deref();
                continue;
            };
            (input_y, field_input, metrics.input_height, description)
        };

        let input_clip_top =
            u16::try_from((i32::from(field_input.y) - input_y).max(0)).unwrap_or(u16::MAX);
        fields.push(FormFieldLayout {
            arg_id: item.arg.id.clone(),
            heading,
            label,
            input: field_input,
            input_clip_top,
            full_input_height,
            description,
        });

        y += i32::from(metrics.total_height);
        previous_heading = item.section_heading.as_deref();
    }

    fields
}

fn hidden_input_rect(x: u16, width: u16, input_y: i32, bounds: Rect) -> Rect {
    let top = input_y.clamp(
        i32::from(bounds.y),
        i32::from(bounds.y.saturating_add(bounds.height)),
    );
    Rect::new(x, u16::try_from(top).unwrap_or(u16::MAX), width, 0)
}

#[allow(clippy::too_many_arguments)]
fn form_description_rect(
    item: &OrderedArg<'_>,
    y: i32,
    area: Rect,
    content_area: Rect,
    show_description: bool,
    preferred_label_width: u16,
    input_height_override: Option<u16>,
    label_height_override: Option<u16>,
) -> Option<Rect> {
    show_description.then_some(())?;
    let description_y = y + i32::from(field_description_offset_with_layout_overrides(
        item.arg,
        show_description,
        input_height_override,
        label_height_override,
    )?);
    let (_, _, input_x, input_width) =
        field_description_geometry(area, form::field_is_in_section(item), preferred_label_width);
    clipped_rect(
        input_x,
        input_width,
        description_y,
        field_metrics_with_description_and_layout_overrides(
            item.arg,
            show_description,
            input_height_override,
            label_height_override,
        )
        .description_height
        .max(1),
        content_area,
    )
}

fn intersect_rects(rect: Rect, bounds: Rect) -> Option<Rect> {
    let left = rect.x.max(bounds.x);
    let top = rect.y.max(bounds.y);
    let right = rect
        .x
        .saturating_add(rect.width)
        .min(bounds.x.saturating_add(bounds.width));
    let bottom = rect
        .y
        .saturating_add(rect.height)
        .min(bounds.y.saturating_add(bounds.height));

    if left >= right || top >= bottom {
        return None;
    }

    Some(Rect::new(
        left,
        top,
        right.saturating_sub(left),
        bottom.saturating_sub(top),
    ))
}

fn clipped_rect(x: u16, width: u16, top: i32, height: u16, bounds: Rect) -> Option<Rect> {
    let bounded_top = top.max(i32::from(bounds.y));
    let bounded_bottom = top
        .saturating_add(i32::from(height))
        .min(i32::from(bounds.y.saturating_add(bounds.height)));
    if bounded_top >= bounded_bottom {
        return None;
    }

    let y = u16::try_from(bounded_top).ok()?;
    let clipped_height = u16::try_from(bounded_bottom.saturating_sub(bounded_top)).ok()?;
    intersect_rects(Rect::new(x, y, width, clipped_height), bounds)
}

#[cfg(test)]
mod tests {
    use clap::Command;

    use super::{
        FieldWidget, INPUT_RIGHT_PADDING, field_content_bounds, field_content_geometry,
        field_description_offset, field_input_offset, field_metrics, hit_test_form_content,
        measure_fields_height,
    };
    use crate::input::ActiveTab;
    use crate::query::form::{visible_args, widget_for};
    use crate::spec::{ArgKind, ArgSpec, CommandSpec};

    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: crate::spec::ValueCardinality::One,
            value_hint: None,
            ..ArgSpec::default()
        }
    }

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

    #[test]
    fn field_metrics_match_single_and_multi_line_fields() {
        let single = arg("target", "--target", ArgKind::Option);
        let mut multi = arg("paths", "--path", ArgKind::Option);
        multi.value_cardinality = crate::spec::ValueCardinality::Many;
        multi.help = Some("paths".to_string());
        let mut flag = arg("verbose", "--verbose", ArgKind::Flag);
        flag.help = Some("toggle".to_string());

        assert_eq!(field_metrics(&single).total_height, 4);
        assert_eq!(field_metrics(&multi).total_height, 7);
        assert_eq!(field_metrics(&flag).total_height, 5);
    }

    #[test]
    fn autogenerated_version_flags_use_toggle_widget_geometry() {
        let command = CommandSpec::from_command(&Command::new("tool").version("1.2.3"));
        let version = command
            .args
            .iter()
            .find(|arg| arg.display_name == "--version")
            .expect("version arg should be present");

        assert!(matches!(widget_for(version), FieldWidget::Toggle));
        assert_eq!(field_metrics(version).total_height, 5);
        assert_eq!(field_input_offset(version), 0);
        assert_eq!(field_description_offset(version), Some(3));
    }

    #[test]
    fn described_options_place_help_below_inline_input_content() {
        let mut option = arg("config", "--config", ArgKind::Option);
        option.help = Some("Path to config".to_string());
        let flag = arg("quiet", "--quiet", ArgKind::Flag);

        assert_eq!(field_description_offset(&option), Some(3));
        assert_eq!(field_input_offset(&option), 0);
        assert_eq!(field_description_offset(&flag), None);
        assert_eq!(field_input_offset(&flag), 0);
    }

    #[test]
    fn field_bounds_and_hit_testing_share_same_geometry() {
        let mut positional = arg("path", "path", ArgKind::Positional);
        positional.position = Some(1);
        positional.help = Some("required".to_string());
        let command = command(vec![positional]);
        let visible = visible_args(&command, ActiveTab::Inputs);

        assert_eq!(measure_fields_height(&visible), 5);
        assert_eq!(field_content_bounds(&visible, 0), Some((0, 3)));

        let label_hit = hit_test_form_content(&visible, 0).expect("label hit");
        assert!(label_hit.in_label);
        assert!(label_hit.in_input);
        assert!(!label_hit.in_description);

        let input_hit = hit_test_form_content(&visible, 1).expect("input hit");
        assert!(matches!(input_hit.widget, FieldWidget::SingleText));
        assert!(input_hit.in_input);
        assert!(!input_hit.in_description);

        let description_hit = hit_test_form_content(&visible, 3).expect("description hit");
        assert!(description_hit.in_description);
        assert!(!description_hit.in_input);

        assert!(hit_test_form_content(&visible, 4).is_none());
    }

    #[test]
    fn flag_metrics_and_hit_testing_use_compact_control_row() {
        let mut flag = arg("verbose", "--verbose", ArgKind::Flag);
        flag.help = Some("Enable verbose output".to_string());
        let command = command(vec![flag]);
        let visible = visible_args(&command, ActiveTab::Inputs);

        assert_eq!(measure_fields_height(&visible), 5);
        assert_eq!(field_content_bounds(&visible, 0), Some((0, 3)));

        let input_hit = hit_test_form_content(&visible, 0).expect("input hit");
        assert!(matches!(input_hit.widget, FieldWidget::Toggle));
        assert!(input_hit.in_input);
        assert!(input_hit.in_label);

        let description_hit = hit_test_form_content(&visible, 3).expect("description hit");
        assert!(description_hit.in_description);
        assert!(!description_hit.in_input);
    }

    #[test]
    fn hit_testing_offsets_follow_preceding_multiline_field_height() {
        let mut multi = arg("paths", "--path", ArgKind::Option);
        multi.value_cardinality = crate::spec::ValueCardinality::Many;
        multi.help = Some("multiple paths".to_string());
        let mut flag = arg("verbose", "--verbose", ArgKind::Flag);
        flag.help = Some("Enable verbose output".to_string());
        let command = command(vec![multi, flag]);
        let visible = visible_args(&command, ActiveTab::Inputs);

        assert_eq!(field_content_bounds(&visible, 1), Some((7, 10)));

        let second_input = hit_test_form_content(&visible, 7).expect("second field input");
        assert_eq!(second_input.arg_id, "verbose");
        assert!(second_input.in_input);
        assert!(matches!(second_input.widget, FieldWidget::Toggle));
    }

    #[test]
    fn heading_rows_contribute_to_form_height_and_offsets() {
        let mut first = arg("first", "--first", ArgKind::Option);
        first.metadata.display.help_heading = Some("Inputs".to_string());
        let mut second = arg("second", "--second", ArgKind::Option);
        second.metadata.display.help_heading = Some("Inputs".to_string());
        let mut third = arg("third", "--third", ArgKind::Option);
        third.metadata.display.help_heading = Some("Outputs".to_string());
        let command = command(vec![first, second, third]);
        let visible = visible_args(&command, ActiveTab::Inputs);

        assert_eq!(
            crate::query::form::field_heading(None, &visible[0]),
            Some("Inputs")
        );
        assert_eq!(
            crate::query::form::field_heading(Some("Inputs"), &visible[1]),
            None
        );
        assert_eq!(
            crate::query::form::field_heading(Some("Inputs"), &visible[2]),
            Some("Outputs")
        );
        assert_eq!(measure_fields_height(&visible), 14);
        assert_eq!(field_content_bounds(&visible, 0), Some((1, 4)));
        assert_eq!(field_content_bounds(&visible, 2), Some((10, 13)));
    }

    #[test]
    fn field_geometry_reserves_right_padding_after_input_column() {
        let area = ratatui::layout::Rect::new(0, 0, 44, 10);
        let (_, _, input_x, input_width) = field_content_geometry(area, false, 12);

        assert_eq!(
            input_x.saturating_add(input_width),
            area.width - INPUT_RIGHT_PADDING
        );
    }

    #[test]
    fn narrow_field_geometry_keeps_input_width_before_right_padding() {
        let area = ratatui::layout::Rect::new(0, 0, 20, 10);
        let (_, _, _, input_width) = field_content_geometry(area, false, 12);

        assert_eq!(input_width, 7);
    }
}
