use std::borrow::Cow;

use crate::editor_state::{TextEditor, TextPosition};
use crate::input::{
    AppState, ArgInput, ArgValue, InputSource, InputValueOccurrence, UiState,
    render_occurrence_text, split_occurrence_values,
};
use crate::runtime::AppKeyCode;
use crate::runtime::AppKeyEvent;
use crate::spec::{ArgModel, CommandPath};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EditResult {
    Ignored,
    Handled,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RepeatedRowEditResult {
    Ignored,
    HandledNoFocusChange,
    Handled {
        focus_changed: bool,
        structure_changed: bool,
        active_row_index: Option<usize>,
    },
}

impl RepeatedRowEditResult {
    pub(crate) fn should_recompute_visibility(self) -> bool {
        matches!(
            self,
            Self::Handled {
                focus_changed: true,
                ..
            } | Self::Handled {
                structure_changed: true,
                ..
            }
        )
    }

    pub(crate) fn active_row_index(self) -> Option<usize> {
        match self {
            Self::Handled {
                active_row_index, ..
            } => active_row_index,
            Self::Ignored | Self::HandledNoFocusChange => None,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RowEditorMode {
    Occurrence,
    FixedArityOccurrence { values_per_occurrence: usize },
    GroupedValues,
}

pub(crate) enum RenderEditor<'a> {
    Stored(&'a TextEditor),
    Displayed(&'a str),
}

impl<'a> RenderEditor<'a> {
    pub(crate) fn row_count(&self) -> usize {
        match self {
            Self::Stored(editor) => editor.row_count(),
            Self::Displayed(displayed) => displayed.split('\n').count(),
        }
    }

    pub(crate) fn current_row(&self) -> usize {
        match self {
            Self::Stored(editor) => editor.current_row(),
            Self::Displayed(_) => 0,
        }
    }

    pub(crate) fn cursor(&self) -> TextPosition {
        match self {
            Self::Stored(editor) => editor.cursor(),
            Self::Displayed(_) => TextPosition::default(),
        }
    }

    pub(crate) fn line(&self, index: usize) -> Cow<'a, str> {
        match self {
            Self::Stored(editor) => editor.line(index).map_or(Cow::Borrowed(""), Cow::Borrowed),
            Self::Displayed(displayed) => displayed
                .split('\n')
                .nth(index)
                .map_or(Cow::Borrowed(""), Cow::Borrowed),
        }
    }
}

pub(crate) fn displayed_text(state: &AppState, arg: &ArgModel) -> String {
    if uses_row_editor(arg)
        && let Some(text) = row_editor_displayed_text(state, arg)
    {
        return text;
    }
    if let Some(inputs) = state.domain.current_form()
        && let Some(ArgValue::Text(text)) = inputs.compatibility_value(arg)
    {
        return text;
    }
    if arg.default_value().is_some() && !state.domain.is_touched(&arg.id) {
        return arg.default_value().unwrap_or_default().to_string();
    }
    String::new()
}

pub(crate) fn click_cursor_position(
    state: &AppState,
    arg: &ArgModel,
    row: u16,
    col: u16,
) -> (u16, u16) {
    if shows_untouched_default_value(state, arg) {
        (0, 0)
    } else {
        (row, col)
    }
}

pub(crate) fn editor_for_render(
    ui: &UiState,
    command_key: &CommandPath,
    arg: &ArgModel,
    displayed: &str,
) -> TextEditor {
    match editor_view_for_render(ui, command_key, arg, displayed) {
        RenderEditor::Stored(editor) => editor.clone(),
        RenderEditor::Displayed(displayed) => TextEditor::from_displayed(displayed),
    }
}

pub(crate) fn editor_view_for_render<'a>(
    ui: &'a UiState,
    command_key: &CommandPath,
    arg: &ArgModel,
    displayed: &'a str,
) -> RenderEditor<'a> {
    ui.editors
        .editor(command_key, &arg.id)
        .filter(|editor| editor_matches_displayed(arg, editor, displayed))
        .map_or(RenderEditor::Displayed(displayed), RenderEditor::Stored)
}

pub(crate) fn ensure_editor<'a>(
    ui: &'a mut UiState,
    command_key: &CommandPath,
    arg: &ArgModel,
    displayed: &str,
) -> &'a mut TextEditor {
    ui.editors
        .ensure_editor_with(command_key, &arg.id, displayed, |editor, current| {
            editor_matches_displayed(arg, editor, current)
        })
}

