use crate::diff_render::display_path_for;
use crate::external_agent_config_migration_model::ExternalAgentConfigMigrationGroupModel;
use crate::external_agent_config_migration_model::external_agent_config_migration_groups;
use crate::external_agent_config_migration_model::external_agent_config_migration_item_detail;
use crate::external_agent_config_migration_model::external_agent_config_migration_item_label;
use crate::tui::FrameRequester;
use crate::tui::Tui;
use crate::tui::TuiEvent;
use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
use codex_app_server_protocol::PluginsMigration;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use ratatui::prelude::Stylize as _;
use ratatui::text::Line;
use tokio_stream::StreamExt;

mod render;

#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ExternalAgentConfigMigrationOutcome {
    Proceed(Vec<ExternalAgentConfigMigrationItem>),
    Skip,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FocusArea {
    Items,
    Actions,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ActionMenuOption {
    Proceed,
    Customize,
    Skip,
    Back,
}

impl ActionMenuOption {
    fn label(self) -> &'static str {
        match self {
            Self::Proceed => "Import selected",
            Self::Customize => "Customize selection",
            Self::Skip => "Cancel",
            Self::Back => "Review selection",
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MigrationView {
    Summary,
    Customize,
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct MigrationSelection {
    item: ExternalAgentConfigMigrationItem,
    enabled: bool,
}

struct RenderLineEntry {
    item_idx: Option<usize>,
    kind: RenderLineKind,
    line: Line<'static>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RenderLineKind {
    Section,
    Item,
    ItemDetail,
}

pub(crate) async fn run_external_agent_config_migration_prompt(
    tui: &mut Tui,
    items: &[ExternalAgentConfigMigrationItem],
    selected_items: &[ExternalAgentConfigMigrationItem],
    error: Option<&str>,
) -> ExternalAgentConfigMigrationOutcome {
    let mut screen = ExternalAgentConfigMigrationScreen::new(
        tui.frame_requester(),
        items,
        selected_items,
        error.map(str::to_owned),
    );

    let _ = tui.draw(u16::MAX, |frame| {
        frame.render_widget_ref(&screen, frame.area());
    });

    let events = tui.event_stream();
    tokio::pin!(events);

    while !screen.is_done() {
        if let Some(event) = events.next().await {
            match event {
                TuiEvent::Key(key_event) => screen.handle_key(key_event),
                TuiEvent::Paste(_) => {}
                TuiEvent::Draw | TuiEvent::Resize => {
                    let _ = tui.draw(u16::MAX, |frame| {
                        frame.render_widget_ref(&screen, frame.area());
                    });
                }
            }
        } else {
            screen.skip();
            break;
        }
    }

    screen.outcome()
}

struct ExternalAgentConfigMigrationScreen {
    request_frame: FrameRequester,
    items: Vec<MigrationSelection>,
    groups: Vec<ExternalAgentConfigMigrationGroupModel>,
    view: MigrationView,
    selected_item_idx: Option<usize>,
    scroll_top: usize,
    focus: FocusArea,
    highlighted_action: ActionMenuOption,
    done: bool,
    outcome: ExternalAgentConfigMigrationOutcome,
    error: Option<String>,
}

impl ExternalAgentConfigMigrationScreen {
    fn proceed_enabled(&self) -> bool {
        self.selected_count() > 0
    }

    fn first_available_action(&self) -> ActionMenuOption {
        match self.available_actions().first() {
            Some(action) => *action,
            None => ActionMenuOption::Back,
        }
    }

    fn last_available_action(&self) -> ActionMenuOption {
        match self.available_actions().last() {
            Some(action) => *action,
            None => ActionMenuOption::Back,
        }
    }

    fn previous_available_action(&self, action: ActionMenuOption) -> Option<ActionMenuOption> {
        let actions = self.available_actions();
        actions
            .iter()
            .position(|candidate| *candidate == action)
            .and_then(|idx| idx.checked_sub(1))
            .and_then(|idx| actions.get(idx))
            .copied()
    }

    fn next_available_action(&self, action: ActionMenuOption) -> Option<ActionMenuOption> {
        let actions = self.available_actions();
        actions
            .iter()
            .position(|candidate| *candidate == action)
            .and_then(|idx| actions.get(idx + 1))
            .copied()
    }

    fn available_actions(&self) -> Vec<ActionMenuOption> {
        match self.view {
            MigrationView::Summary => {
                let mut actions = Vec::new();
                if self.proceed_enabled() {
                    actions.push(ActionMenuOption::Proceed);
                }
                actions.extend([ActionMenuOption::Customize, ActionMenuOption::Skip]);
                actions
            }
            MigrationView::Customize => vec![ActionMenuOption::Back],
        }
    }

    fn normalize_highlighted_action(&mut self) {
        if !self.available_actions().contains(&self.highlighted_action) {
            self.highlighted_action = self.first_available_action();
        }
    }

    fn display_description(item: &ExternalAgentConfigMigrationItem) -> String {
        // App-server descriptions use migration vocabulary. Normalize that prefix so the TUI
        // consistently uses the user-facing import vocabulary.
        let description = item
            .description
            .strip_prefix("Migrate ")
            .map_or_else(|| item.description.clone(), |rest| format!("Import {rest}"));
        let Some(cwd) = item.cwd.as_deref() else {
            return description;
        };

        fn reformat_description(
            description: &str,
            prefix: &str,
            separator: &str,
            cwd: &std::path::Path,
        ) -> Option<String> {
            let remainder = description.strip_prefix(prefix)?;
            let (left, right) = remainder.split_once(separator)?;
            Some(format!(
                "{prefix}{}{}{}",
                display_path_for(std::path::Path::new(left), cwd),
                separator,
                display_path_for(std::path::Path::new(right), cwd)
            ))
        }

        if let Some(reformatted) = reformat_description(&description, "Import ", " into ", cwd) {
            return reformatted;
        }

        if let Some(reformatted) =
            reformat_description(&description, "Import skills from ", " to ", cwd)
        {
            return reformatted;
        }

        if let Some(reformatted) = reformat_description(&description, "Import ", " to ", cwd) {
            return reformatted;
        }

        if let Some(source) = description.strip_prefix("Import enabled plugins from ") {
            let description = format!(
                "Import enabled plugins from {}",
                display_path_for(std::path::Path::new(source), cwd)
            );
            if let Some(details) = &item.details {
                let marketplace_count = details.plugins.len();
                let plugin_count = details
                    .plugins
                    .iter()
                    .map(|plugin_group| plugin_group.plugin_names.len())
                    .sum::<usize>();
                return format!(
                    "{description} ({marketplace_count} {}, {plugin_count} {})",
                    if marketplace_count == 1 {
                        "marketplace"
                    } else {
                        "marketplaces"
                    },
                    if plugin_count == 1 {
                        "plugin"
                    } else {
                        "plugins"
                    }
                );
            }
            return description;
        }

        description
    }

    fn new(
        request_frame: FrameRequester,
        items: &[ExternalAgentConfigMigrationItem],
        selected_items: &[ExternalAgentConfigMigrationItem],
        error: Option<String>,
    ) -> Self {
        let groups = external_agent_config_migration_groups(items);
        let items = items
            .iter()
            .cloned()
            .map(|item| MigrationSelection {
                enabled: selected_items.contains(&item),
                item,
            })
            .collect::<Vec<_>>();
        let selected_item_idx = (!groups.is_empty()).then_some(0);
        let mut screen = Self {
            request_frame,
            items,
            groups,
            view: MigrationView::Summary,
            selected_item_idx,
            scroll_top: 0,
            focus: FocusArea::Actions,
            highlighted_action: ActionMenuOption::Proceed,
            done: false,
            outcome: ExternalAgentConfigMigrationOutcome::Skip,
            error,
        };
        screen.normalize_highlighted_action();
        screen
    }

    fn plugin_detail_lines(plugin_groups: &[PluginsMigration]) -> Vec<Line<'static>> {
        let mut lines = plugin_groups
            .iter()
            .take(3)
            .map(|plugin_group| {
                let mut plugin_names = plugin_group
                    .plugin_names
                    .iter()
                    .take(2)
                    .cloned()
                    .collect::<Vec<_>>();
                let hidden_plugin_count = plugin_group
                    .plugin_names
                    .len()
                    .saturating_sub(plugin_names.len());
                if hidden_plugin_count > 0 {
                    plugin_names.push(format!("+{hidden_plugin_count} more"));
                }
                Line::from(format!(
                    "      • {}: {}",
                    plugin_group.marketplace_name,
                    plugin_names.join(", ")
                ))
            })
            .collect::<Vec<_>>();
        let hidden_marketplace_count = plugin_groups.len().saturating_sub(lines.len());
        if hidden_marketplace_count > 0 {
            lines.push(Line::from(format!(
                "      • +{hidden_marketplace_count} more marketplaces"
            )));
        }
        lines
    }

    fn is_done(&self) -> bool {
        self.done
    }

    fn outcome(&self) -> ExternalAgentConfigMigrationOutcome {
        self.outcome.clone()
    }

    fn finish_with(&mut self, outcome: ExternalAgentConfigMigrationOutcome) {
        self.outcome = outcome;
        self.done = true;
        self.request_frame.schedule_frame();
    }

    fn proceed(&mut self) {
        let selected = self.selected_items();
        self.finish_with(ExternalAgentConfigMigrationOutcome::Proceed(selected));
    }

    fn skip(&mut self) {
        self.finish_with(ExternalAgentConfigMigrationOutcome::Skip);
    }

    fn selected_items(&self) -> Vec<ExternalAgentConfigMigrationItem> {
        self.items
            .iter()
            .filter(|item| item.enabled)
            .map(|item| item.item.clone())
            .collect()
    }

    fn selected_count(&self) -> usize {
        self.items.iter().filter(|item| item.enabled).count()
    }

    fn group_selection_marker(
        &self,
        group: &ExternalAgentConfigMigrationGroupModel,
    ) -> &'static str {
        let enabled_count = group
            .item_indices
            .iter()
            .filter(|idx| self.items[**idx].enabled)
            .count();
        match enabled_count {
            0 => " ",
            count if count == group.item_indices.len() => "x",
            _ => "-",
        }
    }

    fn set_all_enabled(&mut self, enabled: bool) {
        for item in &mut self.items {
            item.enabled = enabled;
        }
        self.error = None;
        self.normalize_highlighted_action();
        self.request_frame.schedule_frame();
    }

    fn toggle_selected_item(&mut self) {
        if self.view != MigrationView::Customize || self.focus != FocusArea::Items {
            return;
        }
        let Some(selected_idx) = self.selected_item_idx else {
            return;
        };
        let Some(item) = self.items.get_mut(selected_idx) else {
            return;
        };
        item.enabled = !item.enabled;
        self.error = None;
        self.normalize_highlighted_action();
        self.request_frame.schedule_frame();
    }

    fn customize(&mut self) {
        self.view = MigrationView::Customize;
        self.selected_item_idx = (!self.items.is_empty()).then_some(0);
        self.scroll_top = 0;
        self.focus = FocusArea::Items;
        self.highlighted_action = ActionMenuOption::Back;
        self.request_frame.schedule_frame();
    }

    fn back_to_summary(&mut self) {
        self.view = MigrationView::Summary;
        self.selected_item_idx = (!self.groups.is_empty()).then_some(0);
        self.scroll_top = 0;
        self.focus = FocusArea::Actions;
        self.highlighted_action = self.first_available_action();
        self.request_frame.schedule_frame();
    }

    fn move_up(&mut self) {
        if self.view == MigrationView::Summary {
            self.focus = FocusArea::Actions;
            self.highlighted_action = self
                .previous_available_action(self.highlighted_action)
                .unwrap_or_else(|| self.last_available_action());
            self.request_frame.schedule_frame();
            return;
        }
        match self.focus {
            FocusArea::Items => match self.selected_item_idx {
                Some(0) => {
                    self.focus = FocusArea::Actions;
                    self.highlighted_action = self.last_available_action();
                }
                Some(idx) => {
                    self.selected_item_idx = Some(idx.saturating_sub(1));
                }
                None => {
                    self.focus = FocusArea::Actions;
                    self.highlighted_action = self.last_available_action();
                }
            },
            FocusArea::Actions => {
                if let Some(previous) = self.previous_available_action(self.highlighted_action) {
                    self.highlighted_action = previous;
                } else {
                    self.focus = FocusArea::Items;
                    if !self.items.is_empty() {
                        self.selected_item_idx = Some(self.items.len() - 1);
                    }
                }
            }
        }
        self.ensure_selected_item_visible();
        self.request_frame.schedule_frame();
    }

    fn move_down(&mut self) {
        if self.view == MigrationView::Summary {
            self.focus = FocusArea::Actions;
            self.highlighted_action = self
                .next_available_action(self.highlighted_action)
                .unwrap_or_else(|| self.first_available_action());
            self.request_frame.schedule_frame();
            return;
        }
        match self.focus {
            FocusArea::Items => match self.selected_item_idx {
                Some(idx) if idx + 1 < self.items.len() => {
                    self.selected_item_idx = Some(idx + 1);
                }
                _ => {
                    self.focus = FocusArea::Actions;
                    self.highlighted_action = self.first_available_action();
                }
            },
            FocusArea::Actions => {
                if let Some(next) = self.next_available_action(self.highlighted_action) {
                    self.highlighted_action = next;
                } else {
                    self.focus = FocusArea::Items;
                    if !self.items.is_empty() {
                        self.selected_item_idx = Some(0);
                    }
                }
            }
        }
        self.ensure_selected_item_visible();
        self.request_frame.schedule_frame();
    }

    fn confirm_selection(&mut self) {
        match self.focus {
            FocusArea::Items => self.toggle_selected_item(),
            FocusArea::Actions => match self.highlighted_action {
                ActionMenuOption::Proceed => self.proceed(),
                ActionMenuOption::Customize => self.customize(),
                ActionMenuOption::Skip => self.skip(),
                ActionMenuOption::Back => self.back_to_summary(),
            },
        }
    }

    fn handle_key(&mut self, key_event: KeyEvent) {
        if key_event.kind == KeyEventKind::Release {
            return;
        }
        if is_ctrl_exit_combo(key_event) {
            self.skip();
            return;
        }

        match key_event.code {
            KeyCode::Up | KeyCode::Char('k') => self.move_up(),
            KeyCode::Down | KeyCode::Char('j') => self.move_down(),
            KeyCode::Char(number @ '1'..='9') => self.select_numbered_action(number),
            KeyCode::Char('c') if self.view == MigrationView::Summary => self.customize(),
            KeyCode::Char('b') if self.view == MigrationView::Customize => self.back_to_summary(),
            KeyCode::Char(' ') if self.view == MigrationView::Customize => {
                self.toggle_selected_item();
            }
            KeyCode::Char('a') if self.view == MigrationView::Customize => {
                self.set_all_enabled(/*enabled*/ true);
            }
            KeyCode::Char('n') if self.view == MigrationView::Customize => {
                self.set_all_enabled(/*enabled*/ false);
            }
            KeyCode::Enter => self.confirm_selection(),
            KeyCode::Esc => match self.view {
                MigrationView::Summary => self.skip(),
                MigrationView::Customize => self.back_to_summary(),
            },
            _ => {}
        }
    }

    fn select_numbered_action(&mut self, number: char) {
        let Some(index) = number.to_digit(10).and_then(|number| number.checked_sub(1)) else {
            return;
        };
        let Some(action) = self.available_actions().get(index as usize).copied() else {
            return;
        };
        self.focus = FocusArea::Actions;
        self.highlighted_action = action;
        self.confirm_selection();
    }

    fn ensure_selected_item_visible(&mut self) {
        let Some(selected_idx) = self.selected_item_idx else {
            self.scroll_top = 0;
            return;
        };
        let selected_render_idx = self.selected_render_line_index(selected_idx);
        let visible_rows = self.render_line_count().max(1);
        if selected_render_idx < self.scroll_top {
            self.scroll_top = selected_render_idx;
        } else {
            let bottom = self.scroll_top + visible_rows.saturating_sub(1);
            if selected_render_idx > bottom {
                self.scroll_top = selected_render_idx + 1 - visible_rows;
            }
        }
    }

    fn render_line_count(&self) -> usize {
        self.build_render_lines().len()
    }

    fn selected_render_line_index(&self, selected_item_idx: usize) -> usize {
        self.build_render_lines()
            .iter()
            .position(|entry| entry.item_idx == Some(selected_item_idx))
            .unwrap_or(selected_item_idx)
    }

    fn section_title(cwd: Option<&std::path::Path>) -> Line<'static> {
        match cwd {
            Some(cwd) => Line::from(vec![
                "Current project: ".bold(),
                cwd.display().to_string().dim(),
            ]),
            None => Line::from("Home".bold()),
        }
    }

    fn build_render_lines(&self) -> Vec<RenderLineEntry> {
        match self.view {
            MigrationView::Summary => self.build_summary_render_lines(),
            MigrationView::Customize => self.build_customize_render_lines(),
        }
    }

    fn build_summary_render_lines(&self) -> Vec<RenderLineEntry> {
        self.groups
            .iter()
            .enumerate()
            .flat_map(|(idx, group)| {
                [
                    RenderLineEntry {
                        item_idx: Some(idx),
                        kind: RenderLineKind::Item,
                        line: Line::from(format!(
                            "  [{}] {}",
                            self.group_selection_marker(group),
                            group.label
                        )),
                    },
                    RenderLineEntry {
                        item_idx: None,
                        kind: RenderLineKind::ItemDetail,
                        line: Line::from(format!("      {}", group.description)),
                    },
                ]
            })
            .collect()
    }

    fn build_customize_render_lines(&self) -> Vec<RenderLineEntry> {
        let mut lines = Vec::new();
        let mut current_scope: Option<Option<&std::path::Path>> = None;
        for (idx, item) in self.items.iter().enumerate() {
            let scope = item.item.cwd.as_deref();
            if current_scope != Some(scope) {
                if current_scope.is_some() {
                    lines.push(RenderLineEntry {
                        item_idx: None,
                        kind: RenderLineKind::Section,
                        line: Line::from(""),
                    });
                }
                lines.push(RenderLineEntry {
                    item_idx: None,
                    kind: RenderLineKind::Section,
                    line: Self::section_title(scope),
                });
                current_scope = Some(scope);
            }
            lines.push(RenderLineEntry {
                item_idx: Some(idx),
                kind: RenderLineKind::Item,
                line: Line::from(vec![
                    "  ".into(),
                    format!(
                        "[{}] {}",
                        if item.enabled { "x" } else { " " },
                        external_agent_config_migration_item_label(&item.item)
                    )
                    .into(),
                ]),
            });
            lines.push(RenderLineEntry {
                item_idx: None,
                kind: RenderLineKind::ItemDetail,
                line: Line::from(format!("      {}", Self::display_description(&item.item))),
            });
            if let Some(details) = external_agent_config_migration_item_detail(&item.item) {
                lines.push(RenderLineEntry {
                    item_idx: None,
                    kind: RenderLineKind::ItemDetail,
                    line: Line::from(format!("      {details}")),
                });
            }
            if let Some(details) = &item.item.details {
                for line in Self::plugin_detail_lines(&details.plugins) {
                    lines.push(RenderLineEntry {
                        item_idx: None,
                        kind: RenderLineKind::ItemDetail,
                        line,
                    });
                }
            }
        }
        lines
    }
}

fn is_ctrl_exit_combo(key_event: KeyEvent) -> bool {
    matches!(key_event.code, KeyCode::Char('c' | 'd'))
        && key_event.modifiers.contains(KeyModifiers::CONTROL)
}

#[cfg(test)]
mod tests {
    use super::ExternalAgentConfigMigrationOutcome;
    use super::ExternalAgentConfigMigrationScreen;
    use super::MigrationView;
    use crate::custom_terminal::Terminal;
    use crate::test_backend::VT100Backend;
    use crate::tui::FrameRequester;
    use codex_app_server_protocol::ExternalAgentConfigMigrationItem;
    use codex_app_server_protocol::ExternalAgentConfigMigrationItemType;
    use codex_app_server_protocol::PluginsMigration;
    use codex_app_server_protocol::SessionMigration;
    use crossterm::event::KeyCode;
    use crossterm::event::KeyEvent;
    use crossterm::event::KeyModifiers;
    use insta::assert_snapshot;
    use pretty_assertions::assert_eq;
    use ratatui::layout::Rect;
    use std::path::PathBuf;

    fn sample_plugin_details() -> codex_app_server_protocol::MigrationDetails {
        codex_app_server_protocol::MigrationDetails {
            plugins: vec![
                PluginsMigration {
                    marketplace_name: "acme-tools".to_string(),
                    plugin_names: vec![
                        "deployer".to_string(),
                        "formatter".to_string(),
                        "lint".to_string(),
                    ],
                },
                PluginsMigration {
                    marketplace_name: "team-marketplace".to_string(),
                    plugin_names: vec!["asana".to_string()],
                },
                PluginsMigration {
                    marketplace_name: "debug".to_string(),
                    plugin_names: vec!["sample".to_string()],
                },
                PluginsMigration {
                    marketplace_name: "data-tools".to_string(),
                    plugin_names: vec!["warehouse".to_string()],
                },
            ],
            ..Default::default()
        }
    }

    #[cfg(windows)]
    fn sample_project_root() -> PathBuf {
        PathBuf::from(r"C:\workspace\project")
    }

    #[cfg(not(windows))]
    fn sample_project_root() -> PathBuf {
        PathBuf::from("/workspace/project")
    }

    fn sample_project_path(path: &str) -> String {
        sample_project_root().join(path).display().to_string()
    }

    fn sample_items() -> Vec<ExternalAgentConfigMigrationItem> {
        let project_root = sample_project_root();
        vec![
            ExternalAgentConfigMigrationItem {
                item_type: ExternalAgentConfigMigrationItemType::Config,
                description:
                    "Migrate /Users/alex/.claude/settings.json into /Users/alex/.codex/config.toml"
                        .to_string(),
                cwd: None,
                details: None,
            },
            ExternalAgentConfigMigrationItem {
                item_type: ExternalAgentConfigMigrationItemType::Sessions,
                description: "Migrate recent Claude Code sessions".to_string(),
                cwd: None,
                details: Some(codex_app_server_protocol::MigrationDetails {
                    sessions: vec![SessionMigration {
                        path: PathBuf::from("/Users/alex/.claude/projects/project/session.jsonl"),
                        cwd: project_root.clone(),
                        title: Some("Investigate migration UX".to_string()),
                    }],
                    ..Default::default()
                }),
            },
            ExternalAgentConfigMigrationItem {
                item_type: ExternalAgentConfigMigrationItemType::Plugins,
                description: format!(
                    "Migrate enabled plugins from {}",
                    sample_project_path(".claude/settings.json")
                ),
                cwd: Some(project_root.clone()),
                details: Some(sample_plugin_details()),
            },
            ExternalAgentConfigMigrationItem {
                item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
                description: format!(
                    "Migrate {} to {}",
                    sample_project_path("CLAUDE.md"),
                    sample_project_path("AGENTS.md")
                ),
                cwd: Some(project_root),
                details: None,
            },
        ]
    }

    fn render_screen(
        screen: &ExternalAgentConfigMigrationScreen,
        width: u16,
        height: u16,
    ) -> String {
        let backend = VT100Backend::new(width, height);
        let mut terminal = Terminal::with_options(backend).expect("terminal");
        terminal.set_viewport_area(Rect::new(0, 0, width, height));
        {
            let mut frame = terminal.get_frame();
            frame.render_widget_ref(screen, frame.area());
        }
        terminal.flush().expect("flush");
        terminal
            .backend()
            .to_string()
            .lines()
            .map(str::trim_end)
            .collect::<Vec<_>>()
            .join("\n")
    }

    #[test]
    fn prompt_snapshot() {
        let items = sample_items();
        let screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );

        let rendered = render_screen(&screen, /*width*/ 80, /*height*/ 24);
        #[cfg(windows)]
        assert_snapshot!("external_agent_config_migration_prompt_windows", rendered);
        #[cfg(not(windows))]
        assert_snapshot!("external_agent_config_migration_prompt", rendered);
    }

    #[test]
    fn customize_snapshot() {
        let items = sample_items();
        let mut screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );
        screen.customize();

        let rendered = render_screen(&screen, /*width*/ 80, /*height*/ 30);
        #[cfg(windows)]
        assert_snapshot!(
            "external_agent_config_migration_customize_windows",
            rendered
        );
        #[cfg(not(windows))]
        assert_snapshot!("external_agent_config_migration_customize", rendered);
    }

    #[test]
    fn customize_action_snapshot() {
        let items = sample_items();
        let mut screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );
        screen.customize();
        screen.move_up();

        let rendered = render_screen(&screen, /*width*/ 80, /*height*/ 30);
        #[cfg(windows)]
        assert_snapshot!(
            "external_agent_config_migration_customize_action_windows",
            rendered
        );
        #[cfg(not(windows))]
        assert_snapshot!("external_agent_config_migration_customize_action", rendered);
    }

    #[test]
    fn proceed_returns_selected_items() {
        let items = sample_items();
        let mut screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );

        screen.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));

        assert!(screen.is_done());
        assert_eq!(
            screen.outcome(),
            ExternalAgentConfigMigrationOutcome::Proceed(items)
        );
    }

    #[test]
    fn toggle_item_then_proceed_keeps_remaining_selection() {
        let items = sample_items();
        let mut screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );

        screen.handle_key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
        screen.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
        screen.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
        screen.handle_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE));

        assert!(screen.is_done());
        assert_eq!(
            screen.outcome(),
            ExternalAgentConfigMigrationOutcome::Proceed(vec![
                items[1].clone(),
                items[2].clone(),
                items[3].clone(),
            ])
        );
    }

    #[test]
    fn escape_skips_prompt() {
        let items = sample_items();
        let mut screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );

        screen.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));

        assert!(screen.is_done());
        assert_eq!(screen.outcome(), ExternalAgentConfigMigrationOutcome::Skip);
    }

    #[test]
    fn numeric_shortcuts_follow_visible_actions_when_proceed_is_disabled() {
        let items = sample_items();
        let mut screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );

        screen.handle_key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
        screen.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));
        screen.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
        screen.handle_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE));

        assert_eq!(screen.view, MigrationView::Customize);
    }

    #[test]
    fn empty_selection_enter_opens_customize_instead_of_proceeding() {
        let items = sample_items();
        let mut screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &[],
            /*error*/ None,
        );

        screen.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));

        assert!(!screen.is_done());
        assert_eq!(screen.view, MigrationView::Customize);
    }

    #[test]
    fn control_exit_shortcuts_cancel_prompt() {
        let items = sample_items();
        for key_code in [KeyCode::Char('c'), KeyCode::Char('d')] {
            let mut screen = ExternalAgentConfigMigrationScreen::new(
                FrameRequester::test_dummy(),
                &items,
                &items,
                /*error*/ None,
            );

            screen.handle_key(KeyEvent::new(key_code, KeyModifiers::CONTROL));

            assert!(screen.is_done());
            assert_eq!(screen.outcome(), ExternalAgentConfigMigrationOutcome::Skip);
        }
    }

    #[test]
    fn numeric_shortcuts_choose_actions() {
        let items = sample_items();

        let mut proceed_screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );
        proceed_screen.handle_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE));
        assert_eq!(
            proceed_screen.outcome(),
            ExternalAgentConfigMigrationOutcome::Proceed(items.clone())
        );

        let mut customize_screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );
        customize_screen.handle_key(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE));
        assert_eq!(customize_screen.view, MigrationView::Customize);
        customize_screen.handle_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE));
        assert_eq!(customize_screen.view, MigrationView::Summary);

        let mut skip_screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );
        skip_screen.handle_key(KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE));
        assert_eq!(
            skip_screen.outcome(),
            ExternalAgentConfigMigrationOutcome::Skip
        );
    }

    #[test]
    fn summary_does_not_toggle_selection() {
        let items = sample_items();
        let mut screen = ExternalAgentConfigMigrationScreen::new(
            FrameRequester::test_dummy(),
            &items,
            &items,
            /*error*/ None,
        );

        screen.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));

        assert_eq!(screen.selected_items(), items);
    }
}
