//! E2E coverage for the Orchestrator "New Workspace" form's path-
//! completion popup behaviour:
//!
//! 1. The dropdown renders inside a bordered box (it used to be bare
//!    overlay rows painted on top of the worktree toggle).
//! 2. Tab accepts the highlighted suggestion into the field.
//! 3. Enter does NOT accept the suggestion — it leaves the typed
//!    text intact and proceeds (matches bash / fish / readline
//!    path-completion conventions). Before the fix, the host's
//!    picker-style smart-key wiring fired the completion list's
//!    activate event on Enter and silently overwrote the field.
//!
//! Each test sets up a workspace with two predictable subdirs
//! (`alpha_dir/` and `alpha_two/`). The Project Path is driven via
//! an absolute path (`<workspace>/al`) so the plugin's
//! `fetchPathCompletions` reads the workspace directly — its
//! `parent = "."` branch for un-slashed inputs would resolve
//! against the cargo-test process cwd, not the harness workspace.

#![cfg(feature = "plugins")]

use crate::common::harness::{copy_plugin, copy_plugin_lib, EditorTestHarness};
use crossterm::event::{KeyCode, KeyModifiers};
use std::fs;
use std::path::PathBuf;

/// Build a workspace with two `alpha*` subdirs and the orchestrator
/// plugin installed. Returns (tempdir guard, canonicalized
/// workspace path). The path is canonicalized so screen matching
/// is stable on systems where `/tmp` is a symlink (e.g. macOS).
fn set_up_workspace() -> (tempfile::TempDir, PathBuf) {
    fresh::i18n::set_locale("en");

    let temp = tempfile::tempdir().unwrap();
    let workspace = temp.path().canonicalize().unwrap();

    fs::create_dir(workspace.join("alpha_dir")).unwrap();
    fs::create_dir(workspace.join("alpha_two")).unwrap();

    let plugins_dir = workspace.join("plugins");
    fs::create_dir_all(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir);
    copy_plugin(&plugins_dir, "orchestrator");

    (temp, workspace)
}

/// Workspace variant for the "popup with > visible_rows
/// candidates" scenario. Creates 10 `alpha_NN` subdirs so the
/// default-5 popup needs to scroll. Returns workspace path +
/// the sorted candidate-name list so callers can spot-check
/// which entries are visible / hidden in any given scroll
/// position.
fn set_up_workspace_many_alphas() -> (tempfile::TempDir, PathBuf, Vec<String>) {
    fresh::i18n::set_locale("en");

    let temp = tempfile::tempdir().unwrap();
    let workspace = temp.path().canonicalize().unwrap();

    let mut names: Vec<String> = (0..10).map(|i| format!("alpha_{:02}", i)).collect();
    for n in &names {
        fs::create_dir(workspace.join(n)).unwrap();
    }
    names.sort();

    let plugins_dir = workspace.join("plugins");
    fs::create_dir_all(&plugins_dir).unwrap();
    copy_plugin_lib(&plugins_dir);
    copy_plugin(&plugins_dir, "orchestrator");

    (temp, workspace, names)
}

fn wait_for_new_session_command(harness: &mut EditorTestHarness) {
    harness
        .wait_until(|h| {
            let reg = h.editor().command_registry().read().unwrap();
            reg.get_all()
                .iter()
                .any(|c| c.get_localized_name() == "Orchestrator: New Workspace")
        })
        .unwrap();
}

fn open_new_session_form(harness: &mut EditorTestHarness) {
    harness
        .send_key(KeyCode::Char('p'), KeyModifiers::CONTROL)
        .unwrap();
    harness.wait_for_prompt().unwrap();
    harness.type_text("Orchestrator: New Workspace").unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("Orchestrator: New Workspace"))
        .unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness
        .wait_until(|h| {
            h.screen_to_string()
                .contains("ORCHESTRATOR :: New Workspace")
        })
        .unwrap();
}

/// Read the bracketed text inside the Project Path field from the
/// rendered screen. The field renders as `│ [<value>...] │` on the
/// row after the `Project Path` label. Returns the trimmed value.
fn project_path_field_value(screen: &str) -> String {
    let lines: Vec<&str> = screen.lines().collect();
    let label_row = lines
        .iter()
        .position(|l| l.contains("Project Path"))
        .expect("Project Path label must appear on screen");
    for next in lines.iter().skip(label_row + 1).take(3) {
        if let Some(open) = next.find('[') {
            if let Some(close_rel) = next[open + 1..].find(']') {
                return next[open + 1..open + 1 + close_rel].trim().to_string();
            }
        }
    }
    panic!(
        "Could not find [...] field after Project Path label.\nScreen:\n{}",
        screen
    );
}

/// True when the rendered screen contains a dim `┄┄┄...┄┄┄`
/// separator row — the host-rendered popup's replacement for
/// the input field's normal `╰─...─╯` bottom border. Its
/// presence is the load-bearing visual cue that input + popup
/// are part of one unified box: above the separator is the
/// active input, below it (and inside the labeled section's
/// side borders) are the candidate rows.
fn screen_has_completion_dim_separator(screen: &str) -> bool {
    screen.lines().any(|l| {
        if let Some(start) = l.find('┄') {
            let rest = &l[start..];
            let run: String = rest.chars().take_while(|c| *c == '┄').collect();
            return run.chars().count() >= 8;
        }
        false
    })
}

/// Type `<workspace>/al` into the focused Project Path field and
/// wait for the completion dropdown to surface both `alpha_dir/`
/// and `alpha_two/` candidates. Returns the typed prefix so the
/// caller can compare against the field value.
fn type_alpha_prefix_and_wait(
    harness: &mut EditorTestHarness,
    workspace: &std::path::Path,
) -> String {
    let prefix = format!("{}/al", workspace.display());
    harness.type_text(&prefix).unwrap();
    harness
        .wait_until(|h| {
            let s = h.screen_to_string();
            s.contains("alpha_dir/") && s.contains("alpha_two/")
        })
        .unwrap();
    prefix
}

/// The host-rendered popup integrates with the wrapping
/// labeled-section chrome: the input field's normal bottom
/// border becomes a dim `┄┄┄...┄┄┄` separator (cueing that the
/// box has extended downward), and the side borders continue
/// past the input through the candidate rows.
#[test]
fn completion_popup_renders_with_dim_separator() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    type_alpha_prefix_and_wait(&mut harness, &workspace);

    let screen = harness.screen_to_string();
    assert!(
        screen_has_completion_dim_separator(&screen),
        "completion popup must render with a dim `┄┄┄...┄┄┄` separator \
         between input and candidates. Screen:\n{}",
        screen,
    );
}

/// Tab on an *un-engaged* popup (one the user hasn't stepped into with
/// ↑/↓, so no row is highlighted) does NOT accept — it moves focus to
/// the next field, closing the popup and leaving the typed text intact.
/// (Accepting is `↓`-then-Tab/Enter, or a click — see
/// `down_then_tab_accepts_completion`.) Before the focus-model fix, Tab
/// always fired the completion's accept, so the number of Tabs to reach
/// a button was unpredictable.
#[test]
fn tab_advances_focus_without_accepting_completion() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    let typed = type_alpha_prefix_and_wait(&mut harness, &workspace);

    harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
    // Popup closes (Tab moved focus); the typed text is untouched —
    // Tab doesn't accept `alpha_dir/` because the user never stepped
    // into the dropdown.
    harness
        .wait_until(|h| !h.screen_to_string().contains("alpha_two/"))
        .unwrap();
    assert_eq!(
        project_path_field_value(&harness.screen_to_string()),
        typed,
        "Tab must leave the typed text intact (it advances focus, it does not accept). \
         Screen:\n{}",
        harness.screen_to_string(),
    );
    // Focus left the Project Path field: exactly one `▸` marker is on
    // screen, and it's no longer on the path line.
    let screen = harness.screen_to_string();
    assert_eq!(
        screen.matches('▸').count(),
        1,
        "exactly one control is focused at a time. Screen:\n{}",
        screen,
    );
    let path_line = screen
        .lines()
        .find(|l| l.contains(&typed))
        .expect("project path line present");
    assert!(
        !path_line.contains('▸'),
        "Tab moved focus off Project Path, so its `▸` marker is gone. Line: {:?}",
        path_line,
    );
}