fn editor_matches_displayed(arg: &ArgModel, editor: &TextEditor, displayed: &str) -> bool {
    if editor.matches_displayed(displayed) {
        return true;
    }

    uses_row_editor(arg)
        && normalize_row_editor_rows(arg, editor.lines().iter().map(String::as_str)) == displayed
}

fn normalize_row_editor_rows<'a>(arg: &ArgModel, rows: impl Iterator<Item = &'a str>) -> String {
    let rows = rows.filter(|value| !value.is_empty()).collect::<Vec<_>>();
    match row_editor_mode(arg) {
        Some(RowEditorMode::Occurrence) => rows
            .into_iter()
            .map(|row| render_occurrence_row_text(arg, &split_row_occurrence_values(arg, row)))
            .filter(|value| !value.is_empty())
            .collect::<Vec<_>>()
            .join("\n"),
        Some(RowEditorMode::FixedArityOccurrence {
            values_per_occurrence,
        }) => group_occurrence_values(
            rows.into_iter()
                .flat_map(|row| split_row_occurrence_values(arg, row))
                .filter(|value| !value.is_empty())
                .collect(),
            values_per_occurrence,
        )
        .into_iter()
        .map(|values| render_occurrence_row_text(arg, &values))
        .filter(|value| !value.is_empty())
        .collect::<Vec<_>>()
        .join("\n"),
        Some(RowEditorMode::GroupedValues) => rows
            .into_iter()
            .flat_map(|row| split_occurrence_values(arg, row))
            .filter(|value| !value.is_empty())
            .collect::<Vec<_>>()
            .join("\n"),
        None => rows.join("\n"),
    }
}

pub(crate) fn apply_key_to_text_field(
    state: &mut AppState,
    arg: &ArgModel,
    key: AppKeyEvent,
) -> EditResult {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let is_touched = state.domain.is_touched(&arg.id);
    let has_default = arg.default_value().is_some();
    let repeated_backspace_rows = {
        let textarea = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
        handle_repeated_empty_backspace(textarea, key, arg)
    };
    if let Some(rows) = repeated_backspace_rows {
        sync_row_editor_values(state, arg, &rows);
        return EditResult::Handled;
    }
    let textarea = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    if row_editor_boundary_merge(textarea, key, arg) {
        return EditResult::Ignored;
    }
    if has_default && !is_touched {
        match key.code {
            AppKeyCode::Char(_) | AppKeyCode::Backspace => {
                *textarea = TextEditor::from_displayed("");
            }
            _ => {}
        }
    }
    if !textarea.apply_key(key) {
        return EditResult::Ignored;
    }
    let text = textarea.text();
    let rows = uses_row_editor(arg).then(|| textarea.lines().to_vec());
    sync_text_editor_value(state, arg, &text, rows.as_deref());
    EditResult::Handled
}

pub(crate) fn activate_repeated_row(state: &mut AppState, arg: &ArgModel) -> RepeatedRowEditResult {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let editor = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    let current_row = editor.current_row();
    let current_col = editor.cursor().col;
    if current_row + 1 < editor.row_count() {
        let next_col = current_col.min(editor.lines()[current_row + 1].len());
        editor.move_cursor_to(
            u16::try_from(current_row + 1).unwrap_or(u16::MAX),
            u16::try_from(next_col).unwrap_or(u16::MAX),
        );
        editor.cancel_selection();
        return RepeatedRowEditResult::Handled {
            focus_changed: true,
            structure_changed: false,
            active_row_index: Some(current_row + 1),
        };
    }

    editor.insert_row_below();
    let rows = editor.lines().to_vec();
    sync_row_editor_values(state, arg, &rows);
    RepeatedRowEditResult::Handled {
        focus_changed: true,
        structure_changed: true,
        active_row_index: Some(current_row.saturating_add(1)),
    }
}

