use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;

use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::app::{App, DetailAnimation, FormField, HostForm, ProviderFormFields, Screen, ViewMode};
use crate::clipboard;
use crate::event::AppEvent;
use crate::ping;
use crate::preferences;
use crate::providers;
use crate::quick_add;
use crate::ssh_config::model::{ConfigElement, HostEntry};

/// Create a sender that maps SnippetEvent to AppEvent.
fn snippet_event_bridge(tx: &mpsc::Sender<AppEvent>) -> mpsc::Sender<crate::snippet::SnippetEvent> {
    let (stx, srx) = mpsc::channel::<crate::snippet::SnippetEvent>();
    let tx = tx.clone();
    std::thread::Builder::new()
        .name("snippet-bridge".into())
        .spawn(move || {
            while let Ok(evt) = srx.recv() {
                let app_evt = match evt {
                    crate::snippet::SnippetEvent::HostDone {
                        run_id,
                        alias,
                        stdout,
                        stderr,
                        exit_code,
                    } => AppEvent::SnippetHostDone {
                        run_id,
                        alias,
                        stdout,
                        stderr,
                        exit_code,
                    },
                    crate::snippet::SnippetEvent::Progress {
                        run_id,
                        completed,
                        total,
                    } => AppEvent::SnippetProgress {
                        run_id,
                        completed,
                        total,
                    },
                    crate::snippet::SnippetEvent::AllDone { run_id } => {
                        AppEvent::SnippetAllDone { run_id }
                    }
                };
                if tx.send(app_evt).is_err() {
                    break;
                }
            }
        })
        .expect("failed to spawn snippet bridge");
    stx
}

/// Handle a key event based on the current screen.
pub fn handle_key_event(
    app: &mut App,
    key: KeyEvent,
    events_tx: &mpsc::Sender<AppEvent>,
) -> Result<()> {
    // Global Ctrl+C handler — screen-conditional for SnippetOutput
    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
        if matches!(app.screen, Screen::SnippetOutput { .. }) {
            if let Some(ref state) = app.snippet_output {
                if !state.all_done {
                    if state.cancel.load(Ordering::Relaxed) {
                        // Second Ctrl+C: cancel already pending, force close
                    } else {
                        // First Ctrl+C: request cancellation
                        state.cancel.store(true, Ordering::Relaxed);
                        return Ok(());
                    }
                }
            }
            app.snippet_output = None;
            app.screen = Screen::HostList;
            return Ok(());
        }
        app.running = false;
        return Ok(());
    }

    match &app.screen {
        Screen::HostList => {
            if app.search.query.is_some() {
                handle_host_list_search(app, key, events_tx);
            } else {
                handle_host_list(app, key, events_tx);
            }
        }
        Screen::AddHost | Screen::EditHost { .. } => handle_form(app, key),
        Screen::ConfirmDelete { .. } => handle_confirm_delete(app, key),
        Screen::Help { .. } => handle_help(app, key),
        Screen::KeyList => handle_key_list(app, key),
        Screen::KeyDetail { .. } => handle_key_detail(app, key),
        Screen::HostDetail { .. } => handle_host_detail(app, key),
        Screen::TagPicker => handle_tag_picker_screen(app, key),
        Screen::Providers => handle_provider_list(app, key, events_tx),
        Screen::ProviderForm { .. } => handle_provider_form(app, key, events_tx),
        Screen::TunnelList { .. } => handle_tunnel_list(app, key),
        Screen::TunnelForm { .. } => handle_tunnel_form(app, key),
        Screen::SnippetPicker { .. } => handle_snippet_picker(app, key, events_tx),
        Screen::SnippetForm { .. } => handle_snippet_form(app, key),
        Screen::SnippetOutput { .. } => handle_snippet_output(app, key),
        Screen::SnippetParamForm { .. } => handle_snippet_param_form(app, key, events_tx),
        Screen::ConfirmHostKeyReset { .. } => handle_confirm_host_key_reset(app, key),
        Screen::ConfirmImport { .. } => {
            if key.code == KeyCode::Char('y') || key.code == KeyCode::Char('Y') {
                app.screen = Screen::HostList;
                execute_known_hosts_import(app);
            } else if key.code == KeyCode::Esc
                || key.code == KeyCode::Char('n')
                || key.code == KeyCode::Char('N')
            {
                app.screen = Screen::HostList;
            }
        }
        Screen::ConfirmPurgeStale { provider: p, .. } => {
            let provider = p.clone();
            if key.code == KeyCode::Char('y') || key.code == KeyCode::Char('Y') {
                execute_purge_stale(app, provider.as_deref());
                if provider.is_some() {
                    app.screen = Screen::Providers;
                } else {
                    app.screen = Screen::HostList;
                }
            } else if key.code == KeyCode::Esc
                || key.code == KeyCode::Char('n')
                || key.code == KeyCode::Char('N')
            {
                if provider.is_some() {
                    app.screen = Screen::Providers;
                } else {
                    app.screen = Screen::HostList;
                }
            }
        }
        Screen::FileBrowser { .. } => handle_file_browser(app, key, events_tx),
        Screen::Containers { .. } => handle_containers(app, key, events_tx)?,
        Screen::Welcome {
            known_hosts_count, ..
        } => {
            let known_hosts_count = *known_hosts_count;
            if key.code == KeyCode::Char('?') {
                app.screen = Screen::Help {
                    return_screen: Box::new(Screen::HostList),
                };
            } else if key.code == KeyCode::Char('I') && known_hosts_count > 0 {
                app.screen = Screen::HostList;
                execute_known_hosts_import(app);
            } else {
                app.screen = Screen::HostList;
            }
        }
    }
    Ok(())
}

/// Run known_hosts import and set status. Used by both ConfirmImport and Welcome handlers.
fn execute_known_hosts_import(app: &mut App) {
    let config_backup = app.config.clone();
    match crate::import::import_from_known_hosts(&mut app.config, Some("known_hosts")) {
        Ok((imported, skipped, _, _)) => {
            if imported > 0 {
                if let Err(e) = app.config.write() {
                    app.config = config_backup;
                    app.set_status(format!("Failed to save: {}", e), true);
                    return;
                }
                app.reload_hosts();
                app.set_status(
                    format!(
                        "Imported {} host{}, skipped {} duplicate{}.",
                        imported,
                        if imported == 1 { "" } else { "s" },
                        skipped,
                        if skipped == 1 { "" } else { "s" },
                    ),
                    false,
                );
            } else {
                app.set_status(
                    if skipped == 1 {
                        "Host already exists.".to_string()
                    } else {
                        format!("All {} hosts already exist.", skipped)
                    },
                    false,
                );
            }
            app.known_hosts_count = 0;
        }
        Err(e) => {
            app.set_status(e, true);
        }
    }
}

fn execute_purge_stale(app: &mut App, provider: Option<&str>) {
    let stale = app.config.stale_hosts();
    if stale.is_empty() {
        return;
    }
    // Filter by provider if specified
    let targets: Vec<(String, u64)> = if let Some(prov) = provider {
        stale
            .into_iter()
            .filter(|(alias, _)| {
                app.config
                    .host_entries()
                    .iter()
                    .any(|e| e.alias == *alias && e.provider.as_deref() == Some(prov))
            })
            .collect()
    } else {
        stale
    };
    if targets.is_empty() {
        return;
    }
    let config_backup = app.config.clone();
    let count = targets.len();
    for (alias, _) in &targets {
        app.config.delete_host(alias);
    }
    if let Err(e) = app.config.write() {
        app.config = config_backup;
        app.set_status(format!("Failed to save: {}", e), true);
        return;
    }
    // Kill active tunnels only after successful write (no rollback needed)
    for (alias, _) in &targets {
        if let Some(mut tunnel) = app.active_tunnels.remove(alias) {
            let _ = tunnel.child.kill();
            let _ = tunnel.child.wait();
        }
    }
    app.undo_stack.clear();
    app.update_last_modified();
    app.reload_hosts();
    let msg = if let Some(prov) = provider {
        let display = crate::providers::provider_display_name(prov);
        format!(
            "Removed {} stale {} host{}.",
            count,
            display,
            if count == 1 { "" } else { "s" }
        )
    } else {
        format!(
            "Removed {} stale host{}.",
            count,
            if count == 1 { "" } else { "s" }
        )
    };
    app.set_status(msg, false);
}

/// Build a provider hint string for stale host messages, e.g. " gone from DigitalOcean".
fn stale_provider_hint(host: &crate::ssh_config::model::HostEntry) -> String {
    host.provider
        .as_ref()
        .map(|p| format!(" gone from {}", crate::providers::provider_display_name(p)))
        .unwrap_or_default()
}

/// Open the edit form for `host`. Returns `true` if the form was opened,
/// `false` if the host is from an include file (status message set instead).
fn open_edit_form(app: &mut App, host: HostEntry) -> bool {
    if let Some(ref source) = host.source_file {
        app.set_status(
            format!(
                "{} lives in {}. Edit it there.",
                host.alias,
                source.display()
            ),
            true,
        );
        return false;
    }
    let stale_hint = host.stale.is_some().then(|| stale_provider_hint(&host));
    app.form = HostForm::from_entry(&host);
    if let Some(hint) = stale_hint {
        app.set_status(format!("Stale host.{}", hint), true);
    }
    app.screen = Screen::EditHost { alias: host.alias };
    app.capture_form_mtime();
    app.capture_form_baseline();
    true
}

fn handle_host_list(app: &mut App, key: KeyEvent, events_tx: &mpsc::Sender<AppEvent>) {
    // Handle tag input mode
    if app.tag_input.is_some() {
        handle_tag_input(app, key);
        return;
    }

    match key.code {
        KeyCode::Char('q') => {
            app.running = false;
        }
        KeyCode::Esc => {
            if app.group_filter.is_some() {
                app.clear_group_filter();
            } else {
                app.running = false;
            }
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.select_next_skipping_headers();
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.select_prev_skipping_headers();
        }
        KeyCode::Tab => {
            app.next_group_tab();
        }
        KeyCode::BackTab => {
            app.prev_group_tab();
        }
        KeyCode::PageDown => {
            app.page_down_host();
        }
        KeyCode::PageUp => {
            app.page_up_host();
        }
        KeyCode::Enter => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(host) = app.selected_host() {
                let alias = host.alias.clone();
                let askpass = host.askpass.clone();
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                app.pending_connect = Some((alias, askpass));
            }
        }
        KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            let visible_indices: Vec<usize> = app
                .display_list
                .iter()
                .filter_map(|item| match item {
                    crate::app::HostListItem::Host { index } => Some(*index),
                    _ => None,
                })
                .collect();
            let all_selected = !visible_indices.is_empty()
                && visible_indices
                    .iter()
                    .all(|idx| app.multi_select.contains(idx));
            if all_selected {
                app.multi_select.clear();
            } else {
                for idx in visible_indices {
                    app.multi_select.insert(idx);
                }
            }
        }
        KeyCode::Char('a') => {
            app.form = HostForm::new();
            app.screen = Screen::AddHost;
            app.capture_form_mtime();
            app.capture_form_baseline();
        }
        KeyCode::Char('A') => {
            app.form = HostForm::new_pattern();
            app.screen = Screen::AddHost;
            app.capture_form_mtime();
            app.capture_form_baseline();
        }
        KeyCode::Char('e') => {
            if let Some(pattern) = app.selected_pattern().cloned() {
                if pattern.source_file.is_some() {
                    app.set_status(
                        format!("{} is in an included file. Edit it there.", pattern.pattern),
                        true,
                    );
                    return;
                }
                app.form = HostForm::from_pattern_entry(&pattern);
                app.screen = Screen::EditHost {
                    alias: pattern.pattern,
                };
                app.capture_form_mtime();
                app.capture_form_baseline();
            } else if let Some(host) = app.selected_host().cloned() {
                open_edit_form(app, host);
            }
        }
        KeyCode::Char('d') => {
            if let Some(pattern) = app.selected_pattern() {
                if pattern.source_file.is_some() {
                    app.set_status(
                        format!(
                            "{} is in an included file. Delete it there.",
                            pattern.pattern
                        ),
                        true,
                    );
                    return;
                }
                let alias = pattern.pattern.clone();
                app.screen = Screen::ConfirmDelete { alias };
            } else if let Some(host) = app.selected_host() {
                if let Some(ref source) = host.source_file {
                    let alias = host.alias.clone();
                    let path = source.display();
                    app.set_status(format!("{} lives in {}. Edit it there.", alias, path), true);
                    return;
                }
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                let alias = host.alias.clone();
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                app.screen = Screen::ConfirmDelete { alias };
            }
        }
        KeyCode::Char('c') => {
            if let Some(pattern) = app.selected_pattern() {
                if pattern.source_file.is_some() {
                    app.set_status(
                        format!(
                            "{} is in an included file. Clone it there.",
                            pattern.pattern
                        ),
                        true,
                    );
                    return;
                }
                let mut form = HostForm::from_pattern_entry(pattern);
                form.alias.clear();
                form.cursor_pos = 0;
                app.form = form;
                app.screen = Screen::AddHost;
                app.capture_form_mtime();
                app.capture_form_baseline();
            } else if let Some(host) = app.selected_host() {
                if let Some(ref source) = host.source_file {
                    let alias = host.alias.clone();
                    let path = source.display();
                    app.set_status(
                        format!("{} lives in {}. Clone it there.", alias, path),
                        true,
                    );
                    return;
                }
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                let copy_alias = format!("{}-copy", host.alias);
                let mut form = HostForm::from_entry(host);
                form.alias = copy_alias;
                form.cursor_pos = form.alias.chars().count();
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                app.form = form;
                app.screen = Screen::AddHost;
                app.capture_form_mtime();
                app.capture_form_baseline();
            }
        }
        KeyCode::Char('y') => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(host) = app.selected_host() {
                let cmd = host.ssh_command(&app.reload.config_path);
                let alias = host.alias.clone();
                match clipboard::copy_to_clipboard(&cmd) {
                    Ok(()) => {
                        app.set_status(format!("Copied SSH command for {}.", alias), false);
                    }
                    Err(e) => {
                        app.set_status(e, true);
                    }
                }
            }
        }
        KeyCode::Char('x') => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(host) = app.selected_host() {
                let alias = host.alias.clone();
                if let Some(block) =
                    serialize_host_block(&app.config.elements, &alias, app.config.crlf)
                {
                    match clipboard::copy_to_clipboard(&block) {
                        Ok(()) => {
                            app.set_status(format!("Copied config block for {}.", alias), false);
                        }
                        Err(e) => {
                            app.set_status(e, true);
                        }
                    }
                }
            }
        }
        KeyCode::Char('p') => {
            if app.is_pattern_selected() {
                return;
            }
            if !app.ping_status.is_empty() {
                app.ping_status.clear();
                app.filter_down_only = false;
                app.ping_checked_at = None;
                app.ping_generation += 1;
                app.status = None;
            } else {
                ping_selected_host(app, events_tx, true);
            }
        }
        KeyCode::Char('P') => {
            if !app.ping_status.is_empty() {
                app.ping_status.clear();
                app.filter_down_only = false;
                app.ping_checked_at = None;
                app.ping_generation += 1;
                app.status = None;
            } else {
                let hosts_to_ping: Vec<(String, String, u16)> = app
                    .hosts
                    .iter()
                    .filter(|h| !h.hostname.is_empty() && h.proxy_jump.is_empty())
                    .map(|h| (h.alias.clone(), h.hostname.clone(), h.port))
                    .collect();
                // Mark ProxyJump hosts as Checking (their status will be
                // inherited from the bastion once it responds).
                for h in &app.hosts {
                    if !h.proxy_jump.is_empty() {
                        app.ping_status
                            .insert(h.alias.clone(), crate::app::PingStatus::Checking);
                    }
                }
                if !hosts_to_ping.is_empty() {
                    for (alias, _, _) in &hosts_to_ping {
                        app.ping_status
                            .insert(alias.clone(), crate::app::PingStatus::Checking);
                    }
                    app.set_status("Pinging all the things...", false);
                    ping::ping_all(&hosts_to_ping, events_tx.clone(), app.ping_generation);
                }
            }
        }
        KeyCode::Char('!') => {
            if app.ping_status.is_empty() {
                app.set_status("Ping first (p/P), then filter with !.", true);
            } else {
                app.filter_down_only = !app.filter_down_only;
                if app.filter_down_only {
                    // Activate search mode to trigger filtering
                    if app.search.query.is_none() {
                        app.search.query = Some(String::new());
                    }
                    app.apply_filter();
                    let count = app.search.filtered_indices.len();
                    app.set_status(
                        format!(
                            "Showing {} unreachable host{}.",
                            count,
                            if count == 1 { "" } else { "s" }
                        ),
                        false,
                    );
                } else {
                    // If search was only active for down-only, clear it
                    if app.search.query.as_ref().is_some_and(|q| q.is_empty()) {
                        app.search.query = None;
                        app.search.filtered_indices.clear();
                        app.search.filtered_pattern_indices.clear();
                    } else {
                        app.apply_filter();
                    }
                    app.status = None;
                }
            }
        }
        KeyCode::Char('/') => {
            app.start_search();
        }
        KeyCode::Char('K') => {
            app.scan_keys();
            app.screen = Screen::KeyList;
        }
        KeyCode::Char('t') => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(host) = app.selected_host() {
                if let Some(ref source) = host.source_file {
                    let alias = host.alias.clone();
                    let path = source.display();
                    app.set_status(
                        format!("{} is included from {}. Tag it there.", alias, path),
                        true,
                    );
                    return;
                }
                let current_tags = host.tags.join(", ");
                app.tag_input_cursor = current_tags.chars().count();
                app.tag_input = Some(current_tags);
            }
        }
        KeyCode::Char('s') => {
            app.sort_mode = app.sort_mode.next();
            app.apply_sort();
            if let Err(e) = preferences::save_sort_mode(app.sort_mode) {
                app.set_status(
                    format!("Sorted by {}. (save failed: {})", app.sort_mode.label(), e),
                    true,
                );
            } else {
                app.set_status(format!("Sorted by {}.", app.sort_mode.label()), false);
            }
        }
        KeyCode::Char('g') => {
            use crate::app::GroupBy;
            match &app.group_by {
                GroupBy::None => {
                    app.group_by = GroupBy::Provider;
                    app.group_filter = None;
                    app.apply_sort();
                    if let Err(e) = preferences::save_group_by(&app.group_by) {
                        app.set_status(
                            format!("Grouped by {}. (save failed: {})", app.group_by.label(), e),
                            true,
                        );
                    } else {
                        app.set_status(format!("Grouped by {}.", app.group_by.label()), false);
                    }
                }
                GroupBy::Provider => {
                    let user_tags: Vec<String> = {
                        let mut seen = std::collections::HashSet::new();
                        let mut tags = Vec::new();
                        for host in &app.hosts {
                            for tag in &host.tags {
                                if seen.insert(tag.clone()) {
                                    tags.push(tag.clone());
                                }
                            }
                        }
                        tags.sort_by_cached_key(|a| a.to_lowercase());
                        tags
                    };
                    if user_tags.is_empty() {
                        app.group_by = GroupBy::None;
                        app.group_filter = None;
                        app.apply_sort();
                        if let Err(e) = preferences::save_group_by(&app.group_by) {
                            app.set_status(format!("Ungrouped. (save failed: {})", e), true);
                        } else {
                            app.set_status("Ungrouped.", false);
                        }
                    } else {
                        // Switch to tag mode directly. The nav bar shows all
                        // tags as tabs, no picker needed.
                        app.group_by = GroupBy::Tag(String::new());
                        app.group_filter = None;
                        app.apply_sort();
                        if let Err(e) = preferences::save_group_by(&app.group_by) {
                            app.set_status(format!("Grouped by tag. (save failed: {})", e), true);
                        } else {
                            app.set_status("Grouped by tag.", false);
                        }
                    }
                }
                GroupBy::Tag(_) => {
                    app.group_by = GroupBy::None;
                    app.group_filter = None;
                    app.apply_sort();
                    if let Err(e) = preferences::save_group_by(&app.group_by) {
                        app.set_status(format!("Ungrouped. (save failed: {})", e), true);
                    } else {
                        app.set_status("Ungrouped.", false);
                    }
                }
            }
        }
        KeyCode::Char('i') => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(index) = app.selected_host_index() {
                app.screen = Screen::HostDetail { index };
            }
        }
        KeyCode::Char('v') => {
            let opening = app.view_mode == ViewMode::Compact;
            // Determine current progress if mid-animation
            let start_progress = if let Some(p) = app.detail_anim_progress() {
                p
            } else if opening {
                0.0
            } else {
                1.0
            };
            app.view_mode = if opening {
                ViewMode::Detailed
            } else {
                ViewMode::Compact
            };
            app.detail_anim = Some(DetailAnimation {
                start: std::time::Instant::now(),
                opening,
                start_progress,
            });
            app.ui.detail_scroll = 0;
            let _ = preferences::save_view_mode(app.view_mode);
        }
        KeyCode::Char(']') if app.view_mode == ViewMode::Detailed => {
            app.ui.detail_scroll = app.ui.detail_scroll.saturating_add(1);
        }
        KeyCode::Char('[') if app.view_mode == ViewMode::Detailed => {
            app.ui.detail_scroll = app.ui.detail_scroll.saturating_sub(1);
        }
        KeyCode::Char('u') => {
            if let Some(deleted) = app.undo_stack.pop() {
                let alias = match &deleted.element {
                    ConfigElement::HostBlock(block) => block.host_pattern.clone(),
                    _ => "host".to_string(),
                };
                app.config.insert_host_at(deleted.element, deleted.position);
                if let Err(e) = app.config.write() {
                    // Rollback: remove re-inserted host and restore undo buffer
                    if let Some((element, position)) = app.config.delete_host_undoable(&alias) {
                        app.undo_stack
                            .push(crate::app::DeletedHost { element, position });
                    }
                    app.set_status(format!("Failed to save: {}", e), true);
                } else {
                    app.update_last_modified();
                    app.reload_hosts();
                    app.set_status(format!("{} is back from the dead.", alias), false);
                }
            } else {
                app.set_status("Nothing to undo.", true);
            }
        }
        KeyCode::Char('#') => {
            app.open_tag_picker();
        }
        KeyCode::Char('T') => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(host) = app.selected_host() {
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                let alias = host.alias.clone();
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                app.refresh_tunnel_list(&alias);
                app.ui.tunnel_list_state = ratatui::widgets::ListState::default();
                if !app.tunnel_list.is_empty() {
                    app.ui.tunnel_list_state.select(Some(0));
                }
                app.screen = Screen::TunnelList { alias };
            }
        }
        KeyCode::Char('S') => {
            app.provider_config = crate::providers::config::ProviderConfig::load();
            app.ui.provider_list_state = ratatui::widgets::ListState::default();
            app.ui.provider_list_state.select(Some(0));
            app.screen = Screen::Providers;
        }
        KeyCode::Char('I') => {
            let count = crate::import::count_known_hosts_candidates();
            if count > 0 {
                app.screen = Screen::ConfirmImport { count };
            } else {
                app.set_status("No importable hosts in known_hosts.", true);
            }
        }
        KeyCode::Char('X') => {
            let stale = app.config.stale_hosts();
            if stale.is_empty() {
                app.set_status("No stale hosts.", true);
            } else {
                let aliases: Vec<String> = stale.into_iter().map(|(a, _)| a).collect();
                app.screen = Screen::ConfirmPurgeStale {
                    aliases,
                    provider: None,
                };
            }
        }
        KeyCode::Char(' ') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(idx) = app.selected_host_index() {
                if app.multi_select.contains(&idx) {
                    app.multi_select.remove(&idx);
                } else {
                    app.multi_select.insert(idx);
                }
            }
        }
        KeyCode::Char('r') => {
            if app.is_pattern_selected() {
                return;
            }
            let (aliases, stale_hint): (Vec<String>, Option<String>) =
                if app.multi_select.is_empty() {
                    if let Some(host) = app.selected_host() {
                        let hint = if host.stale.is_some() {
                            Some(stale_provider_hint(host))
                        } else {
                            None
                        };
                        (vec![host.alias.clone()], hint)
                    } else {
                        (Vec::new(), None)
                    }
                } else {
                    let has_stale = app
                        .multi_select
                        .iter()
                        .any(|&idx| app.hosts.get(idx).is_some_and(|h| h.stale.is_some()));
                    (
                        app.multi_select
                            .iter()
                            .filter_map(|&idx| app.hosts.get(idx).map(|h| h.alias.clone()))
                            .collect(),
                        if has_stale {
                            Some(" Selection includes stale hosts.".to_string())
                        } else {
                            None
                        },
                    )
                };
            if let Some(hint) = stale_hint {
                app.set_status(format!("Stale host.{}", hint), true);
            }
            if aliases.is_empty() {
                app.set_status("No host selected.", true);
            } else {
                open_snippet_picker(app, aliases);
            }
        }
        KeyCode::Char('R') => {
            if app.is_pattern_selected() {
                return;
            }
            let aliases: Vec<String> = app
                .display_list
                .iter()
                .filter_map(|item| match item {
                    crate::app::HostListItem::Host { index } => {
                        Some(app.hosts[*index].alias.clone())
                    }
                    _ => None,
                })
                .collect();
            if aliases.is_empty() {
                app.set_status("No hosts to run on.", true);
            } else {
                open_snippet_picker(app, aliases);
            }
        }
        KeyCode::Char('F') => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(host) = app.selected_host() {
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                let alias = host.alias.clone();
                let askpass = host.askpass.clone();
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                let has_tunnel = app.active_tunnels.contains_key(&alias);
                let (local_path, remote_path) = app
                    .file_browser_paths
                    .get(&alias)
                    .cloned()
                    .unwrap_or_else(|| {
                        (
                            std::env::current_dir()
                                .unwrap_or_else(|_| std::path::PathBuf::from("/")),
                            String::new(),
                        )
                    });
                let (local_entries, local_error) = match crate::file_browser::list_local(
                    &local_path,
                    false,
                    crate::file_browser::BrowserSort::Name,
                ) {
                    Ok(entries) => (entries, None),
                    Err(e) => (Vec::new(), Some(e.to_string())),
                };
                let mut local_list_state = ratatui::widgets::ListState::default();
                local_list_state.select(Some(0)); // Always select ".." entry
                let fb = crate::file_browser::FileBrowserState {
                    alias: alias.clone(),
                    askpass: askpass.clone(),
                    active_pane: crate::file_browser::BrowserPane::Local,
                    local_path,
                    local_entries,
                    local_list_state,
                    local_selected: std::collections::HashSet::new(),
                    local_error,
                    remote_path: String::new(),
                    remote_entries: Vec::new(),
                    remote_list_state: ratatui::widgets::ListState::default(),
                    remote_selected: std::collections::HashSet::new(),
                    remote_error: None,
                    remote_loading: true,
                    show_hidden: false,
                    sort: crate::file_browser::BrowserSort::Name,
                    confirm_copy: None,
                    transferring: None,
                    transfer_error: None,
                    connection_recorded: false,
                };
                app.file_browser = Some(fb);
                app.screen = Screen::FileBrowser {
                    alias: alias.clone(),
                };
                // Fetch remote home dir in background
                let config_path = app.reload.config_path.clone();
                let tx = events_tx.clone();
                let bw = app.bw_session.clone();
                let remote = remote_path;
                std::thread::spawn(move || {
                    let home = if remote.is_empty() {
                        match crate::file_browser::get_remote_home(
                            &alias,
                            &config_path,
                            askpass.as_deref(),
                            bw.as_deref(),
                            has_tunnel,
                        ) {
                            Ok(h) => h,
                            Err(e) => {
                                let _ = tx.send(crate::event::AppEvent::FileBrowserListing {
                                    alias,
                                    path: String::new(),
                                    entries: Err(e.to_string()),
                                });
                                return;
                            }
                        }
                    } else {
                        remote
                    };
                    crate::file_browser::spawn_remote_listing(
                        alias,
                        config_path,
                        home,
                        false,
                        crate::file_browser::BrowserSort::Name,
                        askpass,
                        bw,
                        has_tunnel,
                        fb_send(tx),
                    );
                });
            }
        }
        KeyCode::Char('C') => {
            if app.is_pattern_selected() {
                return;
            }
            if let Some(host) = app.selected_host() {
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                let alias = host.alias.clone();
                let askpass = host.askpass.clone();
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                let (cached_runtime, cached_containers) =
                    if let Some(entry) = app.container_cache.get(&alias) {
                        (Some(entry.runtime), entry.containers.clone())
                    } else {
                        (None, Vec::new())
                    };
                let mut list_state = ratatui::widgets::ListState::default();
                if !cached_containers.is_empty() {
                    list_state.select(Some(0));
                }
                app.container_state = Some(crate::app::ContainerState {
                    alias: alias.clone(),
                    askpass: askpass.clone(),
                    runtime: cached_runtime,
                    containers: cached_containers,
                    list_state,
                    loading: true,
                    error: None,
                    action_in_progress: None,
                    confirm_action: None,
                });
                app.screen = Screen::Containers {
                    alias: alias.clone(),
                };
                let has_tunnel = app.active_tunnels.contains_key(&alias);
                let config_path = app.reload.config_path.clone();
                let bw = app.bw_session.clone();
                let tx = events_tx.clone();
                crate::containers::spawn_container_listing(
                    alias,
                    config_path,
                    askpass,
                    bw,
                    has_tunnel,
                    cached_runtime,
                    move |a, result| {
                        let _ = tx.send(AppEvent::ContainerListing { alias: a, result });
                    },
                );
            }
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        _ => {}
    }
}

fn handle_host_list_search(app: &mut App, key: KeyEvent, events_tx: &mpsc::Sender<AppEvent>) {
    match key.code {
        KeyCode::Esc => {
            app.cancel_search();
        }
        KeyCode::Enter => {
            if let Some(host) = app.selected_host() {
                let alias = host.alias.clone();
                let askpass = host.askpass.clone();
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                app.cancel_search();
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                app.pending_connect = Some((alias, askpass));
            }
        }
        KeyCode::Down | KeyCode::Tab => {
            app.select_next();
        }
        KeyCode::Up | KeyCode::BackTab => {
            app.select_prev();
        }
        KeyCode::PageDown => {
            app.page_down_host();
        }
        KeyCode::PageUp => {
            app.page_up_host();
        }
        KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            if !app.ping_status.is_empty() {
                app.ping_status.clear();
                app.ping_checked_at = None;
                app.ping_generation += 1;
                if app.filter_down_only {
                    app.cancel_search();
                } else {
                    app.filter_down_only = false;
                }
                app.status = None;
            } else {
                ping_selected_host(app, events_tx, false);
            }
        }
        KeyCode::Char(' ') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            if let Some(idx) = app.selected_host_index() {
                if app.multi_select.contains(&idx) {
                    app.multi_select.remove(&idx);
                } else {
                    app.multi_select.insert(idx);
                }
            }
        }
        KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            let visible_indices: Vec<usize> = app.search.filtered_indices.clone();
            let all_selected = !visible_indices.is_empty()
                && visible_indices
                    .iter()
                    .all(|idx| app.multi_select.contains(idx));
            if all_selected {
                app.multi_select.clear();
            } else {
                for idx in visible_indices {
                    app.multi_select.insert(idx);
                }
            }
        }
        KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            if let Some(host) = app.selected_host().cloned() {
                open_edit_form(app, host);
            }
        }
        KeyCode::Char('!') if app.filter_down_only => {
            app.filter_down_only = false;
            if app.search.query.as_ref().is_some_and(|q| q.is_empty()) {
                app.cancel_search();
            } else {
                app.apply_filter();
            }
            app.status = None;
        }
        KeyCode::Char(c) => {
            if let Some(ref mut query) = app.search.query {
                query.push(c);
            }
            app.apply_filter();
        }
        KeyCode::Backspace => {
            if let Some(ref mut query) = app.search.query {
                query.pop();
            }
            app.apply_filter();
        }
        _ => {}
    }
}