/// `↓` steps into the open dropdown (highlighting a row) and then Tab
/// applies that highlighted candidate into the field — the "accept if
/// engaged" half of Tab's behaviour. Focus stays on the field (the
/// accepted value is left visible/editable), unlike an un-engaged Tab
/// which advances. Mirrors `down_then_enter_accepts_completion`.
#[test]
fn down_then_tab_accepts_completion() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    type_alpha_prefix_and_wait(&mut harness, &workspace);

    // Step into the dropdown (↓ highlights the top row), then Tab
    // applies it.
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();

    let expected = format!("{}/alpha_dir/", workspace.display());
    harness
        .wait_until(|h| project_path_field_value(&h.screen_to_string()) == expected)
        .unwrap();

    // Tab-accept keeps focus on the Project Path field (it didn't
    // advance): the `▸` marker is still on the path line.
    let screen = harness.screen_to_string();
    let path_line = screen
        .lines()
        .find(|l| l.contains("alpha_dir/"))
        .expect("project path line present");
    assert!(
        path_line.contains('▸'),
        "Tab-accept should leave focus on the Project Path field. Line: {:?}\nScreen:\n{}",
        path_line,
        screen,
    );
}

/// `↓` steps into the open dropdown (highlighting a row) and then
/// Enter accepts the highlighted candidate into the field. Together
/// with `down_then_tab_accepts_completion`, the keyboard accept paths
/// are `↓`-then-Enter and `↓`-then-Tab (plus a click).
#[test]
fn down_then_enter_accepts_completion() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    type_alpha_prefix_and_wait(&mut harness, &workspace);

    // Step into the dropdown, then accept.
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    let expected = format!("{}/alpha_dir/", workspace.display());
    harness
        .wait_until(|h| project_path_field_value(&h.screen_to_string()) == expected)
        .unwrap();
}

/// Enter must NOT accept the highlighted completion. Before the
/// fix, Enter routed through the host's picker-style smart-key
/// wiring and overwrote the field with the highlighted suggestion.
/// After the fix, the form's explicit Enter binding closes the
/// dropdown without accepting and forwards Enter through to the
/// smart-key dispatcher's focus-advance branch — leaving the typed
/// text intact.
#[test]
fn enter_keeps_typed_text_when_completion_open() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    let typed = type_alpha_prefix_and_wait(&mut harness, &workspace);

    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();

    // Dropdown should close — neither `alpha_dir/` nor `alpha_two/`
    // should remain on screen as suggestions. Wait for that so
    // we're reading a steady state, not the in-flight render.
    harness
        .wait_until(|h| !h.screen_to_string().contains("alpha_two/"))
        .unwrap();

    assert_eq!(
        project_path_field_value(&harness.screen_to_string()),
        typed,
        "Enter must leave the typed text intact (not accept the highlighted suggestion). \
         Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// Type the `<workspace>/alpha_` prefix into Project Path and
/// wait until the popup has surfaced at least the first
/// candidate (`alpha_00/`). With 10 candidates the popup spans
/// `total - visible = 5` extra rows that the user must scroll
/// to reach.
fn type_many_alphas_prefix_and_wait(harness: &mut EditorTestHarness, workspace: &std::path::Path) {
    let prefix = format!("{}/alpha_", workspace.display());
    harness.type_text(&prefix).unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("alpha_00/"))
        .unwrap();
}

/// With more candidates than the default visible-rows cap, the
/// popup paints exactly `5` rows + scrollbar — never the whole
/// list. The first batch of `alpha_NN/` directories sits in the
/// window; the tail ones (`alpha_07/` … `alpha_09/`) are
/// off-screen until the user scrolls. This pins the host's
/// fixed visible-rows behaviour against accidental "render all
/// candidates" regressions.
#[test]
fn completion_popup_caps_at_visible_rows() {
    let (_temp, workspace, names) = set_up_workspace_many_alphas();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    type_many_alphas_prefix_and_wait(&mut harness, &workspace);

    let screen = harness.screen_to_string();
    let visible: Vec<&String> = names
        .iter()
        .filter(|n| screen.contains(&format!("{}/", n)))
        .collect();
    assert_eq!(
        visible.len(),
        5,
        "default `completions_visible_rows = 5` should cap the painted candidates to 5; \
         saw {} on screen ({:?}).\nScreen:\n{}",
        visible.len(),
        visible,
        screen,
    );
    // The first five (`alpha_00` … `alpha_04`) should be the
    // ones in view since the host starts the scroll at 0.
    for n in names.iter().take(5) {
        assert!(
            screen.contains(&format!("{}/", n)),
            "candidate `{}/` should be in the initial window. Screen:\n{}",
            n,
            screen,
        );
    }
}

/// Pressing Down past the bottom of the visible window scrolls
/// the candidate list — earlier candidates fall out the top,
/// later ones (`alpha_07/`, `alpha_08/`, `alpha_09/`) come into
/// view. Verifies the host's auto-scroll-to-keep-selection-in-
/// view path.
#[test]
fn completion_popup_scrolls_with_down_arrow() {
    let (_temp, workspace, _names) = set_up_workspace_many_alphas();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    type_many_alphas_prefix_and_wait(&mut harness, &workspace);

    // Sanity: tail candidate is off-screen before any Down.
    assert!(
        !harness.screen_to_string().contains("alpha_09/"),
        "precondition: `alpha_09/` must be off-screen before scrolling",
    );

    // Press Down enough times to walk selection to the last candidate.
    // The first `↓` only *steps into* the dropdown (highlighting the top
    // row without moving), so reaching the 10th candidate (index 9) from
    // the top takes one enter-press plus nine moves = ten presses.
    // Auto-scroll should snap the window so `alpha_09/` is visible at the
    // bottom.
    for _ in 0..10 {
        harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    }
    harness
        .wait_until(|h| h.screen_to_string().contains("alpha_09/"))
        .unwrap();

    let screen = harness.screen_to_string();
    assert!(
        !screen.contains("alpha_00/"),
        "after scrolling to the bottom, the first candidate `alpha_00/` should fall \
         off the top of the window. Screen:\n{}",
        screen,
    );
}

/// When the candidate count exceeds the visible-rows cap, the
/// popup paints a scrollbar in the right edge — at minimum, a
/// solid block (`█`) glyph appears somewhere inside the popup
/// area. Pinning this prevents a future "the host stopped
/// drawing the scrollbar" regression that would silently make
/// the popup feel un-scrollable.
#[test]
fn completion_popup_renders_scrollbar_when_overflowing() {
    let (_temp, workspace, _names) = set_up_workspace_many_alphas();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    type_many_alphas_prefix_and_wait(&mut harness, &workspace);

    let screen = harness.screen_to_string();
    assert!(
        screen.contains('█'),
        "scrollbar thumb glyph `█` should paint when the popup has more \
         candidates than fit visible rows. Screen:\n{}",
        screen,
    );
}