pub(crate) fn navigate_repeated_row(
    state: &mut AppState,
    arg: &ArgModel,
    delta: i32,
) -> RepeatedRowEditResult {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let editor = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    let current_row = editor.current_row();
    let next_row = if delta < 0 {
        current_row.checked_sub(1)
    } else {
        (current_row + 1 < editor.row_count()).then_some(current_row + 1)
    };
    let Some(next_row) = next_row else {
        return RepeatedRowEditResult::Ignored;
    };
    let next_col = editor.cursor().col.min(editor.lines()[next_row].len());
    editor.move_cursor_to(
        u16::try_from(next_row).unwrap_or(u16::MAX),
        u16::try_from(next_col).unwrap_or(u16::MAX),
    );
    editor.cancel_selection();
    RepeatedRowEditResult::Handled {
        focus_changed: true,
        structure_changed: false,
        active_row_index: Some(next_row),
    }
}

pub(crate) fn apply_paste_to_text_field(
    state: &mut AppState,
    arg: &ArgModel,
    text: &str,
) -> EditResult {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let is_touched = state.domain.is_touched(&arg.id);
    let has_default = arg.default_value().is_some();
    let textarea = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    if has_default && !is_touched {
        *textarea = TextEditor::from_displayed("");
    }
    if !textarea.insert_str(text) {
        return EditResult::Ignored;
    }

    let text = textarea.text();
    let rows = uses_row_editor(arg).then(|| textarea.lines().to_vec());
    sync_text_editor_value(state, arg, &text, rows.as_deref());
    EditResult::Handled
}

fn sync_text_editor_value(
    state: &mut AppState,
    arg: &ArgModel,
    text: &str,
    rows: Option<&[String]>,
) {
    if let Some(rows) = rows {
        sync_row_editor_values(state, arg, rows);
        return;
    }
    if text.is_empty() && arg.uses_optional_value_semantics() {
        state.domain.toggle_optional_value_flag(&arg.id, true);
    } else if text.is_empty() {
        state.domain.clear_value_and_untouch(&arg.id);
    } else {
        state.domain.set_text_value(&arg.id, text);
        state.domain.mark_touched(&arg.id);
    }
}

fn sync_row_editor_values(state: &mut AppState, arg: &ArgModel, rows: &[String]) {
    let occurrences = match row_editor_mode(arg) {
        Some(RowEditorMode::Occurrence) => rows
            .iter()
            .filter(|value| !value.is_empty())
            .map(|value| InputValueOccurrence {
                values: split_row_occurrence_values(arg, value),
                source: InputSource::User,
            })
            .filter(|occurrence| !occurrence.values.is_empty())
            .collect(),
        Some(RowEditorMode::FixedArityOccurrence {
            values_per_occurrence,
        }) => group_occurrence_values(
            rows.iter()
                .filter(|value| !value.is_empty())
                .flat_map(|value| split_row_occurrence_values(arg, value))
                .filter(|value| !value.is_empty())
                .collect(),
            values_per_occurrence,
        )
        .into_iter()
        .map(|values| InputValueOccurrence {
            values,
            source: InputSource::User,
        })
        .collect(),
        Some(RowEditorMode::GroupedValues) => {
            let values = rows
                .iter()
                .filter(|value| !value.is_empty())
                .flat_map(|value| split_occurrence_values(arg, value))
                .filter(|value| !value.is_empty())
                .collect::<Vec<_>>();
            if values.is_empty() {
                Vec::new()
            } else {
                vec![InputValueOccurrence {
                    values,
                    source: InputSource::User,
                }]
            }
        }
        None => Vec::new(),
    };
    state.domain.replace_occurrences(&arg.id, occurrences);
}

pub(crate) fn clear_selection(state: &mut AppState, arg: &ArgModel) {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let textarea = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    textarea.cancel_selection();
}

pub(crate) fn start_selection(state: &mut AppState, arg: &ArgModel, row: u16, col: u16) {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let textarea = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    textarea.start_selection(row, col);
}