fn handle_form(app: &mut App, key: KeyEvent) {
    // Dispatch to password picker if it's open
    if app.ui.show_password_picker {
        handle_password_picker(app, key);
        return;
    }

    // Dispatch to key picker if it's open
    if app.ui.show_key_picker {
        handle_key_picker_shared(app, key, false);
        return;
    }

    // Dispatch to proxyjump picker if it's open
    if app.ui.show_proxyjump_picker {
        handle_proxyjump_picker(app, key);
        return;
    }

    // Handle discard confirmation dialog
    if app.pending_discard_confirm {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                app.pending_discard_confirm = false;
                app.clear_form_mtime();
                app.form_baseline = None;
                app.screen = Screen::HostList;
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.pending_discard_confirm = false;
            }
            _ => {}
        }
        return;
    }

    match key.code {
        KeyCode::Esc => {
            if app.host_form_is_dirty() {
                app.pending_discard_confirm = true;
            } else {
                app.clear_form_mtime();
                app.form_baseline = None;
                app.screen = Screen::HostList;
            }
        }
        KeyCode::Tab | KeyCode::Down => {
            // Smart paste detection: when leaving Alias field, check for user@host:port
            if app.form.focused_field == FormField::Alias {
                maybe_smart_paste(app);
            }
            app.form.focused_field = app.form.focused_field.next();
            app.form.sync_cursor_to_end();
            app.form.update_hint();
        }
        KeyCode::BackTab | KeyCode::Up => {
            app.form.focused_field = app.form.focused_field.prev();
            app.form.sync_cursor_to_end();
            app.form.update_hint();
        }
        KeyCode::Left => {
            if app.form.cursor_pos > 0 {
                app.form.cursor_pos -= 1;
            }
        }
        KeyCode::Right => {
            let len = app.form.focused_value().chars().count();
            if app.form.cursor_pos < len {
                app.form.cursor_pos += 1;
            }
        }
        KeyCode::Home => {
            app.form.cursor_pos = 0;
        }
        KeyCode::End => {
            app.form.sync_cursor_to_end();
        }
        KeyCode::Enter => match app.form.focused_field {
            FormField::IdentityFile => {
                app.scan_keys();
                app.ui.show_key_picker = true;
                app.ui.key_picker_state = ratatui::widgets::ListState::default();
                if !app.keys.is_empty() {
                    app.ui.key_picker_state.select(Some(0));
                }
            }
            FormField::ProxyJump => {
                let candidates = app.proxyjump_candidates();
                app.ui.show_proxyjump_picker = true;
                app.ui.proxyjump_picker_state = ratatui::widgets::ListState::default();
                if !candidates.is_empty() {
                    app.ui.proxyjump_picker_state.select(Some(0));
                }
            }
            FormField::AskPass => {
                app.ui.show_password_picker = true;
                app.ui.password_picker_state = ratatui::widgets::ListState::default();
                app.ui.password_picker_state.select(Some(0));
            }
            FormField::Alias => {
                maybe_smart_paste(app);
                submit_form(app);
            }
            _ => {
                submit_form(app);
            }
        },
        KeyCode::Char(c) => {
            app.form.insert_char(c);
            app.form.update_hint();
        }
        KeyCode::Backspace => {
            app.form.delete_char_before_cursor();
            app.form.update_hint();
        }
        _ => {}
    }
}

/// If the alias field contains something like user@host:port, auto-parse and fill fields.
fn maybe_smart_paste(app: &mut App) {
    let alias_value = app.form.alias.clone();
    if !quick_add::looks_like_target(&alias_value) {
        return;
    }
    if let Ok(parsed) = quick_add::parse_target(&alias_value) {
        // Only auto-fill if other fields are still at defaults
        if app.form.hostname.is_empty() {
            app.form.hostname = parsed.hostname.clone();
        }
        if app.form.user.is_empty() && !parsed.user.is_empty() {
            app.form.user = parsed.user;
        }
        if app.form.port == "22" && parsed.port != 22 {
            app.form.port = parsed.port.to_string();
        }
        // Generate a clean alias from the hostname
        let clean_alias = parsed
            .hostname
            .split('.')
            .next()
            .unwrap_or(&parsed.hostname)
            .to_string();
        app.form.alias = clean_alias;
        app.set_status("Smart-parsed that for you. Check the fields.", false);
    }
}

/// After a picker selection, try to auto-submit the host form if all required fields are filled.
fn try_auto_submit_after_picker(app: &mut App) {
    if !app.form.alias.is_empty() && !app.form.hostname.is_empty() {
        submit_form(app);
    }
}

fn submit_form(app: &mut App) {
    // Check for external config changes since form was opened
    if app.config_changed_since_form_open() {
        app.set_status(
            "Config changed externally. Press Esc and re-open to pick up changes.",
            true,
        );
        return;
    }

    // Validate
    if let Err(msg) = app.form.validate() {
        app.set_status(msg, true);
        return;
    }

    // Track old askpass to detect keychain removal
    let old_askpass = match &app.screen {
        Screen::EditHost { alias } => app
            .hosts
            .iter()
            .find(|h| h.alias == *alias)
            .and_then(|h| h.askpass.clone()),
        _ => None,
    };

    let result = match &app.screen {
        Screen::AddHost => app.add_host_from_form(),
        Screen::EditHost { alias } => {
            let old = alias.clone();
            app.edit_host_from_form(&old)
        }
        _ => return,
    };
    match result {
        Ok(msg) => {
            // Clear undo buffer after successful write
            app.undo_stack.clear();
            // Handle keychain changes on edit
            let mut final_msg = msg;
            if old_askpass.as_deref() == Some("keychain") {
                if app.form.askpass != "keychain" {
                    // Source changed away from keychain — remove old entry
                    if let Screen::EditHost { ref alias } = app.screen {
                        let _ = crate::askpass::remove_from_keychain(alias);
                    }
                    final_msg = format!("{}. Keychain entry removed.", final_msg);
                } else if let Screen::EditHost { ref alias } = app.screen {
                    // Alias renamed — migrate keychain entry
                    if *alias != app.form.alias {
                        if let Ok(pw) = crate::askpass::retrieve_keychain_password(alias) {
                            if crate::askpass::store_in_keychain(&app.form.alias, &pw).is_ok() {
                                let _ = crate::askpass::remove_from_keychain(alias);
                            }
                        }
                    }
                }
            }
            app.set_status(final_msg, false);
        }
        Err(msg) => {
            app.set_status(msg, true);
            return;
        }
    }

    let target_alias = app.form.alias.trim().to_string();
    // Editing a stale host means the user asserts it is still wanted
    if let Screen::EditHost { ref alias } = app.screen {
        app.config.clear_host_stale(alias);
        // If alias was renamed, also clear on the new alias
        if *alias != target_alias {
            app.config.clear_host_stale(&target_alias);
        }
    }
    app.clear_form_mtime();
    app.form_baseline = None;
    app.screen = Screen::HostList;
    app.select_host_by_alias(&target_alias);
}

fn handle_confirm_delete(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Char('y') | KeyCode::Char('Y') => {
            if let Screen::ConfirmDelete { ref alias } = app.screen {
                let alias = alias.clone();
                if let Some((element, position)) = app.config.delete_host_undoable(&alias) {
                    if let Err(e) = app.config.write() {
                        // Restore the element on write failure
                        app.config.insert_host_at(element, position);
                        app.set_status(format!("Failed to save: {}", e), true);
                    } else {
                        // Stop active tunnel for the deleted host
                        if let Some(mut tunnel) = app.active_tunnels.remove(&alias) {
                            let _ = tunnel.child.kill();
                            let _ = tunnel.child.wait();
                        }
                        app.undo_stack
                            .push(crate::app::DeletedHost { element, position });
                        if app.undo_stack.len() > 50 {
                            app.undo_stack.remove(0);
                        }
                        app.update_last_modified();
                        app.reload_hosts();
                        app.set_status(
                            format!("Goodbye, {}. We barely knew ye. (u to undo)", alias),
                            false,
                        );
                    }
                } else {
                    app.set_status(format!("Host '{}' not found.", alias), true);
                }
            }
            app.screen = Screen::HostList;
        }
        KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
            app.screen = Screen::HostList;
        }
        _ => {}
    }
}

fn handle_confirm_host_key_reset(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Char('y') | KeyCode::Char('Y') => {
            if let Screen::ConfirmHostKeyReset {
                ref alias,
                ref hostname,
                ref known_hosts_path,
                ref askpass,
            } = app.screen
            {
                let alias = alias.clone();
                let hostname = hostname.clone();
                let known_hosts_path = known_hosts_path.clone();
                let askpass = askpass.clone();

                let output = std::process::Command::new("ssh-keygen")
                    .arg("-R")
                    .arg(&hostname)
                    .arg("-f")
                    .arg(&known_hosts_path)
                    .output();

                match output {
                    Ok(result) if result.status.success() => {
                        app.set_status(
                            format!("Removed host key for {}. Reconnecting...", hostname),
                            false,
                        );
                        app.pending_connect = Some((alias, askpass));
                    }
                    Ok(result) => {
                        let stderr = String::from_utf8_lossy(&result.stderr);
                        app.set_status(
                            format!("Failed to remove host key: {}", stderr.trim()),
                            true,
                        );
                    }
                    Err(e) => {
                        app.set_status(format!("Failed to run ssh-keygen: {}", e), true);
                    }
                }
            }
            app.screen = Screen::HostList;
        }
        KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
            app.screen = Screen::HostList;
        }
        _ => {}
    }
}

fn handle_help(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('?') => {
            app.ui.help_scroll = 0;
            let return_screen = match std::mem::replace(&mut app.screen, Screen::HostList) {
                Screen::Help { return_screen } => *return_screen,
                _ => Screen::HostList,
            };
            app.screen = return_screen;
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.ui.help_scroll = app.ui.help_scroll.saturating_add(1);
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.ui.help_scroll = app.ui.help_scroll.saturating_sub(1);
        }
        KeyCode::PageDown => {
            app.ui.help_scroll = app.ui.help_scroll.saturating_add(10);
        }
        KeyCode::PageUp => {
            app.ui.help_scroll = app.ui.help_scroll.saturating_sub(10);
        }
        _ => {}
    }
}

fn handle_key_list(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Char('q') | KeyCode::Esc | KeyCode::Char('K') => {
            app.screen = Screen::HostList;
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.select_next_key();
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.select_prev_key();
        }
        KeyCode::PageDown => {
            crate::app::page_down(&mut app.ui.key_list_state, app.keys.len(), 10);
        }
        KeyCode::PageUp => {
            crate::app::page_up(&mut app.ui.key_list_state, app.keys.len(), 10);
        }
        KeyCode::Enter => {
            if let Some(index) = app.ui.key_list_state.selected() {
                if index < app.keys.len() {
                    app.screen = Screen::KeyDetail { index };
                }
            }
        }
        _ => {}
    }
}

fn handle_key_detail(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => {
            app.screen = Screen::KeyList;
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        _ => {}
    }
}

/// Serialize a host block to its raw SSH config text.
fn serialize_host_block(elements: &[ConfigElement], alias: &str, crlf: bool) -> Option<String> {
    let line_ending = if crlf { "\r\n" } else { "\n" };
    for element in elements {
        match element {
            ConfigElement::HostBlock(block) if block.host_pattern == alias => {
                let mut output = block.raw_host_line.clone();
                for directive in &block.directives {
                    output.push_str(line_ending);
                    output.push_str(&directive.raw_line);
                }
                return Some(output);
            }
            ConfigElement::Include(include) => {
                for file in &include.resolved_files {
                    if let Some(result) = serialize_host_block(&file.elements, alias, crlf) {
                        return Some(result);
                    }
                }
            }
            _ => {}
        }
    }
    None
}

fn handle_tag_input(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Enter => {
            if let Some(ref input) = app.tag_input {
                let tags: Vec<String> = input
                    .split(',')
                    .map(|t| t.trim().to_string())
                    .filter(|t| !t.is_empty())
                    .collect();
                if let Some(host) = app.selected_host() {
                    let alias = host.alias.clone();
                    let old_tags = host.tags.clone();
                    app.config.set_host_tags(&alias, &tags);
                    if let Err(e) = app.config.write() {
                        // Restore old tags on write failure
                        app.config.set_host_tags(&alias, &old_tags);
                        app.set_status(format!("Failed to save: {}", e), true);
                    } else {
                        app.update_last_modified();
                        let count = tags.len();
                        app.reload_hosts();
                        app.select_host_by_alias(&alias);
                        app.set_status(
                            format!(
                                "Tagged {} with {} label{}.",
                                alias,
                                count,
                                if count == 1 { "" } else { "s" }
                            ),
                            false,
                        );
                    }
                }
            }
            app.tag_input = None;
            app.tag_input_cursor = 0;
        }
        KeyCode::Esc => {
            app.tag_input = None;
            app.tag_input_cursor = 0;
        }
        KeyCode::Left => {
            if app.tag_input_cursor > 0 {
                app.tag_input_cursor -= 1;
            }
        }
        KeyCode::Right => {
            if let Some(ref input) = app.tag_input {
                if app.tag_input_cursor < input.chars().count() {
                    app.tag_input_cursor += 1;
                }
            }
        }
        KeyCode::Home => {
            app.tag_input_cursor = 0;
        }
        KeyCode::End => {
            if let Some(ref input) = app.tag_input {
                app.tag_input_cursor = input.chars().count();
            }
        }
        KeyCode::Char(c) => {
            if let Some(ref mut input) = app.tag_input {
                let byte_pos = crate::app::char_to_byte_pos(input, app.tag_input_cursor);
                input.insert(byte_pos, c);
                app.tag_input_cursor += 1;
            }
        }
        KeyCode::Backspace => {
            if app.tag_input_cursor > 0 {
                if let Some(ref mut input) = app.tag_input {
                    let byte_pos = crate::app::char_to_byte_pos(input, app.tag_input_cursor);
                    let prev = crate::app::char_to_byte_pos(input, app.tag_input_cursor - 1);
                    input.drain(prev..byte_pos);
                    app.tag_input_cursor -= 1;
                }
            }
        }
        _ => {}
    }
}

fn handle_host_detail(app: &mut App, key: KeyEvent) {
    let index = match app.screen {
        Screen::HostDetail { index } => index,
        _ => return,
    };
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('i') => {
            app.screen = Screen::HostList;
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        KeyCode::Char('e') => {
            if let Some(host) = app.hosts.get(index).cloned() {
                open_edit_form(app, host);
            }
        }
        KeyCode::Char('T') => {
            if let Some(host) = app.hosts.get(index) {
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                let alias = host.alias.clone();
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                app.refresh_tunnel_list(&alias);
                app.ui.tunnel_list_state = ratatui::widgets::ListState::default();
                if !app.tunnel_list.is_empty() {
                    app.ui.tunnel_list_state.select(Some(0));
                }
                app.screen = Screen::TunnelList { alias };
            }
        }
        KeyCode::Char('r') => {
            if let Some(host) = app.hosts.get(index) {
                let stale_hint = if host.stale.is_some() {
                    Some(stale_provider_hint(host))
                } else {
                    None
                };
                let alias = host.alias.clone();
                if let Some(hint) = stale_hint {
                    app.set_status(format!("Stale host.{}", hint), true);
                }
                app.screen = Screen::SnippetPicker {
                    target_aliases: vec![alias],
                };
                app.ui.snippet_picker_state = ratatui::widgets::ListState::default();
                let indices = app.filtered_snippet_indices();
                if !indices.is_empty() {
                    app.ui.snippet_picker_state.select(Some(0));
                }
            }
        }
        _ => {}
    }
}

fn handle_tag_picker_screen(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('#') => {
            app.screen = Screen::HostList;
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.select_next_tag();
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.select_prev_tag();
        }
        KeyCode::PageDown => {
            crate::app::page_down(&mut app.ui.tag_picker_state, app.tag_list.len(), 10);
        }
        KeyCode::PageUp => {
            crate::app::page_up(&mut app.ui.tag_picker_state, app.tag_list.len(), 10);
        }
        KeyCode::Enter => {
            if let Some(index) = app.ui.tag_picker_state.selected() {
                if let Some(tag) = app.tag_list.get(index) {
                    let tag: String = tag.clone();
                    app.screen = Screen::HostList;
                    app.start_search();
                    app.search.query = Some(format!("tag={}", tag));
                    app.apply_filter();
                }
            }
        }
        _ => {}
    }
}

fn handle_provider_list(app: &mut App, key: KeyEvent, events_tx: &mpsc::Sender<AppEvent>) {
    // Handle pending provider delete confirmation first
    if app.pending_provider_delete.is_some() && key.code != KeyCode::Char('?') {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                let name = app.pending_provider_delete.take().unwrap();
                if let Some(old_section) = app.provider_config.section(name.as_str()).cloned() {
                    app.provider_config.remove_section(name.as_str());
                    if let Err(e) = app.provider_config.save() {
                        app.provider_config.set_section(old_section);
                        app.set_status(format!("Failed to save: {}", e), true);
                    } else {
                        app.sync_history.remove(name.as_str());
                        crate::app::SyncRecord::save_all(&app.sync_history);
                        let display_name = crate::providers::provider_display_name(name.as_str());
                        app.set_status(
                            format!(
                                "Removed {} configuration. Synced hosts remain in your SSH config.",
                                display_name
                            ),
                            false,
                        );
                    }
                }
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.pending_provider_delete = None;
            }
            _ => {}
        }
        return;
    }

    let provider_count = app.sorted_provider_names().len();
    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => {
            // Cancel all running syncs
            for cancel_flag in app.syncing_providers.values() {
                cancel_flag.store(true, Ordering::Relaxed);
            }
            app.screen = Screen::HostList;
        }
        KeyCode::Char('j') | KeyCode::Down => {
            crate::app::cycle_selection(&mut app.ui.provider_list_state, provider_count, true);
        }
        KeyCode::Char('k') | KeyCode::Up => {
            crate::app::cycle_selection(&mut app.ui.provider_list_state, provider_count, false);
        }
        KeyCode::PageDown => {
            crate::app::page_down(&mut app.ui.provider_list_state, provider_count, 10);
        }
        KeyCode::PageUp => {
            crate::app::page_up(&mut app.ui.provider_list_state, provider_count, 10);
        }
        KeyCode::Enter => {
            if let Some(index) = app.ui.provider_list_state.selected() {
                let sorted = app.sorted_provider_names();
                if let Some(name) = sorted.get(index) {
                    let provider_impl = providers::get_provider(name.as_str());
                    let short_label = provider_impl
                        .as_ref()
                        .map(|p| p.short_label().to_string())
                        .unwrap_or_else(|| name.clone());

                    // Pre-fill form from existing config or defaults
                    let first_field = crate::app::ProviderFormField::fields_for(name.as_str())[0];
                    app.provider_form = if let Some(section) =
                        app.provider_config.section(name.as_str())
                    {
                        let cursor_pos = match first_field {
                            crate::app::ProviderFormField::Url => section.url.chars().count(),
                            crate::app::ProviderFormField::Token => section.token.chars().count(),
                            _ => 0,
                        };
                        ProviderFormFields {
                            url: section.url.clone(),
                            token: section.token.clone(),
                            profile: section.profile.clone(),
                            project: section.project.clone(),
                            compartment: section.compartment.clone(),
                            regions: section.regions.clone(),
                            alias_prefix: section.alias_prefix.clone(),
                            user: section.user.clone(),
                            identity_file: section.identity_file.clone(),
                            verify_tls: section.verify_tls,
                            auto_sync: section.auto_sync,
                            focused_field: first_field,
                            cursor_pos,
                        }
                    } else {
                        ProviderFormFields {
                            url: String::new(),
                            token: String::new(),
                            profile: String::new(),
                            project: String::new(),
                            compartment: String::new(),
                            regions: String::new(),
                            alias_prefix: short_label,
                            user: "root".to_string(),
                            identity_file: String::new(),
                            verify_tls: true,
                            auto_sync: !matches!(name.as_str(), "proxmox"),
                            focused_field: first_field,
                            cursor_pos: 0,
                        }
                    };
                    app.screen = Screen::ProviderForm {
                        provider: name.clone(),
                    };
                    app.capture_provider_form_mtime();
                    app.capture_provider_form_baseline();
                }
            }
        }
        KeyCode::Char('s') => {
            if let Some(index) = app.ui.provider_list_state.selected() {
                let sorted = app.sorted_provider_names();
                if let Some(name) = sorted.get(index) {
                    if let Some(section) = app.provider_config.section(name.as_str()).cloned() {
                        if !app.syncing_providers.contains_key(name.as_str()) {
                            let cancel = Arc::new(AtomicBool::new(false));
                            app.syncing_providers.insert(name.clone(), cancel.clone());
                            let display_name =
                                crate::providers::provider_display_name(name.as_str());
                            app.set_status(format!("Syncing {}...", display_name), false);
                            spawn_provider_sync(&section, events_tx.clone(), cancel);
                        }
                    } else {
                        let display_name = crate::providers::provider_display_name(name.as_str());
                        app.set_status(
                            format!("Configure {} first. Press Enter to set up.", display_name),
                            true,
                        );
                    }
                }
            }
        }
        KeyCode::Char('d') => {
            if let Some(index) = app.ui.provider_list_state.selected() {
                let sorted = app.sorted_provider_names();
                if let Some(name) = sorted.get(index) {
                    if app.provider_config.section(name.as_str()).is_some() {
                        app.pending_provider_delete = Some(name.clone());
                    } else {
                        let display_name = crate::providers::provider_display_name(name.as_str());
                        app.set_status(
                            format!("{} is not configured. Nothing to remove.", display_name),
                            false,
                        );
                    }
                }
            }
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        KeyCode::Char('X') => {
            if let Some(index) = app.ui.provider_list_state.selected() {
                let sorted = app.sorted_provider_names();
                if let Some(name) = sorted.get(index) {
                    let stale = app.config.stale_hosts();
                    let provider_stale: Vec<_> = stale
                        .iter()
                        .filter(|(alias, _)| {
                            app.config.host_entries().iter().any(|e| {
                                e.alias == *alias && e.provider.as_deref() == Some(name.as_str())
                            })
                        })
                        .collect();
                    if provider_stale.is_empty() {
                        let display = crate::providers::provider_display_name(name);
                        app.set_status(format!("No stale hosts for {}.", display), true);
                    } else {
                        let aliases: Vec<String> =
                            provider_stale.into_iter().map(|(a, _)| a.clone()).collect();
                        app.screen = Screen::ConfirmPurgeStale {
                            aliases,
                            provider: Some(name.clone()),
                        };
                    }
                }
            }
        }
        _ => {}
    }
}

/// Show a non-blocking warning when leaving the Token field with an invalid format.
fn warn_aws_token_format(app: &mut App, provider_name: &str) {
    if provider_name != "aws" {
        return;
    }
    if app.provider_form.focused_field != crate::app::ProviderFormField::Token {
        return;
    }
    let token = app.provider_form.token.trim();
    if token.is_empty() {
        return;
    }
    if !token.contains(':') {
        app.set_status("Token format: AccessKeyId:SecretAccessKey", true);
    }
}

fn handle_provider_form(app: &mut App, key: KeyEvent, events_tx: &mpsc::Sender<AppEvent>) {
    // Dispatch to key picker if open
    if app.ui.show_key_picker {
        handle_key_picker_shared(app, key, true);
        return;
    }

    // Dispatch to region picker if open
    if app.ui.show_region_picker {
        handle_region_picker(app, key);
        return;
    }

    let provider_name = match &app.screen {
        Screen::ProviderForm { provider } => provider.clone(),
        _ => return,
    };
    let fields = crate::app::ProviderFormField::fields_for(&provider_name);
    let is_toggle = |f: crate::app::ProviderFormField| {
        matches!(
            f,
            crate::app::ProviderFormField::VerifyTls | crate::app::ProviderFormField::AutoSync
        )
    };
    let is_picker = |f: crate::app::ProviderFormField| {
        matches!(f, crate::app::ProviderFormField::IdentityFile)
            || (f == crate::app::ProviderFormField::Regions
                && matches!(provider_name.as_str(), "aws" | "scaleway" | "gcp"))
    };

    // Handle discard confirmation dialog
    if app.pending_discard_confirm {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                app.pending_discard_confirm = false;
                app.clear_form_mtime();
                app.provider_form_baseline = None;
                app.screen = Screen::Providers;
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.pending_discard_confirm = false;
            }
            _ => {}
        }
        return;
    }

    match key.code {
        KeyCode::Esc => {
            if app.provider_form_is_dirty() {
                app.pending_discard_confirm = true;
            } else {
                app.clear_form_mtime();
                app.provider_form_baseline = None;
                app.screen = Screen::Providers;
            }
        }
        KeyCode::Tab | KeyCode::Down => {
            warn_aws_token_format(app, &provider_name);
            app.provider_form.focused_field = app.provider_form.focused_field.next(fields);
            app.provider_form.sync_cursor_to_end();
        }
        KeyCode::BackTab | KeyCode::Up => {
            warn_aws_token_format(app, &provider_name);
            app.provider_form.focused_field = app.provider_form.focused_field.prev(fields);
            app.provider_form.sync_cursor_to_end();
        }
        KeyCode::Left => {
            if app.provider_form.cursor_pos > 0 {
                app.provider_form.cursor_pos -= 1;
            }
        }
        KeyCode::Right => {
            let len = app.provider_form.focused_value().chars().count();
            if app.provider_form.cursor_pos < len {
                app.provider_form.cursor_pos += 1;
            }
        }
        KeyCode::Home => {
            app.provider_form.cursor_pos = 0;
        }
        KeyCode::End => {
            app.provider_form.sync_cursor_to_end();
        }
        KeyCode::Enter => {
            let f = app.provider_form.focused_field;
            if f == crate::app::ProviderFormField::IdentityFile {
                app.scan_keys();
                app.ui.show_key_picker = true;
                app.ui.key_picker_state = ratatui::widgets::ListState::default();
                if !app.keys.is_empty() {
                    app.ui.key_picker_state.select(Some(0));
                }
            } else if f == crate::app::ProviderFormField::Regions
                && matches!(
                    provider_name.as_str(),
                    "aws" | "scaleway" | "gcp" | "oracle" | "ovh"
                )
            {
                app.ui.show_region_picker = true;
                app.ui.region_picker_cursor = 0;
            } else {
                submit_provider_form(app, events_tx);
            }
        }
        KeyCode::Char(' ')
            if app.provider_form.focused_field == crate::app::ProviderFormField::VerifyTls =>
        {
            app.provider_form.verify_tls = !app.provider_form.verify_tls;
        }
        KeyCode::Char(' ')
            if app.provider_form.focused_field == crate::app::ProviderFormField::AutoSync =>
        {
            app.provider_form.auto_sync = !app.provider_form.auto_sync;
        }
        KeyCode::Char(c) => {
            let f = app.provider_form.focused_field;
            if !is_toggle(f) && !is_picker(f) {
                app.provider_form.insert_char(c);
            }
        }
        KeyCode::Backspace => {
            let f = app.provider_form.focused_field;
            if !is_toggle(f) && !is_picker(f) {
                app.provider_form.delete_char_before_cursor();
            }
        }
        _ => {}
    }
}

/// Build the same row list used by the region picker renderer.
fn region_picker_rows(provider: &str) -> Vec<Option<&'static str>> {
    let (zones, groups) = zone_data_for(provider);
    let mut rows = Vec::new();
    for &(_, start, end) in groups {
        rows.push(None); // group header
        for &(code, _) in &zones[start..end] {
            rows.push(Some(code));
        }
    }
    rows
}

/// Rebuild the regions string from the selected set, preserving display order.
fn rebuild_regions_string(selected: &std::collections::HashSet<String>, provider: &str) -> String {
    let (zones, _) = zone_data_for(provider);
    let ordered: Vec<&str> = zones
        .iter()
        .filter(|(code, _)| selected.contains(*code))
        .map(|(code, _)| *code)
        .collect();
    ordered.join(",")
}

type ZoneList = &'static [(&'static str, &'static str)];
type ZoneGroups = &'static [(&'static str, usize, usize)];

/// Return the zone/region data for a provider (aws or scaleway).
pub(crate) fn zone_data_for(provider: &str) -> (ZoneList, ZoneGroups) {
    match provider {
        "scaleway" => (
            crate::providers::scaleway::SCW_ZONES,
            crate::providers::scaleway::SCW_ZONE_GROUPS,
        ),
        "aws" => (
            crate::providers::aws::AWS_REGIONS,
            crate::providers::aws::AWS_REGION_GROUPS,
        ),
        "gcp" => (
            crate::providers::gcp::GCP_ZONES,
            crate::providers::gcp::GCP_ZONE_GROUPS,
        ),
        "oracle" => (
            crate::providers::oracle::OCI_REGIONS,
            crate::providers::oracle::OCI_REGION_GROUPS,
        ),
        "ovh" => (
            crate::providers::ovh::OVH_ENDPOINTS,
            crate::providers::ovh::OVH_ENDPOINT_GROUPS,
        ),
        _ => unreachable!(
            "zone_data_for called for unsupported provider: {}",
            provider
        ),
    }
}

fn handle_region_picker(app: &mut App, key: KeyEvent) {
    let provider_name = match &app.screen {
        Screen::ProviderForm { provider } => provider.clone(),
        _ => return,
    };
    let rows = region_picker_rows(&provider_name);
    let total = rows.len();

    // Parse current regions into a set for toggling
    let mut selected: std::collections::HashSet<String> = app
        .provider_form
        .regions
        .split(',')
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .collect();

    let zone_label = if matches!(provider_name.as_str(), "scaleway" | "gcp") {
        "zone"
    } else if provider_name == "ovh" {
        "endpoint"
    } else {
        "region"
    };

    match key.code {
        KeyCode::Esc => {
            app.provider_form.regions = rebuild_regions_string(&selected, &provider_name);
            app.provider_form.sync_cursor_to_end();
            app.ui.show_region_picker = false;
            let count = selected.len();
            if count > 0 {
                app.set_status(
                    format!(
                        "{} {}{} selected.",
                        count,
                        zone_label,
                        if count == 1 { "" } else { "s" }
                    ),
                    false,
                );
            }
        }
        KeyCode::Enter => {
            // For single-select providers (OVH): Enter on an item selects it
            // exclusively and closes. For multi-select: Enter confirms current
            // selection (same as Esc).
            if provider_name == "ovh" {
                let cursor = app.ui.region_picker_cursor;
                if let Some(Some(code)) = rows.get(cursor) {
                    selected.clear();
                    selected.insert(code.to_string());
                }
            }
            app.provider_form.regions = rebuild_regions_string(&selected, &provider_name);
            app.provider_form.sync_cursor_to_end();
            app.ui.show_region_picker = false;
            let count = selected.len();
            if count > 0 {
                app.set_status(
                    format!(
                        "{} {}{} selected.",
                        count,
                        zone_label,
                        if count == 1 { "" } else { "s" }
                    ),
                    false,
                );
            }
        }
        KeyCode::Down | KeyCode::Char('j') => {
            if app.ui.region_picker_cursor + 1 < total {
                app.ui.region_picker_cursor += 1;
            }
        }
        KeyCode::Up | KeyCode::Char('k') => {
            if app.ui.region_picker_cursor > 0 {
                app.ui.region_picker_cursor -= 1;
            }
        }
        KeyCode::Char(' ') => {
            let cursor = app.ui.region_picker_cursor;
            if let Some(Some(code)) = rows.get(cursor) {
                // Toggle single region
                if selected.contains(*code) {
                    selected.remove(*code);
                } else {
                    selected.insert(code.to_string());
                }
            } else {
                // Group header: toggle all regions in this group
                let group_codes: Vec<&str> = rows[cursor + 1..]
                    .iter()
                    .take_while(|r| r.is_some())
                    .filter_map(|r| *r)
                    .collect();
                let all_selected = group_codes.iter().all(|c| selected.contains(*c));
                for code in group_codes {
                    if all_selected {
                        selected.remove(code);
                    } else {
                        selected.insert(code.to_string());
                    }
                }
            }
            app.provider_form.regions = rebuild_regions_string(&selected, &provider_name);
        }
        _ => {}
    }
}