/// The selected candidate's row paints with `popup_selection_bg`
/// across the candidate text + trailing pad + scrollbar column,
/// but the popup's `│` side borders must stay outside the
/// highlight — the right `│` in particular must keep the
/// popup's base bg (`theme.suggestion_bg`), not the selection
/// blue. Regression guard for a bug where the row-level
/// selection style propagated onto the wrapping `│ ... │` entry
/// and the per-border fg-only inline overlay could not paint
/// the bg back, so the right border sat on selection blue.
#[test]
fn selection_highlight_does_not_overlap_right_border() {
    let (_temp, workspace, _names) = set_up_workspace_many_alphas();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    type_many_alphas_prefix_and_wait(&mut harness, &workspace);

    // A freshly surfaced popup paints no highlighted row — Enter still
    // acts on the form. The first `↓` steps into the dropdown and
    // highlights the top row (`alpha_00/`, index 0) without moving the
    // selection off it, so we get a selected row to check the border
    // against.
    harness.send_key(KeyCode::Down, KeyModifiers::NONE).unwrap();
    harness.tick_and_render().unwrap();

    // `alpha_00/` is the first candidate, now selected after the `↓`
    // stepped into the dropdown (the host keeps `selectedIndex` at 0
    // when first entering the list).
    let (_text_col, row) = harness
        .find_text_on_screen("alpha_00/")
        .expect("`alpha_00/` should be visible as the first candidate row");

    // Scan the selected row right-to-left for the popup's
    // right `│` border. The dialog that wraps the form draws
    // its own `│` one column further out, so the rightmost
    // `│` is the dialog's border — the popup's right border
    // is the next `│` inward, identified as the rightmost `│`
    // whose left neighbor is NOT also `│` (the dialog border
    // would have the popup's `│` immediately to its left).
    let width = harness.buffer().area.width;
    let mut right_border_col: Option<u16> = None;
    for x in (1..width).rev() {
        if harness.get_cell(x, row).as_deref() == Some("│")
            && harness.get_cell(x - 1, row).as_deref() != Some("│")
        {
            right_border_col = Some(x);
            break;
        }
    }
    let right_border_col = right_border_col.unwrap_or_else(|| {
        panic!(
            "popup right `│` border should be visible on the selected candidate row.\nScreen:\n{}",
            harness.screen_to_string(),
        )
    });

    let (popup_selection_bg, suggestion_bg) = {
        let theme = harness.editor().theme();
        (theme.popup_selection_bg, theme.suggestion_bg)
    };
    let border_style = harness
        .get_cell_style(right_border_col, row)
        .expect("right border cell should have a style");

    assert_ne!(
        border_style.bg,
        Some(popup_selection_bg),
        "right `│` border on the selected candidate row must NOT paint with \
         `popup_selection_bg` ({:?}); the selection highlight should stop \
         inside the border, not overlap it. Border cell bg: {:?}.\nScreen:\n{}",
        popup_selection_bg,
        border_style.bg,
        harness.screen_to_string(),
    );
    assert_eq!(
        border_style.bg,
        Some(suggestion_bg),
        "right `│` border on the selected candidate row should paint on the \
         popup's base background (`suggestion_bg` = {:?}), not {:?}.\nScreen:\n{}",
        suggestion_bg,
        border_style.bg,
        harness.screen_to_string(),
    );

    // Also confirm the cell *inside* the popup's right border
    // (which is either the `█` scrollbar thumb or a pad
    // space, depending on whether the popup overflows) DOES
    // carry the selection bg. Pins the "highlight reads as a
    // single solid block, not truncated at end of text"
    // requirement: the selection should extend all the way to
    // the inside of the border, including the scrollbar
    // column on selected rows.
    let inside_col = right_border_col.saturating_sub(1);
    let inside_style = harness
        .get_cell_style(inside_col, row)
        .expect("inside cell should have a style");
    assert_eq!(
        inside_style.bg,
        Some(popup_selection_bg),
        "selection highlight should extend across the row's interior up to \
         the inside of the right border (including the scrollbar column \
         when present). Cell at col {} ({:?}) bg: {:?}.\nScreen:\n{}",
        inside_col,
        harness.get_cell(inside_col, row),
        inside_style.bg,
        harness.screen_to_string(),
    );
}

/// Completion candidates render left-aligned with the input
/// field's value. The input row's leading chrome is `│ [` —
/// dialog border + section padding + value's `[` bracket —
/// putting the value's first char three columns inside the
/// dialog border. The popup row's leading chrome should match,
/// so the candidate's first char sits directly under the
/// value's first char.
#[test]
fn completion_candidates_left_aligned_with_input_value() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    let prefix = type_alpha_prefix_and_wait(&mut harness, &workspace);
    let _ = prefix;

    // The typed value starts with the workspace path; the candidate
    // popup row begins with the same workspace path followed by
    // `alpha_dir/`. So the workspace path string is the natural
    // anchor on both rows — locating its first occurrence on each
    // gives the column of the first char of the value (input row)
    // and of the candidate text (popup row).
    //
    // We must not anchor on `'/'` here: on Windows the workspace
    // path is rendered as `\\?\C:\...` so the first `/` lands deep
    // inside the path, far past the actual start of the candidate.
    let workspace_str = workspace.display().to_string();
    let screen = harness.screen_to_string();
    let lines: Vec<&str> = screen.lines().collect();
    let input_row = lines
        .iter()
        .position(|l| l.contains('[') && l.contains(']') && l.contains(&workspace_str))
        .expect("input row with [<typed path>] should be on screen");
    let popup_row = lines
        .iter()
        .position(|l| l.contains("alpha_dir/") && l.contains(&workspace_str))
        .expect("popup row with `alpha_dir/` should be on screen");

    // Compare *display* columns, not byte offsets: the focus-marker
    // gutter puts a multibyte `▸` (3 bytes, 1 column) ahead of the input
    // value, so a raw `find()` byte offset would diverge from the popup
    // row's even when the two are visually aligned. Counting chars up to
    // the match gives the column (every glyph in the leading chrome —
    // `│`, `▸`, spaces, `[` — is one display column wide).
    let display_col = |line: &str| -> usize {
        let b = line
            .find(&workspace_str)
            .expect("row should contain the workspace path");
        line[..b].chars().count()
    };
    let input_col = display_col(lines[input_row]);
    let popup_col = display_col(lines[popup_row]);

    assert_eq!(
        input_col, popup_col,
        "popup candidate's first column ({}) should match the input value's first column ({}); \
         the candidate text must sit directly under the typed value, not one column to the \
         left of it.\nInput row {}:\n{}\nPopup row {}:\n{}",
        popup_col, input_col, input_row, lines[input_row], popup_row, lines[popup_row],
    );
}