pub(crate) fn set_cursor_from_click(state: &mut AppState, arg: &ArgModel, row: u16, col: u16) {
    let (row, col) = click_cursor_position(state, arg, row, col);
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let textarea = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    textarea.move_cursor_to(row, col);
}

pub(crate) fn insert_repeated_row(state: &mut AppState, arg: &ArgModel) -> RepeatedRowEditResult {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let editor = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    editor.insert_row_below();
    let active_row_index = editor.current_row();
    let rows = editor.lines().to_vec();
    sync_row_editor_values(state, arg, &rows);
    RepeatedRowEditResult::Handled {
        focus_changed: true,
        structure_changed: true,
        active_row_index: Some(active_row_index),
    }
}

pub(crate) fn remove_repeated_row(state: &mut AppState, arg: &ArgModel) -> RepeatedRowEditResult {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let editor = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    let before = editor.text();
    editor.remove_current_row();
    let text = editor.text();
    if text == before {
        return RepeatedRowEditResult::Ignored;
    }
    let active_row_index = editor.current_row();
    let rows = editor.lines().to_vec();
    sync_row_editor_values(state, arg, &rows);
    RepeatedRowEditResult::Handled {
        focus_changed: true,
        structure_changed: true,
        active_row_index: Some(active_row_index),
    }
}

pub(crate) fn move_repeated_row_up(state: &mut AppState, arg: &ArgModel) -> RepeatedRowEditResult {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let editor = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    if editor.row_count() <= 1 || editor.current_row() == 0 {
        return RepeatedRowEditResult::Ignored;
    }
    editor.move_current_row_up();
    let active_row_index = editor.current_row();
    let rows = editor.lines().to_vec();
    sync_row_editor_values(state, arg, &rows);
    RepeatedRowEditResult::Handled {
        focus_changed: true,
        structure_changed: true,
        active_row_index: Some(active_row_index),
    }
}

pub(crate) fn move_repeated_row_down(
    state: &mut AppState,
    arg: &ArgModel,
) -> RepeatedRowEditResult {
    let displayed = displayed_text(state, arg);
    let command_key = arg.owner_path().clone();
    let editor = ensure_editor(&mut state.ui, &command_key, arg, &displayed);
    if editor.row_count() <= 1 || editor.current_row() + 1 >= editor.row_count() {
        return RepeatedRowEditResult::Ignored;
    }
    editor.move_current_row_down();
    let active_row_index = editor.current_row();
    let rows = editor.lines().to_vec();
    sync_row_editor_values(state, arg, &rows);
    RepeatedRowEditResult::Handled {
        focus_changed: true,
        structure_changed: true,
        active_row_index: Some(active_row_index),
    }
}

fn handle_repeated_empty_backspace(
    editor: &mut TextEditor,
    key: AppKeyEvent,
    arg: &ArgModel,
) -> Option<Vec<String>> {
    if !uses_row_editor(arg)
        || !matches!(key.code, AppKeyCode::Backspace)
        || key.modifiers.control
        || key.modifiers.alt
        || key.modifiers.shift
    {
        return None;
    }

    let row = editor.current_row();
    if row == 0 || editor.lines().get(row).is_none_or(|line| !line.is_empty()) {
        return None;
    }

    let previous_col = editor.cursor().col;
    editor.remove_current_row();
    let target_row = row.saturating_sub(1);
    let target_col = previous_col.min(editor.lines()[target_row].len());
    editor.move_cursor_to(
        u16::try_from(target_row).unwrap_or(u16::MAX),
        u16::try_from(target_col).unwrap_or(u16::MAX),
    );
    Some(editor.lines().to_vec())
}

fn uses_row_editor(arg: &ArgModel) -> bool {
    row_editor_mode(arg).is_some()
}

fn uses_occurrence_rows(arg: &ArgModel) -> bool {
    matches!(
        row_editor_mode(arg),
        Some(RowEditorMode::Occurrence | RowEditorMode::FixedArityOccurrence { .. })
    )
}

fn uses_grouped_value_rows(arg: &ArgModel) -> bool {
    matches!(row_editor_mode(arg), Some(RowEditorMode::GroupedValues))
}