fn submit_provider_form(app: &mut App, events_tx: &mpsc::Sender<AppEvent>) {
    let provider_name = match &app.screen {
        Screen::ProviderForm { provider } => provider.clone(),
        _ => return,
    };

    // Check for external provider config changes since form was opened
    if app.provider_config_changed_since_form_open() {
        app.set_status(
            "Provider config changed externally. Press Esc and re-open to pick up changes.",
            true,
        );
        return;
    }

    // Reject control characters in all fields (prevents INI injection)
    let pf_fields = [
        (&app.provider_form.url, "URL"),
        (&app.provider_form.token, "Token"),
        (&app.provider_form.alias_prefix, "Alias Prefix"),
        (&app.provider_form.user, "User"),
        (&app.provider_form.identity_file, "Identity File"),
        (&app.provider_form.profile, "Profile"),
        (&app.provider_form.project, "Project ID"),
        (&app.provider_form.regions, "Regions"),
    ];
    for (value, name) in &pf_fields {
        if value.chars().any(|c| c.is_control()) {
            app.set_status(format!("{} contains control characters.", name), true);
            return;
        }
    }

    // Proxmox requires a URL
    if provider_name == "proxmox" {
        let url = app.provider_form.url.trim();
        if url.is_empty() {
            app.set_status("URL is required for Proxmox VE.", true);
            return;
        }
        if !url.to_ascii_lowercase().starts_with("https://") {
            app.set_status(
                "URL must start with https://. Toggle Verify TLS off for self-signed certificates.",
                true,
            );
            return;
        }
    }

    // AWS allows empty token when profile is set (credentials from ~/.aws/credentials)
    if app.provider_form.token.trim().is_empty()
        && provider_name != "tailscale"
        && (provider_name != "aws" || app.provider_form.profile.trim().is_empty())
    {
        let hint = if provider_name == "gcp" {
            "Token can't be empty. Provide a service account JSON key file path or access token."
                .to_string()
        } else if provider_name == "oracle" {
            "Token can't be empty. Provide the path to your OCI config file (e.g. ~/.oci/config)."
                .to_string()
        } else {
            let display_name = crate::providers::provider_display_name(provider_name.as_str());
            format!(
                "Token can't be empty. Grab one from your {} dashboard.",
                display_name
            )
        };
        app.set_status(hint, true);
        return;
    }

    // GCP requires a project ID
    if provider_name == "gcp" && app.provider_form.project.trim().is_empty() {
        app.set_status("Project ID can't be empty. Set your GCP project ID.", true);
        return;
    }

    // Oracle requires a compartment OCID
    if provider_name == "oracle" && app.provider_form.compartment.trim().is_empty() {
        app.set_status(
            "Compartment can't be empty. Set your OCI compartment OCID.",
            true,
        );
        return;
    }

    // AWS/Scaleway require at least one region/zone
    if provider_name == "aws" && app.provider_form.regions.trim().is_empty() {
        app.set_status("Select at least one AWS region.", true);
        return;
    }
    if provider_name == "scaleway" && app.provider_form.regions.trim().is_empty() {
        app.set_status("Select at least one Scaleway zone.", true);
        return;
    }
    if provider_name == "azure" {
        let subs = app.provider_form.regions.trim();
        if subs.is_empty() {
            app.set_status("Enter at least one Azure subscription ID.", true);
            return;
        }
        for sub in subs.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) {
            if !crate::providers::azure::is_valid_subscription_id(sub) {
                app.set_status(
                    format!("Invalid subscription ID '{}'. Expected UUID format (e.g. 12345678-1234-1234-1234-123456789012).", sub),
                    true,
                );
                return;
            }
        }
    }

    let token = app.provider_form.token.trim().to_string();
    let alias_prefix = app.provider_form.alias_prefix.trim().to_string();
    if crate::ssh_config::model::is_host_pattern(&alias_prefix) {
        app.set_status(
            "Alias prefix can't contain spaces or pattern characters (*, ?, [, !).",
            true,
        );
        return;
    }

    let user = {
        let u = app.provider_form.user.trim();
        if u.is_empty() {
            "root".to_string()
        } else {
            u.to_string()
        }
    };
    if user.contains(char::is_whitespace) {
        app.set_status("User can't contain whitespace.", true);
        return;
    }

    let section = providers::config::ProviderSection {
        provider: provider_name.clone(),
        token: token.clone(),
        alias_prefix,
        user,
        identity_file: app.provider_form.identity_file.trim().to_string(),
        url: app.provider_form.url.trim().to_string(),
        verify_tls: app.provider_form.verify_tls,
        auto_sync: app.provider_form.auto_sync,
        profile: app.provider_form.profile.trim().to_string(),
        regions: app.provider_form.regions.trim().to_string(),
        project: app.provider_form.project.trim().to_string(),
        compartment: app.provider_form.compartment.trim().to_string(),
    };

    let old_section = app.provider_config.section(&provider_name).cloned();
    app.provider_config.set_section(section);
    if let Err(e) = app.provider_config.save() {
        // Rollback: restore previous state
        match old_section {
            Some(old) => app.provider_config.set_section(old),
            None => app.provider_config.remove_section(&provider_name),
        }
        app.set_status(format!("Failed to save: {}", e), true);
        return;
    }

    let display_name = crate::providers::provider_display_name(provider_name.as_str());

    if !app.syncing_providers.contains_key(&provider_name) {
        let sync_section = app.provider_config.section(&provider_name).cloned();
        if let Some(sync_section) = sync_section {
            let cancel = Arc::new(AtomicBool::new(false));
            app.syncing_providers
                .insert(provider_name.clone(), cancel.clone());
            app.set_status(
                format!("Saved {} configuration. Syncing...", display_name),
                false,
            );
            spawn_provider_sync(&sync_section, events_tx.clone(), cancel);
        }
    } else {
        app.set_status(format!("Saved {} configuration.", display_name), false);
    }
    app.clear_form_mtime();
    app.provider_form_baseline = None;
    app.screen = Screen::Providers;
}

/// Password source picker handler.
fn handle_password_picker(app: &mut App, key: KeyEvent) {
    // Ctrl+D sets selected source as global default
    if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('d') {
        if let Some(index) = app.ui.password_picker_state.selected() {
            if let Some(source) = crate::askpass::PASSWORD_SOURCES.get(index) {
                let is_none = source.label == "None";
                let value = if is_none { "" } else { source.value };
                match crate::preferences::save_askpass_default(value) {
                    Ok(()) => {
                        if is_none {
                            app.set_status("Global default cleared.", false);
                        } else {
                            app.set_status(
                                format!("Global default set to {}.", source.label),
                                false,
                            );
                        }
                    }
                    Err(e) => {
                        app.set_status(format!("Failed to save default: {}", e), true);
                    }
                }
            }
        }
        app.ui.show_password_picker = false;
        return;
    }

    match key.code {
        KeyCode::Esc => {
            app.ui.show_password_picker = false;
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.select_next_password_source();
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.select_prev_password_source();
        }
        KeyCode::Enter => {
            let mut needs_more_input = false;
            if let Some(index) = app.ui.password_picker_state.selected() {
                if let Some(source) = crate::askpass::PASSWORD_SOURCES.get(index) {
                    let is_none = source.label == "None";
                    let is_custom_cmd = source.label == "Custom command";
                    let is_prefix = source.value.ends_with(':') || source.value.ends_with("//");
                    if is_none {
                        app.form.askpass = String::new();
                        app.form.sync_cursor_to_end();
                        app.set_status("Password source cleared.", false);
                    } else if is_custom_cmd {
                        app.form.askpass = String::new();
                        app.form.focused_field = FormField::AskPass;
                        app.form.sync_cursor_to_end();
                        app.set_status(
                            "Type your command. Use %a (alias) and %h (hostname) as placeholders.",
                            false,
                        );
                        needs_more_input = true;
                    } else if is_prefix {
                        app.form.askpass = source.value.to_string();
                        app.form.focused_field = FormField::AskPass;
                        app.form.sync_cursor_to_end();
                        app.set_status(format!("Complete the {} path.", source.label), false);
                        needs_more_input = true;
                    } else {
                        app.form.askpass = source.value.to_string();
                        app.form.sync_cursor_to_end();
                        app.set_status(format!("Password source set to {}.", source.label), false);
                    }
                }
            }
            app.ui.show_password_picker = false;
            if !needs_more_input {
                try_auto_submit_after_picker(app);
            }
        }
        _ => {}
    }
}

/// Unified key picker handler for both host form and provider form.
fn handle_key_picker_shared(app: &mut App, key: KeyEvent, for_provider: bool) {
    match key.code {
        KeyCode::Esc => {
            app.ui.show_key_picker = false;
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.select_next_picker_key();
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.select_prev_picker_key();
        }
        KeyCode::Enter => {
            if let Some(index) = app.ui.key_picker_state.selected() {
                if let Some(key_info) = app.keys.get(index) {
                    if for_provider {
                        app.provider_form.identity_file = key_info.display_path.clone();
                        app.provider_form.sync_cursor_to_end();
                    } else {
                        app.form.identity_file = key_info.display_path.clone();
                        app.form.sync_cursor_to_end();
                    }
                    app.set_status(format!("Locked and loaded with {}.", key_info.name), false);
                }
            }
            app.ui.show_key_picker = false;
            if !for_provider {
                try_auto_submit_after_picker(app);
            }
        }
        _ => {}
    }
}

/// ProxyJump picker handler for the host form.
fn handle_proxyjump_picker(app: &mut App, key: KeyEvent) {
    match key.code {
        KeyCode::Esc => {
            app.ui.show_proxyjump_picker = false;
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.select_next_proxyjump();
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.select_prev_proxyjump();
        }
        KeyCode::Enter => {
            let candidates = app.proxyjump_candidates();
            if let Some(index) = app.ui.proxyjump_picker_state.selected() {
                if let Some((alias, _)) = candidates.get(index) {
                    app.form.proxy_jump = alias.clone();
                    app.form.sync_cursor_to_end();
                    app.set_status(format!("Jumping through {}.", alias), false);
                }
            }
            app.ui.show_proxyjump_picker = false;
            try_auto_submit_after_picker(app);
        }
        _ => {}
    }
}

/// Ping the currently selected host (shared by 'p' key and Ctrl+P in search mode).
fn ping_selected_host(app: &mut App, events_tx: &mpsc::Sender<AppEvent>, show_hint: bool) {
    if let Some(host) = app.selected_host() {
        let alias = host.alias.clone();
        // For ProxyJump hosts, ping the bastion instead and propagate the
        // result to all dependents (handled in main.rs PingResult handler).
        let (ping_alias, hostname, port) = if !host.proxy_jump.is_empty() {
            let bastion_alias = host.proxy_jump.clone();
            if let Some(bastion) = app.hosts.iter().find(|h| h.alias == bastion_alias) {
                app.ping_status
                    .insert(alias.clone(), crate::app::PingStatus::Checking);
                (
                    bastion.alias.clone(),
                    bastion.hostname.clone(),
                    bastion.port,
                )
            } else {
                app.set_status(
                    format!("Bastion {} not found in config.", bastion_alias),
                    true,
                );
                return;
            }
        } else {
            (alias.clone(), host.hostname.clone(), host.port)
        };
        app.ping_status
            .insert(ping_alias.clone(), crate::app::PingStatus::Checking);
        if show_hint && !app.has_pinged {
            app.set_status(
                format!("Pinging {}... (Shift+P pings all)", ping_alias),
                false,
            );
            app.has_pinged = true;
        } else {
            app.set_status(format!("Pinging {}...", ping_alias), false);
        }
        ping::ping_host(
            ping_alias,
            hostname,
            port,
            events_tx.clone(),
            app.ping_generation,
        );
    }
}

fn handle_tunnel_list(app: &mut App, key: KeyEvent) {
    let alias = match &app.screen {
        Screen::TunnelList { alias } => alias.clone(),
        _ => return,
    };

    // Handle pending tunnel delete confirmation first
    if app.pending_tunnel_delete.is_some() && key.code != KeyCode::Char('?') {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                let sel = app.pending_tunnel_delete.take().unwrap();
                if let Some(rule) = app.tunnel_list.get(sel) {
                    let key = rule.tunnel_type.directive_key().to_string();
                    let value = rule.to_directive_value();
                    let config_backup = app.config.clone();
                    if !app.config.remove_forward(&alias, &key, &value) {
                        app.set_status("Tunnel not found in config.", true);
                        return;
                    }
                    if let Err(e) = app.config.write() {
                        app.config = config_backup;
                        app.set_status(format!("Failed to save: {}", e), true);
                    } else {
                        app.update_last_modified();
                        app.refresh_tunnel_list(&alias);
                        app.reload_hosts();
                        if app.tunnel_list.is_empty() {
                            app.ui.tunnel_list_state.select(None);
                        } else if sel >= app.tunnel_list.len() {
                            app.ui
                                .tunnel_list_state
                                .select(Some(app.tunnel_list.len() - 1));
                        }
                        app.set_status("Tunnel removed.", false);
                    }
                }
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.pending_tunnel_delete = None;
            }
            _ => {}
        }
        return;
    }

    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => {
            app.screen = Screen::HostList;
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.select_next_tunnel();
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.select_prev_tunnel();
        }
        KeyCode::PageDown => {
            crate::app::page_down(&mut app.ui.tunnel_list_state, app.tunnel_list.len(), 10);
        }
        KeyCode::PageUp => {
            crate::app::page_up(&mut app.ui.tunnel_list_state, app.tunnel_list.len(), 10);
        }
        KeyCode::Char('a') => {
            // Check if host is from an included file (read-only)
            if let Some(host) = app.hosts.iter().find(|h| h.alias == alias) {
                if host.source_file.is_some() {
                    app.set_status("Included host. Tunnels are read-only.", true);
                    return;
                }
            }
            app.tunnel_form = crate::app::TunnelForm::new();
            app.screen = Screen::TunnelForm {
                alias: alias.clone(),
                editing: None,
            };
            app.capture_form_mtime();
            app.capture_tunnel_form_baseline();
        }
        KeyCode::Char('e') => {
            // Check if host is from an included file (read-only)
            if let Some(host) = app.hosts.iter().find(|h| h.alias == alias) {
                if host.source_file.is_some() {
                    app.set_status("Included host. Tunnels are read-only.", true);
                    return;
                }
            }
            if let Some(sel) = app.ui.tunnel_list_state.selected() {
                if let Some(rule) = app.tunnel_list.get(sel) {
                    app.tunnel_form = crate::app::TunnelForm::from_rule(rule);
                    app.screen = Screen::TunnelForm {
                        alias: alias.clone(),
                        editing: Some(sel),
                    };
                    app.capture_form_mtime();
                    app.capture_tunnel_form_baseline();
                }
            }
        }
        KeyCode::Char('d') => {
            // Check if host is from an included file (read-only)
            if let Some(host) = app.hosts.iter().find(|h| h.alias == alias) {
                if host.source_file.is_some() {
                    app.set_status("Included host. Tunnels are read-only.", true);
                    return;
                }
            }
            if let Some(sel) = app.ui.tunnel_list_state.selected() {
                if sel < app.tunnel_list.len() {
                    app.pending_tunnel_delete = Some(sel);
                }
            }
        }
        KeyCode::Enter => {
            // Start/stop tunnel
            if app.active_tunnels.contains_key(&alias) {
                // Stop
                if let Some(mut tunnel) = app.active_tunnels.remove(&alias) {
                    let _ = tunnel.child.kill();
                    let _ = tunnel.child.wait();
                    app.set_status(format!("Tunnel for {} stopped.", alias), false);
                }
            } else if !app.tunnel_list.is_empty() {
                // Start
                let askpass = app
                    .hosts
                    .iter()
                    .find(|h| h.alias == alias)
                    .and_then(|h| h.askpass.clone());
                match crate::tunnel::start_tunnel(
                    &alias,
                    &app.reload.config_path,
                    askpass.as_deref(),
                    app.bw_session.as_deref(),
                ) {
                    Ok(child) => {
                        app.active_tunnels
                            .insert(alias.clone(), crate::tunnel::ActiveTunnel { child });
                        app.set_status(format!("Tunnel for {} started.", alias), false);
                    }
                    Err(e) => {
                        app.set_status(format!("Failed to start tunnel: {}", e), true);
                    }
                }
            }
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        _ => {}
    }
}

fn handle_tunnel_form(app: &mut App, key: KeyEvent) {
    let (alias, editing) = match &app.screen {
        Screen::TunnelForm { alias, editing } => (alias.clone(), *editing),
        _ => return,
    };

    // Handle discard confirmation dialog
    if app.pending_discard_confirm {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                app.pending_discard_confirm = false;
                app.clear_form_mtime();
                app.tunnel_form_baseline = None;
                app.screen = Screen::TunnelList { alias };
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.pending_discard_confirm = false;
            }
            _ => {}
        }
        return;
    }

    match key.code {
        KeyCode::Esc => {
            if app.tunnel_form_is_dirty() {
                app.pending_discard_confirm = true;
            } else {
                app.clear_form_mtime();
                app.tunnel_form_baseline = None;
                app.screen = Screen::TunnelList { alias };
            }
        }
        KeyCode::Tab | KeyCode::Down => {
            app.tunnel_form.focused_field = app
                .tunnel_form
                .focused_field
                .next(app.tunnel_form.tunnel_type);
            app.tunnel_form.sync_cursor_to_end();
        }
        KeyCode::BackTab | KeyCode::Up => {
            app.tunnel_form.focused_field = app
                .tunnel_form
                .focused_field
                .prev(app.tunnel_form.tunnel_type);
            app.tunnel_form.sync_cursor_to_end();
        }
        KeyCode::Left => {
            if app.tunnel_form.cursor_pos > 0 {
                app.tunnel_form.cursor_pos -= 1;
            }
        }
        KeyCode::Right => {
            let len = app
                .tunnel_form
                .focused_value()
                .map(|v| v.chars().count())
                .unwrap_or(0);
            if app.tunnel_form.cursor_pos < len {
                app.tunnel_form.cursor_pos += 1;
            }
        }
        KeyCode::Home => {
            app.tunnel_form.cursor_pos = 0;
        }
        KeyCode::End => {
            app.tunnel_form.sync_cursor_to_end();
        }
        KeyCode::Enter => {
            submit_tunnel_form(app, &alias, editing);
        }
        KeyCode::Char(' ')
            if app.tunnel_form.focused_field == crate::app::TunnelFormField::Type =>
        {
            app.tunnel_form.tunnel_type = app.tunnel_form.tunnel_type.next();
        }
        KeyCode::Char(c) => {
            app.tunnel_form.insert_char(c);
        }
        KeyCode::Backspace => {
            app.tunnel_form.delete_char_before_cursor();
        }
        _ => {}
    }
}

fn submit_tunnel_form(app: &mut App, alias: &str, editing: Option<usize>) {
    // Check for external config changes since form was opened
    if app.config_changed_since_form_open() {
        app.set_status(
            "Config changed externally. Press Esc and re-open to pick up changes.",
            true,
        );
        return;
    }

    if let Err(msg) = app.tunnel_form.validate() {
        app.set_status(msg, true);
        return;
    }

    let (directive_key, directive_value) = app.tunnel_form.to_directive();
    let config_backup = app.config.clone();

    // If editing, remove the old directive first
    if let Some(idx) = editing {
        if let Some(old_rule) = app.tunnel_list.get(idx) {
            let old_key = old_rule.tunnel_type.directive_key().to_string();
            let old_value = old_rule.to_directive_value();
            if !app.config.remove_forward(alias, &old_key, &old_value) {
                app.config = config_backup;
                app.set_status("Original tunnel not found in config.", true);
                return;
            }
        } else {
            // Index out of bounds (external config change) — abort
            app.set_status(
                "Tunnel list changed externally. Press Esc and re-open.",
                true,
            );
            return;
        }
    }

    // Duplicate detection (runs after old directive removal for edits)
    if app
        .config
        .has_forward(alias, directive_key, &directive_value)
    {
        app.config = config_backup;
        app.set_status("Duplicate tunnel already configured.", true);
        return;
    }

    app.config
        .add_forward(alias, directive_key, &directive_value);
    if let Err(e) = app.config.write() {
        app.config = config_backup;
        app.set_status(format!("Failed to save: {}", e), true);
        return;
    }

    app.undo_stack.clear(); // Clear undo buffer — positions may have shifted
    app.update_last_modified();
    app.refresh_tunnel_list(alias);
    app.reload_hosts();
    // Fix selection after list change
    if app.tunnel_list.is_empty() {
        app.ui.tunnel_list_state.select(None);
    } else if let Some(sel) = app.ui.tunnel_list_state.selected() {
        if sel >= app.tunnel_list.len() {
            app.ui
                .tunnel_list_state
                .select(Some(app.tunnel_list.len() - 1));
        }
    } else {
        // First tunnel added to empty list — initialize selection
        app.ui.tunnel_list_state.select(Some(0));
    }
    app.clear_form_mtime();
    app.tunnel_form_baseline = None;
    app.set_status("Tunnel saved.", false);
    app.screen = Screen::TunnelList {
        alias: alias.to_string(),
    };
}

fn open_snippet_picker(app: &mut App, aliases: Vec<String>) {
    app.snippet_store = crate::snippet::SnippetStore::load();
    app.ui.snippet_picker_state = ratatui::widgets::ListState::default();
    if !app.snippet_store.snippets.is_empty() {
        app.ui.snippet_picker_state.select(Some(0));
    }
    app.screen = Screen::SnippetPicker {
        target_aliases: aliases,
    };
}

fn handle_snippet_picker(app: &mut App, key: KeyEvent, events_tx: &mpsc::Sender<AppEvent>) {
    let target_aliases = match &app.screen {
        Screen::SnippetPicker { target_aliases } => target_aliases.clone(),
        _ => return,
    };

    // Allow ? to open help even during search
    if key.code == KeyCode::Char('?') {
        let old = std::mem::replace(&mut app.screen, Screen::HostList);
        app.screen = Screen::Help {
            return_screen: Box::new(old),
        };
        return;
    }

    // Search mode dispatch
    if app.ui.snippet_search.is_some() {
        handle_snippet_picker_search(app, key, &target_aliases, events_tx);
        return;
    }

    // Handle pending snippet delete confirmation
    if app.pending_snippet_delete.is_some() && key.code != KeyCode::Char('?') {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                let sel = app.pending_snippet_delete.take().unwrap();
                if sel < app.snippet_store.snippets.len() {
                    let removed = app.snippet_store.snippets.remove(sel);
                    if let Err(e) = app.snippet_store.save() {
                        app.snippet_store.snippets.insert(sel, removed);
                        app.set_status(format!("Failed to save: {}", e), true);
                    } else {
                        if app.snippet_store.snippets.is_empty() {
                            app.ui.snippet_picker_state.select(None);
                        } else if sel >= app.snippet_store.snippets.len() {
                            app.ui
                                .snippet_picker_state
                                .select(Some(app.snippet_store.snippets.len() - 1));
                        }
                        app.set_status(format!("Removed snippet '{}'.", removed.name), false);
                    }
                }
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.pending_snippet_delete = None;
            }
            _ => {}
        }
        return;
    }

    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => {
            app.ui.snippet_search = None;
            app.pending_snippet_delete = None;
            app.screen = Screen::HostList;
        }
        KeyCode::Char('/') => {
            app.ui.snippet_search = Some(String::new());
        }
        KeyCode::Char('j') | KeyCode::Down => {
            app.select_next_snippet();
        }
        KeyCode::Char('k') | KeyCode::Up => {
            app.select_prev_snippet();
        }
        KeyCode::PageDown => {
            crate::app::page_down(
                &mut app.ui.snippet_picker_state,
                app.snippet_store.snippets.len(),
                10,
            );
        }
        KeyCode::PageUp => {
            crate::app::page_up(
                &mut app.ui.snippet_picker_state,
                app.snippet_store.snippets.len(),
                10,
            );
        }
        KeyCode::Char('a') => {
            app.snippet_form = crate::app::SnippetForm::new();
            app.screen = Screen::SnippetForm {
                target_aliases: target_aliases.clone(),
                editing: None,
            };
            app.capture_snippet_form_baseline();
        }
        KeyCode::Char('e') => {
            if let Some(sel) = app.ui.snippet_picker_state.selected() {
                if let Some(snippet) = app.snippet_store.snippets.get(sel) {
                    app.snippet_form = crate::app::SnippetForm::from_snippet(snippet);
                    app.screen = Screen::SnippetForm {
                        target_aliases: target_aliases.clone(),
                        editing: Some(sel),
                    };
                    app.capture_snippet_form_baseline();
                }
            }
        }
        KeyCode::Char('d') => {
            if let Some(sel) = app.ui.snippet_picker_state.selected() {
                if sel < app.snippet_store.snippets.len() {
                    app.pending_snippet_delete = Some(sel);
                }
            }
        }
        KeyCode::Enter => {
            if let Some(sel) = app.ui.snippet_picker_state.selected() {
                if let Some(snippet) = app.snippet_store.snippets.get(sel).cloned() {
                    run_or_prompt_params(app, snippet, target_aliases, false, events_tx);
                }
            }
        }
        KeyCode::Char('!') => {
            if let Some(sel) = app.ui.snippet_picker_state.selected() {
                if let Some(snippet) = app.snippet_store.snippets.get(sel).cloned() {
                    run_or_prompt_params(app, snippet, target_aliases, true, events_tx);
                }
            }
        }
        _ => {}
    }
}

/// Run a snippet (captured output) or open param form if it has parameters.
fn run_or_prompt_params(
    app: &mut App,
    snippet: crate::snippet::Snippet,
    target_aliases: Vec<String>,
    terminal_mode: bool,
    events_tx: &mpsc::Sender<AppEvent>,
) {
    let params = crate::snippet::parse_params(&snippet.command);
    if !params.is_empty() {
        app.snippet_param_form = Some(crate::app::SnippetParamFormState::new(&params));
        app.pending_snippet_terminal = terminal_mode;
        app.screen = Screen::SnippetParamForm {
            snippet,
            target_aliases,
        };
    } else if terminal_mode {
        app.pending_snippet = Some((snippet, target_aliases));
        app.multi_select.clear();
        app.screen = Screen::HostList;
    } else {
        app.multi_select.clear();
        start_snippet_output(app, &snippet, &target_aliases, events_tx);
    }
}

/// Monotonically increasing run ID to distinguish snippet execution runs.
static SNIPPET_RUN_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);

/// Start in-TUI snippet execution.
fn start_snippet_output(
    app: &mut App,
    snippet: &crate::snippet::Snippet,
    target_aliases: &[String],
    events_tx: &mpsc::Sender<AppEvent>,
) {
    let cancel = Arc::new(AtomicBool::new(false));

    let askpass_map: Vec<(String, Option<String>)> = target_aliases
        .iter()
        .map(|alias| {
            let askpass = app
                .hosts
                .iter()
                .find(|h| h.alias == *alias)
                .and_then(|h| h.askpass.clone())
                .or_else(preferences::load_askpass_default);
            (alias.clone(), askpass)
        })
        .collect();

    let tunnel_aliases: std::collections::HashSet<String> =
        app.active_tunnels.keys().cloned().collect();

    let run_id = SNIPPET_RUN_COUNTER.fetch_add(1, Ordering::Relaxed);

    app.snippet_output = Some(crate::app::SnippetOutputState {
        run_id,
        results: Vec::new(),
        scroll_offset: 0,
        completed: 0,
        total: target_aliases.len(),
        all_done: false,
        cancel: cancel.clone(),
    });

    app.screen = Screen::SnippetOutput {
        snippet_name: snippet.name.clone(),
        target_aliases: target_aliases.to_vec(),
    };

    let stx = snippet_event_bridge(events_tx);
    crate::snippet::spawn_snippet_execution(
        run_id,
        askpass_map,
        app.reload.config_path.clone(),
        snippet.command.clone(),
        app.bw_session.clone(),
        tunnel_aliases,
        cancel,
        stx,
        target_aliases.len() > 1,
    );
}

/// Compute the line count for a snippet host result, matching the UI renderer.
fn snippet_result_lines(r: &crate::app::SnippetHostOutput) -> usize {
    let content = if r.stdout.is_empty() && r.stderr.is_empty() {
        1 // "[No output]" placeholder
    } else {
        let stdout_lines = if r.stdout.is_empty() {
            0
        } else {
            r.stdout.lines().count()
        };
        let stderr_lines = if r.stderr.is_empty() {
            0
        } else {
            r.stderr.lines().count()
        };
        stdout_lines + stderr_lines
    };
    // header + content + blank line
    1 + content + 1
}

fn handle_snippet_output(app: &mut App, key: KeyEvent) {
    let total_lines = app
        .snippet_output
        .as_ref()
        .map(|s| s.results.iter().map(snippet_result_lines).sum::<usize>())
        .unwrap_or(0);

    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => {
            if let Some(ref state) = app.snippet_output {
                if !state.all_done {
                    state.cancel.store(true, Ordering::Relaxed);
                }
            }
            app.snippet_output = None;
            app.screen = Screen::HostList;
        }
        KeyCode::Char('j') | KeyCode::Down => {
            if let Some(ref mut state) = app.snippet_output {
                state.scroll_offset = state.scroll_offset.saturating_add(1);
            }
        }
        KeyCode::Char('k') | KeyCode::Up => {
            if let Some(ref mut state) = app.snippet_output {
                state.scroll_offset = state.scroll_offset.saturating_sub(1);
            }
        }
        KeyCode::PageDown => {
            if let Some(ref mut state) = app.snippet_output {
                state.scroll_offset = state.scroll_offset.saturating_add(20);
            }
        }
        KeyCode::PageUp => {
            if let Some(ref mut state) = app.snippet_output {
                state.scroll_offset = state.scroll_offset.saturating_sub(20);
            }
        }
        KeyCode::Char('G') => {
            if let Some(ref mut state) = app.snippet_output {
                state.scroll_offset = total_lines.saturating_sub(1);
            }
        }
        KeyCode::Char('g') => {
            if let Some(ref mut state) = app.snippet_output {
                state.scroll_offset = 0;
            }
        }
        KeyCode::Char('n') => {
            // Jump to next host header
            if let Some(ref mut state) = app.snippet_output {
                let current = state.scroll_offset;
                let mut line = 0;
                for result in &state.results {
                    let section = snippet_result_lines(result);
                    if line > current {
                        state.scroll_offset = line;
                        return;
                    }
                    line += section;
                }
            }
        }
        KeyCode::Char('N') => {
            // Jump to previous host header
            if let Some(ref mut state) = app.snippet_output {
                let current = state.scroll_offset;
                let mut offsets = Vec::new();
                let mut line = 0;
                for result in &state.results {
                    offsets.push(line);
                    line += snippet_result_lines(result);
                }
                for &off in offsets.iter().rev() {
                    if off < current {
                        state.scroll_offset = off;
                        return;
                    }
                }
                state.scroll_offset = 0;
            }
        }
        KeyCode::Char('c') => {
            // Copy all output
            if let Some(ref state) = app.snippet_output {
                let mut text = String::new();
                for result in &state.results {
                    text.push_str(&format!("-- {} --\n", result.alias));
                    if !result.stdout.is_empty() {
                        text.push_str(&result.stdout);
                        text.push('\n');
                    }
                    if !result.stderr.is_empty() {
                        text.push_str(&result.stderr);
                        text.push('\n');
                    }
                    text.push('\n');
                }
                match clipboard::copy_to_clipboard(&text) {
                    Ok(()) => app.set_status("Output copied.", false),
                    Err(e) => app.set_status(format!("Copy failed: {}", e), true),
                }
            }
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        _ => {}
    }
}

/// Reset snippet picker selection to first match after search query changes.
fn reset_snippet_search_selection(app: &mut App) {
    let filtered = app.filtered_snippet_indices();
    if filtered.is_empty() {
        app.ui.snippet_picker_state.select(None);
    } else {
        app.ui.snippet_picker_state.select(Some(0));
    }
}

fn handle_snippet_picker_search(
    app: &mut App,
    key: KeyEvent,
    target_aliases: &[String],
    events_tx: &mpsc::Sender<AppEvent>,
) {
    match key.code {
        KeyCode::Esc => {
            app.ui.snippet_search = None;
            // Restore selection to full list
            if !app.snippet_store.snippets.is_empty()
                && app.ui.snippet_picker_state.selected().is_none()
            {
                app.ui.snippet_picker_state.select(Some(0));
            }
        }
        KeyCode::Enter => {
            let filtered = app.filtered_snippet_indices();
            if let Some(sel) = app.ui.snippet_picker_state.selected() {
                if sel < filtered.len() {
                    let real_idx = filtered[sel];
                    if let Some(snippet) = app.snippet_store.snippets.get(real_idx).cloned() {
                        app.ui.snippet_search = None;
                        run_or_prompt_params(
                            app,
                            snippet,
                            target_aliases.to_vec(),
                            false,
                            events_tx,
                        );
                    }
                }
            }
        }
        KeyCode::Char(c) => {
            if let Some(ref mut query) = app.ui.snippet_search {
                query.push(c);
            }
            reset_snippet_search_selection(app);
        }
        KeyCode::Backspace => {
            if let Some(ref mut query) = app.ui.snippet_search {
                query.pop();
                if query.is_empty() {
                    app.ui.snippet_search = None;
                    if !app.snippet_store.snippets.is_empty() {
                        app.ui.snippet_picker_state.select(Some(0));
                    }
                    return;
                }
            }
            reset_snippet_search_selection(app);
        }
        KeyCode::Down => {
            let count = app.filtered_snippet_indices().len();
            if let Some(sel) = app.ui.snippet_picker_state.selected() {
                if sel + 1 < count {
                    app.ui.snippet_picker_state.select(Some(sel + 1));
                }
            }
        }
        KeyCode::Up => {
            if let Some(sel) = app.ui.snippet_picker_state.selected() {
                if sel > 0 {
                    app.ui.snippet_picker_state.select(Some(sel - 1));
                }
            }
        }
        _ => {}
    }
}