/// After the popup dismisses on Enter and a second Enter advances
/// focus to the next tabbable widget, the plugin's local focus
/// mirror must advance in lockstep with the host. Without this,
/// the plugin still thinks the text input is focused and the
/// next Up/Down walks the *previous* text input's history,
/// silently rewriting its value. This was the user-reported
/// breakage: "after using the popup and pressing enter to visit
/// the checkbox/next field, up/down still modify the input box".
///
/// We verify by typing a marker character after the focus
/// advance and asserting the original input is unchanged. If
/// focus is correctly on the next tabbable, the marker goes
/// elsewhere; if focus is stuck on the original text input, the
/// marker extends its value.
#[test]
fn enter_advance_after_popup_dismiss_moves_plugin_focus_mirror() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    let typed = type_alpha_prefix_and_wait(&mut harness, &workspace);

    // Popup is open. Enter #1 dismisses it (stays on the
    // Project Path text input).
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness
        .wait_until(|h| !h.screen_to_string().contains("alpha_two/"))
        .unwrap();
    assert_eq!(
        project_path_field_value(&harness.screen_to_string()),
        typed,
        "Enter #1 should dismiss the popup without changing the typed text",
    );

    // Enter #2 advances focus to the next tabbable widget.
    // (The worktree checkbox is disabled because the typed
    // `<workspace>/al` path doesn't exist; the focus cycle
    // skips it.)
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();

    // Now type a marker character. If the plugin's focus
    // mirror correctly advanced past Project Path, this char
    // lands somewhere else (Workspace Name's input replaces the
    // placeholder, etc.) and the Project Path value stays at
    // `typed`. If the plugin's mirror is stuck on
    // project_path, the marker extends it.
    harness.type_text("Z").unwrap();
    harness.tick_and_render().unwrap();

    assert_eq!(
        project_path_field_value(&harness.screen_to_string()),
        typed,
        "after Enter-Enter, focus must have advanced past the Project Path \
         text input; a subsequent keystroke must not extend the Project Path \
         value. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// Mouse wheel over the popup scrolls its candidate list — same
/// behaviour the user gets from Down arrow, except the selected
/// index stays put (it's a scroll, not a selection move). Goes
/// directly through `Editor::handle_mouse` since SGR mouse
/// escape sequences sent via `tmux send-keys` get filtered by
/// tmux's pane-input pipeline and never reach crossterm, so
/// interactive tmux verification isn't possible without a real
/// mouse device.
#[test]
fn completion_popup_scrolls_with_mouse_wheel() {
    let (_temp, workspace, _names) = set_up_workspace_many_alphas();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);
    type_many_alphas_prefix_and_wait(&mut harness, &workspace);

    // Sanity: bottom candidate is off-screen before scrolling.
    assert!(
        !harness.screen_to_string().contains("alpha_09/"),
        "precondition: `alpha_09/` must be off-screen before scrolling",
    );

    // Locate a row owned by the popup so the wheel lands on its
    // hit-test target. `alpha_00/` is the top candidate row when
    // the popup just opened; find its on-screen row and scroll
    // there. Column is irrelevant for the host's wheel routing
    // (it only checks `last_inner_rect` containment), but pick
    // a column inside the panel for realism.
    let (col, row) = harness
        .find_text_on_screen("alpha_00/")
        .expect("`alpha_00/` should be visible before scrolling");
    let _ = col;

    // Each ScrollDown event ticks the popup's host-side scroll
    // by 3 (the editor's default wheel step). 5 events is enough
    // to reach the end of the 10-row list regardless of which
    // direction the step is clamped from.
    for _ in 0..5 {
        harness.mouse_scroll_down(80, row).unwrap();
    }
    harness
        .wait_until(|h| h.screen_to_string().contains("alpha_09/"))
        .unwrap();

    let screen = harness.screen_to_string();
    assert!(
        !screen.contains("alpha_00/"),
        "after scrolling down with the mouse wheel, `alpha_00/` should fall off \
         the top of the visible window. Screen:\n{}",
        screen,
    );
}

/// A terminal bracketed paste (`Event::Paste`, the single-event path
/// a real terminal emits for clipboard paste — distinct from typed
/// keys and from `Ctrl+V`) must land in the focused Project Path
/// field, NOT in the buffer obscured by the modal.
///
/// Regression for the reported bug: with the New-Session dialog open,
/// pasting via the terminal dumped the text into the underlying file
/// buffer instead of the focused dialog field. Bracketed paste arrives
/// as one `Event::Paste` that never passes through the floating-panel
/// key dispatcher, so it fell straight through to the buffer paste
/// path. Without the fix the Project Path field stays unchanged (the
/// text went to the buffer) and this assertion fails.
#[test]
fn bracketed_paste_routes_to_focused_dialog_field() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);

    // The Project Path text field is focused on open. Paste a marker
    // through the real bracketed-paste event path.
    harness.send_paste("PASTEDMARKER").unwrap();
    harness
        .wait_until(|h| project_path_field_value(&h.screen_to_string()).contains("PASTEDMARKER"))
        .unwrap();

    assert!(
        project_path_field_value(&harness.screen_to_string()).contains("PASTEDMARKER"),
        "bracketed paste should land in the focused Project Path field. Screen:\n{}",
        harness.screen_to_string(),
    );

    // And it must not have leaked into the buffer underneath: close
    // the dialog (Esc cancels) and confirm the marker is nowhere on
    // the now-revealed buffer.
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness
        .wait_until(|h| {
            !h.screen_to_string()
                .contains("ORCHESTRATOR :: New Workspace")
        })
        .unwrap();
    assert!(
        !harness.screen_to_string().contains("PASTEDMARKER"),
        "bracketed paste must not leak into the buffer behind the dialog. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// When the dialog is open but focus rests on a non-text widget (a
/// button / backend tab), a terminal bracketed paste must be ignored
/// — neither inserted into any field nor leaked into the obscured
/// buffer.
///
/// The form's tab cycle always starts with the "Run in:" backend
/// tabs (`[local, ssh, kubernetes, devcontainer, project_path, …]`)
/// and opens with `project_path` focused, so a single Shift+Tab walks
/// focus back onto the `devcontainer` backend tab — a non-text Button,
/// regardless of git / worktree state. Pasting there must be
/// swallowed. Without the fix the paste falls through to the buffer;
/// revealing the buffer after Esc shows the marker and this fails.
#[test]
fn bracketed_paste_ignored_when_non_text_widget_focused() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);

    // Walk focus off the Project Path text field onto a backend tab
    // (a non-text Button) — the tabbable immediately before it.
    harness
        .send_key(KeyCode::BackTab, KeyModifiers::NONE)
        .unwrap();

    harness.send_paste("IGNORED_PASTE").unwrap();
    harness.tick_and_render().unwrap();

    // No field absorbed it (it would render inside the dialog).
    assert!(
        !harness.screen_to_string().contains("IGNORED_PASTE"),
        "paste onto a non-text widget must not appear in any field. Screen:\n{}",
        harness.screen_to_string(),
    );

    // And — the load-bearing check — it must not have leaked into the
    // buffer underneath. Without the fix the paste falls through to
    // the hidden buffer; revealing it after Esc surfaces the marker.
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness
        .wait_until(|h| {
            !h.screen_to_string()
                .contains("ORCHESTRATOR :: New Workspace")
        })
        .unwrap();
    assert!(
        !harness.screen_to_string().contains("IGNORED_PASTE"),
        "an ignored paste must not leak into the buffer behind the dialog. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// `Ctrl+V` must paste the editor clipboard into the focused dialog
/// field — same destination as a terminal bracketed paste, different
/// entry path (a key event resolved through the keybinding table, not
/// an `Event::Paste`).
///
/// Regression: the centered modal's key dispatcher swallowed every
/// Ctrl-chord that had no mode binding, so Ctrl+V (and Ctrl+A / Ctrl+C /
/// Ctrl+X) died before reaching the focused Text widget. Text copied
/// from a buffer with Ctrl+C could not be pasted into the New-Workspace
/// or Run-Agent dialogs at all.
#[test]
fn ctrl_v_pastes_into_focused_dialog_field() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    harness
        .editor_mut()
        .set_clipboard_for_test("CTRLV_MARKER".to_string());

    open_new_session_form(&mut harness);

    // The Project Path text field is focused on open.
    harness
        .send_key(KeyCode::Char('v'), KeyModifiers::CONTROL)
        .unwrap();
    harness
        .wait_until(|h| project_path_field_value(&h.screen_to_string()).contains("CTRLV_MARKER"))
        .unwrap();

    // And it must not have leaked into the buffer underneath.
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness
        .wait_until(|h| {
            !h.screen_to_string()
                .contains("ORCHESTRATOR :: New Workspace")
        })
        .unwrap();
    assert!(
        !harness.screen_to_string().contains("CTRLV_MARKER"),
        "Ctrl+V must not leak into the buffer behind the dialog. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// `Ctrl+A` selects the focused field's text, so a following paste
/// replaces it instead of appending — the standard select-all-then-paste
/// gesture. Same regression class as `ctrl_v_pastes_into_focused_dialog_field`:
/// the modal swallowed the chord, the selection never happened, and the
/// paste appended after the old text.
#[test]
fn ctrl_a_selects_field_text_so_paste_replaces_it() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);

    open_new_session_form(&mut harness);

    harness.type_text("OLDTEXT").unwrap();
    harness
        .wait_until(|h| project_path_field_value(&h.screen_to_string()).contains("OLDTEXT"))
        .unwrap();

    harness
        .send_key(KeyCode::Char('a'), KeyModifiers::CONTROL)
        .unwrap();
    harness.send_paste("NEWTEXT").unwrap();
    harness
        .wait_until(|h| project_path_field_value(&h.screen_to_string()).contains("NEWTEXT"))
        .unwrap();

    let value = project_path_field_value(&harness.screen_to_string());
    assert!(
        !value.contains("OLDTEXT"),
        "paste after Ctrl+A must replace the selected field text, not append. \
         Field value: {:?}. Screen:\n{}",
        value,
        harness.screen_to_string(),
    );
}