fn row_editor_mode(arg: &ArgModel) -> Option<RowEditorMode> {
    if arg.allows_multiple_occurrences() {
        if arg.accepts_multiple_values_per_occurrence()
            && arg.metadata.syntax.value_delimiter.is_none()
            && let Some(values_per_occurrence) = fixed_occurrence_group_size(arg)
        {
            return Some(RowEditorMode::FixedArityOccurrence {
                values_per_occurrence,
            });
        }
        return Some(RowEditorMode::Occurrence);
    }

    if arg.accepts_multiple_values_per_occurrence() {
        return Some(RowEditorMode::GroupedValues);
    }

    None
}

fn fixed_occurrence_group_size(arg: &ArgModel) -> Option<usize> {
    let cardinality = arg.metadata.cardinality;
    match cardinality.max_values {
        Some(max_values) if cardinality.min_values == max_values && max_values > 1 => {
            Some(max_values)
        }
        _ => None,
    }
}

fn split_row_occurrence_values(arg: &ArgModel, text: &str) -> Vec<String> {
    if arg.accepts_multiple_values_per_occurrence() && arg.allows_multiple_occurrences() {
        if arg.metadata.syntax.value_delimiter.is_some() {
            return vec![text.to_string()];
        }
        return text.split_whitespace().map(str::to_string).collect();
    }

    split_occurrence_values(arg, text)
}

fn render_occurrence_row_text(arg: &ArgModel, values: &[String]) -> String {
    if arg.accepts_multiple_values_per_occurrence() && arg.allows_multiple_occurrences() {
        if arg.metadata.syntax.value_delimiter.is_some() {
            return values.first().cloned().unwrap_or_default();
        }
        return values.join(" ");
    }

    render_occurrence_text(arg, values)
}

fn group_occurrence_values(values: Vec<String>, values_per_occurrence: usize) -> Vec<Vec<String>> {
    if values_per_occurrence == 0 {
        return Vec::new();
    }

    let mut grouped = Vec::new();
    let mut current = Vec::new();
    for value in values {
        current.push(value);
        if current.len() == values_per_occurrence {
            grouped.push(std::mem::take(&mut current));
        }
    }
    if !current.is_empty() {
        grouped.push(current);
    }
    grouped
}

fn row_editor_displayed_text(state: &AppState, arg: &ArgModel) -> Option<String> {
    let form = state.domain.current_form()?;
    let input = form.input(&arg.id)?;
    let ArgValue::Text(fallback) = input.compatibility_value(arg)? else {
        return None;
    };
    let ArgInput::Values { occurrences } = &input.value else {
        return Some(fallback);
    };

    if uses_occurrence_rows(arg) {
        return Some(
            occurrences
                .iter()
                .map(|occurrence| render_occurrence_row_text(arg, &occurrence.values))
                .filter(|value| !value.is_empty())
                .collect::<Vec<_>>()
                .join("\n"),
        );
    }

    if uses_grouped_value_rows(arg) {
        return Some(
            occurrences
                .first()
                .map(|occurrence| {
                    occurrence
                        .values
                        .iter()
                        .filter(|value| !value.is_empty())
                        .cloned()
                        .collect::<Vec<_>>()
                        .join("\n")
                })
                .unwrap_or_default(),
        );
    }

    Some(fallback)
}

fn row_editor_boundary_merge(editor: &TextEditor, key: AppKeyEvent, arg: &ArgModel) -> bool {
    if !uses_row_editor(arg) || editor.selection_anchor().is_some() {
        return false;
    }

    let cursor = editor.cursor();
    match key.code {
        AppKeyCode::Backspace => cursor.col == 0 && cursor.row > 0,
        AppKeyCode::Delete => {
            cursor.col >= editor.current_line_len() && cursor.row + 1 < editor.row_count()
        }
        _ => false,
    }
}