fn handle_snippet_param_form(app: &mut App, key: KeyEvent, events_tx: &mpsc::Sender<AppEvent>) {
    let (snippet, target_aliases) = match &app.screen {
        Screen::SnippetParamForm {
            snippet,
            target_aliases,
        } => (snippet.clone(), target_aliases.clone()),
        _ => return,
    };

    let form = match app.snippet_param_form.as_mut() {
        Some(f) => f,
        None => return,
    };

    // Handle discard confirmation dialog
    if app.pending_discard_confirm {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                app.pending_discard_confirm = false;
                app.snippet_param_form = None;
                app.pending_snippet_terminal = false;
                app.screen = Screen::SnippetPicker { target_aliases };
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.pending_discard_confirm = false;
            }
            _ => {}
        }
        return;
    }

    match key.code {
        KeyCode::Esc => {
            if form.is_dirty() {
                app.pending_discard_confirm = true;
            } else {
                app.snippet_param_form = None;
                app.pending_snippet_terminal = false;
                app.screen = Screen::SnippetPicker { target_aliases };
            }
        }
        KeyCode::Tab | KeyCode::Down => {
            if form.focused_index + 1 < form.params.len() {
                form.focused_index += 1;
                form.cursor_pos = form.values[form.focused_index].chars().count();
                let vis = form.visible_count.max(1);
                if form.focused_index >= form.scroll_offset + vis {
                    form.scroll_offset = form.focused_index.saturating_sub(vis - 1);
                }
            }
        }
        KeyCode::BackTab | KeyCode::Up => {
            if form.focused_index > 0 {
                form.focused_index -= 1;
                form.cursor_pos = form.values[form.focused_index].chars().count();
                if form.focused_index < form.scroll_offset {
                    form.scroll_offset = form.focused_index;
                }
            }
        }
        KeyCode::Left => {
            if form.cursor_pos > 0 {
                form.cursor_pos -= 1;
            }
        }
        KeyCode::Right => {
            let len = form.values[form.focused_index].chars().count();
            if form.cursor_pos < len {
                form.cursor_pos += 1;
            }
        }
        KeyCode::Enter => {
            let values_map = form.values_map();
            let mut resolved = snippet.clone();
            resolved.command = crate::snippet::substitute_params(&snippet.command, &values_map);

            let terminal_mode = app.pending_snippet_terminal;
            app.snippet_param_form = None;
            app.pending_snippet_terminal = false;

            if terminal_mode {
                app.pending_snippet = Some((resolved, target_aliases));
                app.multi_select.clear();
                app.screen = Screen::HostList;
            } else {
                app.multi_select.clear();
                start_snippet_output(app, &resolved, &target_aliases, events_tx);
            }
        }
        KeyCode::Char(c) => {
            if c.is_control() {
                return;
            }
            form.insert_char(c);
        }
        KeyCode::Backspace => {
            form.delete_char_before_cursor();
        }
        _ => {}
    }
}

fn handle_snippet_form(app: &mut App, key: KeyEvent) {
    let (target_aliases, editing) = match &app.screen {
        Screen::SnippetForm {
            target_aliases,
            editing,
        } => (target_aliases.clone(), *editing),
        _ => return,
    };

    // Handle discard confirmation dialog
    if app.pending_discard_confirm {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                app.pending_discard_confirm = false;
                app.snippet_form_baseline = None;
                app.screen = Screen::SnippetPicker {
                    target_aliases: target_aliases.clone(),
                };
            }
            KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
                app.pending_discard_confirm = false;
            }
            _ => {}
        }
        return;
    }

    match key.code {
        KeyCode::Esc => {
            if app.snippet_form_is_dirty() {
                app.pending_discard_confirm = true;
            } else {
                app.snippet_form_baseline = None;
                app.screen = Screen::SnippetPicker {
                    target_aliases: target_aliases.clone(),
                };
            }
        }
        KeyCode::Tab | KeyCode::Down => {
            app.snippet_form.focused_field = app.snippet_form.focused_field.next();
            app.snippet_form.sync_cursor_to_end();
        }
        KeyCode::BackTab | KeyCode::Up => {
            app.snippet_form.focused_field = app.snippet_form.focused_field.prev();
            app.snippet_form.sync_cursor_to_end();
        }
        KeyCode::Left => {
            if app.snippet_form.cursor_pos > 0 {
                app.snippet_form.cursor_pos -= 1;
            }
        }
        KeyCode::Right => {
            let len = app.snippet_form.focused_value().chars().count();
            if app.snippet_form.cursor_pos < len {
                app.snippet_form.cursor_pos += 1;
            }
        }
        KeyCode::Home => {
            app.snippet_form.cursor_pos = 0;
        }
        KeyCode::End => {
            app.snippet_form.sync_cursor_to_end();
        }
        KeyCode::Enter => {
            submit_snippet_form(app, &target_aliases, editing);
        }
        KeyCode::Char(c) => {
            app.snippet_form.insert_char(c);
        }
        KeyCode::Backspace => {
            app.snippet_form.delete_char_before_cursor();
        }
        _ => {}
    }
}

fn submit_snippet_form(app: &mut App, target_aliases: &[String], editing: Option<usize>) {
    if let Err(msg) = app.snippet_form.validate() {
        app.set_status(msg, true);
        return;
    }

    let new_name = app.snippet_form.name.trim().to_string();
    let new_command = app.snippet_form.command.trim().to_string();
    let new_description = app.snippet_form.description.trim().to_string();

    // Check for duplicate name (skip the snippet being edited)
    let old_name =
        editing.and_then(|idx| app.snippet_store.snippets.get(idx).map(|s| s.name.clone()));
    let name_taken = app
        .snippet_store
        .snippets
        .iter()
        .any(|s| s.name == new_name && Some(&s.name) != old_name.as_ref());
    if name_taken {
        app.set_status(format!("'{}' already exists.", new_name), true);
        return;
    }

    let snippet = crate::snippet::Snippet {
        name: new_name,
        command: new_command,
        description: new_description,
    };

    // Save a snapshot for rollback
    let snapshot = app.snippet_store.snippets.clone();

    // If editing and name changed, remove the old one
    if let Some(old_name) = &old_name {
        if *old_name != snippet.name {
            app.snippet_store.remove(old_name);
        }
    }

    let is_new = editing.is_none();
    app.snippet_store.set(snippet);

    if let Err(e) = app.snippet_store.save() {
        app.snippet_store.snippets = snapshot;
        app.set_status(format!("Failed to save: {}", e), true);
        return;
    }

    // Re-select in picker
    let name = app.snippet_form.name.trim().to_string();
    let new_idx = app
        .snippet_store
        .snippets
        .iter()
        .position(|s| s.name == name);
    app.ui.snippet_picker_state.select(new_idx);

    app.snippet_form_baseline = None;
    if is_new {
        app.set_status(format!("Added snippet '{}'.", name), false);
    } else {
        app.set_status(format!("Updated snippet '{}'.", name), false);
    }
    app.screen = Screen::SnippetPicker {
        target_aliases: target_aliases.to_vec(),
    };
}

/// Spawn a background thread to fetch hosts from a cloud provider.
pub fn spawn_provider_sync(
    section: &crate::providers::config::ProviderSection,
    tx: mpsc::Sender<AppEvent>,
    cancel: Arc<AtomicBool>,
) {
    let name = section.provider.clone();
    let token = section.token.clone();
    let section_clone = section.clone();
    let tx_fallback = tx.clone();
    let name_fallback = name.clone();
    if std::thread::Builder::new()
        .name(format!("sync-{}", name))
        .spawn(move || {
            let provider = match crate::providers::get_provider_with_config(&name, &section_clone) {
                Some(p) => p,
                None => {
                    let _ = tx.send(AppEvent::SyncError {
                        provider: name,
                        message: "Unknown provider.".to_string(),
                    });
                    return;
                }
            };
            let progress_tx = tx.clone();
            let progress_name = name.clone();
            let progress = move |msg: &str| {
                let _ = progress_tx.send(AppEvent::SyncProgress {
                    provider: progress_name.clone(),
                    message: msg.to_string(),
                });
            };
            match provider.fetch_hosts_with_progress(&token, &cancel, &progress) {
                Ok(hosts) => {
                    let _ = tx.send(AppEvent::SyncComplete {
                        provider: name,
                        hosts,
                    });
                }
                Err(crate::providers::ProviderError::PartialResult {
                    hosts,
                    failures,
                    total,
                }) => {
                    let _ = tx.send(AppEvent::SyncPartial {
                        provider: name,
                        hosts,
                        failures,
                        total,
                    });
                }
                Err(e) => {
                    let _ = tx.send(AppEvent::SyncError {
                        provider: name,
                        message: e.to_string(),
                    });
                }
            }
        })
        .is_err()
    {
        let _ = tx_fallback.send(AppEvent::SyncError {
            provider: name_fallback,
            message: "Failed to start sync thread.".to_string(),
        });
    }
}

fn fb_send(
    tx: mpsc::Sender<AppEvent>,
) -> impl FnOnce(String, String, Result<Vec<crate::file_browser::FileEntry>, String>) + Send + 'static
{
    move |alias, path, entries| {
        let _ = tx.send(AppEvent::FileBrowserListing {
            alias,
            path,
            entries,
        });
    }
}

fn handle_containers(
    app: &mut App,
    key: KeyEvent,
    events_tx: &mpsc::Sender<AppEvent>,
) -> Result<()> {
    // Block all keys except y/Y and Esc/q when a confirmation is pending
    if let Some(ref state) = app.container_state {
        if state.confirm_action.is_some() {
            match key.code {
                KeyCode::Char('y')
                | KeyCode::Char('Y')
                | KeyCode::Esc
                | KeyCode::Char('q')
                | KeyCode::Char('?') => {}
                _ => return Ok(()),
            }
        }
    }

    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => {
            if let Some(ref mut state) = app.container_state {
                if state.confirm_action.is_some() {
                    // Cancel pending confirmation, stay in overlay
                    state.confirm_action = None;
                    return Ok(());
                }
            }
            app.container_state = None;
            app.screen = Screen::HostList;
        }
        KeyCode::Up | KeyCode::Char('k') => {
            if let Some(ref mut state) = app.container_state {
                let len = state.containers.len();
                if len > 0 {
                    let i = state.list_state.selected().unwrap_or(0);
                    state
                        .list_state
                        .select(Some(if i == 0 { len - 1 } else { i - 1 }));
                }
            }
        }
        KeyCode::Down | KeyCode::Char('j') => {
            if let Some(ref mut state) = app.container_state {
                let len = state.containers.len();
                if len > 0 {
                    let i = state.list_state.selected().unwrap_or(0);
                    state
                        .list_state
                        .select(Some(if i + 1 >= len { 0 } else { i + 1 }));
                }
            }
        }
        KeyCode::PageDown => {
            if let Some(ref mut state) = app.container_state {
                let len = state.containers.len();
                if len > 0 {
                    let i = state.list_state.selected().unwrap_or(0);
                    state.list_state.select(Some((i + 10).min(len - 1)));
                }
            }
        }
        KeyCode::PageUp => {
            if let Some(ref mut state) = app.container_state {
                let len = state.containers.len();
                if len > 0 {
                    let i = state.list_state.selected().unwrap_or(0);
                    state.list_state.select(Some(i.saturating_sub(10)));
                }
            }
        }
        KeyCode::Char('s') => {
            container_action(app, events_tx, crate::containers::ContainerAction::Start);
        }
        KeyCode::Char('x') => {
            // Stop requires confirmation
            if let Some(ref mut state) = app.container_state {
                if state.action_in_progress.is_some() || state.confirm_action.is_some() {
                    return Ok(());
                }
                if let Some(idx) = state.list_state.selected() {
                    if let Some(container) = state.containers.get(idx) {
                        state.confirm_action = Some((
                            crate::containers::ContainerAction::Stop,
                            container.names.clone(),
                            container.id.clone(),
                        ));
                    }
                }
            }
        }
        KeyCode::Char('r') => {
            // Restart requires confirmation
            if let Some(ref mut state) = app.container_state {
                if state.action_in_progress.is_some() || state.confirm_action.is_some() {
                    return Ok(());
                }
                if let Some(idx) = state.list_state.selected() {
                    if let Some(container) = state.containers.get(idx) {
                        state.confirm_action = Some((
                            crate::containers::ContainerAction::Restart,
                            container.names.clone(),
                            container.id.clone(),
                        ));
                    }
                }
            }
        }
        KeyCode::Char('y') | KeyCode::Char('Y') => {
            // Confirm pending action
            if let Some(ref mut state) = app.container_state {
                if let Some((action, _name, _id)) = state.confirm_action.take() {
                    container_action(app, events_tx, action);
                }
            }
        }
        KeyCode::Char('R') => {
            // Refresh container list
            if let Some(ref mut state) = app.container_state {
                if state.action_in_progress.is_some() {
                    return Ok(());
                }
                state.loading = true;
                state.error = None;
                let alias = state.alias.clone();
                let askpass = state.askpass.clone();
                let has_tunnel = app.active_tunnels.contains_key(&alias);
                let cached_runtime = state.runtime;
                let config_path = app.reload.config_path.clone();
                let bw = app.bw_session.clone();
                let tx = events_tx.clone();
                crate::containers::spawn_container_listing(
                    alias,
                    config_path,
                    askpass,
                    bw,
                    has_tunnel,
                    cached_runtime,
                    move |a, result| {
                        let _ = tx.send(AppEvent::ContainerListing { alias: a, result });
                    },
                );
            }
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        _ => {}
    }
    Ok(())
}

fn container_action(
    app: &mut App,
    events_tx: &mpsc::Sender<AppEvent>,
    action: crate::containers::ContainerAction,
) {
    let Some(ref mut state) = app.container_state else {
        return;
    };
    if state.action_in_progress.is_some() {
        return;
    }
    let Some(idx) = state.list_state.selected() else {
        return;
    };
    let Some(container) = state.containers.get(idx) else {
        return;
    };
    if crate::containers::validate_container_id(&container.id).is_err() {
        return;
    }
    let Some(runtime) = state.runtime else {
        return;
    };
    let container_id = container.id.clone();
    let container_name = container.names.clone();
    state.action_in_progress = Some(format!("{} {}...", action.as_str(), container_name));
    let alias = state.alias.clone();
    let askpass = state.askpass.clone();
    let has_tunnel = app.active_tunnels.contains_key(&alias);
    let config_path = app.reload.config_path.clone();
    let bw = app.bw_session.clone();
    let tx = events_tx.clone();
    crate::containers::spawn_container_action(
        alias,
        config_path,
        runtime,
        action,
        container_id,
        askpass,
        bw,
        has_tunnel,
        move |a, act, result| {
            let _ = tx.send(AppEvent::ContainerActionComplete {
                alias: a,
                action: act,
                result,
            });
        },
    );
}