// ===========================================================================
// Focus model: visible `▸` marker, linear Tab, ←/→ within selectors,
// scoped Esc, and the Ctrl+Enter submit shortcut.
// ===========================================================================

/// The single screen line carrying the `▸` focus marker (the focused
/// control). Panics if zero or more than one marker is present — the
/// invariant the focus model guarantees is "exactly one control
/// focused at a time".
fn focused_line(screen: &str) -> String {
    let lines: Vec<&str> = screen.lines().filter(|l| l.contains('▸')).collect();
    assert_eq!(
        lines.len(),
        1,
        "exactly one `▸` focus marker must be on screen, found {}. Screen:\n{}",
        lines.len(),
        screen,
    );
    lines[0].to_string()
}

/// Open the form on a non-git workspace (so the worktree toggle and
/// Branch field are absent — a predictable, minimal field set).
fn open_form_on(workspace: &PathBuf) -> EditorTestHarness {
    let mut harness = EditorTestHarness::with_working_dir(160, 50, workspace.clone()).unwrap();
    harness.tick_and_render().unwrap();
    wait_for_new_session_command(&mut harness);
    open_new_session_form(&mut harness);
    harness
}

/// Tab moves linearly between *fields* — one stop per radio group, not
/// one per option. Walking a full cycle lands the marker on the active
/// "Run in:" tab exactly once and the active "Agent:" preset exactly
/// once (never on an inactive option), and reaches `[ Create & Visit ]`.
#[test]
fn tab_is_linear_one_stop_per_radio_group() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    // Advance to the single "Run in:" stop — the anchor for one cycle.
    // (The form opens focused on Project Path, so the "Run in:" tab is a
    // few stops away.)
    let mut guard = 0;
    while !focused_line(&harness.screen_to_string()).contains("Run in:") {
        harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
        harness.tick_and_render().unwrap();
        guard += 1;
        assert!(
            guard < 20,
            "Tab never reached the 'Run in:' stop. Screen:\n{}",
            harness.screen_to_string(),
        );
    }

    // Walk exactly one full cycle: collect the focused line at each stop
    // starting from the "Run in:" anchor, tabbing until the marker lands
    // back on a "Run in:" line. Counting over a fixed number of presses
    // would over-count once the press count exceeds the cycle length;
    // bounding on "back to the anchor" makes the assertions independent
    // of how many fields the active backend has.
    let mut cycle_lines = vec![focused_line(&harness.screen_to_string())];
    loop {
        harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
        harness.tick_and_render().unwrap();
        let line = focused_line(&harness.screen_to_string());
        if line.contains("Run in:") {
            break; // back to the anchor — one full cycle walked
        }
        cycle_lines.push(line);
        assert!(
            cycle_lines.len() < 20,
            "focus cycle never returned to the 'Run in:' anchor. Screen:\n{}",
            harness.screen_to_string(),
        );
    }

    let mut run_in_stops = 0;
    let mut agent_stops = 0;
    let mut saw_create = false;
    let mut saw_inactive_option = false;
    for line in &cycle_lines {
        if line.contains("Run in:") {
            run_in_stops += 1;
            // The marker must precede the *active* backend (Local), not
            // an inactive option.
            if line.contains("▸ [ SSH ]")
                || line.contains("▸ [ Kubernetes ]")
                || line.contains("▸ [ Devcontainer ]")
            {
                saw_inactive_option = true;
            }
        }
        if line.contains("Agent:") {
            agent_stops += 1;
            if line.contains("▸ [ claude")
                || line.contains("▸ [ aider")
                || line.contains("▸ [ custom")
            {
                saw_inactive_option = true;
            }
        }
        if line.contains("Create Workspace") {
            saw_create = true;
        }
    }

    assert!(
        !saw_inactive_option,
        "Tab must never land on an inactive radio option (←/→ changes the option). \
         Screen:\n{}",
        harness.screen_to_string(),
    );
    assert_eq!(
        run_in_stops, 1,
        "the 'Run in:' group is a single Tab stop per cycle (got {run_in_stops})",
    );
    assert_eq!(
        agent_stops, 1,
        "the 'Agent:' group is a single Tab stop per cycle (got {agent_stops})",
    );
    assert!(saw_create, "Tab must reach the [ Create Workspace ] button");
}

/// ←/→ changes the option *within* the "Run in:" selector (and swaps
/// the body), while Tab leaves the option alone. This is the split the
/// help line documents: Tab between fields, ←/→ within a group.
#[test]
fn arrows_switch_run_in_selector_option() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    // Shift+Tab from the initial Project Path focus wraps to the active
    // "Run in:" tab (the first stop in the cycle).
    harness
        .send_key(KeyCode::BackTab, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    assert!(
        focused_line(&harness.screen_to_string()).contains("Run in:"),
        "Shift+Tab should land focus on the Run in selector. Screen:\n{}",
        harness.screen_to_string(),
    );

    // → moves to the next option (SSH) and the body swaps to SSH fields.
    harness
        .send_key(KeyCode::Right, KeyModifiers::NONE)
        .unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("Host  ("))
        .unwrap();
    assert!(
        focused_line(&harness.screen_to_string()).contains("▸ [ SSH ]"),
        "→ should move the focus marker onto the SSH option. Screen:\n{}",
        harness.screen_to_string(),
    );

    // ← moves back to Local and restores the local body (Project Path).
    harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("Project Path"))
        .unwrap();
    assert!(
        focused_line(&harness.screen_to_string()).contains("▸ [ Local ]"),
        "← should move the focus marker back onto the Local option. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// Esc is scoped: the first Esc closes an open completion dropdown
/// (the form stays open with its input intact), and a second Esc
/// cancels the dialog.
#[test]
fn esc_closes_dropdown_first_then_cancels_dialog() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);
    let typed = type_alpha_prefix_and_wait(&mut harness, &workspace);

    // First Esc: dropdown closes, form stays open, input preserved.
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness
        .wait_until(|h| !h.screen_to_string().contains("alpha_two/"))
        .unwrap();
    let screen = harness.screen_to_string();
    assert!(
        screen.contains("ORCHESTRATOR :: New Workspace"),
        "first Esc must NOT cancel the dialog. Screen:\n{}",
        screen,
    );
    assert_eq!(
        project_path_field_value(&screen),
        typed,
        "first Esc must preserve the typed input. Screen:\n{}",
        screen,
    );

    // Second Esc: dialog cancels.
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness
        .wait_until(|h| {
            !h.screen_to_string()
                .contains("ORCHESTRATOR :: New Workspace")
        })
        .unwrap();
}