fn shows_untouched_default_value(state: &AppState, arg: &ArgModel) -> bool {
    !state.domain.is_touched(&arg.id)
        && !arg.default_values.is_empty()
        && state
            .domain
            .current_form()
            .and_then(|form| form.input_source(&arg.id))
            == Some(InputSource::Default)
}

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

    use super::{EditResult, apply_key_to_text_field, click_cursor_position};
    use crate::input::{AppState, ArgInput, InputSource, InputValueOccurrence};
    use crate::runtime::{AppKeyCode, AppKeyEvent, AppKeyModifiers};

    fn key(code: AppKeyCode) -> AppKeyEvent {
        AppKeyEvent::new(code, AppKeyModifiers::default())
    }

    fn include_arg(state: &AppState) -> crate::spec::ArgSpec {
        state
            .domain
            .current_command()
            .args
            .iter()
            .find(|arg| arg.id == "include")
            .cloned()
            .expect("include arg")
    }

    #[test]
    fn clearing_a_single_text_field_removes_the_option_from_argv() {
        let mut state =
            AppState::from_command(&Command::new("tool").arg(Arg::new("token").long("token")));
        state.domain.set_text_value("token", "abc");
        state.domain.mark_touched("token");
        let arg = state
            .domain
            .current_command()
            .args
            .iter()
            .find(|arg| arg.id == "token")
            .cloned()
            .expect("token arg");
        super::set_cursor_from_click(&mut state, &arg, 0, 3);

        for _ in 0..3 {
            let result = apply_key_to_text_field(&mut state, &arg, key(AppKeyCode::Backspace));
            assert_eq!(result, EditResult::Handled);
        }

        assert_eq!(
            crate::pipeline::build_authoritative_command_line(&state),
            vec!["tool".to_string()]
        );
        assert!(
            state
                .domain
                .current_form()
                .is_none_or(|form| form.input("token").is_none())
        );
    }

    #[test]
    fn repeated_text_backspace_at_row_start_does_not_merge_occurrences() {
        let mut state = AppState::from_command(
            &Command::new("tool").arg(
                Arg::new("include")
                    .long("include")
                    .action(ArgAction::Append)
                    .num_args(1),
            ),
        );
        state.domain.set_text_value("include", "alpha\nbeta");
        let arg = include_arg(&state);
        super::set_cursor_from_click(&mut state, &arg, 1, 0);

        let result = apply_key_to_text_field(&mut state, &arg, key(AppKeyCode::Backspace));

        assert_eq!(result, EditResult::Ignored);
        assert_eq!(
            crate::pipeline::build_authoritative_command_line(&state),
            vec![
                "tool".to_string(),
                "--include".to_string(),
                "alpha".to_string(),
                "--include".to_string(),
                "beta".to_string(),
            ]
        );
    }

    #[test]
    fn repeated_text_delete_at_row_end_does_not_merge_occurrences() {
        let mut state = AppState::from_command(
            &Command::new("tool").arg(
                Arg::new("include")
                    .long("include")
                    .action(ArgAction::Append)
                    .num_args(1),
            ),
        );
        state.domain.set_text_value("include", "alpha\nbeta");
        let arg = include_arg(&state);
        super::set_cursor_from_click(&mut state, &arg, 0, 5);

        let result = apply_key_to_text_field(&mut state, &arg, key(AppKeyCode::Delete));

        assert_eq!(result, EditResult::Ignored);
        assert_eq!(
            crate::pipeline::build_authoritative_command_line(&state),
            vec![
                "tool".to_string(),
                "--include".to_string(),
                "alpha".to_string(),
                "--include".to_string(),
                "beta".to_string(),
            ]
        );
    }

    #[test]
    fn repeated_text_editor_sync_preserves_other_occurrence_whitespace() {
        let mut state = AppState::from_command(
            &Command::new("tool").arg(
                Arg::new("include")
                    .long("include")
                    .action(ArgAction::Append)
                    .num_args(1),
            ),
        );
        state.domain.replace_occurrences(
            "include",
            vec![
                InputValueOccurrence {
                    values: vec!["  alpha  ".to_string()],
                    source: InputSource::User,
                },
                InputValueOccurrence {
                    values: vec!["beta".to_string()],
                    source: InputSource::User,
                },
            ],
        );
        let arg = include_arg(&state);
        super::set_cursor_from_click(&mut state, &arg, 1, 4);

        let result = apply_key_to_text_field(&mut state, &arg, key(AppKeyCode::Char('!')));

        assert_eq!(result, EditResult::Handled);
        let form = state.domain.current_form().expect("form");
        let input = form.input("include").expect("include input");
        match &input.value {
            ArgInput::Values { occurrences } => {
                assert_eq!(occurrences[0].values, vec!["  alpha  ".to_string()]);
                assert_eq!(occurrences[1].values, vec!["beta!".to_string()]);
            }
            other => panic!("expected value occurrences, got {other:?}"),
        }
    }

    #[test]
    fn grouped_row_editor_keeps_values_in_one_occurrence() {
        let mut state = AppState::from_command(
            &Command::new("tool").arg(Arg::new("pair").long("pair").num_args(1..)),
        );
        state.domain.set_text_value("pair", "alpha\nbeta");
        let arg = state
            .domain
            .current_command()
            .args
            .iter()
            .find(|arg| arg.id == "pair")
            .cloned()
            .expect("pair arg");
        super::set_cursor_from_click(&mut state, &arg, 1, 4);

        let result = apply_key_to_text_field(&mut state, &arg, key(AppKeyCode::Char('!')));

        assert_eq!(result, EditResult::Handled);
        let form = state.domain.current_form().expect("form");
        let input = form.input("pair").expect("pair input");
        match &input.value {
            ArgInput::Values { occurrences } => {
                assert_eq!(occurrences.len(), 1);
                assert_eq!(
                    occurrences[0].values,
                    vec!["alpha".to_string(), "beta!".to_string()]
                );
            }
            other => panic!("expected grouped values, got {other:?}"),
        }
        assert_eq!(
            crate::pipeline::build_authoritative_command_line(&state),
            vec![
                "tool".to_string(),
                "--pair".to_string(),
                "alpha".to_string(),
                "beta!".to_string(),
            ]
        );
    }

    #[test]
    fn untouched_default_text_click_targets_the_start_of_the_value() {
        let state = AppState::from_command(
            &Command::new("tool").arg(Arg::new("config").long("config").default_value("prod.toml")),
        );
        let arg = state
            .domain
            .arg_for_input("config")
            .expect("config arg")
            .clone();

        assert_eq!(click_cursor_position(&state, &arg, 0, 5), (0, 0));
    }

    #[test]
    fn touched_text_click_keeps_the_clicked_cursor_position() {
        let mut state = AppState::from_command(
            &Command::new("tool").arg(Arg::new("config").long("config").default_value("prod.toml")),
        );
        state.domain.set_text_value("config", "custom.toml");
        state.domain.mark_touched("config");
        let arg = state
            .domain
            .arg_for_input("config")
            .expect("config arg")
            .clone();

        assert_eq!(click_cursor_position(&state, &arg, 0, 5), (0, 5));
    }

    #[test]
    fn delimited_repeated_rows_keep_comma_text_in_a_single_occurrence() {
        let mut state = AppState::from_command(
            &Command::new("tool").arg(
                Arg::new("tag")
                    .long("tag")
                    .action(ArgAction::Append)
                    .num_args(1..)
                    .value_delimiter(','),
            ),
        );
        let arg = state
            .domain
            .current_command()
            .args
            .iter()
            .find(|arg| arg.id == "tag")
            .cloned()
            .expect("tag arg");

        let result = super::apply_paste_to_text_field(&mut state, &arg, "alpha,beta");

        assert_eq!(result, EditResult::Handled);
        let form = state.domain.current_form().expect("form");
        let input = form.input("tag").expect("tag input");
        match &input.value {
            ArgInput::Values { occurrences } => {
                assert_eq!(occurrences.len(), 1);
                assert_eq!(occurrences[0].values, vec!["alpha,beta".to_string()]);
            }
            other => panic!("expected value occurrences, got {other:?}"),
        }
        assert_eq!(
            crate::pipeline::build_authoritative_command_line(&state),
            vec!["tool".to_string(), "--tag=alpha,beta".to_string()]
        );
    }
}