fn handle_file_browser(app: &mut App, key: KeyEvent, events_tx: &mpsc::Sender<AppEvent>) {
    use crate::file_browser::{BrowserPane, CopyRequest};

    let fb = match app.file_browser.as_mut() {
        Some(fb) => fb,
        None => return,
    };

    // Block input while transfer is running
    if fb.transferring.is_some() {
        return;
    }

    // Dismiss transfer error dialog
    if fb.transfer_error.is_some() && key.code != KeyCode::Char('?') {
        match key.code {
            KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
                fb.transfer_error = None;
            }
            _ => {}
        }
        return;
    }

    // If confirm dialog is showing, handle that first
    if fb.confirm_copy.is_some() && key.code != KeyCode::Char('?') {
        match key.code {
            KeyCode::Char('y') | KeyCode::Char('Y') => {
                let req = fb.confirm_copy.take().unwrap();
                let alias = fb.alias.clone();
                let askpass = fb.askpass.clone();
                let has_active_tunnel = app.active_tunnels.contains_key(&alias);
                let local_path = fb.local_path.clone();
                let remote_path = if fb.remote_path.ends_with('/') {
                    fb.remote_path.clone()
                } else {
                    format!("{}/", fb.remote_path)
                };
                let scp_args = crate::file_browser::build_scp_args(
                    &alias,
                    req.source_pane,
                    &local_path,
                    &remote_path,
                    &req.sources,
                    req.has_dirs,
                );

                // Show transfer status in the file browser
                let label = if req.sources.len() == 1 {
                    format!("Copying {}...", req.sources[0])
                } else {
                    format!("Copying {} files...", req.sources.len())
                };
                fb.transferring = Some(label);

                // Run scp in background thread
                let config_path = app.reload.config_path.clone();
                let bw = app.bw_session.clone();
                let tx = events_tx.clone();
                std::thread::spawn(move || {
                    let result = crate::file_browser::run_scp(
                        &alias,
                        &config_path,
                        askpass.as_deref(),
                        bw.as_deref(),
                        has_active_tunnel,
                        &scp_args,
                    );
                    let (success, message) = match result {
                        Ok(r) if r.status.success() => (true, String::new()),
                        Ok(r) => {
                            let code = r.status.code().unwrap_or(1);
                            let err = crate::file_browser::filter_ssh_warnings(&r.stderr_output);
                            if err.is_empty() {
                                (false, format!("Copy failed (exit code {}).", code))
                            } else {
                                (false, err)
                            }
                        }
                        Err(e) => (false, format!("scp failed: {}", e)),
                    };
                    let _ = tx.send(crate::event::AppEvent::ScpComplete {
                        alias,
                        success,
                        message,
                    });
                });
            }
            KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => {
                fb.confirm_copy = None;
            }
            _ => {}
        }
        return;
    }

    match key.code {
        KeyCode::Esc | KeyCode::Char('q') => {
            // Save paths for next time
            if let Some(ref fb) = app.file_browser {
                let alias = fb.alias.clone();
                let local = fb.local_path.clone();
                let remote = fb.remote_path.clone();
                app.file_browser_paths.insert(alias, (local, remote));
            }
            app.file_browser = None;
            app.screen = Screen::HostList;
        }
        KeyCode::Tab => {
            fb.active_pane = match fb.active_pane {
                BrowserPane::Local => BrowserPane::Remote,
                BrowserPane::Remote => BrowserPane::Local,
            };
        }
        KeyCode::Char('j') | KeyCode::Down => {
            match fb.active_pane {
                BrowserPane::Local => {
                    let len = fb.local_entries.len() + 1; // +1 for ..
                    crate::app::cycle_selection(&mut fb.local_list_state, len, true);
                }
                BrowserPane::Remote => {
                    if !fb.remote_loading && fb.remote_error.is_none() {
                        let len = fb.remote_entries.len() + 1;
                        crate::app::cycle_selection(&mut fb.remote_list_state, len, true);
                    }
                }
            }
        }
        KeyCode::Char('k') | KeyCode::Up => match fb.active_pane {
            BrowserPane::Local => {
                let len = fb.local_entries.len() + 1;
                crate::app::cycle_selection(&mut fb.local_list_state, len, false);
            }
            BrowserPane::Remote => {
                if !fb.remote_loading && fb.remote_error.is_none() {
                    let len = fb.remote_entries.len() + 1;
                    crate::app::cycle_selection(&mut fb.remote_list_state, len, false);
                }
            }
        },
        KeyCode::PageDown => match fb.active_pane {
            BrowserPane::Local => {
                let len = fb.local_entries.len() + 1;
                crate::app::page_down(&mut fb.local_list_state, len, 10);
            }
            BrowserPane::Remote => {
                let len = fb.remote_entries.len() + 1;
                crate::app::page_down(&mut fb.remote_list_state, len, 10);
            }
        },
        KeyCode::PageUp => match fb.active_pane {
            BrowserPane::Local => {
                let len = fb.local_entries.len() + 1;
                crate::app::page_up(&mut fb.local_list_state, len, 10);
            }
            BrowserPane::Remote => {
                let len = fb.remote_entries.len() + 1;
                crate::app::page_up(&mut fb.remote_list_state, len, 10);
            }
        },
        KeyCode::Enter => {
            match fb.active_pane {
                BrowserPane::Local => {
                    let idx = fb.local_list_state.selected().unwrap_or(0);
                    if idx == 0 {
                        // ".." - go up
                        if let Some(parent) = fb.local_path.parent() {
                            fb.local_path = parent.to_path_buf();
                            match crate::file_browser::list_local(
                                &fb.local_path,
                                fb.show_hidden,
                                fb.sort,
                            ) {
                                Ok(entries) => {
                                    fb.local_entries = entries;
                                    fb.local_error = None;
                                }
                                Err(e) => {
                                    fb.local_entries = Vec::new();
                                    fb.local_error = Some(e.to_string());
                                }
                            }
                            fb.local_list_state.select(Some(0));
                            fb.local_selected.clear();
                        }
                    } else if let Some(entry) = fb.local_entries.get(idx - 1).cloned() {
                        if !fb.local_selected.is_empty() {
                            // Multi-select active: copy all selected items
                            if fb.remote_path.is_empty() {
                                return;
                            }
                            let sources: Vec<String> = fb.local_selected.iter().cloned().collect();
                            let has_dirs = sources
                                .iter()
                                .any(|n| fb.local_entries.iter().any(|e| e.name == *n && e.is_dir));
                            fb.confirm_copy = Some(CopyRequest {
                                sources,
                                source_pane: BrowserPane::Local,
                                has_dirs,
                            });
                        } else if entry.is_dir {
                            // No selection: navigate into directory
                            fb.local_path = fb.local_path.join(&entry.name);
                            match crate::file_browser::list_local(
                                &fb.local_path,
                                fb.show_hidden,
                                fb.sort,
                            ) {
                                Ok(entries) => {
                                    fb.local_entries = entries;
                                    fb.local_error = None;
                                }
                                Err(e) => {
                                    fb.local_entries = Vec::new();
                                    fb.local_error = Some(e.to_string());
                                }
                            }
                            fb.local_list_state.select(Some(0));
                            fb.local_selected.clear();
                        } else {
                            // No selection, cursor on file: copy single file
                            if fb.remote_path.is_empty() {
                                return;
                            }
                            fb.confirm_copy = Some(CopyRequest {
                                sources: vec![entry.name.clone()],
                                source_pane: BrowserPane::Local,
                                has_dirs: false,
                            });
                        }
                    }
                }
                BrowserPane::Remote => {
                    if fb.remote_loading || fb.remote_error.is_some() {
                        return;
                    }
                    let idx = fb.remote_list_state.selected().unwrap_or(0);
                    if idx == 0 {
                        // ".." - go up
                        let path = fb.remote_path.clone();
                        let parent = if path == "/" {
                            "/".to_string()
                        } else {
                            let trimmed = path.trim_end_matches('/');
                            match trimmed.rfind('/') {
                                Some(0) => "/".to_string(),
                                Some(pos) => trimmed[..pos].to_string(),
                                None => "/".to_string(),
                            }
                        };
                        if parent != fb.remote_path {
                            fb.remote_path = parent.clone();
                            fb.remote_loading = true;
                            fb.remote_entries.clear();
                            fb.remote_selected.clear();
                            fb.remote_error = None;
                            fb.remote_list_state = ratatui::widgets::ListState::default();
                            let alias = fb.alias.clone();
                            let askpass = fb.askpass.clone();
                            let config_path = app.reload.config_path.clone();
                            let has_tunnel = app.active_tunnels.contains_key(&alias);
                            let bw = app.bw_session.clone();
                            let show_hidden = fb.show_hidden;
                            let sort = fb.sort;
                            crate::file_browser::spawn_remote_listing(
                                alias,
                                config_path,
                                parent,
                                show_hidden,
                                sort,
                                askpass,
                                bw,
                                has_tunnel,
                                fb_send(events_tx.clone()),
                            );
                        }
                    } else if let Some(entry) = fb.remote_entries.get(idx - 1).cloned() {
                        if !fb.remote_selected.is_empty() {
                            // Multi-select active: copy all selected items
                            let sources: Vec<String> = fb.remote_selected.iter().cloned().collect();
                            let has_dirs = sources.iter().any(|n| {
                                fb.remote_entries.iter().any(|e| e.name == *n && e.is_dir)
                            });
                            fb.confirm_copy = Some(CopyRequest {
                                sources,
                                source_pane: BrowserPane::Remote,
                                has_dirs,
                            });
                        } else if entry.is_dir {
                            // No selection: navigate into directory
                            let new_path = if fb.remote_path.ends_with('/') {
                                format!("{}{}", fb.remote_path, entry.name)
                            } else {
                                format!("{}/{}", fb.remote_path, entry.name)
                            };
                            fb.remote_path = new_path.clone();
                            fb.remote_loading = true;
                            fb.remote_entries.clear();
                            fb.remote_selected.clear();
                            fb.remote_error = None;
                            fb.remote_list_state = ratatui::widgets::ListState::default();
                            let alias = fb.alias.clone();
                            let askpass = fb.askpass.clone();
                            let config_path = app.reload.config_path.clone();
                            let has_tunnel = app.active_tunnels.contains_key(&alias);
                            let bw = app.bw_session.clone();
                            let show_hidden = fb.show_hidden;
                            let sort = fb.sort;
                            crate::file_browser::spawn_remote_listing(
                                alias,
                                config_path,
                                new_path,
                                show_hidden,
                                sort,
                                askpass,
                                bw,
                                has_tunnel,
                                fb_send(events_tx.clone()),
                            );
                        } else {
                            // No selection, cursor on file: copy single file
                            fb.confirm_copy = Some(CopyRequest {
                                sources: vec![entry.name.clone()],
                                source_pane: BrowserPane::Remote,
                                has_dirs: false,
                            });
                        }
                    }
                }
            }
        }
        KeyCode::Backspace => {
            // Go up in the active pane
            match fb.active_pane {
                BrowserPane::Local => {
                    if let Some(parent) = fb.local_path.parent() {
                        fb.local_path = parent.to_path_buf();
                        match crate::file_browser::list_local(
                            &fb.local_path,
                            fb.show_hidden,
                            fb.sort,
                        ) {
                            Ok(entries) => {
                                fb.local_entries = entries;
                                fb.local_error = None;
                            }
                            Err(e) => {
                                fb.local_entries = Vec::new();
                                fb.local_error = Some(e.to_string());
                            }
                        }
                        fb.local_list_state.select(Some(0));
                        fb.local_selected.clear();
                    }
                }
                BrowserPane::Remote => {
                    let path = fb.remote_path.clone();
                    let parent = if path == "/" {
                        "/".to_string()
                    } else {
                        let trimmed = path.trim_end_matches('/');
                        match trimmed.rfind('/') {
                            Some(0) => "/".to_string(),
                            Some(pos) => trimmed[..pos].to_string(),
                            None => "/".to_string(),
                        }
                    };
                    if parent != fb.remote_path {
                        fb.remote_path = parent.clone();
                        fb.remote_loading = true;
                        fb.remote_entries.clear();
                        fb.remote_selected.clear();
                        fb.remote_error = None;
                        fb.remote_list_state = ratatui::widgets::ListState::default();
                        let alias = fb.alias.clone();
                        let askpass = fb.askpass.clone();
                        let config_path = app.reload.config_path.clone();
                        let has_tunnel = app.active_tunnels.contains_key(&alias);
                        let bw = app.bw_session.clone();
                        let show_hidden = fb.show_hidden;
                        let sort = fb.sort;
                        crate::file_browser::spawn_remote_listing(
                            alias,
                            config_path,
                            parent,
                            show_hidden,
                            sort,
                            askpass,
                            bw,
                            has_tunnel,
                            fb_send(events_tx.clone()),
                        );
                    }
                }
            }
        }
        KeyCode::Char(' ') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            // Toggle multi-select
            match fb.active_pane {
                BrowserPane::Local => {
                    let idx = fb.local_list_state.selected().unwrap_or(0);
                    if idx > 0 {
                        if let Some(entry) = fb.local_entries.get(idx - 1) {
                            let name = entry.name.clone();
                            if fb.local_selected.contains(&name) {
                                fb.local_selected.remove(&name);
                            } else {
                                fb.local_selected.insert(name);
                            }
                        }
                    }
                }
                BrowserPane::Remote => {
                    let idx = fb.remote_list_state.selected().unwrap_or(0);
                    if idx > 0 {
                        if let Some(entry) = fb.remote_entries.get(idx - 1) {
                            let name = entry.name.clone();
                            if fb.remote_selected.contains(&name) {
                                fb.remote_selected.remove(&name);
                            } else {
                                fb.remote_selected.insert(name);
                            }
                        }
                    }
                }
            }
        }
        KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
            // Select all / deselect all (toggle)
            match fb.active_pane {
                BrowserPane::Local => {
                    if fb.local_selected.len() == fb.local_entries.len()
                        && !fb.local_entries.is_empty()
                    {
                        fb.local_selected.clear();
                    } else {
                        fb.local_selected =
                            fb.local_entries.iter().map(|e| e.name.clone()).collect();
                    }
                }
                BrowserPane::Remote => {
                    if fb.remote_selected.len() == fb.remote_entries.len()
                        && !fb.remote_entries.is_empty()
                    {
                        fb.remote_selected.clear();
                    } else {
                        fb.remote_selected =
                            fb.remote_entries.iter().map(|e| e.name.clone()).collect();
                    }
                }
            }
        }
        KeyCode::Char('.') => {
            fb.show_hidden = !fb.show_hidden;
            // Refresh local
            match crate::file_browser::list_local(&fb.local_path, fb.show_hidden, fb.sort) {
                Ok(entries) => {
                    fb.local_entries = entries;
                    fb.local_error = None;
                }
                Err(e) => {
                    fb.local_entries = Vec::new();
                    fb.local_error = Some(e.to_string());
                }
            }
            fb.local_list_state.select(Some(0));
            fb.local_selected.clear();
            // Refresh remote
            if !fb.remote_path.is_empty() {
                fb.remote_loading = true;
                fb.remote_entries.clear();
                fb.remote_selected.clear();
                fb.remote_error = None;
                fb.remote_list_state = ratatui::widgets::ListState::default();
                let alias = fb.alias.clone();
                let askpass = fb.askpass.clone();
                let config_path = app.reload.config_path.clone();
                let has_tunnel = app.active_tunnels.contains_key(&alias);
                let bw = app.bw_session.clone();
                let path = fb.remote_path.clone();
                let show_hidden = fb.show_hidden;
                let sort = fb.sort;
                crate::file_browser::spawn_remote_listing(
                    alias,
                    config_path,
                    path,
                    show_hidden,
                    sort,
                    askpass,
                    bw,
                    has_tunnel,
                    fb_send(events_tx.clone()),
                );
            }
        }
        KeyCode::Char('R') => {
            // Refresh both panes
            match crate::file_browser::list_local(&fb.local_path, fb.show_hidden, fb.sort) {
                Ok(entries) => {
                    fb.local_entries = entries;
                    fb.local_error = None;
                }
                Err(e) => {
                    fb.local_entries = Vec::new();
                    fb.local_error = Some(e.to_string());
                }
            }
            fb.local_list_state.select(Some(0));
            fb.local_selected.clear();
            if !fb.remote_path.is_empty() {
                fb.remote_loading = true;
                fb.remote_entries.clear();
                fb.remote_selected.clear();
                fb.remote_error = None;
                fb.remote_list_state = ratatui::widgets::ListState::default();
                let alias = fb.alias.clone();
                let askpass = fb.askpass.clone();
                let config_path = app.reload.config_path.clone();
                let has_tunnel = app.active_tunnels.contains_key(&alias);
                let bw = app.bw_session.clone();
                let path = fb.remote_path.clone();
                let show_hidden = fb.show_hidden;
                let sort = fb.sort;
                crate::file_browser::spawn_remote_listing(
                    alias,
                    config_path,
                    path,
                    show_hidden,
                    sort,
                    askpass,
                    bw,
                    has_tunnel,
                    fb_send(events_tx.clone()),
                );
            }
        }
        KeyCode::Char('s') => {
            // Toggle sort mode
            fb.sort = match fb.sort {
                crate::file_browser::BrowserSort::Name => crate::file_browser::BrowserSort::Date,
                crate::file_browser::BrowserSort::Date => crate::file_browser::BrowserSort::DateAsc,
                crate::file_browser::BrowserSort::DateAsc => crate::file_browser::BrowserSort::Name,
            };
            // Re-sort entries in place
            crate::file_browser::sort_entries(&mut fb.local_entries, fb.sort);
            crate::file_browser::sort_entries(&mut fb.remote_entries, fb.sort);
            fb.local_list_state.select(Some(0));
            fb.remote_list_state.select(Some(0));
        }
        KeyCode::Char('?') => {
            let old = std::mem::replace(&mut app.screen, Screen::HostList);
            app.screen = Screen::Help {
                return_screen: Box::new(old),
            };
        }
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::app::{App, ProviderFormField, ProviderFormFields, Screen};
    use crate::providers::config::{ProviderConfig, ProviderSection};
    use crate::ssh_config::model::SshConfigFile;
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
    use std::path::PathBuf;
    use std::sync::mpsc;

    fn test_provider_config() -> ProviderConfig {
        ProviderConfig {
            path_override: Some(PathBuf::from("/tmp/purple_test_providers")),
            ..Default::default()
        }
    }

    fn make_app(content: &str) -> App {
        let config = SshConfigFile {
            elements: SshConfigFile::parse_content(content),
            path: PathBuf::from("/tmp/test_config"),
            crlf: false,
            bom: false,
        };
        let mut app = App::new(config);
        // Never write to the real ~/.purple during tests
        app.provider_config = test_provider_config();
        crate::preferences::set_path_override(PathBuf::from("/tmp/purple_test_preferences"));
        app
    }

    fn key(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::NONE)
    }

    /// App met een geconfigureerde DigitalOcean (auto_sync=true) en een nieuw Proxmox.
    fn make_providers_app_with_do() -> App {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        app.provider_config.set_section(ProviderSection {
            provider: "digitalocean".to_string(),
            token: "tok".to_string(),
            alias_prefix: "do".to_string(),
            user: "root".to_string(),
            identity_file: String::new(),
            url: String::new(),
            verify_tls: true,
            auto_sync: true,
            profile: String::new(),
            regions: String::new(),
            project: String::new(),
            compartment: String::new(),
        });
        app
    }

    fn make_providers_app_with_proxmox() -> App {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        app.provider_config.set_section(ProviderSection {
            provider: "proxmox".to_string(),
            token: "user@pam!t=secret".to_string(),
            alias_prefix: "pve".to_string(),
            user: "root".to_string(),
            identity_file: String::new(),
            url: "https://pve.local:8006".to_string(),
            verify_tls: true,
            auto_sync: false,
            profile: String::new(),
            regions: String::new(),
            project: String::new(),
            compartment: String::new(),
        });
        app
    }

    /// Positioneer de cursor op een bepaalde provider in de lijst en stuur Enter.
    fn open_provider_form(app: &mut App, provider_name: &str) {
        let sorted = app.sorted_provider_names();
        let idx = sorted.iter().position(|n| n == provider_name).unwrap();
        app.ui.provider_list_state.select(Some(idx));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(app, key(KeyCode::Enter), &tx);
    }

    // --- Form initialisatie ---

    #[test]
    fn test_provider_form_init_existing_do_preserves_auto_sync_true() {
        let mut app = make_providers_app_with_do();
        open_provider_form(&mut app, "digitalocean");
        assert!(
            app.provider_form.auto_sync,
            "Bestaande DO provider (auto_sync=true) moet true blijven in het form"
        );
    }

    #[test]
    fn test_provider_form_init_existing_proxmox_preserves_auto_sync_false() {
        let mut app = make_providers_app_with_proxmox();
        open_provider_form(&mut app, "proxmox");
        assert!(
            !app.provider_form.auto_sync,
            "Bestaande Proxmox provider (auto_sync=false) moet false blijven in het form"
        );
    }

    #[test]
    fn test_provider_form_init_existing_do_explicit_false_preserved() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        // DO met auto_sync=false (gebruiker heeft het handmatig uitgezet)
        app.provider_config.set_section(ProviderSection {
            provider: "digitalocean".to_string(),
            token: "tok".to_string(),
            alias_prefix: "do".to_string(),
            user: "root".to_string(),
            identity_file: String::new(),
            url: String::new(),
            verify_tls: true,
            auto_sync: false,
            profile: String::new(),
            regions: String::new(),
            project: String::new(),
            compartment: String::new(),
        });
        open_provider_form(&mut app, "digitalocean");
        assert!(
            !app.provider_form.auto_sync,
            "DO met auto_sync=false moet false blijven"
        );
    }

    #[test]
    fn test_provider_form_init_new_proxmox_defaults_to_false() {
        // Proxmox zonder bestaande config: default auto_sync=false
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config(); // geen config voor proxmox
        open_provider_form(&mut app, "proxmox");
        assert!(
            !app.provider_form.auto_sync,
            "Nieuw Proxmox form moet auto_sync=false als default tonen"
        );
    }

    #[test]
    fn test_provider_form_init_new_digitalocean_defaults_to_true() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        open_provider_form(&mut app, "digitalocean");
        assert!(
            app.provider_form.auto_sync,
            "Nieuw DigitalOcean form moet auto_sync=true als default tonen"
        );
    }

    // --- Space toggle ---

    fn make_form_app_focused_on(provider: &str, field: ProviderFormField) -> App {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ProviderForm {
            provider: provider.to_string(),
        };
        app.provider_form = ProviderFormFields {
            url: String::new(),
            token: "tok".to_string(),
            profile: String::new(),
            project: String::new(),
            compartment: String::new(),
            regions: String::new(),
            alias_prefix: "do".to_string(),
            user: "root".to_string(),
            identity_file: String::new(),
            verify_tls: true,
            auto_sync: true,
            focused_field: field,
            cursor_pos: 0,
        };
        app
    }

    /// Submit provider form with fresh mtime capture to minimize race window.
    fn submit_form(app: &mut App) {
        app.capture_provider_form_mtime();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(app, key(KeyCode::Enter), &tx);
    }

    /// Assert that the status message contains the expected validation error.
    /// Tolerates the conflict-detection race: if another parallel test wrote
    /// to ~/.purple/providers between mtime capture and submit, the conflict
    /// check fires before validation and the test is inconclusive (not a bug).
    fn assert_status_contains(app: &App, expected: &str) {
        let msg = &app.status.as_ref().expect("status should be set").text;
        if msg.contains("changed externally") {
            return; // inconclusive due to race
        }
        assert!(
            msg.contains(expected),
            "Expected status to contain '{}', got: '{}'",
            expected,
            msg
        );
    }

    fn assert_status_not_contains(app: &App, not_expected: &str) {
        let msg = app.status.as_ref().map(|s| s.text.as_str()).unwrap_or("");
        if msg.contains("changed externally") {
            return; // inconclusive due to race
        }
        assert!(
            !msg.contains(not_expected),
            "Status should NOT contain '{}', got: '{}'",
            not_expected,
            msg
        );
    }

    #[test]
    fn test_space_toggles_auto_sync_true_to_false() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::AutoSync);
        assert!(app.provider_form.auto_sync);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert!(!app.provider_form.auto_sync);
    }

    #[test]
    fn test_space_toggles_auto_sync_false_to_true() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::AutoSync);
        app.provider_form.auto_sync = false;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert!(app.provider_form.auto_sync);
    }

    #[test]
    fn test_space_on_other_field_does_not_affect_auto_sync() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.auto_sync = true;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        // Space op Token voegt spatie toe aan tekstveld; auto_sync ongewijzigd
        assert!(app.provider_form.auto_sync);
    }

    // --- Char/Backspace blokkering op AutoSync ---

    #[test]
    fn test_char_input_blocked_when_auto_sync_focused() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::AutoSync);
        let original_token = app.provider_form.token.clone();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('x')), &tx);
        // Geen enkel tekstveld mag gewijzigd zijn
        assert_eq!(app.provider_form.token, original_token);
        assert_eq!(app.provider_form.alias_prefix, "do");
    }

    #[test]
    fn test_backspace_blocked_when_auto_sync_focused() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::AutoSync);
        let original_token = app.provider_form.token.clone();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        assert_eq!(app.provider_form.token, original_token);
    }

    // --- Submit persisteert auto_sync ---

    #[test]
    fn test_submit_provider_form_persists_auto_sync_false() {
        // Submit met auto_sync=false moet de sectie opslaan met auto_sync=false.
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ProviderForm {
            provider: "digitalocean".to_string(),
        };
        app.provider_config = test_provider_config();
        app.provider_form = ProviderFormFields {
            url: String::new(),
            token: "tok".to_string(),
            profile: String::new(),
            project: String::new(),
            compartment: String::new(),
            regions: String::new(),
            alias_prefix: "do".to_string(),
            user: "root".to_string(),
            identity_file: String::new(),
            verify_tls: true,
            auto_sync: false,
            focused_field: ProviderFormField::Token,
            cursor_pos: 0,
        };

        let (tx, _rx) = mpsc::channel();
        // Enter triggert submit; save() kan falen zonder ~/.purple dir, maar de
        // in-memory sectie wordt altijd bijgewerkt vóór de save.
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);

        // Ongeacht of save() slaagde: de sectie in provider_config is bijgewerkt.
        if let Some(section) = app.provider_config.section("digitalocean") {
            assert!(
                !section.auto_sync,
                "Opgeslagen sectie moet auto_sync=false hebben"
            );
        }
        // Als het form is gesloten (save geslaagd), controleert de screen-state
        // dat de toggle correct is doorgegeven.
    }

    #[test]
    fn test_submit_provider_form_persists_auto_sync_true() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ProviderForm {
            provider: "digitalocean".to_string(),
        };
        app.provider_config = test_provider_config();
        app.provider_form = ProviderFormFields {
            url: String::new(),
            token: "tok".to_string(),
            profile: String::new(),
            project: String::new(),
            compartment: String::new(),
            regions: String::new(),
            alias_prefix: "do".to_string(),
            user: "root".to_string(),
            identity_file: String::new(),
            verify_tls: true,
            auto_sync: true,
            focused_field: ProviderFormField::Token,
            cursor_pos: 0,
        };

        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);

        if let Some(section) = app.provider_config.section("digitalocean") {
            assert!(
                section.auto_sync,
                "Opgeslagen sectie moet auto_sync=true hebben"
            );
        }
    }

    // =========================================================================
    // Provider form validation tests
    // =========================================================================

    #[test]
    fn test_submit_provider_form_rejects_control_chars_in_token() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.token = "tok\x01en".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "control characters");
    }

    #[test]
    fn test_submit_provider_form_rejects_control_chars_in_alias_prefix() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.alias_prefix = "do\x00".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "control characters");
    }

    #[test]
    fn test_submit_provider_form_rejects_control_chars_in_url() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::Url);
        app.provider_form.url = "https://pve\x0a.local:8006".to_string();
        app.provider_form.token = "user@pam!t=secret".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "control characters");
    }

    #[test]
    fn test_submit_provider_form_rejects_control_chars_in_user() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.user = "ro\tot".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "control characters");
    }

    #[test]
    fn test_submit_provider_form_rejects_control_chars_in_identity_file() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.identity_file = "~/.ssh/id\x1b_rsa".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "control characters");
    }

    #[test]
    fn test_submit_proxmox_rejects_empty_url() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::Url);
        app.provider_form.url = "".to_string();
        app.provider_form.token = "user@pam!t=secret".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "URL is required");
    }

    #[test]
    fn test_submit_proxmox_rejects_http_url() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::Url);
        app.provider_form.url = "http://pve.local:8006".to_string();
        app.provider_form.token = "user@pam!t=secret".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "https://");
    }

    #[test]
    fn test_submit_proxmox_accepts_https_url() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::Url);
        app.provider_form.url = "https://pve.local:8006".to_string();
        app.provider_form.token = "user@pam!t=secret".to_string();
        submit_form(&mut app);
        assert_status_not_contains(&app, "URL is required");
        assert_status_not_contains(&app, "https://");
    }

    #[test]
    fn test_submit_proxmox_rejects_bare_hostname_url() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::Url);
        app.provider_form.url = "pve.local:8006".to_string();
        app.provider_form.token = "user@pam!t=secret".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "https://");
    }

    #[test]
    fn test_submit_provider_form_rejects_empty_token() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.token = "".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "Token");
    }

    #[test]
    fn test_submit_provider_form_rejects_whitespace_only_token() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.token = "   ".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "Token");
    }

    #[test]
    fn test_submit_provider_form_rejects_pattern_alias_prefix() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.alias_prefix = "do*".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "pattern");
    }

    #[test]
    fn test_submit_provider_form_rejects_question_mark_alias() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.alias_prefix = "do?".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "pattern");
    }

    #[test]
    fn test_submit_provider_form_rejects_negation_alias() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.alias_prefix = "!do".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "pattern");
    }

    #[test]
    fn test_submit_provider_form_rejects_whitespace_in_user() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.user = "my user".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "whitespace");
    }

    // =========================================================================
    // GCP-specific form validation
    // =========================================================================

    fn make_gcp_form_app() -> App {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ProviderForm {
            provider: "gcp".to_string(),
        };
        app.provider_form = ProviderFormFields {
            url: String::new(),
            token: "/path/to/sa.json".to_string(),
            profile: String::new(),
            project: "my-project".to_string(),
            compartment: String::new(),
            regions: String::new(),
            alias_prefix: "gcp".to_string(),
            user: "root".to_string(),
            identity_file: String::new(),
            verify_tls: true,
            auto_sync: true,
            focused_field: ProviderFormField::Token,
            cursor_pos: 0,
        };
        app
    }

    #[test]
    fn test_submit_gcp_rejects_empty_project() {
        let mut app = make_gcp_form_app();
        app.provider_form.project = "".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "Project ID");
    }

    #[test]
    fn test_submit_gcp_rejects_whitespace_only_project() {
        let mut app = make_gcp_form_app();
        app.provider_form.project = "   ".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "Project ID");
    }

    #[test]
    fn test_submit_gcp_rejects_empty_token() {
        let mut app = make_gcp_form_app();
        app.provider_form.token = "".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "Token");
    }

    #[test]
    fn test_submit_gcp_empty_token_shows_gcp_specific_hint() {
        let mut app = make_gcp_form_app();
        app.provider_form.token = "".to_string();
        submit_form(&mut app);
        assert_status_contains(&app, "service account");
    }

    #[test]
    fn test_gcp_form_has_project_field() {
        let fields = ProviderFormField::fields_for("gcp");
        assert!(fields.contains(&ProviderFormField::Project));
    }

    #[test]
    fn test_gcp_form_tab_cycles_through_project() {
        let mut app = make_gcp_form_app();
        app.provider_form.focused_field = ProviderFormField::Token;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.provider_form.focused_field, ProviderFormField::Project);
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.provider_form.focused_field, ProviderFormField::Regions);
    }

    #[test]
    fn test_provider_form_init_new_gcp_defaults() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        open_provider_form(&mut app, "gcp");
        assert!(app.provider_form.project.is_empty());
        assert!(app.provider_form.auto_sync);
    }

    // =========================================================================
    // Azure-specific form validation
    // =========================================================================

    fn make_azure_form_app() -> App {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ProviderForm {
            provider: "azure".to_string(),
        };
        app.provider_config = test_provider_config();
        app.provider_form = ProviderFormFields {
            url: String::new(),
            token: "fake-token".to_string(),
            profile: String::new(),
            project: String::new(),
            compartment: String::new(),
            regions: "12345678-1234-1234-1234-123456789012".to_string(),
            alias_prefix: "az".to_string(),
            user: "azureuser".to_string(),
            identity_file: String::new(),
            verify_tls: true,
            auto_sync: true,
            focused_field: ProviderFormField::Token,
            cursor_pos: 0,
        };
        app
    }

    #[test]
    fn test_submit_azure_rejects_empty_subscriptions() {
        let mut app = make_azure_form_app();
        app.provider_form.regions = "".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "subscription");
    }

    #[test]
    fn test_submit_azure_rejects_whitespace_only_subscriptions() {
        let mut app = make_azure_form_app();
        app.provider_form.regions = "   ".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "subscription");
    }

    #[test]
    fn test_azure_form_has_regions_field() {
        let fields = ProviderFormField::fields_for("azure");
        assert!(fields.contains(&ProviderFormField::Regions));
        assert!(!fields.contains(&ProviderFormField::Project));
        assert!(!fields.contains(&ProviderFormField::Url));
        assert!(!fields.contains(&ProviderFormField::Profile));
    }

    #[test]
    fn test_azure_form_tab_cycles_through_regions() {
        let mut app = make_azure_form_app();
        app.provider_form.focused_field = ProviderFormField::Token;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.provider_form.focused_field, ProviderFormField::Regions);
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.provider_form.focused_field,
            ProviderFormField::AliasPrefix
        );
    }

    #[test]
    fn test_azure_regions_field_accepts_typing() {
        let mut app = make_azure_form_app();
        app.provider_form.focused_field = ProviderFormField::Regions;
        app.provider_form.regions = String::new();
        app.provider_form.cursor_pos = 0;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('a')), &tx);
        assert_eq!(app.provider_form.regions, "a");
    }

    fn make_ovh_form_app() -> App {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ProviderForm {
            provider: "ovh".to_string(),
        };
        app.provider_form = ProviderFormFields {
            url: String::new(),
            token: "ak:as:ck".to_string(),
            profile: String::new(),
            project: "proj-123".to_string(),
            compartment: String::new(),
            regions: String::new(),
            alias_prefix: "ovh".to_string(),
            user: "ubuntu".to_string(),
            identity_file: String::new(),
            verify_tls: true,
            auto_sync: true,
            focused_field: ProviderFormField::Token,
            cursor_pos: 0,
        };
        app
    }

    #[test]
    fn test_ovh_enter_on_regions_opens_picker() {
        let mut app = make_ovh_form_app();
        app.provider_form.focused_field = ProviderFormField::Regions;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(
            app.ui.show_region_picker,
            "Enter on OVH Regions should open picker"
        );
        assert_eq!(app.ui.region_picker_cursor, 0);
    }

    #[test]
    fn test_ovh_picker_select_eu() {
        let mut app = make_ovh_form_app();
        app.provider_form.focused_field = ProviderFormField::Regions;
        app.ui.show_region_picker = true;
        app.ui.region_picker_cursor = 0;

        // Cursor starts on group header "API Endpoint" (row 0).
        // Row 1 = "eu", Row 2 = "ca", Row 3 = "us"
        // Move down to "eu" (row 1)
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        assert_eq!(app.ui.region_picker_cursor, 1);

        // Press Space to select "eu"
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert_eq!(app.provider_form.regions, "eu");

        // Press Enter to confirm
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_region_picker);
        assert_eq!(app.provider_form.regions, "eu");
    }

    #[test]
    fn test_ovh_picker_select_us() {
        let mut app = make_ovh_form_app();
        app.ui.show_region_picker = true;
        app.ui.region_picker_cursor = 0;
        app.screen = Screen::ProviderForm {
            provider: "ovh".to_string(),
        };

        // Move to "us" (row 3: header=0, eu=1, ca=2, us=3)
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        assert_eq!(app.ui.region_picker_cursor, 3);

        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert_eq!(app.provider_form.regions, "us");

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_region_picker);
        assert_eq!(app.provider_form.regions, "us");
    }

    #[test]
    fn test_ovh_picker_space_on_header_toggles_all() {
        let mut app = make_ovh_form_app();
        app.ui.show_region_picker = true;
        app.ui.region_picker_cursor = 0; // Group header
        app.screen = Screen::ProviderForm {
            provider: "ovh".to_string(),
        };

        let (tx, _rx) = mpsc::channel();
        // Space on header selects all endpoints
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        // All three should be selected (order preserved by OVH_ENDPOINTS)
        assert_eq!(app.provider_form.regions, "eu,ca,us");

        // Space again on header deselects all
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert_eq!(app.provider_form.regions, "");
    }

    #[test]
    fn test_ovh_endpoint_picker_rows() {
        let rows = region_picker_rows("ovh");
        assert_eq!(rows.len(), 4); // 1 header + 3 endpoints
        assert_eq!(rows[0], None); // group header
        assert_eq!(rows[1], Some("eu"));
        assert_eq!(rows[2], Some("ca"));
        assert_eq!(rows[3], Some("us"));
    }

    #[test]
    fn test_ovh_picker_enter_selects_and_closes() {
        // OVH is single-select: Enter on an item should select it and close
        let mut app = make_ovh_form_app();
        app.ui.show_region_picker = true;
        app.ui.region_picker_cursor = 0;
        app.screen = Screen::ProviderForm {
            provider: "ovh".to_string(),
        };

        let (tx, _rx) = mpsc::channel();
        // Move to "ca" (row 2)
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        assert_eq!(app.ui.region_picker_cursor, 2);

        // Enter directly (no Space needed) selects "ca" and closes
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_region_picker);
        assert_eq!(app.provider_form.regions, "ca");
    }

    #[test]
    fn test_ovh_picker_enter_on_header_closes_without_select() {
        let mut app = make_ovh_form_app();
        app.ui.show_region_picker = true;
        app.ui.region_picker_cursor = 0; // group header
        app.screen = Screen::ProviderForm {
            provider: "ovh".to_string(),
        };

        let (tx, _rx) = mpsc::channel();
        // Enter on header: no item to select, just closes
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_region_picker);
        assert_eq!(app.provider_form.regions, "");
    }

    #[test]
    fn test_ovh_picker_enter_replaces_previous_selection() {
        let mut app = make_ovh_form_app();
        app.provider_form.regions = "eu".to_string(); // previously selected EU
        app.ui.show_region_picker = true;
        app.ui.region_picker_cursor = 3; // "us"
        app.screen = Screen::ProviderForm {
            provider: "ovh".to_string(),
        };

        let (tx, _rx) = mpsc::channel();
        // Enter on "us" should replace "eu" with "us" (single-select)
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.provider_form.regions, "us");
    }

    #[test]
    fn test_azure_enter_on_regions_does_not_open_picker() {
        let mut app = make_azure_form_app();
        app.provider_form.focused_field = ProviderFormField::Regions;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Must NOT open region picker (Azure uses text input, not picker)
        assert!(!app.ui.show_region_picker);
        // Screen should no longer be ProviderForm (submit transitions away)
        // or validation error sets status (screen stays on form)
        // Either way: not a picker.
    }

    #[test]
    fn test_submit_azure_rejects_invalid_subscription_id() {
        let mut app = make_azure_form_app();
        app.provider_form.regions = "not-a-uuid".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "Invalid subscription ID");
    }

    #[test]
    fn test_submit_azure_rejects_mixed_valid_invalid_subscriptions() {
        let mut app = make_azure_form_app();
        app.provider_form.regions = "12345678-1234-1234-1234-123456789012,bad-id".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert_status_contains(&app, "Invalid subscription ID");
    }

    // =========================================================================
    // Provider form navigation tests
    // =========================================================================

    #[test]
    fn test_provider_form_tab_cycles_cloud_fields() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.provider_form.focused_field,
            ProviderFormField::AliasPrefix
        );
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.provider_form.focused_field, ProviderFormField::User);
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.provider_form.focused_field,
            ProviderFormField::IdentityFile
        );
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.provider_form.focused_field, ProviderFormField::AutoSync);
    }

    #[test]
    fn test_provider_form_shift_tab_reverse() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::AutoSync);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::BackTab), &tx);
        assert_eq!(
            app.provider_form.focused_field,
            ProviderFormField::IdentityFile
        );
    }

    #[test]
    fn test_provider_form_proxmox_has_extra_fields() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::Url);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.provider_form.focused_field, ProviderFormField::Token);
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.provider_form.focused_field,
            ProviderFormField::AliasPrefix
        );
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.provider_form.focused_field, ProviderFormField::User);
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.provider_form.focused_field,
            ProviderFormField::IdentityFile
        );
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.provider_form.focused_field,
            ProviderFormField::VerifyTls
        );
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.provider_form.focused_field, ProviderFormField::AutoSync);
    }

    #[test]
    fn test_provider_form_esc_returns_to_provider_list() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::Providers));
    }

    #[test]
    fn test_provider_form_space_toggles_verify_tls() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::VerifyTls);
        assert!(app.provider_form.verify_tls);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert!(!app.provider_form.verify_tls);
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert!(app.provider_form.verify_tls);
    }

    #[test]
    fn test_provider_form_char_input_verify_tls_blocked() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::VerifyTls);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('x')), &tx);
        // No text field should have changed
        assert_eq!(app.provider_form.token, "tok");
    }

    #[test]
    fn test_provider_form_backspace_verify_tls_blocked() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::VerifyTls);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        assert_eq!(app.provider_form.token, "tok");
    }

    #[test]
    fn test_provider_form_enter_opens_key_picker() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::IdentityFile);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.ui.show_key_picker);
    }

    #[test]
    fn test_provider_form_char_appended_to_focused_field() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.token = "tok".to_string();
        app.provider_form.cursor_pos = 3;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('X')), &tx);
        assert_eq!(app.provider_form.token, "tokX");
    }

    #[test]
    fn test_provider_form_backspace_removes_from_focused_field() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.token = "tok".to_string();
        app.provider_form.cursor_pos = 3;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        assert_eq!(app.provider_form.token, "to");
    }

    // =========================================================================
    // Provider list interaction tests
    // =========================================================================

    #[test]
    fn test_provider_list_esc_returns_to_host_list() {
        let mut app = make_providers_app_with_do();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn test_provider_list_q_returns_to_host_list() {
        let mut app = make_providers_app_with_do();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('q')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn test_provider_list_j_selects_next() {
        let mut app = make_providers_app_with_do();
        app.ui.provider_list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        // Should advance (wrapping depends on count)
        assert!(app.ui.provider_list_state.selected().is_some());
    }

    #[test]
    fn test_provider_list_k_selects_prev() {
        let mut app = make_providers_app_with_do();
        app.ui.provider_list_state.select(Some(1));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        assert!(app.ui.provider_list_state.selected().is_some());
    }

    #[test]
    fn test_provider_list_sync_unconfigured_shows_status() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        // No config for digitalocean - select it and press s
        let sorted = app.sorted_provider_names();
        let idx = sorted.iter().position(|n| n == "digitalocean").unwrap();
        app.ui.provider_list_state.select(Some(idx));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('s')), &tx);
        assert!(app.status.as_ref().unwrap().text.contains("Configure"));
    }

    #[test]
    fn test_provider_list_delete_removes_config() {
        let mut app = make_providers_app_with_do();
        let sorted = app.sorted_provider_names();
        let idx = sorted.iter().position(|n| n == "digitalocean").unwrap();
        app.ui.provider_list_state.select(Some(idx));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        // d now triggers confirmation
        assert!(app.pending_provider_delete.is_some());
        // Confirm with y
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(app.pending_provider_delete.is_none());
        // Save may fail in tests (no ~/.purple), triggering rollback. Just verify handler ran.
        assert!(app.status.is_some());
    }

    #[test]
    fn test_provider_list_delete_unconfigured_is_noop() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        let sorted = app.sorted_provider_names();
        let idx = sorted.iter().position(|n| n == "digitalocean").unwrap();
        app.ui.provider_list_state.select(Some(idx));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        // No status message because no section existed to delete
        assert!(app.status.is_none() || !app.status.as_ref().unwrap().text.contains("Removed"));
    }

    #[test]
    fn test_provider_list_esc_cancels_running_syncs() {
        let mut app = make_providers_app_with_do();
        let cancel = Arc::new(AtomicBool::new(false));
        app.syncing_providers
            .insert("digitalocean".to_string(), cancel.clone());
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(
            cancel.load(Ordering::Relaxed),
            "Cancel flag should be set on Esc"
        );
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn test_provider_list_enter_opens_form_with_existing_config() {
        let mut app = make_providers_app_with_do();
        open_provider_form(&mut app, "digitalocean");
        assert!(
            matches!(app.screen, Screen::ProviderForm { ref provider } if provider == "digitalocean")
        );
        assert_eq!(app.provider_form.token, "tok");
        assert_eq!(app.provider_form.alias_prefix, "do");
        assert_eq!(app.provider_form.user, "root");
    }

    #[test]
    fn test_provider_list_enter_opens_form_with_defaults() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        open_provider_form(&mut app, "vultr");
        assert!(matches!(app.screen, Screen::ProviderForm { ref provider } if provider == "vultr"));
        assert_eq!(app.provider_form.token, "");
        assert_eq!(app.provider_form.user, "root");
        assert!(app.provider_form.auto_sync); // vultr default true
    }

    #[test]
    fn test_provider_form_proxmox_default_alias_prefix() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        open_provider_form(&mut app, "proxmox");
        // Proxmox short_label is "pve"
        assert_eq!(app.provider_form.alias_prefix, "pve");
    }

    // =========================================================================
    // Provider form all-providers init defaults
    // =========================================================================

    #[test]
    fn test_all_cloud_providers_default_auto_sync_true() {
        for provider in &[
            "digitalocean",
            "vultr",
            "linode",
            "hetzner",
            "upcloud",
            "aws",
            "scaleway",
            "gcp",
            "azure",
            "tailscale",
        ] {
            let mut app = make_app("Host test\n  HostName test.com\n");
            app.screen = Screen::Providers;
            app.provider_config = test_provider_config();
            open_provider_form(&mut app, provider);
            assert!(
                app.provider_form.auto_sync,
                "{} should default auto_sync=true",
                provider
            );
        }
    }

    #[test]
    fn test_proxmox_defaults_auto_sync_false() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        open_provider_form(&mut app, "proxmox");
        assert!(!app.provider_form.auto_sync);
    }

    #[test]
    fn test_submit_proxmox_https_case_insensitive() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::Url);
        app.provider_form.url = "HTTPS://pve.local:8006".to_string();
        app.provider_form.token = "user@pam!t=secret".to_string();
        submit_form(&mut app);
        assert_status_not_contains(&app, "https://");
    }

    #[test]
    fn test_submit_non_proxmox_url_not_required() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.url = "".to_string();
        submit_form(&mut app);
        assert_status_not_contains(&app, "URL is required");
    }

    #[test]
    fn test_submit_provider_form_accepts_empty_alias_prefix() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.alias_prefix = "".to_string();
        submit_form(&mut app);
        assert_status_not_contains(&app, "pattern");
    }

    #[test]
    fn test_submit_provider_form_accepts_hyphenated_alias() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.alias_prefix = "my-cloud".to_string();
        submit_form(&mut app);
        assert_status_not_contains(&app, "pattern");
    }

    #[test]
    fn test_submit_provider_form_rejects_space_in_alias_prefix() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.provider_form.alias_prefix = "my cloud".to_string();
        submit_form(&mut app);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        let msg = &app.status.as_ref().unwrap().text;
        if !msg.contains("changed externally") {
            assert!(msg.contains("pattern") || msg.contains("spaces"));
        }
    }

    // =========================================================================
    // Password picker tests
    // =========================================================================

    fn ctrl_key(c: char) -> KeyEvent {
        KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
    }

    fn make_form_app() -> App {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::AddHost;
        app.form = crate::app::HostForm::new();
        app
    }

    // --- Enter on AskPass opens picker ---

    #[test]
    fn test_enter_on_askpass_opens_password_picker() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.ui.show_password_picker);
        assert_eq!(app.ui.password_picker_state.selected(), Some(0));
    }

    // --- Esc closes picker ---

    #[test]
    fn test_password_picker_esc_closes() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(2));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(!app.ui.show_password_picker);
        // Form field should be unchanged
        assert_eq!(app.form.askpass, "");
    }

    // --- Navigation j/k ---

    #[test]
    fn test_password_picker_j_moves_down() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        assert_eq!(app.ui.password_picker_state.selected(), Some(1));
    }

    #[test]
    fn test_password_picker_k_moves_up() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(2));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        assert_eq!(app.ui.password_picker_state.selected(), Some(1));
    }

    #[test]
    fn test_password_picker_down_arrow() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Down), &tx);
        assert_eq!(app.ui.password_picker_state.selected(), Some(1));
    }

    #[test]
    fn test_password_picker_up_arrow() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(3));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Up), &tx);
        assert_eq!(app.ui.password_picker_state.selected(), Some(2));
    }

    #[test]
    fn test_password_picker_wraps_around_bottom() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        let last = crate::askpass::PASSWORD_SOURCES.len() - 1;
        app.ui.password_picker_state.select(Some(last));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        assert_eq!(app.ui.password_picker_state.selected(), Some(0));
    }

    #[test]
    fn test_password_picker_wraps_around_top() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        let last = crate::askpass::PASSWORD_SOURCES.len() - 1;
        assert_eq!(app.ui.password_picker_state.selected(), Some(last));
    }

    // --- Enter selects source: OS Keychain ---

    #[test]
    fn test_password_picker_select_keychain() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(0)); // OS Keychain
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_password_picker);
        assert_eq!(app.form.askpass, "keychain");
    }

    // --- Enter selects source: 1Password (prefix) ---

    #[test]
    fn test_password_picker_select_1password() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(1)); // 1Password
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_password_picker);
        assert_eq!(app.form.askpass, "op://");
        assert_eq!(app.form.focused_field, FormField::AskPass);
    }

    // --- Enter selects source: Bitwarden (prefix) ---

    #[test]
    fn test_password_picker_select_bitwarden() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(2)); // Bitwarden
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_password_picker);
        assert_eq!(app.form.askpass, "bw:");
        assert_eq!(app.form.focused_field, FormField::AskPass);
    }

    // --- Enter selects source: pass (prefix) ---

    #[test]
    fn test_password_picker_select_pass() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(3)); // pass
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_password_picker);
        assert_eq!(app.form.askpass, "pass:");
        assert_eq!(app.form.focused_field, FormField::AskPass);
    }

    // --- Enter selects source: HashiCorp Vault (prefix) ---

    #[test]
    fn test_password_picker_select_vault() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(4)); // HashiCorp Vault
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_password_picker);
        assert_eq!(app.form.askpass, "vault:");
        assert_eq!(app.form.focused_field, FormField::AskPass);
    }

    // --- Enter selects source: Custom command ---

    #[test]
    fn test_password_picker_select_custom() {
        let mut app = make_form_app();
        app.form.askpass = "old-value".to_string();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(5)); // Custom command
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_password_picker);
        assert_eq!(app.form.askpass, "");
    }

    // --- Enter selects source: None (clears) ---

    #[test]
    fn test_password_picker_select_none() {
        let mut app = make_form_app();
        app.form.askpass = "keychain".to_string();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(6)); // None
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_password_picker);
        assert_eq!(app.form.askpass, "");
    }

    // --- Picker blocks other form input ---

    #[test]
    fn test_password_picker_blocks_char_input() {
        let mut app = make_form_app();
        app.form.askpass = "".to_string();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('x')), &tx);
        // 'x' should not be appended to any form field
        assert_eq!(app.form.askpass, "");
        assert_eq!(app.form.alias, "");
    }

    #[test]
    fn test_password_picker_blocks_tab() {
        let mut app = make_form_app();
        let original_field = app.form.focused_field;
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        // Tab should not change focused field
        assert_eq!(app.form.focused_field, original_field);
    }

    // --- Picker on EditHost screen ---

    #[test]
    fn test_password_picker_works_on_edit_host() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::EditHost {
            alias: "test".to_string(),
        };
        app.form = crate::app::HostForm::new();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.ui.show_password_picker);
        // Select keychain
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.askpass, "keychain");
    }

    // --- Picker priority over key picker ---

    #[test]
    fn test_password_picker_takes_priority_over_key_picker() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.show_key_picker = true;
        app.ui.password_picker_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        // Esc should close password picker, not key picker
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(!app.ui.show_password_picker);
        assert!(app.ui.show_key_picker); // still open
    }

    // =========================================================================
    // Host list Enter carries askpass in pending_connect
    // =========================================================================

    #[test]
    fn test_host_list_enter_carries_askpass() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass keychain\n");
        app.screen = Screen::HostList;
        // Select the host
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let pending = app.pending_connect.as_ref().unwrap();
        assert_eq!(pending.0, "myserver");
        assert_eq!(pending.1, Some("keychain".to_string()));
    }

    #[test]
    fn test_host_list_enter_carries_vault_askpass() {
        let mut app = make_app(
            "Host myserver\n  HostName 10.0.0.1\n  # purple:askpass vault:secret/ssh#pass\n",
        );
        app.screen = Screen::HostList;
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let pending = app.pending_connect.as_ref().unwrap();
        assert_eq!(pending.1, Some("vault:secret/ssh#pass".to_string()));
    }

    #[test]
    fn test_host_list_enter_no_askpass() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n");
        app.screen = Screen::HostList;
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let pending = app.pending_connect.as_ref().unwrap();
        assert_eq!(pending.0, "myserver");
        assert_eq!(pending.1, None);
    }

    // =========================================================================
    // Search mode Enter carries askpass in pending_connect
    // =========================================================================

    #[test]
    fn test_search_enter_carries_askpass() {
        let mut app =
            make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass op://V/I/p\n");
        app.screen = Screen::HostList;
        app.start_search();
        // In search mode, filtered_indices should contain our host
        assert!(!app.search.filtered_indices.is_empty());
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let pending = app.pending_connect.as_ref().unwrap();
        assert_eq!(pending.0, "myserver");
        assert_eq!(pending.1, Some("op://V/I/p".to_string()));
        // Search should be cancelled after Enter
        assert!(app.search.query.is_none());
    }

    #[test]
    fn test_search_enter_no_askpass() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n");
        app.screen = Screen::HostList;
        app.start_search();
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let pending = app.pending_connect.as_ref().unwrap();
        assert_eq!(pending.1, None);
    }

    // =========================================================================
    // Ctrl+E edits selected host during search
    // =========================================================================

    #[test]
    fn test_search_ctrl_e_opens_edit_form() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n");
        app.screen = Screen::HostList;
        app.start_search();
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, ctrl_key('e'), &tx);
        assert!(matches!(app.screen, Screen::EditHost { ref alias } if alias == "myserver"));
        // Search query should be preserved so user returns to filtered list
        assert!(app.search.query.is_some());
    }

    #[test]
    fn test_search_ctrl_e_blocks_included_host() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n");
        // Simulate an included host by setting source_file
        if let Some(host) = app.hosts.first_mut() {
            host.source_file = Some(std::path::PathBuf::from("/etc/ssh/config.d/test"));
        }
        app.screen = Screen::HostList;
        app.start_search();
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, ctrl_key('e'), &tx);
        // Should remain in search mode (not open edit form)
        assert!(matches!(app.screen, Screen::HostList));
        assert!(app.status.is_some());
    }

    // =========================================================================
    // Tunnel start reads askpass from host
    // =========================================================================

    #[test]
    fn test_tunnel_handler_reads_askpass_from_hosts() {
        // Verify the askpass lookup logic: find host by alias and extract askpass
        let app = make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass bw:my-item\n");
        let askpass = app
            .hosts
            .iter()
            .find(|h| h.alias == "myserver")
            .and_then(|h| h.askpass.clone());
        assert_eq!(askpass, Some("bw:my-item".to_string()));
    }

    #[test]
    fn test_tunnel_handler_askpass_none_when_absent() {
        let app = make_app("Host myserver\n  HostName 10.0.0.1\n");
        let askpass = app
            .hosts
            .iter()
            .find(|h| h.alias == "myserver")
            .and_then(|h| h.askpass.clone());
        assert_eq!(askpass, None);
    }

    // =========================================================================
    // Edit host form populates askpass
    // =========================================================================

    #[test]
    fn test_edit_host_populates_askpass_in_form() {
        let mut app =
            make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass pass:ssh/prod\n");
        app.screen = Screen::HostList;
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        // Press 'e' to edit
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        if matches!(app.screen, Screen::EditHost { .. }) {
            assert_eq!(app.form.askpass, "pass:ssh/prod");
        }
    }

    #[test]
    fn test_edit_host_populates_empty_askpass() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n");
        app.screen = Screen::HostList;
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        if matches!(app.screen, Screen::EditHost { .. }) {
            assert_eq!(app.form.askpass, "");
        }
    }

    // =========================================================================
    // Tab navigation through AskPass field
    // =========================================================================

    #[test]
    fn test_tab_reaches_askpass_field() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::ProxyJump;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.form.focused_field, FormField::AskPass);
    }

    #[test]
    fn test_tab_from_askpass_goes_to_tags() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.form.focused_field, FormField::Tags);
    }

    #[test]
    fn test_shift_tab_from_tags_goes_to_askpass() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::Tags;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::BackTab), &tx);
        assert_eq!(app.form.focused_field, FormField::AskPass);
    }

    #[test]
    fn test_typing_in_askpass_field() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert_eq!(app.form.askpass, "key");
    }

    #[test]
    fn test_backspace_in_askpass_field() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        app.form.askpass = "vault:".to_string();
        app.form.cursor_pos = 6;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        assert_eq!(app.form.askpass, "vault");
    }

    // =========================================================================
    // Picker then type: prefix selection followed by typing
    // =========================================================================

    #[test]
    fn test_picker_select_op_then_type_rest() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        // Open picker
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Navigate to 1Password (index 1)
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        // Select
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.askpass, "op://");
        assert_eq!(app.form.focused_field, FormField::AskPass);
        // Now type the rest of the URI
        let _ = handle_key_event(&mut app, key(KeyCode::Char('V')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('/')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('I')), &tx);
        assert_eq!(app.form.askpass, "op://V/I");
    }

    #[test]
    fn test_picker_select_vault_then_type_rest() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        // Open picker
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Navigate to Vault (index 4)
        for _ in 0..4 {
            let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        }
        assert_eq!(app.ui.password_picker_state.selected(), Some(4));
        // Select
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.askpass, "vault:");
        assert_eq!(app.form.focused_field, FormField::AskPass);
        // Type the path
        for c in "secret/ssh#pass".chars() {
            let _ = handle_key_event(&mut app, key(KeyCode::Char(c)), &tx);
        }
        assert_eq!(app.form.askpass, "vault:secret/ssh#pass");
    }

    #[test]
    fn test_picker_select_keychain_no_further_typing_needed() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        // Open picker via Enter on AskPass
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Select keychain (index 0, already selected)
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.askpass, "keychain");
        // focused_field stays on AskPass (picker was opened from AskPass)
        assert_eq!(app.form.focused_field, FormField::AskPass);
    }

    // =========================================================================
    // Password picker: status messages after selection
    // =========================================================================

    #[test]
    fn test_picker_keychain_sets_status_message() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.status.as_ref().unwrap().text.contains("OS Keychain"));
    }

    #[test]
    fn test_picker_none_sets_cleared_status() {
        let mut app = make_form_app();
        app.form.askpass = "keychain".to_string();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(6)); // None
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.status.as_ref().unwrap().text.contains("cleared"));
    }

    #[test]
    fn test_picker_prefix_source_shows_guidance() {
        // Prefix sources (op://, bw:, etc.) show a guidance message
        let mut app = make_form_app();
        app.status = None;
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(1)); // 1Password (op://)
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.status.as_ref().unwrap().text.contains("Complete"));
        assert_eq!(app.form.focused_field, FormField::AskPass);
    }

    // =========================================================================
    // Backspace after prefix selection
    // =========================================================================

    #[test]
    fn test_backspace_after_prefix_selection() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        // Open picker and select 1Password
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        app.ui.password_picker_state.select(Some(1));
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.askpass, "op://");
        assert_eq!(app.form.focused_field, FormField::AskPass);
        // Type something
        let _ = handle_key_event(&mut app, key(KeyCode::Char('V')), &tx);
        assert_eq!(app.form.askpass, "op://V");
        // Backspace removes last char
        let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        assert_eq!(app.form.askpass, "op://");
        // Another backspace removes the trailing /
        let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        assert_eq!(app.form.askpass, "op:/");
    }

    // =========================================================================
    // Edit form populates askpass from existing host
    // =========================================================================

    #[test]
    fn test_edit_form_populates_askpass() {
        let mut app = make_app(
            "Host myserver\n  HostName 10.0.0.1\n  # purple:askpass vault:secret/ssh#pw\n",
        );
        // Simulate what happens when user presses 'e' on a host
        let entry = app.config.host_entries()[0].clone();
        app.form = crate::app::HostForm::from_entry(&entry);
        assert_eq!(app.form.askpass, "vault:secret/ssh#pw");
    }

    #[test]
    fn test_edit_form_empty_askpass_when_none() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n");
        let entry = app.config.host_entries()[0].clone();
        app.form = crate::app::HostForm::from_entry(&entry);
        assert_eq!(app.form.askpass, "");
    }

    // =========================================================================
    // Password picker: unknown keys are no-ops
    // =========================================================================

    #[test]
    fn test_password_picker_ignores_unknown_keys() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(2));
        let (tx, _rx) = mpsc::channel();
        // F1 key should be a no-op
        let _ = handle_key_event(&mut app, key(KeyCode::F(1)), &tx);
        assert!(app.ui.show_password_picker);
        assert_eq!(app.ui.password_picker_state.selected(), Some(2));
    }

    // =========================================================================
    // Host list search Enter carries askpass
    // =========================================================================

    #[test]
    fn test_search_enter_carries_askpass_op_uri() {
        let mut app =
            make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass op://V/I/p\n");
        app.search.query = Some("myserver".to_string());
        app.apply_filter();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        if let Some((alias, askpass)) = &app.pending_connect {
            assert_eq!(alias, "myserver");
            assert_eq!(askpass.as_deref(), Some("op://V/I/p"));
        } else {
            panic!("Expected pending_connect to be set");
        }
    }

    // =========================================================================
    // UI/UX: placeholder text and picker overlay properties
    // =========================================================================

    #[test]
    fn test_askpass_placeholder_text() {
        let placeholder = crate::ui::host_form::placeholder_text(FormField::AskPass);
        // When no global default is set, shows guidance text
        assert!(
            placeholder.contains("Enter") || placeholder.contains("default:"),
            "Should show guidance or default: {}",
            placeholder
        );
    }

    #[test]
    fn test_password_sources_fit_picker_width() {
        // Picker overlay is 48 chars wide (minus 4 for borders/padding)
        let max_content_width = 44;
        for source in crate::askpass::PASSWORD_SOURCES {
            let total = source.label.len() + 1 + source.hint.len();
            assert!(
                total <= max_content_width,
                "Source '{}' (label={}, hint={}) total {} exceeds max {}",
                source.label,
                source.label.len(),
                source.hint.len(),
                total,
                max_content_width
            );
        }
    }

    #[test]
    fn test_password_picker_item_count_matches_sources() {
        assert_eq!(crate::askpass::PASSWORD_SOURCES.len(), 7);
    }

    // =========================================================================
    // Full picker → type → form submit flow
    // =========================================================================

    #[test]
    fn test_full_flow_picker_to_typed_value() {
        let mut app = make_form_app();
        app.form.alias = "myhost".to_string();
        app.form.hostname = "10.0.0.1".to_string();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();

        // Open picker, select Bitwarden (index 2)
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        app.ui.password_picker_state.select(Some(2));
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);

        // Verify field has prefix
        assert_eq!(app.form.askpass, "bw:");
        assert_eq!(app.form.focused_field, FormField::AskPass);

        // Type the item name
        for c in "my-ssh-server".chars() {
            let _ = handle_key_event(&mut app, key(KeyCode::Char(c)), &tx);
        }
        assert_eq!(app.form.askpass, "bw:my-ssh-server");

        // Verify to_entry produces correct askpass
        let entry = app.form.to_entry();
        assert_eq!(entry.askpass, Some("bw:my-ssh-server".to_string()));
    }

    #[test]
    fn test_full_flow_picker_keychain_then_tab_away() {
        let mut app = make_form_app();
        // Only set alias (not hostname) so auto-submit doesn't trigger after picker
        app.form.alias = "myhost".to_string();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();

        // Open picker via Enter on AskPass, select keychain
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);

        assert_eq!(app.form.askpass, "keychain");
        // Focus stays on AskPass (picker opened from AskPass)
        assert_eq!(app.form.focused_field, FormField::AskPass);

        // Tab to next field
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.form.focused_field, FormField::Tags);
    }

    #[test]
    fn test_full_flow_clear_askpass_via_picker_none() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        app.form.askpass = "op://Vault/Item/pw".to_string();
        let (tx, _rx) = mpsc::channel();

        // Open picker, select None (index 6)
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        for _ in 0..6 {
            let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        }
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);

        assert_eq!(app.form.askpass, "");
        let entry = app.form.to_entry();
        assert_eq!(entry.askpass, None);
    }

    // =========================================================================
    // Askpass with host without askpass (no askpass in pending_connect)
    // =========================================================================

    #[test]
    fn test_host_list_enter_no_askpass_is_none() {
        let mut app = make_app("Host plain\n  HostName 10.0.0.1\n");
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        if let Some((alias, askpass)) = &app.pending_connect {
            assert_eq!(alias, "plain");
            assert!(askpass.is_none());
        } else {
            panic!("Expected pending_connect");
        }
    }

    // =========================================================================
    // Ctrl+P does NOT open password picker on provider form
    // =========================================================================

    #[test]
    fn test_ctrl_p_on_provider_form_does_not_open_password_picker() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ProviderForm {
            provider: "digitalocean".to_string(),
        };
        app.provider_form = crate::app::ProviderFormFields::new();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, ctrl_key('p'), &tx);
        // Provider form does not have a password picker
        assert!(!app.ui.show_password_picker);
    }

    // =========================================================================
    // Multiple hosts: each carries its own askpass in pending_connect
    // =========================================================================

    #[test]
    fn test_multiple_hosts_different_askpass_sources() {
        let config = "\
Host alpha
  HostName a.com
  # purple:askpass keychain

Host beta
  HostName b.com
  # purple:askpass op://Vault/SSH/pw

Host gamma
  HostName c.com
";
        let app = make_app(config);
        assert_eq!(app.hosts.len(), 3);
        assert_eq!(app.hosts[0].askpass, Some("keychain".to_string()));
        assert_eq!(app.hosts[1].askpass, Some("op://Vault/SSH/pw".to_string()));
        assert_eq!(app.hosts[2].askpass, None);
    }

    #[test]
    fn test_select_different_hosts_carries_correct_askpass() {
        let config = "\
Host alpha
  HostName a.com
  # purple:askpass keychain

Host beta
  HostName b.com
  # purple:askpass bw:my-item
";
        let mut app = make_app(config);
        let (tx, _rx) = mpsc::channel();

        // Select alpha (first host) and press Enter
        app.ui.list_state.select(Some(0));
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let (alias, askpass) = app.pending_connect.take().unwrap();
        assert_eq!(alias, "alpha");
        assert_eq!(askpass, Some("keychain".to_string()));

        // Select beta (second host) and press Enter
        app.ui.list_state.select(Some(1));
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let (alias, askpass) = app.pending_connect.take().unwrap();
        assert_eq!(alias, "beta");
        assert_eq!(askpass, Some("bw:my-item".to_string()));
    }

    // =========================================================================
    // Askpass field typing: direct input without picker
    // =========================================================================

    #[test]
    fn test_type_askpass_directly_without_picker() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        for c in "keychain".chars() {
            let _ = handle_key_event(&mut app, key(KeyCode::Char(c)), &tx);
        }
        assert_eq!(app.form.askpass, "keychain");
    }

    #[test]
    fn test_type_custom_command_directly() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        for c in "my-script %a %h".chars() {
            let _ = handle_key_event(&mut app, key(KeyCode::Char(c)), &tx);
        }
        assert_eq!(app.form.askpass, "my-script %a %h");
    }

    #[test]
    fn test_clear_askpass_with_backspace() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        app.form.askpass = "keychain".to_string();
        app.form.cursor_pos = 8;
        let (tx, _rx) = mpsc::channel();
        for _ in 0..8 {
            let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        }
        assert_eq!(app.form.askpass, "");
    }

    // =========================================================================
    // Delete host with askpass: undo restores it
    // =========================================================================

    #[test]
    fn test_delete_undo_preserves_askpass_in_config() {
        let config_str =
            "Host myserver\n  HostName 10.0.0.1\n  # purple:askpass vault:secret/ssh#pw\n";
        let mut app = make_app(config_str);
        // Verify askpass is present
        assert_eq!(
            app.config.host_entries()[0].askpass,
            Some("vault:secret/ssh#pw".to_string())
        );

        // Delete the host (undoable)
        if let Some((element, position)) = app.config.delete_host_undoable("myserver") {
            // Host is gone
            assert!(app.config.host_entries().is_empty());
            // Undo: restore
            app.config.insert_host_at(element, position);
            // Askpass should be restored
            let entries = app.config.host_entries();
            assert_eq!(entries.len(), 1);
            assert_eq!(entries[0].askpass, Some("vault:secret/ssh#pw".to_string()));
        } else {
            panic!("Expected delete_host_undoable to succeed");
        }
    }

    // =========================================================================
    // Askpass with unicode characters
    // =========================================================================

    #[test]
    fn test_askpass_unicode_in_custom_command() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        for c in "get-p\u{00E4}ss %h".chars() {
            let _ = handle_key_event(&mut app, key(KeyCode::Char(c)), &tx);
        }
        assert_eq!(app.form.askpass, "get-p\u{00E4}ss %h");
    }

    // =========================================================================
    // Enter on AskPass field opens picker
    // =========================================================================

    #[test]
    fn test_enter_on_askpass_field_opens_picker() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        app.form.askpass = "old-val".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.ui.show_password_picker);
        // Old value should still be there (picker hasn't committed yet)
        assert_eq!(app.form.askpass, "old-val");
    }

    #[test]
    fn test_enter_on_askpass_field_select_replaces_value() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        app.form.askpass = "old-val".to_string();
        let (tx, _rx) = mpsc::channel();
        // Open picker
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Select keychain
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.askpass, "keychain");
        assert!(!app.ui.show_password_picker);
    }

    // =========================================================================
    // --connect mode askpass lookup logic (replicated)
    // =========================================================================

    #[test]
    fn test_connect_mode_askpass_lookup() {
        let app = make_app("Host srv\n  HostName 1.2.3.4\n  # purple:askpass pass:ssh/srv\n");
        // Simulate --connect lookup logic from main.rs
        let alias = "srv";
        let askpass = app
            .config
            .host_entries()
            .iter()
            .find(|h| h.alias == alias)
            .and_then(|h| h.askpass.clone());
        assert_eq!(askpass, Some("pass:ssh/srv".to_string()));
    }

    #[test]
    fn test_connect_mode_askpass_none() {
        let app = make_app("Host srv\n  HostName 1.2.3.4\n");
        let alias = "srv";
        let askpass = app
            .config
            .host_entries()
            .iter()
            .find(|h| h.alias == alias)
            .and_then(|h| h.askpass.clone());
        assert_eq!(askpass, None);
    }

    #[test]
    fn test_connect_mode_nonexistent_host() {
        let app = make_app("Host srv\n  HostName 1.2.3.4\n");
        let alias = "nonexistent";
        let askpass = app
            .config
            .host_entries()
            .iter()
            .find(|h| h.alias == alias)
            .and_then(|h| h.askpass.clone());
        assert_eq!(askpass, None);
    }

    // =========================================================================
    // 'e' key opens edit form with correct askpass from host list
    // =========================================================================

    #[test]
    fn test_e_key_opens_edit_form_with_askpass() {
        let mut app =
            make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass op://Vault/SSH/pw\n");
        let (tx, _rx) = mpsc::channel();
        // Press 'e' to edit the selected host
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        assert!(matches!(app.screen, Screen::EditHost { .. }));
        assert_eq!(app.form.askpass, "op://Vault/SSH/pw");
        assert_eq!(app.form.hostname, "10.0.0.1");
    }

    #[test]
    fn test_e_key_opens_edit_form_without_askpass() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n");
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        assert!(matches!(app.screen, Screen::EditHost { .. }));
        assert_eq!(app.form.askpass, "");
    }

    // =========================================================================
    // Picker then Esc preserves existing askpass value
    // =========================================================================

    #[test]
    fn test_picker_esc_preserves_existing_askpass() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        app.form.askpass = "vault:secret/ssh#pw".to_string();
        let (tx, _rx) = mpsc::channel();
        // Open picker
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.ui.show_password_picker);
        // Navigate but then Esc
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        // Original value preserved
        assert_eq!(app.form.askpass, "vault:secret/ssh#pw");
    }

    // =========================================================================
    // Extra backspace past empty is no-op
    // =========================================================================

    #[test]
    fn test_backspace_on_empty_askpass_is_noop() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        app.form.askpass = "".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        assert_eq!(app.form.askpass, "");
    }

    // =========================================================================
    // Tab from AskPass goes to Tags, shift-tab goes to ProxyJump
    // =========================================================================

    #[test]
    fn test_tab_from_askpass_to_tags() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(app.form.focused_field, FormField::Tags);
    }

    #[test]
    fn test_shift_tab_from_askpass_to_proxyjump() {
        let mut app = make_form_app();
        app.form.focused_field = FormField::AskPass;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(
            &mut app,
            KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT),
            &tx,
        );
        assert_eq!(app.form.focused_field, FormField::ProxyJump);
    }

    // =========================================================================
    // Tunnel start for host with askpass passes it through
    // =========================================================================

    #[test]
    fn test_tunnel_askpass_lookup_different_sources() {
        let config = "\
Host alpha
  HostName a.com
  # purple:askpass keychain

Host beta
  HostName b.com
  # purple:askpass bw:item

Host gamma
  HostName c.com
";
        let app = make_app(config);
        let lookup = |alias: &str| -> Option<String> {
            app.hosts
                .iter()
                .find(|h| h.alias == alias)
                .and_then(|h| h.askpass.clone())
        };
        assert_eq!(lookup("alpha"), Some("keychain".to_string()));
        assert_eq!(lookup("beta"), Some("bw:item".to_string()));
        assert_eq!(lookup("gamma"), None);
    }

    // =========================================================================
    // Password picker status message tests
    // =========================================================================

    #[test]
    fn test_password_picker_keychain_sets_status_message() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(0)); // Keychain
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let status = app.status.as_ref().unwrap();
        assert!(
            status.text.contains("OS Keychain"),
            "Status should mention OS Keychain, got: {}",
            status.text
        );
    }

    #[test]
    fn test_password_picker_none_sets_cleared_status() {
        let mut app = make_form_app();
        app.form.askpass = "keychain".to_string();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(6)); // None
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        let status = app.status.as_ref().unwrap();
        assert!(
            status.text.contains("cleared"),
            "Status should say cleared, got: {}",
            status.text
        );
    }

    #[test]
    fn test_password_picker_prefix_source_focuses_askpass_field() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(1)); // 1Password (op://)
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(
            app.form.focused_field,
            FormField::AskPass,
            "Prefix source should focus AskPass field"
        );
        // No status message for prefix sources (user needs to keep typing)
        assert!(app.status.is_none() || !app.status.as_ref().unwrap().text.contains("set to"));
    }

    #[test]
    fn test_password_picker_prefix_bw_focuses_askpass() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(2)); // Bitwarden (bw:)
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.focused_field, FormField::AskPass);
        assert_eq!(app.form.askpass, "bw:");
    }

    #[test]
    fn test_password_picker_prefix_pass_focuses_askpass() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(3)); // pass (pass:)
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.focused_field, FormField::AskPass);
        assert_eq!(app.form.askpass, "pass:");
    }

    #[test]
    fn test_password_picker_prefix_vault_focuses_askpass() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(4)); // Vault (vault:)
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert_eq!(app.form.focused_field, FormField::AskPass);
        assert_eq!(app.form.askpass, "vault:");
    }

    // =========================================================================
    // Included host: edit blocked, but askpass visible in pending_connect
    // =========================================================================

    #[test]
    fn test_included_host_edit_blocked() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass keychain\n");
        app.screen = Screen::HostList;
        if let Some(host) = app.hosts.first_mut() {
            host.source_file = Some(std::path::PathBuf::from("/etc/ssh/ssh_config.d/work.conf"));
        }
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn test_included_host_connect_still_carries_askpass() {
        let mut app =
            make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass op://V/I/p\n");
        app.screen = Screen::HostList;
        if let Some(host) = app.hosts.first_mut() {
            host.source_file = Some(std::path::PathBuf::from("/etc/ssh/ssh_config.d/work.conf"));
        }
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        if let Some((alias, askpass)) = &app.pending_connect {
            assert_eq!(alias, "myserver");
            assert_eq!(askpass.as_deref(), Some("op://V/I/p"));
        }
    }

    #[test]
    fn test_included_host_delete_blocked() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass bw:item\n");
        app.screen = Screen::HostList;
        if let Some(host) = app.hosts.first_mut() {
            host.source_file = Some(std::path::PathBuf::from("/etc/ssh/ssh_config.d/work.conf"));
        }
        app.ui.list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
    }

    // =========================================================================
    // Form submit with askpass: verify to_entry() includes askpass
    // =========================================================================

    #[test]
    fn test_form_submit_with_all_password_source_types() {
        let sources = [
            "keychain",
            "op://V/I/p",
            "bw:item",
            "pass:ssh/srv",
            "vault:kv/ssh#pw",
            "my-cmd %h",
        ];
        for source in &sources {
            let mut app = make_app("");
            app.screen = Screen::AddHost;
            app.form.alias = "test-host".to_string();
            app.form.hostname = "10.0.0.1".to_string();
            app.form.askpass = source.to_string();
            let entry = app.form.to_entry();
            assert_eq!(
                entry.askpass.as_deref(),
                Some(*source),
                "Form with askpass '{}' should produce entry with same askpass",
                source
            );
        }
    }

    #[test]
    fn test_form_submit_empty_askpass_is_none() {
        let mut app = make_app("");
        app.screen = Screen::AddHost;
        app.form.alias = "test-host".to_string();
        app.form.hostname = "10.0.0.1".to_string();
        app.form.askpass = "".to_string();
        let entry = app.form.to_entry();
        assert!(entry.askpass.is_none(), "Empty askpass should produce None");
    }

    // =========================================================================
    // Password picker: Enter with no selection is no-op
    // =========================================================================

    #[test]
    fn test_password_picker_enter_with_no_selection() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state = ratatui::widgets::ListState::default(); // no selection
        app.form.askpass = "old".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(!app.ui.show_password_picker);
        assert_eq!(app.form.askpass, "old");
    }

    // =========================================================================
    // BW_SESSION: stored in app state
    // =========================================================================

    #[test]
    fn test_bw_session_stored_in_app() {
        let mut app = make_app("Host srv\n  HostName 1.2.3.4\n  # purple:askpass bw:item\n");
        assert!(app.bw_session.is_none());
        app.bw_session = Some("test-session-token".to_string());
        assert_eq!(app.bw_session.as_deref(), Some("test-session-token"));
    }

    #[test]
    fn test_bw_session_none_for_non_bw_source() {
        let app = make_app("Host srv\n  HostName 1.2.3.4\n  # purple:askpass keychain\n");
        assert!(app.bw_session.is_none());
    }

    // =========================================================================
    // Ctrl+D sets global default in password picker
    // =========================================================================

    #[test]
    fn test_password_picker_ctrl_d_closes_picker() {
        // Use "None" to avoid writing a value to the real preferences file
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(6)); // None
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, ctrl_key('d'), &tx);
        assert!(!app.ui.show_password_picker);
    }

    #[test]
    fn test_password_picker_ctrl_d_does_not_change_form_askpass() {
        let mut app = make_form_app();
        app.form.askpass = "old".to_string();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(6)); // None
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, ctrl_key('d'), &tx);
        // Ctrl+D only sets the global default, not the form field
        assert_eq!(app.form.askpass, "old");
    }

    #[test]
    fn test_password_picker_ctrl_d_none_sets_status() {
        let mut app = make_form_app();
        app.ui.show_password_picker = true;
        app.ui.password_picker_state.select(Some(6)); // None
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, ctrl_key('d'), &tx);
        // Shows "cleared" on success or "Failed to save" if ~/.purple doesn't exist
        assert!(app.status.is_some());
        assert!(!app.ui.show_password_picker);
    }

    #[test]
    fn test_password_picker_ctrl_d_source_label_in_status() {
        // Verify logic: non-None sources produce "Global default set to X." message
        let sources = crate::askpass::PASSWORD_SOURCES;
        for (i, src) in sources.iter().enumerate() {
            if src.label == "None" {
                continue;
            }
            let expected = format!("Global default set to {}.", src.label);
            assert!(expected.contains("default"), "Source {}: {}", i, expected);
        }
    }

    // =========================================================================
    // Keychain removal on askpass source change
    // =========================================================================

    #[test]
    fn test_submit_form_old_askpass_tracked_for_edit() {
        // When editing a host with keychain askpass, the old source is detected
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass keychain\n");
        assert_eq!(app.hosts[0].askpass, Some("keychain".to_string()));
        // Simulate opening edit form
        app.screen = Screen::EditHost {
            alias: "myserver".to_string(),
        };
        app.form.alias = "myserver".to_string();
        app.form.hostname = "10.0.0.1".to_string();
        // Change askpass to something else
        app.form.askpass = "op://Vault/Item/pw".to_string();
        // The old_askpass detection in submit_form looks up app.hosts by alias
        let old = app
            .hosts
            .iter()
            .find(|h| h.alias == "myserver")
            .and_then(|h| h.askpass.clone());
        assert_eq!(old, Some("keychain".to_string()));
    }

    #[test]
    fn test_submit_form_no_keychain_removal_when_unchanged() {
        let mut app = make_app("Host myserver\n  HostName 10.0.0.1\n  # purple:askpass keychain\n");
        app.screen = Screen::EditHost {
            alias: "myserver".to_string(),
        };
        app.form.alias = "myserver".to_string();
        app.form.hostname = "10.0.0.1".to_string();
        // Keep askpass as keychain
        app.form.askpass = "keychain".to_string();
        let old = app
            .hosts
            .iter()
            .find(|h| h.alias == "myserver")
            .and_then(|h| h.askpass.clone());
        // Same source, no removal needed
        assert_eq!(old.as_deref(), Some("keychain"));
        assert_eq!(app.form.askpass, "keychain");
    }

    #[test]
    fn test_submit_form_no_keychain_removal_for_add() {
        // AddHost has no old askpass
        let mut app = make_app("Host existing\n  HostName 1.2.3.4\n");
        app.screen = Screen::AddHost;
        let old: Option<String> = None; // no old host for add
        assert!(old.is_none());
    }

    // =========================================================================
    // Snippet picker
    // =========================================================================

    fn make_snippet_app() -> App {
        let mut app = make_app("Host myserver\n  HostName 1.2.3.4\n");
        let dir = std::env::temp_dir().join(format!(
            "purple_handler_snip_{}_{:?}",
            std::process::id(),
            std::thread::current().id()
        ));
        let _ = std::fs::create_dir_all(&dir);
        app.snippet_store.path_override = Some(dir.join("snippets"));
        app.snippet_store.snippets = vec![
            crate::snippet::Snippet {
                name: "check-disk".to_string(),
                command: "df -h".to_string(),
                description: "Check disk usage".to_string(),
            },
            crate::snippet::Snippet {
                name: "uptime".to_string(),
                command: "uptime".to_string(),
                description: String::new(),
            },
        ];
        let _ = app.snippet_store.save();
        app.ui.snippet_picker_state.select(Some(0));
        app.screen = Screen::SnippetPicker {
            target_aliases: vec!["myserver".to_string()],
        };
        app
    }

    #[test]
    fn test_snippet_picker_nav_down_up() {
        let mut app = make_snippet_app();
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        assert_eq!(app.ui.snippet_picker_state.selected(), Some(1));

        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        assert_eq!(app.ui.snippet_picker_state.selected(), Some(0));
    }

    #[test]
    fn test_snippet_picker_esc_returns_to_hostlist() {
        let mut app = make_snippet_app();
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert_eq!(app.screen, Screen::HostList);
    }

    #[test]
    fn test_snippet_picker_q_returns_to_hostlist() {
        let mut app = make_snippet_app();
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('q')), &tx);
        assert_eq!(app.screen, Screen::HostList);
    }

    #[test]
    fn test_snippet_picker_enter_starts_output() {
        let mut app = make_snippet_app();
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        match &app.screen {
            Screen::SnippetOutput {
                snippet_name,
                target_aliases,
            } => {
                assert_eq!(snippet_name, "check-disk");
                assert_eq!(target_aliases, &vec!["myserver".to_string()]);
            }
            _ => panic!("Expected SnippetOutput screen, got {:?}", app.screen),
        }
        assert!(app.snippet_output.is_some());
    }

    #[test]
    fn test_snippet_picker_enter_clears_multi_select() {
        let mut app = make_snippet_app();
        app.multi_select.insert(0);
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.multi_select.is_empty());
    }

    #[test]
    fn test_snippet_picker_a_opens_add_form() {
        let mut app = make_snippet_app();
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('a')), &tx);
        assert!(matches!(
            app.screen,
            Screen::SnippetForm { editing: None, .. }
        ));
        assert!(app.snippet_form.name.is_empty());
    }

    #[test]
    fn test_snippet_picker_e_opens_edit_form() {
        let mut app = make_snippet_app();
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        assert!(matches!(
            app.screen,
            Screen::SnippetForm {
                editing: Some(0),
                ..
            }
        ));
        assert_eq!(app.snippet_form.name, "check-disk");
        assert_eq!(app.snippet_form.command, "df -h");
    }

    #[test]
    fn test_snippet_picker_d_deletes_and_saves() {
        let mut app = make_snippet_app();
        let _ = app.snippet_store.save(); // ensure file exists
        let (tx, _rx) = mpsc::channel();

        // d sets pending confirmation
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        assert_eq!(app.pending_snippet_delete, Some(0));
        assert_eq!(app.snippet_store.snippets.len(), 2); // not yet deleted

        // y confirms deletion
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert_eq!(app.pending_snippet_delete, None);
        assert_eq!(app.snippet_store.snippets.len(), 1);
        assert_eq!(app.snippet_store.snippets[0].name, "uptime");
        assert_eq!(app.ui.snippet_picker_state.selected(), Some(0));
    }

    #[test]
    fn test_snippet_picker_d_last_item_selects_none() {
        let mut app = make_snippet_app();
        app.snippet_store.snippets = vec![crate::snippet::Snippet {
            name: "only".to_string(),
            command: "ls".to_string(),
            description: String::new(),
        }];
        app.ui.snippet_picker_state.select(Some(0));
        let _ = app.snippet_store.save();
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        assert_eq!(app.pending_snippet_delete, Some(0));

        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(app.snippet_store.snippets.is_empty());
        assert_eq!(app.ui.snippet_picker_state.selected(), None);
    }

    #[test]
    fn test_snippet_picker_d_rollback_on_save_failure() {
        let mut app = make_snippet_app();
        // Point to a non-writable path to force save failure
        app.snippet_store.path_override = Some(PathBuf::from("/nonexistent/dir/snippets"));
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        assert_eq!(app.pending_snippet_delete, Some(0));

        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        // Rollback: snippet should still be there
        assert_eq!(app.snippet_store.snippets.len(), 2);
        assert_eq!(app.snippet_store.snippets[0].name, "check-disk");
        assert!(app.status.as_ref().unwrap().is_error);
    }

    // =========================================================================
    // Snippet form
    // =========================================================================

    #[test]
    fn test_snippet_form_esc_returns_to_picker() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::SnippetPicker { .. }));
    }

    #[test]
    fn test_snippet_form_tab_cycles_fields() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        let (tx, _rx) = mpsc::channel();

        assert_eq!(
            app.snippet_form.focused_field,
            crate::app::SnippetFormField::Name
        );

        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.snippet_form.focused_field,
            crate::app::SnippetFormField::Command
        );

        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.snippet_form.focused_field,
            crate::app::SnippetFormField::Description
        );

        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);
        assert_eq!(
            app.snippet_form.focused_field,
            crate::app::SnippetFormField::Name
        );
    }

    #[test]
    fn test_snippet_form_char_insert() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('a')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('b')), &tx);
        assert_eq!(app.snippet_form.name, "ab");
        assert_eq!(app.snippet_form.cursor_pos, 2);
    }

    #[test]
    fn test_snippet_form_backspace() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.snippet_form.name = "abc".to_string();
        app.snippet_form.cursor_pos = 3;
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Backspace), &tx);
        assert_eq!(app.snippet_form.name, "ab");
        assert_eq!(app.snippet_form.cursor_pos, 2);
    }

    #[test]
    fn test_snippet_form_submit_add() {
        let mut app = make_snippet_app();
        let _ = app.snippet_store.save();
        app.snippet_form = crate::app::SnippetForm::new();
        app.snippet_form.name = "new-cmd".to_string();
        app.snippet_form.command = "whoami".to_string();
        app.snippet_form.cursor_pos = 6;
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(matches!(app.screen, Screen::SnippetPicker { .. }));
        assert_eq!(app.snippet_store.snippets.len(), 3);
        assert!(app.snippet_store.get("new-cmd").is_some());
    }

    #[test]
    fn test_snippet_form_submit_edit() {
        let mut app = make_snippet_app();
        let _ = app.snippet_store.save();
        app.snippet_form =
            crate::app::SnippetForm::from_snippet(&app.snippet_store.snippets[0].clone());
        app.snippet_form.command = "df -hT".to_string();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: Some(0),
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(matches!(app.screen, Screen::SnippetPicker { .. }));
        assert_eq!(app.snippet_store.snippets[0].command, "df -hT");
    }

    #[test]
    fn test_snippet_form_submit_rejects_empty_name() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.snippet_form.command = "ls".to_string();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Should stay on the form with an error
        assert!(matches!(app.screen, Screen::SnippetForm { .. }));
        assert!(app.status.as_ref().unwrap().is_error);
    }

    #[test]
    fn test_snippet_form_submit_rejects_duplicate_name() {
        let mut app = make_snippet_app();
        let _ = app.snippet_store.save();
        app.snippet_form = crate::app::SnippetForm::new();
        app.snippet_form.name = "uptime".to_string();
        app.snippet_form.command = "uptime -s".to_string();
        app.snippet_form.cursor_pos = 9;
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(matches!(app.screen, Screen::SnippetForm { .. }));
        assert!(app.status.as_ref().unwrap().is_error);
    }

    #[test]
    fn test_snippet_form_submit_rollback_on_save_failure() {
        let mut app = make_snippet_app();
        // Force save failure
        app.snippet_store.path_override = Some(PathBuf::from("/nonexistent/dir/snippets"));
        app.snippet_form = crate::app::SnippetForm::new();
        app.snippet_form.name = "new-cmd".to_string();
        app.snippet_form.command = "whoami".to_string();
        app.snippet_form.cursor_pos = 6;
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Rollback: new snippet should not be in the store
        assert_eq!(app.snippet_store.snippets.len(), 2);
        assert!(app.snippet_store.get("new-cmd").is_none());
        assert!(app.status.as_ref().unwrap().is_error);
    }

    #[test]
    fn test_snippet_form_edit_rename_rollback_on_save_failure() {
        let mut app = make_snippet_app();
        // Force save failure
        app.snippet_store.path_override = Some(PathBuf::from("/nonexistent/dir/snippets"));
        app.snippet_form =
            crate::app::SnippetForm::from_snippet(&app.snippet_store.snippets[0].clone());
        app.snippet_form.name = "renamed".to_string();
        app.snippet_form.cursor_pos = 7;
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: Some(0),
        };
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Rollback: original snippets should still be there
        assert_eq!(app.snippet_store.snippets.len(), 2);
        assert!(app.snippet_store.get("check-disk").is_some());
        assert!(app.snippet_store.get("renamed").is_none());
    }

    #[test]
    fn test_snippet_picker_enter_with_no_selection() {
        let mut app = make_snippet_app();
        app.snippet_store.snippets.clear();
        app.ui.snippet_picker_state.select(None);
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Should remain on picker, no pending snippet
        assert!(matches!(app.screen, Screen::SnippetPicker { .. }));
        assert!(app.pending_snippet.is_none());
    }

    #[test]
    fn test_host_list_r_opens_snippet_picker() {
        let mut app = make_app("Host myserver\n  HostName 1.2.3.4\n");
        app.ui.list_state.select(Some(0));
        let dir =
            std::env::temp_dir().join(format!("purple_handler_snip_r_{}", std::process::id()));
        let _ = std::fs::create_dir_all(&dir);
        app.snippet_store.path_override = Some(dir.join("snippets"));
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('r')), &tx);
        match &app.screen {
            Screen::SnippetPicker { target_aliases } => {
                assert_eq!(target_aliases, &vec!["myserver".to_string()]);
            }
            _ => panic!("Expected SnippetPicker screen"),
        }
    }

    #[test]
    fn test_host_list_r_shift_opens_snippet_picker_all() {
        let mut app = make_app("Host a\n  HostName 1.1.1.1\nHost b\n  HostName 2.2.2.2\n");
        app.ui.list_state.select(Some(0));
        let dir =
            std::env::temp_dir().join(format!("purple_handler_snip_R_{}", std::process::id()));
        let _ = std::fs::create_dir_all(&dir);
        app.snippet_store.path_override = Some(dir.join("snippets"));
        let (tx, _rx) = mpsc::channel();

        let _ = handle_key_event(&mut app, key(KeyCode::Char('R')), &tx);
        match &app.screen {
            Screen::SnippetPicker { target_aliases } => {
                assert_eq!(target_aliases.len(), 2);
            }
            _ => panic!("Expected SnippetPicker screen"),
        }
    }

    // --- Tunnel form Space/arrow tests ---

    fn make_tunnel_form_app(field: crate::app::TunnelFormField) -> App {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::TunnelForm {
            alias: "test".to_string(),
            editing: None,
        };
        app.tunnel_form = crate::app::TunnelForm::new();
        app.tunnel_form.focused_field = field;
        app
    }

    #[test]
    fn test_tunnel_form_space_cycles_type_local_to_remote() {
        let mut app = make_tunnel_form_app(crate::app::TunnelFormField::Type);
        assert_eq!(
            app.tunnel_form.tunnel_type,
            crate::tunnel::TunnelType::Local
        );
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert_eq!(
            app.tunnel_form.tunnel_type,
            crate::tunnel::TunnelType::Remote
        );
    }

    #[test]
    fn test_tunnel_form_space_cycles_type_remote_to_dynamic() {
        let mut app = make_tunnel_form_app(crate::app::TunnelFormField::Type);
        app.tunnel_form.tunnel_type = crate::tunnel::TunnelType::Remote;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert_eq!(
            app.tunnel_form.tunnel_type,
            crate::tunnel::TunnelType::Dynamic
        );
    }

    #[test]
    fn test_tunnel_form_space_cycles_type_dynamic_to_local() {
        let mut app = make_tunnel_form_app(crate::app::TunnelFormField::Type);
        app.tunnel_form.tunnel_type = crate::tunnel::TunnelType::Dynamic;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert_eq!(
            app.tunnel_form.tunnel_type,
            crate::tunnel::TunnelType::Local
        );
    }

    #[test]
    fn test_tunnel_form_left_on_type_does_not_cycle() {
        let mut app = make_tunnel_form_app(crate::app::TunnelFormField::Type);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Left), &tx);
        assert_eq!(
            app.tunnel_form.tunnel_type,
            crate::tunnel::TunnelType::Local
        );
    }

    #[test]
    fn test_tunnel_form_right_on_type_does_not_cycle() {
        let mut app = make_tunnel_form_app(crate::app::TunnelFormField::Type);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Right), &tx);
        assert_eq!(
            app.tunnel_form.tunnel_type,
            crate::tunnel::TunnelType::Local
        );
    }

    #[test]
    fn test_tunnel_form_space_on_bind_port_inserts_space() {
        let mut app = make_tunnel_form_app(crate::app::TunnelFormField::BindPort);
        app.tunnel_form.bind_port = "80".to_string();
        app.tunnel_form.cursor_pos = 2;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char(' ')), &tx);
        assert_eq!(app.tunnel_form.bind_port, "80 ");
    }

    #[test]
    fn test_tunnel_form_left_on_text_moves_cursor() {
        let mut app = make_tunnel_form_app(crate::app::TunnelFormField::BindPort);
        app.tunnel_form.bind_port = "8080".to_string();
        app.tunnel_form.cursor_pos = 2;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Left), &tx);
        assert_eq!(app.tunnel_form.cursor_pos, 1);
    }

    // --- Dirty-check tests ---

    #[test]
    fn test_host_form_clean_esc_closes_immediately() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.form = crate::app::HostForm::new();
        app.screen = Screen::AddHost;
        app.capture_form_baseline();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::HostList));
        assert!(!app.pending_discard_confirm);
    }

    #[test]
    fn test_host_form_dirty_esc_shows_confirmation() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.form = crate::app::HostForm::new();
        app.screen = Screen::AddHost;
        app.capture_form_baseline();
        app.form.alias = "dirty".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::AddHost));
        assert!(app.pending_discard_confirm);
    }

    #[test]
    fn test_host_form_dirty_esc_y_closes() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.form = crate::app::HostForm::new();
        app.screen = Screen::AddHost;
        app.capture_form_baseline();
        app.form.alias = "dirty".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
        assert!(app.form_baseline.is_none());
    }

    #[test]
    fn test_host_form_dirty_esc_n_stays() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.form = crate::app::HostForm::new();
        app.screen = Screen::AddHost;
        app.capture_form_baseline();
        app.form.hostname = "changed.com".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('n')), &tx);
        assert!(matches!(app.screen, Screen::AddHost));
        assert!(!app.pending_discard_confirm);
    }

    #[test]
    fn test_host_form_dirty_esc_other_key_ignored() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.form = crate::app::HostForm::new();
        app.screen = Screen::AddHost;
        app.capture_form_baseline();
        app.form.alias = "dirty".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('x')), &tx);
        assert!(app.pending_discard_confirm); // still pending
    }

    #[test]
    fn test_tunnel_form_dirty_esc_shows_confirmation() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::TunnelForm {
            alias: "test".to_string(),
            editing: None,
        };
        app.tunnel_form = crate::app::TunnelForm::new();
        app.capture_tunnel_form_baseline();
        app.tunnel_form.bind_port = "9000".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::TunnelForm { .. }));
        assert!(app.pending_discard_confirm);
    }

    #[test]
    fn test_tunnel_form_clean_esc_closes() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::TunnelForm {
            alias: "test".to_string(),
            editing: None,
        };
        app.tunnel_form = crate::app::TunnelForm::new();
        app.capture_tunnel_form_baseline();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::TunnelList { .. }));
    }

    // --- Delete confirmation tests ---

    #[test]
    fn test_snippet_picker_d_esc_cancels_delete() {
        let mut app = make_snippet_app();
        let _ = app.snippet_store.save();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        assert_eq!(app.pending_snippet_delete, Some(0));
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert_eq!(app.pending_snippet_delete, None);
        assert_eq!(app.snippet_store.snippets.len(), 2);
    }

    #[test]
    fn test_snippet_picker_d_n_cancels_delete() {
        let mut app = make_snippet_app();
        let _ = app.snippet_store.save();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('n')), &tx);
        assert_eq!(app.pending_snippet_delete, None);
        assert_eq!(app.snippet_store.snippets.len(), 2);
    }

    #[test]
    fn test_snippet_picker_d_other_key_ignored() {
        let mut app = make_snippet_app();
        let _ = app.snippet_store.save();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        assert_eq!(app.pending_snippet_delete, Some(0));
        assert_eq!(app.snippet_store.snippets.len(), 2);
    }

    #[test]
    fn test_confirm_import_uppercase_y_works() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ConfirmImport { count: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('Y')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn test_confirm_import_n_cancels() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ConfirmImport { count: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('n')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn test_confirm_import_uppercase_n_cancels() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::ConfirmImport { count: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('N')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
    }

    // --- HostDetail navigation tests ---

    #[test]
    fn test_host_detail_esc_returns_to_host_list() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::HostDetail { index: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn test_host_detail_e_opens_edit() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::HostDetail { index: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        assert!(matches!(app.screen, Screen::EditHost { .. }));
        assert!(app.form_baseline.is_some());
    }

    #[test]
    fn test_host_detail_t_opens_tunnel_list() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::HostDetail { index: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('T')), &tx);
        assert!(matches!(app.screen, Screen::TunnelList { .. }));
    }

    #[test]
    fn test_host_detail_r_opens_snippet_picker() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::HostDetail { index: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('r')), &tx);
        assert!(matches!(app.screen, Screen::SnippetPicker { .. }));
    }

    #[test]
    fn test_host_detail_e_on_included_host_stays() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.hosts[0].source_file = Some(PathBuf::from("/etc/ssh/config.d/test"));
        app.screen = Screen::HostDetail { index: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        assert!(matches!(app.screen, Screen::HostDetail { .. }));
        assert!(app.status.as_ref().unwrap().is_error);
    }

    // --- Provider form: Left/Right on toggle fields does NOT toggle ---

    #[test]
    fn test_provider_form_left_on_verify_tls_stays_same() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::VerifyTls);
        assert!(app.provider_form.verify_tls);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Left), &tx);
        assert!(app.provider_form.verify_tls);
    }

    #[test]
    fn test_provider_form_right_on_verify_tls_stays_same() {
        let mut app = make_form_app_focused_on("proxmox", ProviderFormField::VerifyTls);
        assert!(app.provider_form.verify_tls);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Right), &tx);
        assert!(app.provider_form.verify_tls);
    }

    #[test]
    fn test_provider_form_left_on_auto_sync_stays_same() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::AutoSync);
        assert!(app.provider_form.auto_sync);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Left), &tx);
        assert!(app.provider_form.auto_sync);
    }

    #[test]
    fn test_provider_form_right_on_auto_sync_stays_same() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::AutoSync);
        assert!(app.provider_form.auto_sync);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Right), &tx);
        assert!(app.provider_form.auto_sync);
    }

    // --- Provider form: dirty-check on Esc ---

    #[test]
    fn test_provider_form_clean_esc_with_baseline_closes() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.capture_provider_form_baseline();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::Providers));
        assert!(!app.pending_discard_confirm);
    }

    #[test]
    fn test_provider_form_dirty_esc_shows_confirmation() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.capture_provider_form_baseline();
        app.provider_form.token = "newtoken".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert!(app.pending_discard_confirm);
    }

    #[test]
    fn test_provider_form_dirty_esc_y_closes() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.capture_provider_form_baseline();
        app.provider_form.token = "newtoken".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(matches!(app.screen, Screen::Providers));
        assert!(app.provider_form_baseline.is_none());
    }

    #[test]
    fn test_provider_form_dirty_esc_n_stays() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.capture_provider_form_baseline();
        app.provider_form.token = "newtoken".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('n')), &tx);
        assert!(matches!(app.screen, Screen::ProviderForm { .. }));
        assert!(!app.pending_discard_confirm);
    }

    // --- Snippet form: dirty-check on Esc ---

    #[test]
    fn test_snippet_form_clean_esc_with_baseline_closes() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        app.capture_snippet_form_baseline();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::SnippetPicker { .. }));
        assert!(!app.pending_discard_confirm);
    }

    #[test]
    fn test_snippet_form_dirty_esc_shows_confirmation() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        app.capture_snippet_form_baseline();
        app.snippet_form.name = "dirty".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::SnippetForm { .. }));
        assert!(app.pending_discard_confirm);
    }

    #[test]
    fn test_snippet_form_dirty_esc_y_closes() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        app.capture_snippet_form_baseline();
        app.snippet_form.name = "dirty".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(matches!(app.screen, Screen::SnippetPicker { .. }));
        assert!(app.snippet_form_baseline.is_none());
    }

    // --- Tunnel delete: d/y/Esc/n ---

    #[test]
    fn test_tunnel_list_d_y_deletes_tunnel() {
        let mut app =
            make_app("Host test\n  HostName test.com\n  LocalForward 8080 localhost:80\n");
        app.screen = Screen::TunnelList {
            alias: "test".to_string(),
        };
        app.refresh_tunnel_list("test");
        app.ui.tunnel_list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        assert_eq!(app.pending_tunnel_delete, Some(0));
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(app.pending_tunnel_delete.is_none());
    }

    #[test]
    fn test_tunnel_list_d_esc_cancels_delete() {
        let mut app =
            make_app("Host test\n  HostName test.com\n  LocalForward 8080 localhost:80\n");
        app.screen = Screen::TunnelList {
            alias: "test".to_string(),
        };
        app.refresh_tunnel_list("test");
        app.ui.tunnel_list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        assert_eq!(app.pending_tunnel_delete, Some(0));
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(app.pending_tunnel_delete.is_none());
        assert_eq!(app.tunnel_list.len(), 1);
    }

    #[test]
    fn test_tunnel_list_d_n_cancels_delete() {
        let mut app =
            make_app("Host test\n  HostName test.com\n  LocalForward 8080 localhost:80\n");
        app.screen = Screen::TunnelList {
            alias: "test".to_string(),
        };
        app.refresh_tunnel_list("test");
        app.ui.tunnel_list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('n')), &tx);
        assert!(app.pending_tunnel_delete.is_none());
        assert_eq!(app.tunnel_list.len(), 1);
    }

    // --- Host form: baseline cleared after submit ---

    #[test]
    fn test_host_form_baseline_cleared_after_submit() {
        // Use a unique temp file to avoid race conditions with parallel tests
        // that share /tmp/test_config.
        let unique = format!(
            "/tmp/purple_test_baseline_{:?}",
            std::thread::current().id()
        );
        let config_path = PathBuf::from(&unique);
        std::fs::write(&config_path, "Host test\n  HostName test.com\n").unwrap();
        let config = SshConfigFile {
            elements: SshConfigFile::parse_content("Host test\n  HostName test.com\n"),
            path: config_path.clone(),
            crlf: false,
            bom: false,
        };
        let mut app = App::new(config);
        app.provider_config = test_provider_config();
        crate::preferences::set_path_override(PathBuf::from(format!("{}_prefs", unique)));
        app.form = crate::app::HostForm::new();
        app.form.alias = "newhost".to_string();
        app.form.hostname = "new.example.com".to_string();
        app.screen = Screen::AddHost;
        app.capture_form_mtime();
        app.capture_form_baseline();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.form_baseline.is_none());
        // Cleanup
        let _ = std::fs::remove_file(&unique);
        let _ = std::fs::remove_file(format!("{}_prefs", unique));
    }

    // --- Edge case: uppercase Y in discard confirms ---

    #[test]
    fn test_host_form_dirty_esc_uppercase_y_closes() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.form = crate::app::HostForm::new();
        app.screen = Screen::AddHost;
        app.capture_form_baseline();
        app.form.user = "ubuntu".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('Y')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
        assert!(app.form_baseline.is_none());
    }

    // --- Snippet form: dirty + n stays ---

    #[test]
    fn test_snippet_form_dirty_esc_n_stays() {
        let mut app = make_snippet_app();
        app.snippet_form = crate::app::SnippetForm::new();
        app.screen = Screen::SnippetForm {
            target_aliases: vec!["myserver".to_string()],
            editing: None,
        };
        app.capture_snippet_form_baseline();
        app.snippet_form.command = "changed".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('n')), &tx);
        assert!(matches!(app.screen, Screen::SnippetForm { .. }));
        assert!(!app.pending_discard_confirm);
    }

    // --- Tunnel form: dirty + y closes, dirty + n stays ---

    #[test]
    fn test_tunnel_form_dirty_esc_y_closes() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::TunnelForm {
            alias: "test".to_string(),
            editing: None,
        };
        app.tunnel_form = crate::app::TunnelForm::new();
        app.capture_tunnel_form_baseline();
        app.tunnel_form.remote_host = "db.local".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(matches!(app.screen, Screen::TunnelList { .. }));
        assert!(app.tunnel_form_baseline.is_none());
    }

    #[test]
    fn test_tunnel_form_dirty_esc_n_stays() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        app.screen = Screen::TunnelForm {
            alias: "test".to_string(),
            editing: None,
        };
        app.tunnel_form = crate::app::TunnelForm::new();
        app.capture_tunnel_form_baseline();
        app.tunnel_form.bind_port = "9001".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('n')), &tx);
        assert!(matches!(app.screen, Screen::TunnelForm { .. }));
        assert!(!app.pending_discard_confirm);
    }

    // --- Tunnel delete: other key ignored ---

    #[test]
    fn test_tunnel_delete_other_key_ignored() {
        let mut app =
            make_app("Host test\n  HostName test.com\n  LocalForward 8080 localhost:80\n");
        app.screen = Screen::TunnelList {
            alias: "test".to_string(),
        };
        app.refresh_tunnel_list("test");
        app.ui.tunnel_list_state.select(Some(0));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        assert_eq!(app.pending_tunnel_delete, Some(0));
        let _ = handle_key_event(&mut app, key(KeyCode::Char('z')), &tx);
        assert_eq!(app.pending_tunnel_delete, Some(0));
    }

    // --- Provider form: dirty + other key ignored ---

    #[test]
    fn test_provider_form_dirty_esc_other_key_ignored() {
        let mut app = make_form_app_focused_on("digitalocean", ProviderFormField::Token);
        app.capture_provider_form_baseline();
        app.provider_form.token = "newtoken".to_string();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        let _ = handle_key_event(&mut app, key(KeyCode::Char('x')), &tx);
        assert!(app.pending_discard_confirm);
    }

    // --- Stale purge tests ---

    #[test]
    fn test_x_key_opens_confirm_purge_stale() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('X')), &tx);
        match &app.screen {
            Screen::ConfirmPurgeStale { aliases, provider } => {
                assert_eq!(aliases.len(), 1);
                assert_eq!(aliases[0], "do-web");
                assert!(provider.is_none());
            }
            other => panic!("expected ConfirmPurgeStale, got {:?}", other),
        }
    }

    #[test]
    fn test_x_key_no_stale_shows_status() {
        let mut app = make_app("Host normal\n  HostName 1.2.3.4\n");
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('X')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
        let status = app.status.as_ref().expect("status should be set");
        assert!(
            status.text.contains("No stale hosts"),
            "expected 'No stale hosts' in status, got: {}",
            status.text
        );
    }

    #[test]
    fn test_confirm_purge_stale_y_deletes() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n\nHost keep\n  HostName 5.6.7.8\n",
        );
        app.screen = Screen::ConfirmPurgeStale {
            aliases: vec!["do-web".to_string()],
            provider: None,
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
        // The stale host should be gone, only "keep" remains
        let aliases: Vec<&str> = app.hosts.iter().map(|h| h.alias.as_str()).collect();
        assert!(!aliases.contains(&"do-web"), "stale host should be removed");
        assert!(aliases.contains(&"keep"), "non-stale host should remain");
    }

    #[test]
    fn test_confirm_purge_stale_esc_cancels() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        app.screen = Screen::ConfirmPurgeStale {
            aliases: vec!["do-web".to_string()],
            provider: None,
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::HostList));
        // Host should still exist
        assert_eq!(app.hosts.len(), 1);
        assert_eq!(app.hosts[0].alias, "do-web");
    }

    #[test]
    fn test_e_key_warns_on_stale_host() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('e')), &tx);
        // Edit form should open (warning, not block)
        assert!(matches!(app.screen, Screen::EditHost { .. }));
        let status = app.status.as_ref().expect("status should be set");
        assert!(status.text.contains("Stale host"));
        assert!(status.text.contains("DigitalOcean"));
        assert!(status.is_error);
    }

    #[test]
    fn test_d_key_warns_on_stale_host() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('d')), &tx);
        // Delete confirm should open (warning, not block)
        assert!(matches!(app.screen, Screen::ConfirmDelete { .. }));
        let status = app.status.as_ref().expect("status should be set");
        assert!(status.text.contains("Stale host"));
        assert!(status.is_error);
    }

    #[test]
    fn test_enter_on_stale_host_shows_warning() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        // Connection should still be pending
        assert!(app.pending_connect.is_some());
        // But status should show stale warning
        let status = app.status.as_ref().expect("status should be set");
        assert!(
            status.text.contains("Stale host"),
            "expected stale warning, got: {}",
            status.text
        );
        assert!(status.text.contains("DigitalOcean"));
    }

    #[test]
    fn test_enter_on_normal_host_no_stale_warning() {
        let mut app = make_app("Host normal\n  HostName 1.2.3.4\n");
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.pending_connect.is_some());
        // No stale warning
        assert!(app.status.is_none() || !app.status.as_ref().unwrap().text.contains("Stale"),);
    }

    #[test]
    fn test_search_enter_on_stale_host_shows_warning() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        // Enter search mode
        app.search.query = Some("do-web".to_string());
        app.apply_filter();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(app.pending_connect.is_some());
        let status = app.status.as_ref().expect("status should be set");
        assert!(
            status.text.contains("Stale host"),
            "expected stale warning in search mode, got: {}",
            status.text
        );
    }

    #[test]
    fn test_c_key_warns_on_stale_host() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('c')), &tx);
        assert!(matches!(app.screen, Screen::AddHost));
        let status = app.status.as_ref().expect("status should be set");
        assert!(
            status.text.contains("Stale host"),
            "expected stale warning, got: {}",
            status.text
        );
        assert!(status.is_error);
    }

    #[test]
    fn test_t_key_warns_on_stale_host() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('T')), &tx);
        assert!(
            matches!(app.screen, Screen::TunnelList { .. }),
            "expected TunnelList screen, got: {:?}",
            app.screen
        );
        let status = app.status.as_ref().expect("status should be set");
        assert!(
            status.text.contains("Stale host"),
            "expected stale warning, got: {}",
            status.text
        );
        assert!(status.is_error);
    }

    #[test]
    fn test_provider_x_key_opens_scoped_purge() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        app.screen = Screen::Providers;
        app.provider_config = test_provider_config();
        app.provider_config.set_section(ProviderSection {
            provider: "digitalocean".to_string(),
            token: "tok".to_string(),
            alias_prefix: "do".to_string(),
            user: "root".to_string(),
            identity_file: String::new(),
            url: String::new(),
            verify_tls: true,
            auto_sync: true,
            profile: String::new(),
            regions: String::new(),
            project: String::new(),
            compartment: String::new(),
        });
        // Select the DigitalOcean provider in the list
        let sorted = app.sorted_provider_names();
        let idx = sorted
            .iter()
            .position(|n| n == "digitalocean")
            .expect("digitalocean should be in sorted list");
        app.ui.provider_list_state.select(Some(idx));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('X')), &tx);
        match &app.screen {
            Screen::ConfirmPurgeStale { aliases, provider } => {
                assert_eq!(aliases, &vec!["do-web".to_string()]);
                assert_eq!(provider.as_deref(), Some("digitalocean"));
            }
            other => panic!("expected ConfirmPurgeStale, got {:?}", other),
        }
    }

    #[test]
    fn test_provider_purge_y_returns_to_providers() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        app.screen = Screen::ConfirmPurgeStale {
            aliases: vec!["do-web".to_string()],
            provider: Some("digitalocean".to_string()),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        assert!(
            matches!(app.screen, Screen::Providers),
            "expected Providers screen after provider-scoped purge, got: {:?}",
            app.screen
        );
    }

    #[test]
    fn test_provider_purge_esc_returns_to_providers() {
        let mut app = make_app(
            "Host do-web\n  HostName 1.2.3.4\n  # purple:provider digitalocean:123\n  # purple:stale 1711900000\n",
        );
        app.screen = Screen::ConfirmPurgeStale {
            aliases: vec!["do-web".to_string()],
            provider: Some("digitalocean".to_string()),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(
            matches!(app.screen, Screen::Providers),
            "expected Providers screen after Esc on provider-scoped purge, got: {:?}",
            app.screen
        );
        // Host should still exist (purge was cancelled)
        assert_eq!(app.hosts.len(), 1);
        assert_eq!(app.hosts[0].alias, "do-web");
    }

    // =========================================================================
    // Container handler tests
    // =========================================================================

    fn make_container_state(
        alias: &str,
        containers: Vec<crate::containers::ContainerInfo>,
    ) -> crate::app::ContainerState {
        let mut list_state = ratatui::widgets::ListState::default();
        if !containers.is_empty() {
            list_state.select(Some(0));
        }
        crate::app::ContainerState {
            alias: alias.to_string(),
            askpass: None,
            runtime: Some(crate::containers::ContainerRuntime::Docker),
            containers,
            list_state,
            loading: false,
            error: None,
            action_in_progress: None,
            confirm_action: None,
        }
    }

    fn make_container(id: &str, name: &str, state: &str) -> crate::containers::ContainerInfo {
        crate::containers::ContainerInfo {
            id: id.to_string(),
            names: name.to_string(),
            image: "test:latest".to_string(),
            state: state.to_string(),
            status: "Up".to_string(),
            ports: "".to_string(),
        }
    }

    #[test]
    fn test_shift_c_opens_containers() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('C')), &tx);
        assert!(
            matches!(app.screen, Screen::Containers { .. }),
            "expected Containers screen, got: {:?}",
            app.screen
        );
        assert!(
            app.container_state.is_some(),
            "container_state should be Some after Shift+C"
        );
    }

    #[test]
    fn test_shift_c_no_host_noop() {
        let mut app = make_app("");
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('C')), &tx);
        assert!(
            matches!(app.screen, Screen::HostList),
            "expected HostList when no hosts, got: {:?}",
            app.screen
        );
        assert!(app.container_state.is_none());
    }

    #[test]
    fn test_shift_c_loads_cache() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.container_cache.insert(
            "web".to_string(),
            crate::containers::ContainerCacheEntry {
                timestamp: 100,
                runtime: crate::containers::ContainerRuntime::Docker,
                containers: vec![make_container("abc", "nginx", "running")],
            },
        );
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('C')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert_eq!(state.containers.len(), 1);
        assert_eq!(state.containers[0].id, "abc");
        assert_eq!(
            state.runtime,
            Some(crate::containers::ContainerRuntime::Docker)
        );
    }

    #[test]
    fn test_shift_c_no_cache_empty() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('C')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert!(state.containers.is_empty());
        assert!(state.runtime.is_none());
    }

    #[test]
    fn test_containers_esc_closes() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state("web", vec![]));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::HostList));
        assert!(app.container_state.is_none());
    }

    #[test]
    fn test_containers_q_closes() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state("web", vec![]));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('q')), &tx);
        assert!(matches!(app.screen, Screen::HostList));
        assert!(app.container_state.is_none());
    }

    #[test]
    fn test_containers_j_moves_down() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let containers = vec![
            make_container("a", "web", "running"),
            make_container("b", "db", "running"),
            make_container("c", "cache", "exited"),
        ];
        app.container_state = Some(make_container_state("web", containers));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        let sel = app.container_state.as_ref().unwrap().list_state.selected();
        assert_eq!(sel, Some(1));
    }

    #[test]
    fn test_containers_k_moves_up() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let containers = vec![
            make_container("a", "web", "running"),
            make_container("b", "db", "running"),
        ];
        let mut state = make_container_state("web", containers);
        state.list_state.select(Some(1));
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        let sel = app.container_state.as_ref().unwrap().list_state.selected();
        assert_eq!(sel, Some(0));
    }

    #[test]
    fn test_containers_j_wraps() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let containers = vec![
            make_container("a", "web", "running"),
            make_container("b", "db", "running"),
        ];
        let mut state = make_container_state("web", containers);
        state.list_state.select(Some(1)); // at last
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        let sel = app.container_state.as_ref().unwrap().list_state.selected();
        assert_eq!(sel, Some(0), "j at last item should wrap to 0");
    }

    #[test]
    fn test_containers_k_wraps() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let containers = vec![
            make_container("a", "web", "running"),
            make_container("b", "db", "running"),
        ];
        app.container_state = Some(make_container_state("web", containers));
        // selection starts at 0
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        let sel = app.container_state.as_ref().unwrap().list_state.selected();
        assert_eq!(sel, Some(1), "k at first item should wrap to last");
    }

    #[test]
    fn test_containers_j_empty_noop() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state("web", vec![]));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        let sel = app.container_state.as_ref().unwrap().list_state.selected();
        assert_eq!(sel, None);
    }

    #[test]
    fn test_containers_k_empty_noop() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state("web", vec![]));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        let sel = app.container_state.as_ref().unwrap().list_state.selected();
        assert_eq!(sel, None);
    }

    #[test]
    fn test_containers_page_down() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let containers: Vec<_> = (0..20)
            .map(|i| make_container(&format!("c{i}"), &format!("svc{i}"), "running"))
            .collect();
        app.container_state = Some(make_container_state("web", containers));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::PageDown), &tx);
        let sel = app.container_state.as_ref().unwrap().list_state.selected();
        assert_eq!(sel, Some(10));
    }

    #[test]
    fn test_containers_page_up() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let containers: Vec<_> = (0..20)
            .map(|i| make_container(&format!("c{i}"), &format!("svc{i}"), "running"))
            .collect();
        let mut state = make_container_state("web", containers);
        state.list_state.select(Some(15));
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::PageUp), &tx);
        let sel = app.container_state.as_ref().unwrap().list_state.selected();
        assert_eq!(sel, Some(5));
    }

    #[test]
    fn test_containers_s_sets_action_in_progress() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state(
            "web",
            vec![make_container("abc123", "nginx", "exited")],
        ));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('s')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert!(
            state.action_in_progress.is_some(),
            "action_in_progress should be set after s"
        );
        assert!(
            state.action_in_progress.as_ref().unwrap().contains("start"),
            "action should contain 'start'"
        );
    }

    #[test]
    fn test_containers_x_shows_confirmation() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state(
            "web",
            vec![make_container("abc123", "nginx", "running")],
        ));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('x')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert!(state.confirm_action.is_some());
        let (action, name, _id) = state.confirm_action.as_ref().unwrap();
        assert_eq!(*action, crate::containers::ContainerAction::Stop);
        assert_eq!(name, "nginx");
    }

    #[test]
    fn test_containers_r_shows_confirmation() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state(
            "web",
            vec![make_container("abc123", "nginx", "running")],
        ));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('r')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert!(state.confirm_action.is_some());
        let (action, name, _id) = state.confirm_action.as_ref().unwrap();
        assert_eq!(*action, crate::containers::ContainerAction::Restart);
        assert_eq!(name, "nginx");
    }

    #[test]
    fn test_containers_y_confirms_action() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.confirm_action = Some((
            crate::containers::ContainerAction::Stop,
            "nginx".to_string(),
            "abc123".to_string(),
        ));
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert!(state.confirm_action.is_none());
        assert!(state.action_in_progress.is_some());
    }

    #[test]
    fn test_containers_esc_cancels_confirmation() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.confirm_action = Some((
            crate::containers::ContainerAction::Stop,
            "nginx".to_string(),
            "abc123".to_string(),
        ));
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        // Should cancel confirmation but stay in overlay
        assert!(app.container_state.is_some());
        assert!(
            app.container_state
                .as_ref()
                .unwrap()
                .confirm_action
                .is_none()
        );
        assert!(matches!(app.screen, Screen::Containers { .. }));
    }

    #[test]
    fn test_containers_action_blocked_when_in_progress() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.action_in_progress = Some("stop nginx...".to_string());
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('s')), &tx);
        // action_in_progress should remain the same (not changed to start)
        let state = app.container_state.as_ref().unwrap();
        assert_eq!(state.action_in_progress.as_deref(), Some("stop nginx..."));
    }

    #[test]
    fn test_containers_action_no_selection_noop() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state = make_container_state("web", vec![]);
        state.list_state.select(None);
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('s')), &tx);
        assert!(
            app.container_state
                .as_ref()
                .unwrap()
                .action_in_progress
                .is_none(),
            "no action should start without selection"
        );
    }

    #[test]
    fn test_containers_action_no_runtime_noop() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.runtime = None;
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('s')), &tx);
        assert!(
            app.container_state
                .as_ref()
                .unwrap()
                .action_in_progress
                .is_none(),
            "no action should start without runtime"
        );
    }

    #[test]
    fn test_containers_r_uppercase_refreshes() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state(
            "web",
            vec![make_container("abc123", "nginx", "running")],
        ));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('R')), &tx);
        assert!(
            app.container_state.as_ref().unwrap().loading,
            "loading should be true after R"
        );
    }

    #[test]
    fn test_containers_r_uppercase_blocked_when_in_progress() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.action_in_progress = Some("restart nginx...".to_string());
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('R')), &tx);
        assert!(
            !app.container_state.as_ref().unwrap().loading,
            "loading should remain false when action is in progress"
        );
    }

    #[test]
    fn test_containers_unknown_key_noop() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let containers = vec![make_container("abc123", "nginx", "running")];
        app.container_state = Some(make_container_state("web", containers.clone()));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('z')), &tx);
        assert!(matches!(app.screen, Screen::Containers { .. }));
        let state = app.container_state.as_ref().unwrap();
        assert_eq!(state.list_state.selected(), Some(0));
        assert!(state.action_in_progress.is_none());
        assert!(!state.loading);
    }

    #[test]
    fn test_containers_y_noop_without_pending() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state(
            "web",
            vec![make_container("abc123", "nginx", "running")],
        ));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('y')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert!(
            state.action_in_progress.is_none(),
            "no action should start when confirm_action is None"
        );
        assert!(
            state.confirm_action.is_none(),
            "confirm_action should remain None"
        );
    }

    #[test]
    fn test_containers_x_blocked_when_action_in_progress() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.action_in_progress = Some("stop nginx...".to_string());
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('x')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert!(
            state.confirm_action.is_none(),
            "x should not open confirmation when action is in progress"
        );
    }

    #[test]
    fn test_containers_r_blocked_when_action_in_progress() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.action_in_progress = Some("stop nginx...".to_string());
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('r')), &tx);
        let state = app.container_state.as_ref().unwrap();
        assert!(
            state.confirm_action.is_none(),
            "r should not open confirmation when action is in progress"
        );
    }

    #[test]
    fn test_containers_x_blocked_when_confirm_pending() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.confirm_action = Some((
            crate::containers::ContainerAction::Stop,
            "nginx".to_string(),
            "abc123".to_string(),
        ));
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('x')), &tx);
        let state = app.container_state.as_ref().unwrap();
        let (action, name, _id) = state.confirm_action.as_ref().unwrap();
        assert_eq!(
            *action,
            crate::containers::ContainerAction::Stop,
            "confirm_action should remain the original Stop"
        );
        assert_eq!(name, "nginx");
    }

    #[test]
    fn test_containers_r_blocked_when_confirm_pending() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.confirm_action = Some((
            crate::containers::ContainerAction::Stop,
            "nginx".to_string(),
            "abc123".to_string(),
        ));
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('r')), &tx);
        let state = app.container_state.as_ref().unwrap();
        let (action, name, _id) = state.confirm_action.as_ref().unwrap();
        assert_eq!(
            *action,
            crate::containers::ContainerAction::Stop,
            "confirm_action should remain the original Stop, not change to Restart"
        );
        assert_eq!(name, "nginx");
    }

    // --- Help key (?) tests for all overlay screens ---

    #[test]
    fn test_file_browser_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::FileBrowser {
            alias: "web".to_string(),
        };
        app.file_browser = Some(crate::file_browser::FileBrowserState {
            alias: "web".to_string(),
            askpass: None,
            active_pane: crate::file_browser::BrowserPane::Local,
            local_path: std::path::PathBuf::from("/tmp"),
            local_entries: Vec::new(),
            local_list_state: ratatui::widgets::ListState::default(),
            local_selected: std::collections::HashSet::new(),
            local_error: None,
            remote_path: "/home".to_string(),
            remote_entries: Vec::new(),
            remote_list_state: ratatui::widgets::ListState::default(),
            remote_selected: std::collections::HashSet::new(),
            remote_error: None,
            remote_loading: false,
            show_hidden: false,
            sort: crate::file_browser::BrowserSort::Name,
            confirm_copy: None,
            transferring: None,
            transfer_error: None,
            connection_recorded: false,
        });
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::FileBrowser { .. }));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
    }

    #[test]
    fn test_file_browser_help_esc_returns() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::FileBrowser {
                alias: "web".to_string(),
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::FileBrowser { .. }));
    }

    #[test]
    fn test_snippet_picker_question_opens_help() {
        let mut app = make_snippet_app();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::SnippetPicker { .. }));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
    }

    #[test]
    fn test_snippet_picker_help_esc_returns() {
        let mut app = make_snippet_app();
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::SnippetPicker {
                target_aliases: vec!["myserver".to_string()],
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::SnippetPicker { .. }));
    }

    #[test]
    fn test_snippet_output_question_opens_help() {
        let mut app = make_snippet_app();
        // First enter snippet output by pressing Enter
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Enter), &tx);
        assert!(matches!(app.screen, Screen::SnippetOutput { .. }));

        // Now press ? to open help
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::SnippetOutput { .. }));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
    }

    #[test]
    fn test_snippet_output_help_esc_returns() {
        let mut app = make_snippet_app();
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::SnippetOutput {
                snippet_name: "check-disk".to_string(),
                target_aliases: vec!["myserver".to_string()],
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::SnippetOutput { .. }));
    }

    #[test]
    fn test_containers_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        app.container_state = Some(make_container_state("web", vec![]));
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::Containers { .. }));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
    }

    #[test]
    fn test_containers_help_esc_returns() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::Containers {
                alias: "web".to_string(),
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::Containers { .. }));
    }

    #[test]
    fn test_tunnel_list_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::TunnelList {
            alias: "web".to_string(),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::TunnelList { .. }));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
    }

    #[test]
    fn test_tunnel_list_help_esc_returns() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::TunnelList {
                alias: "web".to_string(),
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert!(matches!(app.screen, Screen::TunnelList { .. }));
    }

    // --- Direct ? from HostList ---

    #[test]
    fn test_host_list_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::HostList));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
    }

    // --- ? guard bypass tests ---

    #[test]
    fn test_tunnel_delete_confirmation_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::TunnelList {
            alias: "web".to_string(),
        };
        app.pending_tunnel_delete = Some(0);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::TunnelList { .. }));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
        assert_eq!(
            app.pending_tunnel_delete,
            Some(0),
            "pending_tunnel_delete should be preserved"
        );
    }

    #[test]
    fn test_container_confirm_action_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Containers {
            alias: "web".to_string(),
        };
        let mut state =
            make_container_state("web", vec![make_container("abc123", "nginx", "running")]);
        state.confirm_action = Some((
            crate::containers::ContainerAction::Stop,
            "nginx".to_string(),
            "abc123".to_string(),
        ));
        app.container_state = Some(state);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::Containers { .. }));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
    }

    #[test]
    fn test_snippet_picker_pending_delete_question_opens_help() {
        let mut app = make_snippet_app();
        app.pending_snippet_delete = Some(0);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::SnippetPicker { .. }));
            }
            other => panic!("Expected Help screen, got {:?}", other),
        }
    }

    // --- Help scroll tests ---

    #[test]
    fn test_help_j_increments_scroll() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::HostList),
        };
        app.ui.help_scroll = 0;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('j')), &tx);
        assert_eq!(app.ui.help_scroll, 1);
    }

    #[test]
    fn test_help_k_does_not_underflow() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::HostList),
        };
        app.ui.help_scroll = 0;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('k')), &tx);
        assert_eq!(app.ui.help_scroll, 0);
    }

    #[test]
    fn test_help_page_down_increments_by_ten() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::HostList),
        };
        app.ui.help_scroll = 0;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::PageDown), &tx);
        assert_eq!(app.ui.help_scroll, 10);
    }

    #[test]
    fn test_help_page_up_does_not_underflow() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::HostList),
        };
        app.ui.help_scroll = 0;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::PageUp), &tx);
        assert_eq!(app.ui.help_scroll, 0);
    }

    #[test]
    fn test_help_scroll_reset_on_close() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::HostList),
        };
        app.ui.help_scroll = 7;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        assert_eq!(app.ui.help_scroll, 0);
        assert!(matches!(app.screen, Screen::HostList));
    }

    // --- Help close via q and ? ---

    #[test]
    fn test_help_q_closes_and_returns() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::TunnelList {
                alias: "web".to_string(),
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('q')), &tx);
        assert!(matches!(app.screen, Screen::TunnelList { .. }));
        assert_eq!(app.ui.help_scroll, 0);
    }

    #[test]
    fn test_help_question_again_closes_and_returns() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::Containers {
                alias: "web".to_string(),
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        assert!(matches!(app.screen, Screen::Containers { .. }));
        assert_eq!(app.ui.help_scroll, 0);
    }

    // --- Return screen field preservation ---

    #[test]
    fn test_file_browser_help_return_preserves_alias() {
        let mut app = make_app("Host myserver\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::FileBrowser {
                alias: "myserver".to_string(),
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        match &app.screen {
            Screen::FileBrowser { alias } => {
                assert_eq!(alias, "myserver");
            }
            other => panic!("Expected FileBrowser, got {:?}", other),
        }
    }

    #[test]
    fn test_snippet_output_help_return_preserves_fields() {
        let mut app = make_app("Host a\n  HostName 1.2.3.4\nHost b\n  HostName 5.6.7.8\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::SnippetOutput {
                snippet_name: "check-disk".to_string(),
                target_aliases: vec!["a".to_string(), "b".to_string()],
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        match &app.screen {
            Screen::SnippetOutput {
                snippet_name,
                target_aliases,
            } => {
                assert_eq!(snippet_name, "check-disk");
                assert_eq!(target_aliases, &vec!["a".to_string(), "b".to_string()]);
            }
            other => panic!("Expected SnippetOutput, got {:?}", other),
        }
    }

    #[test]
    fn test_tunnel_list_help_return_preserves_alias() {
        let mut app = make_app("Host myserver\n  HostName 1.2.3.4\n");
        app.screen = Screen::Help {
            return_screen: Box::new(Screen::TunnelList {
                alias: "myserver".to_string(),
            }),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);
        match &app.screen {
            Screen::TunnelList { alias } => {
                assert_eq!(alias, "myserver");
            }
            other => panic!("Expected TunnelList, got {:?}", other),
        }
    }

    // --- Non-help screens ignore ? ---

    #[test]
    fn test_confirm_delete_question_does_not_open_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::ConfirmDelete {
            alias: "web".to_string(),
        };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        assert!(
            matches!(app.screen, Screen::ConfirmDelete { .. }),
            "Expected ConfirmDelete screen, got {:?}",
            app.screen
        );
    }

    #[test]
    fn test_tag_picker_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::TagPicker;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::TagPicker));
            }
            other => panic!("expected Help, got {:?}", std::mem::discriminant(other)),
        }
    }

    #[test]
    fn test_key_list_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::KeyList;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::KeyList));
            }
            other => panic!("expected Help, got {:?}", std::mem::discriminant(other)),
        }
    }

    #[test]
    fn test_key_detail_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::KeyDetail { index: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::KeyDetail { .. }));
            }
            other => panic!("expected Help, got {:?}", std::mem::discriminant(other)),
        }
    }

    #[test]
    fn test_host_detail_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::HostDetail { index: 0 };
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::HostDetail { .. }));
            }
            other => panic!("expected Help, got {:?}", std::mem::discriminant(other)),
        }
    }

    #[test]
    fn test_providers_question_opens_help() {
        let mut app = make_app("Host web\n  HostName 1.2.3.4\n");
        app.screen = Screen::Providers;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('?')), &tx);
        match &app.screen {
            Screen::Help { return_screen } => {
                assert!(matches!(**return_screen, Screen::Providers));
            }
            other => panic!("expected Help, got {:?}", std::mem::discriminant(other)),
        }
    }

    // --- g-key GroupBy cycle ---

    #[test]
    fn g_key_none_to_provider() {
        let mut app =
            make_app("Host web1\n  HostName 1.2.3.4\n  # purple:provider digitalocean:1\n");
        assert_eq!(app.group_by, crate::app::GroupBy::None);
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('g')), &tx);
        assert_eq!(app.group_by, crate::app::GroupBy::Provider);
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn g_key_provider_to_tag_mode_when_tags_exist() {
        let content = "\
Host web1
  HostName 1.1.1.1
  # purple:tags production
";
        let mut app = make_app(content);
        app.group_by = crate::app::GroupBy::Provider;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('g')), &tx);
        assert!(
            matches!(app.group_by, crate::app::GroupBy::Tag(_)),
            "expected Tag mode, got {:?}",
            app.group_by
        );
        assert!(
            matches!(app.screen, Screen::HostList),
            "should stay on HostList, not open picker"
        );
    }

    #[test]
    fn g_key_provider_to_none_when_no_tags() {
        let content = "\
Host web1
  HostName 1.1.1.1
";
        let mut app = make_app(content);
        app.group_by = crate::app::GroupBy::Provider;
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('g')), &tx);
        assert_eq!(app.group_by, crate::app::GroupBy::None);
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn g_key_tag_to_none() {
        let content = "\
Host web1
  HostName 1.1.1.1
  # purple:tags production
";
        let mut app = make_app(content);
        app.group_by = crate::app::GroupBy::Tag("production".to_string());
        app.apply_sort();
        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Char('g')), &tx);
        assert_eq!(app.group_by, crate::app::GroupBy::None);
        assert!(matches!(app.screen, Screen::HostList));
        assert!(
            app.display_list
                .iter()
                .all(|item| matches!(item, crate::app::HostListItem::Host { .. }))
        );
    }

    #[test]
    fn g_key_full_cycle_with_tags() {
        // None → Provider → Tag → None
        let content = "\
Host web1
  HostName 1.1.1.1
  # purple:tags production
";
        let mut app = make_app(content);
        assert_eq!(app.group_by, crate::app::GroupBy::None);

        let (tx, _rx) = mpsc::channel();

        // None → Provider
        let _ = handle_key_event(&mut app, key(KeyCode::Char('g')), &tx);
        assert_eq!(app.group_by, crate::app::GroupBy::Provider);

        // Provider → Tag (direct, no picker)
        let _ = handle_key_event(&mut app, key(KeyCode::Char('g')), &tx);
        assert!(
            matches!(app.group_by, crate::app::GroupBy::Tag(_)),
            "expected Tag mode, got {:?}",
            app.group_by
        );
        assert!(matches!(app.screen, Screen::HostList));

        // Tag → None
        let _ = handle_key_event(&mut app, key(KeyCode::Char('g')), &tx);
        assert_eq!(app.group_by, crate::app::GroupBy::None);
    }

    #[test]
    fn g_key_tag_to_none_empty_hosts() {
        let (tx, _rx) = std::sync::mpsc::channel();
        let mut app = make_app("");
        app.group_by = crate::app::GroupBy::Tag("production".to_string());

        let key = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
        let _ = handle_key_event(&mut app, key, &tx);

        assert_eq!(app.group_by, crate::app::GroupBy::None);
        assert!(matches!(app.screen, Screen::HostList));
    }

    // =========================================================================
    // Group header collapse tests
    // =========================================================================

    #[test]
    fn test_enter_on_group_header_does_not_connect() {
        // Enter on a group header should not crash or connect — group headers are
        // no longer collapsible. Navigation happens via Tab (group_filter).
        let mut app = make_app(
            "Host web1\n  HostName 1.1.1.1\n  # purple:tags production\n\nHost web2\n  HostName 2.2.2.2\n  # purple:tags staging\n",
        );
        app.group_by = crate::app::GroupBy::Tag("production".to_string());
        app.sort_mode = crate::app::SortMode::AlphaAlias;
        app.apply_sort();

        // Find the group header position
        let header_pos = app
            .display_list
            .iter()
            .position(|item| {
                matches!(item, crate::app::HostListItem::GroupHeader(t) if t == "production")
            })
            .expect("should have a production group header");
        app.ui.list_state.select(Some(header_pos));

        // Press Enter — should not panic and group_filter should remain None
        let (tx, _rx) = mpsc::channel();
        handle_key_event(&mut app, key(KeyCode::Enter), &tx).unwrap();

        assert!(
            app.group_filter.is_none(),
            "group_filter should not be set by Enter on header"
        );
    }

    // =========================================================================
    // Ctrl+A select all tests
    // =========================================================================

    #[test]
    fn test_ctrl_a_selects_all_visible_hosts() {
        let mut app = make_app(
            "Host web1\n  HostName 1.1.1.1\n\nHost web2\n  HostName 2.2.2.2\n\nHost web3\n  HostName 3.3.3.3\n",
        );
        app.apply_sort();
        assert!(app.multi_select.is_empty());

        let (tx, _rx) = mpsc::channel();
        handle_key_event(&mut app, ctrl_key('a'), &tx).unwrap();

        // All 3 hosts should be selected
        assert_eq!(app.multi_select.len(), 3);

        // Press Ctrl+A again to deselect all
        handle_key_event(&mut app, ctrl_key('a'), &tx).unwrap();
        assert!(app.multi_select.is_empty());
    }

    #[test]
    fn test_ctrl_a_in_search_mode_selects_filtered() {
        let mut app = make_app(
            "Host prod-web\n  HostName 1.1.1.1\n\nHost prod-db\n  HostName 2.2.2.2\n\nHost staging-app\n  HostName 3.3.3.3\n",
        );
        app.apply_sort();

        // Enter search mode and filter to "prod"
        app.search.query = Some("prod".to_string());
        app.apply_filter();
        assert_eq!(app.search.filtered_indices.len(), 2);
        assert!(app.multi_select.is_empty());

        // Ctrl+A should select only the 2 filtered hosts
        let (tx, _rx) = mpsc::channel();
        handle_key_event(&mut app, ctrl_key('a'), &tx).unwrap();
        assert_eq!(app.multi_select.len(), 2);

        // Press Ctrl+A again to deselect
        handle_key_event(&mut app, ctrl_key('a'), &tx).unwrap();
        assert!(app.multi_select.is_empty());
    }

    // =========================================================================
    // Tab / Shift+Tab / Esc group-filter tests (HostList screen)
    // =========================================================================

    /// Build an app with two provider-tagged hosts so that group_by=Provider
    /// produces a non-empty group_tab_order after apply_sort().
    fn make_provider_grouped_app() -> App {
        let content = "\
Host aws-web1
  HostName 1.1.1.1
  # purple:provider aws:i-123

Host do-web2
  HostName 2.2.2.2
  # purple:provider digitalocean:abc
";
        let mut app = make_app(content);
        app.group_by = crate::app::GroupBy::Provider;
        app.apply_sort();
        app
    }

    #[test]
    fn tab_on_host_list_filters_to_first_group() {
        let mut app = make_provider_grouped_app();
        assert!(
            !app.group_tab_order.is_empty(),
            "expected non-empty group_tab_order after apply_sort with Provider grouping"
        );
        assert!(app.group_filter.is_none(), "filter should start as None");

        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);

        assert!(
            app.group_filter.is_some(),
            "group_filter should be Some after Tab"
        );
        assert_eq!(
            app.group_filter.as_deref(),
            Some(app.group_tab_order[0].as_str())
        );
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn shift_tab_on_host_list_filters_to_last_group() {
        let mut app = make_provider_grouped_app();
        let last_group = app.group_tab_order.last().unwrap().clone();
        assert!(app.group_filter.is_none(), "filter should start as None");

        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(
            &mut app,
            KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT),
            &tx,
        );

        assert_eq!(
            app.group_filter.as_deref(),
            Some(last_group.as_str()),
            "BackTab from All should land on the last group"
        );
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn tab_cycles_back_to_all() {
        let mut app = make_provider_grouped_app();
        // There are exactly 2 groups (aws, digitalocean). Set filter to the last one.
        let last_group = app.group_tab_order.last().unwrap().clone();
        app.group_filter = Some(last_group);
        app.apply_sort();

        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Tab), &tx);

        assert!(
            app.group_filter.is_none(),
            "Tab past the last group should wrap back to All (None)"
        );
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn esc_clears_group_filter() {
        let mut app = make_provider_grouped_app();
        let first_group = app.group_tab_order[0].clone();
        app.group_filter = Some(first_group);
        app.apply_sort();
        assert!(app.running);

        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);

        assert!(
            app.group_filter.is_none(),
            "Esc with active group_filter should clear it"
        );
        assert!(app.running, "Esc with active filter should NOT quit");
        assert!(matches!(app.screen, Screen::HostList));
    }

    #[test]
    fn esc_quits_when_no_filter() {
        let mut app = make_app("Host test\n  HostName test.com\n");
        assert!(app.group_filter.is_none());
        assert!(app.running);

        let (tx, _rx) = mpsc::channel();
        let _ = handle_key_event(&mut app, key(KeyCode::Esc), &tx);

        assert!(!app.running, "Esc with no group_filter should quit");
    }

    #[test]
    fn test_p_key_clears_ping_increments_generation() {
        let mut app = make_app("Host web1\n  HostName 1.1.1.1\n");
        // Pre-populate ping status to simulate completed pings
        app.ping_status.insert(
            "web1".to_string(),
            crate::app::PingStatus::Reachable { rtt_ms: 10 },
        );
        app.filter_down_only = true;
        app.ping_checked_at = Some(std::time::Instant::now());
        assert_eq!(app.ping_generation, 0);

        let (tx, _rx) = std::sync::mpsc::channel();
        handle_key_event(&mut app, key(KeyCode::Char('P')), &tx).unwrap();

        assert!(app.ping_status.is_empty());
        assert_eq!(app.ping_generation, 1);
        assert!(!app.filter_down_only);
        assert!(app.ping_checked_at.is_none());
    }

    #[test]
    fn test_bang_key_without_pings_shows_error() {
        let mut app = make_app("Host web1\n  HostName 1.1.1.1\n");
        assert!(app.ping_status.is_empty());
        let (tx, _rx) = std::sync::mpsc::channel();
        handle_key_event(&mut app, key(KeyCode::Char('!')), &tx).unwrap();
        assert!(!app.filter_down_only);
        assert!(app.status.as_ref().unwrap().is_error);
    }

    #[test]
    fn test_bang_key_toggles_down_only_on() {
        let mut app = make_app("Host web1\n  HostName 1.1.1.1\nHost web2\n  HostName 2.2.2.2\n");
        app.ping_status
            .insert("web1".to_string(), crate::app::PingStatus::Unreachable);
        app.ping_status.insert(
            "web2".to_string(),
            crate::app::PingStatus::Reachable { rtt_ms: 10 },
        );
        let (tx, _rx) = std::sync::mpsc::channel();
        handle_key_event(&mut app, key(KeyCode::Char('!')), &tx).unwrap();
        assert!(app.filter_down_only);
        assert!(app.search.query.is_some());
        // Only web1 (Unreachable) should be in filtered results
        assert_eq!(app.search.filtered_indices.len(), 1);
    }

    #[test]
    fn test_bang_key_toggles_down_only_off() {
        let mut app = make_app("Host web1\n  HostName 1.1.1.1\nHost web2\n  HostName 2.2.2.2\n");
        app.ping_status
            .insert("web1".to_string(), crate::app::PingStatus::Unreachable);
        app.ping_status.insert(
            "web2".to_string(),
            crate::app::PingStatus::Reachable { rtt_ms: 10 },
        );
        let (tx, _rx) = std::sync::mpsc::channel();
        // Toggle on
        handle_key_event(&mut app, key(KeyCode::Char('!')), &tx).unwrap();
        assert!(app.filter_down_only);
        // Toggle off
        handle_key_event(&mut app, key(KeyCode::Char('!')), &tx).unwrap();
        assert!(!app.filter_down_only);
        assert!(app.search.query.is_none());
    }
}