/// Ctrl+Enter submits the form from anywhere — here from a text field,
/// where a bare Enter would only advance focus. The form leaves its
/// editable state (the "Run in:" selector row disappears as the dialog
/// switches to the connecting/creating view or closes outright).
#[test]
#[cfg_attr(target_os = "windows", ignore)]
fn ctrl_enter_submits_from_a_text_field() {
    use portable_pty::{native_pty_system, PtySize};
    let pty_ok = native_pty_system()
        .openpty(PtySize {
            rows: 1,
            cols: 1,
            pixel_width: 0,
            pixel_height: 0,
        })
        .is_ok();
    if !pty_ok {
        eprintln!("Skipping ctrl_enter_submits test: PTY not available");
        return;
    }

    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    // Focus is on the Project Path text field. A bare Enter here would
    // advance focus (and keep the editable "Run in:" row). Ctrl+Enter
    // must instead submit — the editable selector row goes away.
    assert!(
        harness.screen_to_string().contains("←/→ switch type"),
        "precondition: the editable Run-in selector is showing",
    );
    harness
        .send_key(KeyCode::Enter, KeyModifiers::CONTROL)
        .unwrap();
    harness
        .wait_until(|h| !h.screen_to_string().contains("←/→ switch type"))
        .unwrap();
}

/// Advance Tab focus until the single "Agent:" preset stop is reached.
fn focus_agent_preset_stop(harness: &mut EditorTestHarness) {
    let mut guard = 0;
    while !focused_line(&harness.screen_to_string()).contains("Agent:") {
        harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
        harness.tick_and_render().unwrap();
        guard += 1;
        assert!(
            guard < 20,
            "Tab never reached the 'Agent:' stop. Screen:\n{}",
            harness.screen_to_string(),
        );
    }
}

/// Tab to the collapsed "Advanced…" fold header and activate it, so the
/// folded controls (worktree, branch fields, and the "Teach Fresh CLI"
/// toggle) render. The header glyph flips `▶` → `▼` when expanded.
fn expand_advanced(harness: &mut EditorTestHarness) {
    let mut guard = 0;
    while !focused_line(&harness.screen_to_string()).contains("Advanced") {
        harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
        harness.tick_and_render().unwrap();
        guard += 1;
        assert!(
            guard < 20,
            "Tab never reached the 'Advanced…' fold. Screen:\n{}",
            harness.screen_to_string(),
        );
    }
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("▼ Advanced"))
        .unwrap();
}

/// The launcher prioritises the coding-CLI presets — a bare `terminal`,
/// then `claude`, `codex`, `opencode` — ahead of the long-standing `aider`
/// and the `custom…` escape hatch. The agent selector is a single dropdown
/// (`Agent: [<selected> ▼]`); ←/→ cycles it through the presets in that
/// priority order. We prove the ordering by adjacency — from `claude`, one
/// `→` lands on `codex`, the next on `opencode` — which is independent of
/// whichever preset the dropdown happens to open on.
#[test]
fn preset_row_lists_prioritised_agents_in_order() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    focus_agent_preset_stop(&mut harness);

    // Step ←/→ until `claude` is the selected (focused) preset.
    let mut guard = 0;
    while !focused_line(&harness.screen_to_string()).contains("claude") {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
        harness.tick_and_render().unwrap();
        guard += 1;
        assert!(
            guard < 8,
            "cycling the agent dropdown never reached `claude`. Screen:\n{}",
            harness.screen_to_string(),
        );
    }

    // From claude, → advances to codex, then to opencode — the priority order.
    harness
        .send_key(KeyCode::Right, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    assert!(
        focused_line(&harness.screen_to_string()).contains("codex"),
        "`→` from claude must select codex next. Screen:\n{}",
        harness.screen_to_string(),
    );

    harness
        .send_key(KeyCode::Right, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    assert!(
        focused_line(&harness.screen_to_string()).contains("opencode"),
        "`→` from codex must select opencode next. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// The Agent selector opens its option list as a screen-level FLOATING
/// pop-over: Enter (routed to the host's `set_dropdown_open`, not the
/// no-op `activate()`) reveals every preset at once, layered on top of
/// the form rather than reflowing it, and Esc closes it back to the
/// single compact `Agent: [<selected> ▼]` trigger. A click on an option
/// row selects it and closes the list.
#[test]
fn agent_dropdown_opens_as_floating_popover() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    focus_agent_preset_stop(&mut harness);

    // Distinctive preset names; the closed trigger shows exactly one of
    // them (the selected value), the open pop-over lists several.
    let names = ["terminal", "claude", "codex", "opencode"];
    let count_names = |screen: &str| names.iter().filter(|n| screen.contains(**n)).count();

    let closed_before = count_names(&harness.screen_to_string());

    // Enter opens the pop-over. Before the fix, Enter hit `activate()` — a
    // no-op on a Dropdown — so the list never opened.
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    let open_screen = harness.screen_to_string();
    let open_count = count_names(&open_screen);
    assert!(
        open_count >= 3 && open_count > closed_before,
        "Enter must float the agent option list open (many presets visible \
         at once); before={closed_before}, open={open_count}. Screen:\n{open_screen}",
    );

    // Web-scene parity: the projection the web frontend renders from (the
    // `/state` route → `widgets_view`) must report the dropdown as OPEN, so
    // the web floats its own native option pop-over from the same state.
    {
        let surfaces = harness.editor().widgets_view();
        let modal = surfaces
            .iter()
            .find(|s| s.kind == "floatingModal")
            .expect("the New-Workspace form must appear as a floatingModal in the web scene");
        let inst = modal
            .instances
            .get("agent_dropdown")
            .expect("the web scene must carry the agent_dropdown instance state");
        assert_eq!(
            inst.dropdown_open,
            Some(true),
            "the web scene must report the agent dropdown as open (so the web \
             frontend renders the floating option list)",
        );
    }

    // Esc closes the pop-over, back to the single-value trigger.
    harness.send_key(KeyCode::Esc, KeyModifiers::NONE).unwrap();
    harness.tick_and_render().unwrap();
    let closed_screen = harness.screen_to_string();
    assert!(
        count_names(&closed_screen) < open_count,
        "Esc must close the pop-over back to the compact trigger. Screen:\n{closed_screen}",
    );

    // Re-open and pick an option by clicking its row. `claude` is a stable
    // preset that is not the default selection (`terminal`), so clicking it
    // both changes the value and closes the list.
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    let (col, row) = harness
        .find_text_on_screen("claude")
        .expect("`claude` option row must be visible in the open pop-over");
    harness.mouse_click(col, row).unwrap();
    harness.tick_and_render().unwrap();
    let after_click = harness.screen_to_string();
    assert!(
        count_names(&after_click) < open_count,
        "clicking an option must close the pop-over. Screen:\n{after_click}",
    );
    assert!(
        focused_line(&after_click).contains("claude"),
        "clicking the `claude` option must select it as the Agent value. Screen:\n{after_click}",
    );

    // Web-scene parity again: after the click the projection must show the
    // pop-over closed and the newly-selected index committed — the same
    // state the web `/state` route would report so selection round-trips.
    {
        let surfaces = harness.editor().widgets_view();
        let modal = surfaces
            .iter()
            .find(|s| s.kind == "floatingModal")
            .expect("form surface in web scene after selection");
        let inst = modal
            .instances
            .get("agent_dropdown")
            .expect("agent_dropdown instance in web scene after selection");
        assert_eq!(
            inst.dropdown_open,
            Some(false),
            "picking an option must close the pop-over in the web scene too",
        );
        // `claude` is index 1 in the prioritised preset order (terminal, claude,
        // codex, …), so a successful click committed that selection.
        assert_eq!(
            inst.selected_index,
            Some(1),
            "the clicked `claude` option must be the committed selection in the web scene",
        );
    }
}

/// Clicking the *closed* `Agent: [<value> ▼]` trigger opens the option
/// pop-over. The trigger is not focused when the form opens (Project Path
/// is), so the click must BOTH move focus to the dropdown AND toggle its
/// list open. Regression: the TUI floating-panel click path only focused
/// the dropdown and fired the event to the plugin — it never called
/// `set_dropdown_open`, so a click left the list closed (the pop-over only
/// opened via the keyboard `Enter` path). A second click on the now-open,
/// focused trigger must close it again.
#[test]
fn clicking_agent_dropdown_trigger_toggles_popover() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    // Distinctive preset names: the closed trigger shows exactly one (the
    // selected value), the open pop-over lists several.
    let names = ["terminal", "claude", "codex", "opencode"];
    let count_names = |screen: &str| names.iter().filter(|n| screen.contains(**n)).count();
    let closed_before = count_names(&harness.screen_to_string());

    // Locate the closed trigger by its selected value and click it. `terminal`
    // is the default Agent value and (with Advanced collapsed) appears only in
    // the dropdown trigger, so its cell is inside the `[terminal ▼]` button.
    let (col, row) = harness
        .find_text_on_screen("terminal")
        .expect("the Agent dropdown trigger `[terminal ▼]` must be visible");
    harness.mouse_click(col, row).unwrap();
    harness.tick_and_render().unwrap();

    let open_screen = harness.screen_to_string();
    let open_count = count_names(&open_screen);
    assert!(
        open_count >= 3 && open_count > closed_before,
        "clicking the Agent dropdown trigger must open the option pop-over \
         (many presets visible at once); before={closed_before}, open={open_count}. \
         Screen:\n{open_screen}",
    );

    // Click the trigger again (now focused + open) — it must close.
    let (col2, row2) = harness
        .find_text_on_screen("terminal")
        .expect("the Agent trigger stays visible while open");
    harness.mouse_click(col2, row2).unwrap();
    harness.tick_and_render().unwrap();
    assert!(
        count_names(&harness.screen_to_string()) < open_count,
        "a second click on the open trigger must close the pop-over. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// The open Agent pop-over box anchors its top-left corner directly under
/// the trigger's `[` bracket, not at the panel's left content edge.
/// Regression: the host drew the floating box at `inner.x` (panel left)
/// regardless of the trigger's column, so a labeled `Agent: [terminal ▼]`
/// dropdown showed its option list shifted far to the left of the button.
#[test]
fn agent_dropdown_popover_anchored_under_trigger() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);
    focus_agent_preset_stop(&mut harness);

    // Open the pop-over. The trigger arrow flips `▼` → `▲` and the option
    // list surfaces below.
    harness
        .send_key(KeyCode::Enter, KeyModifiers::NONE)
        .unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("opencode"))
        .unwrap();

    let screen = harness.screen_to_string();
    let lines: Vec<&str> = screen.lines().collect();

    // The focused `▸ Agent: [<value> ▲]` trigger row. Its `[` marks the
    // button's start column (all leading glyphs — `│`, `▸`, spaces, letters
    // — are one display column wide, so a char count == display column).
    let trigger_row = lines
        .iter()
        .position(|l| l.contains("Agent:") && l.contains('▲'))
        .expect("the open Agent trigger row (`▸ Agent: [.. ▲]`) must be on screen");
    let bracket_col = lines[trigger_row]
        .chars()
        .position(|c| c == '[')
        .expect("the trigger row must contain the `[` button bracket");

    // The dialog's own left border is the first `│` on the trigger row; the
    // pop-over must NOT anchor there (that was the bug).
    let dialog_left = lines[trigger_row]
        .chars()
        .position(|c| c == '│')
        .expect("the dialog left border `│` must be on the trigger row");
    assert!(
        bracket_col > dialog_left + 2,
        "precondition: the labeled trigger's `[` ({bracket_col}) sits well right \
         of the dialog's left border ({dialog_left}).",
    );

    // The pop-over opens one row below the trigger; its top-left corner `┌`
    // must land exactly on the trigger's `[` column.
    let corner = harness.get_cell(bracket_col as u16, (trigger_row + 1) as u16);
    assert_eq!(
        corner.as_deref(),
        Some("┌"),
        "the pop-over's top-left corner `┌` must sit under the trigger's `[` \
         (col {bracket_col}, row {}); found {:?}. The box must anchor under the \
         button, not at the panel's left edge.\nScreen:\n{screen}",
        trigger_row + 1,
        corner,
    );
}

/// Picking a coding agent surfaces its per-agent controls — an "Auto
/// mode" checkbox and a "Start prompt" box — which are hidden for the
/// bare `terminal` default. Stepping ←/→ off `terminal` onto the first
/// agent (claude) reveals both.
#[test]
fn selecting_an_agent_reveals_auto_mode_and_start_prompt() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    // The bare-terminal default carries no agent options.
    let screen = harness.screen_to_string();
    assert!(
        !screen.contains("Auto mode") && !screen.contains("Start prompt"),
        "the terminal preset must not show agent-only controls. Screen:\n{screen}",
    );

    focus_agent_preset_stop(&mut harness);

    // ←/→ steps off `terminal` onto the first agent (claude), whose
    // command fills the field and whose controls appear.
    let mut guard = 0;
    while !harness.screen_to_string().contains("Auto mode") {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
        harness.tick_and_render().unwrap();
        guard += 1;
        assert!(
            guard < 8,
            "stepping the preset selector never surfaced Auto mode. Screen:\n{}",
            harness.screen_to_string(),
        );
    }
    assert!(
        harness.screen_to_string().contains("Start prompt"),
        "an agent that takes a launch prompt must show the Start prompt box. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// The agent controls adapt to the selected agent: opencode has no
/// launch bypass-approvals flag (it's config-driven), so it shows the
/// Start prompt box but *not* the Auto mode checkbox.
#[test]
fn opencode_shows_start_prompt_without_auto_mode() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    focus_agent_preset_stop(&mut harness);

    // Step ←/→ until opencode is the selected value in the focused Agent
    // dropdown (`▸ Agent: [opencode ▼]`). The dropdown shows only the
    // selected preset, so the focused line naming opencode is the signal.
    let mut guard = 0;
    while !focused_line(&harness.screen_to_string()).contains("opencode") {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
        harness.tick_and_render().unwrap();
        guard += 1;
        assert!(
            guard < 8,
            "stepping the preset selector never reached opencode. Screen:\n{}",
            harness.screen_to_string(),
        );
    }

    let screen = harness.screen_to_string();
    assert!(
        screen.contains("Start prompt"),
        "opencode takes a launch prompt, so the Start prompt box must show. Screen:\n{screen}",
    );
    assert!(
        !screen.contains("Auto mode"),
        "opencode has no launch auto-mode flag, so no Auto mode checkbox. Screen:\n{screen}",
    );
}

/// The "Teach agent the Fresh CLI" toggle lives under the "Advanced…" fold
/// (enabled by default, but folded away so it doesn't clutter the common
/// case). It's an agent-only control: even with Advanced expanded it stays
/// hidden for the bare `terminal` preset (nothing to teach), and appears once
/// a supporting agent (claude) is selected.
#[test]
fn teach_fresh_cli_toggle_shown_for_agent_hidden_for_terminal() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    // Collapsed Advanced: the toggle is folded away regardless of agent.
    assert!(
        !harness
            .screen_to_string()
            .contains("Teach agent the Fresh CLI"),
        "the Teach Fresh CLI toggle must be hidden while Advanced is collapsed. Screen:\n{}",
        harness.screen_to_string(),
    );

    // Expand Advanced. With the bare-terminal default still active, the toggle
    // stays hidden — a terminal has no system prompt to inject into.
    expand_advanced(&mut harness);
    assert!(
        !harness
            .screen_to_string()
            .contains("Teach agent the Fresh CLI"),
        "the terminal preset must not show the Teach Fresh CLI toggle even under \
         an expanded Advanced fold. Screen:\n{}",
        harness.screen_to_string(),
    );

    // Select the first agent (claude) via the dropdown; the toggle now renders
    // under the (still-expanded) Advanced fold.
    focus_agent_preset_stop(&mut harness);
    let mut guard = 0;
    while !focused_line(&harness.screen_to_string()).contains("claude") {
        harness
            .send_key(KeyCode::Right, KeyModifiers::NONE)
            .unwrap();
        harness.tick_and_render().unwrap();
        guard += 1;
        assert!(
            guard < 8,
            "cycling the agent dropdown never reached `claude`. Screen:\n{}",
            harness.screen_to_string(),
        );
    }
    assert!(
        harness
            .screen_to_string()
            .contains("Teach agent the Fresh CLI"),
        "selecting a supporting agent must reveal the Teach Fresh CLI toggle under \
         the expanded Advanced fold. Screen:\n{}",
        harness.screen_to_string(),
    );
}

/// "Run Agent…" and "New Workspace" are one dialog, distinguished only by
/// where its "Launch in" switch starts — and flipping that switch reshapes the
/// form in place rather than opening a different one.
///
/// This is the structural guard on the unification: two dialogs with two submit
/// paths is what let the current-workspace path silently drop the agent-resume
/// argv, so an agent started there restarted as a bare shell.
///
/// Driven from the New-Workspace entry point (the reliable one — confirming a
/// quick-open entry re-computes the suggestion list and indexes it by row while
/// plugin commands are still registering, so opening by palette twice in one
/// test is racy). That the *Run Agent* entry point opens this same form
/// pre-switched is asserted through the frame title, which follows the switch.
#[test]
fn run_agent_and_new_workspace_are_one_dialog() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    // Opened as "New Workspace": the workspace-creation fields are all here.
    harness.assert_screen_contains("Launch in:");
    harness.assert_screen_contains("New workspace");
    harness.assert_screen_contains("Project Path");
    harness.assert_screen_contains("Workspace Name");
    harness.assert_screen_contains("Advanced");
    harness.assert_screen_contains("Run in:");
    harness.assert_screen_contains("Agent:");

    // Flip "Launch in" to the current workspace. The form opens focused on
    // Project Path, so Shift+Tab twice reaches the switch (via the backend
    // tab group, which is a single stop).
    harness
        .send_key(KeyCode::BackTab, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    harness
        .send_key(KeyCode::BackTab, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    assert!(
        focused_line(&harness.screen_to_string()).contains("Launch in:"),
        "Shift+Tab should reach the Launch-in switch. Screen:\n{}",
        harness.screen_to_string(),
    );
    harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();

    // Everything workspace-shaped is gone — this is exactly what the separate
    // Run-Agent dialog used to show — and the frame now says so.
    harness
        .wait_until(|h| h.screen_to_string().contains("ORCHESTRATOR :: Run Agent"))
        .unwrap();
    harness.assert_screen_contains("Current workspace");
    harness.assert_screen_not_contains("Project Path");
    harness.assert_screen_not_contains("Workspace Name");
    harness.assert_screen_not_contains("Advanced");
    harness.assert_screen_not_contains("Run in:");
    // The agent selector is shared by both shapes, so it stays.
    harness.assert_screen_contains("Agent:");

    // Focus stayed on the switch, so flipping back is one key — the form
    // must not fling focus elsewhere when its shape changes under the user.
    assert!(
        focused_line(&harness.screen_to_string()).contains("Launch in:"),
        "flipping the switch must leave focus on it. Screen:\n{}",
        harness.screen_to_string(),
    );

    // And back again: same dialog, more of it.
    harness
        .send_key(KeyCode::Right, KeyModifiers::NONE)
        .unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("Project Path"))
        .unwrap();
    harness.assert_screen_contains("ORCHESTRATOR :: New Workspace");
    harness.assert_screen_contains("Run in:");
}

/// The agent list ends in "custom…", whose whole purpose is to let the user
/// type an arbitrary command — so the Agent Command field has to be present,
/// and focusable, in the current-workspace shape too.
///
/// It wasn't. On a local *new workspace* that field lives under the Advanced
/// fold, and both the fold and the inline fallback were gated on "creating",
/// so running in the current workspace rendered no command box at all. Picking
/// "custom…" then left the form claiming an agent the user had no way to name,
/// and — because the preset hands focus to a `cmd` field that wasn't in the
/// focus cycle — dropped focus back to the top of the form, where the next
/// arrow key silently flipped "Launch in" to "New workspace".
#[test]
fn custom_agent_is_typable_when_running_in_the_current_workspace() {
    let (_temp, workspace) = set_up_workspace();
    let mut harness = open_form_on(&workspace);

    // Flip "Launch in" to the current workspace (as `Run Agent…` opens it).
    harness
        .send_key(KeyCode::BackTab, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    harness
        .send_key(KeyCode::BackTab, KeyModifiers::NONE)
        .unwrap();
    harness.tick_and_render().unwrap();
    harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("ORCHESTRATOR :: Run Agent"))
        .unwrap();

    // The command box is here even though the workspace-shaped Advanced fold
    // that normally holds it is not.
    harness.assert_screen_contains("Agent Command");
    harness.assert_screen_not_contains("Advanced");

    // Walk to the agent selector and step left, which wraps the list around to
    // "custom…" — the shortest route, and the one that used to strand focus.
    let mut guard = 0;
    while !focused_line(&harness.screen_to_string()).contains("Agent:") {
        harness.send_key(KeyCode::Tab, KeyModifiers::NONE).unwrap();
        harness.tick_and_render().unwrap();
        guard += 1;
        assert!(
            guard < 20,
            "Tab never reached the agent selector. Screen:\n{}",
            harness.screen_to_string(),
        );
    }
    harness.send_key(KeyCode::Left, KeyModifiers::NONE).unwrap();
    harness
        .wait_until(|h| h.screen_to_string().contains("custom"))
        .unwrap();

    // Focus followed the preset onto the command field, so the user can just
    // type — and, critically, is not sitting on the "Launch in" switch.
    let focused = focused_line(&harness.screen_to_string());
    assert!(
        !focused.contains("Launch in:"),
        "picking 'custom…' must not drop focus onto the Launch-in switch — the \
         next arrow key would change the workspace target. Screen:\n{}",
        harness.screen_to_string(),
    );

    // And what gets typed lands in the command box rather than nowhere.
    for ch in "zzcustomcmd".chars() {
        harness
            .send_key(KeyCode::Char(ch), KeyModifiers::NONE)
            .unwrap();
    }
    harness
        .wait_until(|h| h.screen_to_string().contains("zzcustomcmd"))
        .unwrap();
}
