#![cfg(unix)]

mod common;

use std::error::Error;
use std::time::Duration;

use common::{
    assert_success, drain_attach_output, drain_attach_output_bytes, read_until_contains,
    read_until_contains_all, terminate_child, AttachedSession, CliHarness,
};
use rmux_core::{input::InputParser, Screen};
use rmux_proto::TerminalSize as ScreenTerminalSize;
use rmux_pty::TerminalSize;

const IO_TIMEOUT: Duration = Duration::from_secs(5);

type PaneWidthRow = (usize, usize, bool);
type TestResult<T> = Result<T, Box<dyn Error>>;

fn list_pane_widths(harness: &CliHarness, target: &str) -> TestResult<Vec<PaneWidthRow>> {
    let output = harness.run(&[
        "list-panes",
        "-t",
        target,
        "-F",
        "#{pane_index}:#{pane_width}:#{pane_active}",
    ])?;
    common::stdout(&output)
        .lines()
        .filter(|line| !line.is_empty())
        .map(|line| {
            let mut parts = line.split(':');
            let pane_index = parts
                .next()
                .ok_or_else(|| format!("missing pane index in {line:?}"))?
                .parse::<usize>()?;
            let pane_width = parts
                .next()
                .ok_or_else(|| format!("missing pane width in {line:?}"))?
                .parse::<usize>()?;
            let pane_active = parts
                .next()
                .ok_or_else(|| format!("missing pane_active in {line:?}"))?
                == "1";
            if parts.next().is_some() {
                return Err(format!("unexpected pane format {line:?}").into());
            }
            Ok((pane_index, pane_width, pane_active))
        })
        .collect()
}

fn list_pane_geometry(harness: &CliHarness, target: &str) -> Result<Vec<String>, Box<dyn Error>> {
    let output = harness.run(&[
        "list-panes",
        "-t",
        target,
        "-F",
        "#{pane_index}:#{pane_width}:#{pane_height}:#{pane_left}:#{pane_top}:#{pane_active}",
    ])?;
    Ok(common::stdout(&output)
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .map(str::to_owned)
        .collect())
}

fn window_layout(harness: &CliHarness, target: &str) -> Result<String, Box<dyn Error>> {
    let output = harness.run(&["list-windows", "-t", target, "-F", "#{window_layout}"])?;
    Ok(common::stdout(&output).trim().to_owned())
}

fn send_shell_marker(
    harness: &CliHarness,
    target: &str,
    marker: &str,
) -> Result<(), Box<dyn Error>> {
    assert_success(&harness.run(&["send-keys", "-t", target, "C-c"])?);
    std::thread::sleep(Duration::from_millis(30));
    let escaped = marker
        .as_bytes()
        .iter()
        .map(|byte| format!("\\{byte:03o}"))
        .collect::<String>();
    let command = format!("printf '{escaped}\\012'");
    assert_success(&harness.run(&["send-keys", "-t", target, &command, "Enter"])?);
    Ok(())
}

fn capture_attach_transcript(
    screen: &mut Screen,
    parser: &mut InputParser,
    bytes: &[u8],
) -> Result<String, Box<dyn Error>> {
    parser.parse(bytes, screen);
    Ok(String::from_utf8(screen.capture_transcript(
        Default::default(),
        Default::default(),
    ))?)
}

fn transcript_without_status_line(transcript: &str) -> String {
    let mut lines = transcript.lines().collect::<Vec<_>>();
    let _ = lines.pop();
    if lines.is_empty() {
        String::new()
    } else {
        format!("{}\n", lines.join("\n"))
    }
}

fn line_has_isolated_left_chevron(line: &str) -> bool {
    line.contains('│')
        && line
            .chars()
            .collect::<Vec<_>>()
            .windows(3)
            .any(|window| matches!(window, [' ', '<', ' ']))
}

fn apply_quiescent_attach_output(
    attach: &mut AttachedSession,
    screen: &mut Screen,
    parser: &mut InputParser,
    wait: Duration,
) -> Result<String, Box<dyn Error>> {
    std::thread::sleep(wait);
    let bytes = drain_attach_output_bytes(attach.master_mut())?;
    capture_attach_transcript(screen, parser, &bytes)
}

fn wait_for_attach_transcript_matching<F>(
    attach: &mut AttachedSession,
    screen: &mut Screen,
    parser: &mut InputParser,
    timeout: Duration,
    description: &str,
    mut matches: F,
) -> Result<String, Box<dyn Error>>
where
    F: FnMut(&str) -> bool,
{
    let deadline = std::time::Instant::now() + timeout;
    loop {
        let bytes = drain_attach_output_bytes(attach.master_mut())?;
        let transcript = capture_attach_transcript(screen, parser, &bytes)?;
        let content = transcript_without_status_line(&transcript);
        if matches(&content) {
            return Ok(transcript);
        }
        if std::time::Instant::now() >= deadline {
            return Err(format!("timed out waiting for {description}, got:\n{content}").into());
        }
        std::thread::sleep(Duration::from_millis(50));
    }
}

fn wait_for_pane_count(
    harness: &CliHarness,
    target: &str,
    expected: usize,
) -> Result<Vec<String>, Box<dyn Error>> {
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    loop {
        let pane_indexes =
            common::stdout(&harness.run(&["list-panes", "-t", target, "-F", "#{pane_index}"])?)
                .lines()
                .map(str::trim)
                .filter(|line| !line.is_empty())
                .map(str::to_owned)
                .collect::<Vec<_>>();
        if pane_indexes.len() == expected {
            return Ok(pane_indexes);
        }
        if std::time::Instant::now() >= deadline {
            return Err(format!(
                "timed out waiting for {expected} panes in {target}, got {:?}",
                pane_indexes
            )
            .into());
        }
        std::thread::sleep(Duration::from_millis(20));
    }
}

fn wait_for_layout_change(
    harness: &CliHarness,
    target: &str,
    previous_layout: &str,
) -> Result<String, Box<dyn Error>> {
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    loop {
        let current = window_layout(harness, target)?;
        if current != previous_layout {
            return Ok(current);
        }
        if std::time::Instant::now() >= deadline {
            return Err(format!(
                "timed out waiting for window layout change in {target}, still {previous_layout}"
            )
            .into());
        }
        std::thread::sleep(Duration::from_millis(20));
    }
}

fn active_pane_target(harness: &CliHarness, session_name: &str) -> Result<String, Box<dyn Error>> {
    let output = harness.run(&[
        "list-panes",
        "-t",
        session_name,
        "-F",
        "#{pane_index}:#{pane_active}",
    ])?;
    let panes = common::stdout(&output);
    let pane_index = panes
        .lines()
        .find_map(|line| {
            let (pane_index, active) = line.split_once(':')?;
            (active == "1").then_some(pane_index)
        })
        .ok_or_else(|| format!("missing active pane in {session_name}"))?;
    Ok(format!("{session_name}:0.{pane_index}"))
}

fn wait_for_mode_tree_exit(harness: &CliHarness, target: &str) -> Result<(), Box<dyn Error>> {
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    loop {
        let output = harness.run(&[
            "display-message",
            "-p",
            "-t",
            target,
            "#{pane_in_mode}|#{pane_mode}",
        ])?;
        if common::stdout(&output).trim_end() == "0|" {
            return Ok(());
        }
        if std::time::Instant::now() >= deadline {
            return Err(format!("timed out waiting for mode-tree to exit in {target}").into());
        }
        std::thread::sleep(Duration::from_millis(20));
    }
}

fn wait_for_mode_tree_exit_collecting_output(
    harness: &CliHarness,
    attach: &mut AttachedSession,
    target: &str,
) -> Result<Vec<u8>, Box<dyn Error>> {
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    let mut output_bytes = Vec::new();
    loop {
        output_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);
        let output = harness.run(&[
            "display-message",
            "-p",
            "-t",
            target,
            "#{pane_in_mode}|#{pane_mode}",
        ])?;
        if common::stdout(&output).trim_end() == "0|" {
            return Ok(output_bytes);
        }
        if std::time::Instant::now() >= deadline {
            return Err(format!("timed out waiting for mode-tree to exit in {target}").into());
        }
        std::thread::sleep(Duration::from_millis(20));
    }
}

fn wait_for_mode_tree_enter(harness: &CliHarness, target: &str) -> Result<(), Box<dyn Error>> {
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    loop {
        let output = harness.run(&[
            "display-message",
            "-p",
            "-t",
            target,
            "#{pane_in_mode}|#{pane_mode}",
        ])?;
        if common::stdout(&output).trim_end() == "1|tree-mode" {
            return Ok(());
        }
        if std::time::Instant::now() >= deadline {
            return Err(format!("timed out waiting for mode-tree to enter in {target}").into());
        }
        std::thread::sleep(Duration::from_millis(20));
    }
}

fn assert_mode_tree_exited_with_shell_name(output: &std::process::Output) {
    let rendered = common::stdout(output);
    assert!(
        rendered == "bash|0|\n" || rendered == "zsh|0|\n" || rendered == "sh|0|\n",
        "expected shell window name with no active mode, got: {rendered:?}"
    );
}

#[test]
fn attach_session_binary_uses_the_real_attach_stream_and_restores_terminal(
) -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    assert!(
        attach.child_mut().try_wait()?.is_none(),
        "attach-session should remain in the streaming attach loop"
    );

    assert_success(&harness.run(&["send-keys", "-t", "alpha:0.0", "printf READY", "Enter"])?);
    let screen = read_until_contains(attach.master_mut(), "READY", IO_TIMEOUT)?;
    assert!(
        screen.contains("READY"),
        "attach output should contain pane data, got: {screen:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn attach_session_detach_emits_a_single_attach_stop_sequence() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-detach")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "[alpha]", IO_TIMEOUT)?;

    attach.send_bytes(b"\x02d")?;
    let (status, output) = attach.wait_for_exit_with_output(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    assert!(
        output
            .windows(b"[detached (from session alpha)]\r\n".len())
            .any(|window| window == b"[detached (from session alpha)]\r\n"),
        "detach output should contain the detached banner, got: {:?}",
        String::from_utf8_lossy(&output)
    );
    assert_eq!(
        output
            .windows(b"\x1b[?1049l".len())
            .filter(|window| *window == b"\x1b[?1049l")
            .count(),
        1,
        "detach output should contain exactly one alternate-screen exit, got: {:?}",
        String::from_utf8_lossy(&output)
    );

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn inactive_pane_output_refreshes_the_real_attached_client() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-inactive-pane-output")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["select-pane", "-t", "alpha:0.0"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    assert_success(&harness.run(&["send-keys", "-t", "alpha:0.1", "printf INACTIVE", "Enter"])?);
    let refreshed = read_until_contains(attach.master_mut(), "INACTIVE", IO_TIMEOUT)?;
    assert!(
        refreshed.contains("INACTIVE"),
        "inactive pane output should redraw the attached client, got: {refreshed:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn attach_session_survives_manual_mate_terminal_drag_sequence() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-mouse-drag")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "[alpha]", IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;

    attach.send_bytes(b"\x1b[<0;7;1M\x1b[<32;9;1M\x1b[<32;10;1M")?;
    let rendered = read_until_contains(attach.master_mut(), "[0/0]", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    assert!(
        attach.child_mut().try_wait()?.is_none(),
        "attach client should stay alive after the captured mate-terminal drag sequence"
    );
    assert!(
        rendered.contains("\x1b[0;30;43m"),
        "post-drag attach output should contain the copy-mode selection style, got: {rendered:?}"
    );
    let replay = {
        let mut screen = Screen::new(ScreenTerminalSize { cols: 80, rows: 24 }, 0);
        let mut parser = InputParser::new();
        parser.parse(rendered.as_bytes(), &mut screen);
        String::from_utf8(screen.capture_transcript(Default::default(), Default::default()))?
    };
    assert!(
        replay.contains("tester@RMUXHOST"),
        "post-drag attach screen should still show the prompt, got: {replay:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let (status, output) = attach.wait_for_exit_with_output(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;
    assert!(
        !String::from_utf8_lossy(&output).contains("Broken pipe"),
        "attach output should not report a broken pipe: {:?}",
        String::from_utf8_lossy(&output)
    );

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn attach_session_survives_live_mouse_binding_runtime_errors() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-mouse-binding-error")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&[
        "bind-key",
        "-n",
        "MouseDown1Pane",
        "select-pane",
        "-t",
        "missing:9.9",
    ])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "[alpha]", IO_TIMEOUT)?;

    attach.send_bytes(b"\x1b[<0;7;1M\x1b[<0;7;1m")?;
    std::thread::sleep(Duration::from_millis(200));

    assert!(
        attach.child_mut().try_wait()?.is_none(),
        "attach client should stay alive after a mouse binding runtime error"
    );

    let messages = harness.run(&["show-messages"])?;
    let rendered_messages = common::stdout(&messages);
    assert!(
        rendered_messages.contains("can't find session: missing"),
        "show-messages should record the attached mouse binding error, got: {rendered_messages:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let (status, output) = attach.wait_for_exit_with_output(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;
    assert!(
        !String::from_utf8_lossy(&output).contains("Broken pipe"),
        "attach output should not report a broken pipe after a mouse binding error: {:?}",
        String::from_utf8_lossy(&output)
    );

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn attach_session_display_panes_renders_overlay_to_the_real_client() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-display-panes")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["set", "-g", "display-panes-time", "5000"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    assert_success(&harness.run(&["display-panes", "-t", "alpha"])?);

    let overlay = read_until_contains_all(attach.master_mut(), &["\x1b[?25l", "x"], IO_TIMEOUT)?;
    assert!(
        overlay.contains("\x1b[41m") || overlay.contains("\x1b[44m"),
        "display-panes overlay should contain tmux-style pane colours, got: {overlay:?}"
    );
    assert!(
        overlay.contains("40x23")
            || overlay.contains("39x23")
            || overlay.contains("40x24")
            || overlay.contains("39x24"),
        "display-panes overlay should contain pane size labels, got: {overlay:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_q_renders_display_panes_overlay_on_the_real_attached_client() -> Result<(), Box<dyn Error>>
{
    let harness = CliHarness::new("attach-flow-prefix-q")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["set", "-g", "display-panes-time", "5000"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02q")?;

    let overlay = read_until_contains_all(attach.master_mut(), &["\x1b[?25l", "x"], IO_TIMEOUT)?;
    assert!(
        overlay.contains("\x1b[41m") || overlay.contains("\x1b[44m"),
        "prefix q should render the display-panes overlay, got: {overlay:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_n_and_p_on_single_window_show_status_without_closing_attach() -> Result<(), Box<dyn Error>>
{
    let harness = CliHarness::new("attach-flow-prefix-window-edge")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02n")?;
    let next_message = read_until_contains(attach.master_mut(), "No next window", IO_TIMEOUT)?;
    assert!(
        next_message.contains("No next window"),
        "prefix n should show tmux's attached status message, got: {next_message:?}"
    );
    assert!(
        attach.child_mut().try_wait()?.is_none(),
        "prefix n on a single-window session must not close the attached client"
    );

    attach.send_bytes(b"\x02p")?;
    let previous_message =
        read_until_contains(attach.master_mut(), "No previous window", IO_TIMEOUT)?;
    assert!(
        previous_message.contains("No previous window"),
        "prefix p should show tmux's attached status message, got: {previous_message:?}"
    );
    assert!(
        attach.child_mut().try_wait()?.is_none(),
        "prefix p on a single-window session must not close the attached client"
    );

    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:0.0",
        "printf STILL_ATTACHED",
        "Enter",
    ])?);
    let alive_output = read_until_contains(attach.master_mut(), "STILL_ATTACHED", IO_TIMEOUT)?;
    assert!(
        alive_output.contains("STILL_ATTACHED"),
        "attach stream should remain usable after status errors, got: {alive_output:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_x_during_display_panes_opens_kill_pane_prompt() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-display-panes-prefix-x")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["select-pane", "-t", "alpha:0.1"])?);
    assert_success(&harness.run(&["set", "-g", "display-panes-time", "5000"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02q")?;
    let overlay = read_until_contains(attach.master_mut(), "\x1b[?25l", IO_TIMEOUT)?;
    assert!(
        overlay.contains("\x1b[?25l"),
        "prefix q should enter display-panes before the prefix x transition, got: {overlay:?}"
    );

    attach.send_bytes(b"\x02x")?;
    let prompt = read_until_contains(attach.master_mut(), "kill-pane 1? (y/n)", IO_TIMEOUT)?;
    assert!(
        prompt.contains("kill-pane 1? (y/n)"),
        "prefix x during display-panes should open the normal kill-pane prompt, got: {prompt:?}"
    );
    assert!(
        attach.child_mut().try_wait()?.is_none(),
        "prefix x during display-panes must not close the attached client before confirmation"
    );

    attach.send_bytes(b"n")?;
    std::thread::sleep(Duration::from_millis(100));
    drain_attach_output(attach.master_mut())?;

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_split_when_too_small_reports_no_space_without_closing_attach(
) -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-prefix-split-no-space")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(40, 8))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    let mut captured = String::new();
    for _ in 0..20 {
        attach.send_bytes(b"\x02%")?;
        std::thread::sleep(Duration::from_millis(75));
        let chunk = drain_attach_output_bytes(attach.master_mut())?;
        captured.push_str(&String::from_utf8_lossy(&chunk));
        if captured.contains("No space for new pane") {
            break;
        }
    }

    assert!(
        captured.contains("No space for new pane"),
        "prefix % should show tmux's attached no-space status message, got: {captured:?}"
    );
    assert!(
        attach.child_mut().try_wait()?.is_none(),
        "prefix % at pane saturation must not close the attached client"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn choose_tree_moves_down_one_item_per_down_key() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-down")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "alpha", "-n", "beta"])?);
    assert_success(&harness.run(&["send-keys", "-t", "alpha:1.0", "printf beta0", "Enter"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:1.0"])?);
    assert_success(&harness.run(&["send-keys", "-t", "alpha:1.1", "printf beta1", "Enter"])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "alpha", "-n", "gamma"])?);
    assert_success(&harness.run(&["send-keys", "-t", "alpha:2.0", "printf gamma0", "Enter"])?);
    assert_success(&harness.run(&["select-window", "-t", "alpha:0"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "[alpha]", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02w")?;
    let tree = read_until_contains(attach.master_mut(), "alpha: 3 windows", IO_TIMEOUT)?;
    assert!(
        tree.contains("1: beta") && tree.contains("2: gamma"),
        "choose-tree should render all windows, got: {tree:?}"
    );
    wait_for_mode_tree_enter(&harness, "alpha:0.0")?;
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x1b[B")?;
    let after_down = read_until_contains(attach.master_mut(), "beta1", IO_TIMEOUT)?;
    assert!(
        after_down.contains("beta1"),
        "one Down key in choose-tree should move from 0 to beta, got: {after_down:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn choose_tree_filter_prompt_uses_tmux_parenthesized_label() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-filter-prompt")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "alpha", "-n", "beta"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "[alpha]", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "alpha: 2 windows", IO_TIMEOUT)?;
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"f")?;
    let prompt = read_until_contains(attach.master_mut(), "(filter) ", IO_TIMEOUT)?;
    assert!(
        prompt.contains("(filter) "),
        "choose-tree filter prompt should match tmux, got: {prompt:?}"
    );

    attach.send_bytes(b"\x1b\x1b")?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_w_x_opens_the_real_choose_tree_kill_prompt() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-kill-prompt")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "alpha", "-n", "beta"])?);
    assert_success(&harness.run(&["select-window", "-t", "alpha:0"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "[alpha]", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "alpha: 2 windows", IO_TIMEOUT)?;
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"x")?;
    let prompt = read_until_contains(attach.master_mut(), "Kill window 0?", IO_TIMEOUT)?;
    assert!(
        prompt.contains("Kill window 0?"),
        "choose-tree x should open the tmux-style kill prompt for the current window, got: {prompt:?}"
    );

    attach.send_bytes(b"y")?;
    std::thread::sleep(Duration::from_millis(250));
    drain_attach_output(attach.master_mut())?;

    let windows = harness.run(&["list-windows", "-t", "alpha", "-F", "#{window_index}"])?;
    let window_lines = common::stdout(&windows)
        .lines()
        .filter(|line| !line.is_empty())
        .count();
    assert_eq!(
        window_lines, 1,
        "confirming choose-tree x should kill the selected window"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_w_then_prefix_comma_opens_rename_window_prompt() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-prefix-comma")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha:0.0"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "sort: index", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(100));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02,")?;
    let prompt = read_until_contains(attach.master_mut(), "(rename-window) ", IO_TIMEOUT)?;
    assert!(
        prompt.contains("(rename-window) "),
        "prefix comma inside choose-tree should open the normal rename-window prompt, got: {prompt:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_w_then_prefix_x_uses_the_normal_kill_pane_binding() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-prefix-x")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha:0.0"])?);

    let active_pane = common::stdout(&harness.run(&[
        "list-panes",
        "-t",
        "alpha:0",
        "-F",
        "#{pane_index} #{pane_active}",
    ])?)
    .lines()
    .find_map(|line| {
        let mut parts = line.split_whitespace();
        let pane = parts.next()?;
        let active = parts.next()?;
        (active == "1").then_some(pane.to_owned())
    })
    .ok_or("missing active pane after split sequence")?;

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "sort: index", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(100));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02x")?;
    let prompt = read_until_contains(attach.master_mut(), "kill-pane", IO_TIMEOUT)?;
    let expected = format!("kill-pane {active_pane}? (y/n)");
    assert!(
        prompt.contains(&expected),
        "prefix x inside choose-tree should open the normal kill-pane prompt, expected {expected:?}, got: {prompt:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_w_prefix_x_confirmed_clears_choose_tree_when_host_pane_dies() -> Result<(), Box<dyn Error>>
{
    let harness = CliHarness::new("attach-flow-choose-tree-prefix-x-confirm")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:0.0",
        "cd /tmp/rmux-workspace",
        "Enter",
    ])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let initial = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut initial_bytes = initial.into_bytes();
    initial_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);

    let mut screen = Screen::new(
        ScreenTerminalSize {
            cols: 100,
            rows: 30,
        },
        0,
    );
    let mut parser = InputParser::new();
    let _ = capture_attach_transcript(&mut screen, &mut parser, &initial_bytes)?;

    attach.send_bytes(b"\x02%\x02%\x02\"")?;
    let pane_indexes = wait_for_pane_count(&harness, "alpha", 4)?;
    let _ = apply_quiescent_attach_output(
        &mut attach,
        &mut screen,
        &mut parser,
        Duration::from_millis(300),
    )?;
    for pane_index in &pane_indexes {
        assert_success(&harness.run(&[
            "send-keys",
            "-t",
            &format!("alpha:0.{pane_index}"),
            "export PS1='tester@RMUXHOST:~$ '",
            "Enter",
            "clear",
            "Enter",
        ])?);
    }
    std::thread::sleep(Duration::from_millis(300));
    drain_attach_output(attach.master_mut())?;

    let last_marker = pane_indexes
        .iter()
        .take(3)
        .nth(2)
        .map(|pane_index| format!("P{pane_index}"))
        .ok_or("missing pane indexes after split sequence")?;
    let marker_refs = pane_indexes
        .iter()
        .take(3)
        .map(|pane_index| format!("P{pane_index}"))
        .collect::<Vec<_>>();
    let marker_ref_slices = marker_refs.iter().map(String::as_str).collect::<Vec<_>>();
    for pane_index in pane_indexes.iter().take(3) {
        let target = format!("alpha:0.{pane_index}");
        let marker = format!("P{pane_index}");
        send_shell_marker(&harness, &target, &marker)?;
    }
    let marker_output = read_until_contains(attach.master_mut(), &last_marker, IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut marker_bytes = marker_output.into_bytes();
    marker_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);
    let _ = capture_attach_transcript(&mut screen, &mut parser, &marker_bytes)?;
    let _ = wait_for_attach_transcript_matching(
        &mut attach,
        &mut screen,
        &mut parser,
        IO_TIMEOUT,
        "pane markers before choose-tree kill prompt",
        |content| {
            marker_ref_slices
                .iter()
                .all(|marker| content.contains(marker))
        },
    )?;

    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "sort: index", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(100));
    let tree_bytes = drain_attach_output_bytes(attach.master_mut())?;
    let _ = capture_attach_transcript(&mut screen, &mut parser, &tree_bytes)?;

    attach.send_bytes(b"\x02x")?;
    let prompt = read_until_contains(attach.master_mut(), "kill-pane", IO_TIMEOUT)?;
    let _ = capture_attach_transcript(&mut screen, &mut parser, prompt.as_bytes())?;
    assert!(
        prompt.contains("kill-pane"),
        "prefix x inside choose-tree should target a pane, got: {prompt:?}"
    );

    attach.send_bytes(b"y")?;
    wait_for_pane_count(&harness, "alpha", 3)?;
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    let mut redraw_bytes = Vec::new();
    let tree_after_kill = loop {
        std::thread::sleep(Duration::from_millis(50));
        let next_bytes = drain_attach_output_bytes(attach.master_mut())?;
        if !next_bytes.is_empty() {
            redraw_bytes.extend_from_slice(&next_bytes);
            let transcript = capture_attach_transcript(&mut screen, &mut parser, &next_bytes)?;
            if !transcript.contains("sort: index")
                && !transcript.contains("┌ 0 (sort: index)")
                && !transcript.contains("└────────────────")
                && (transcript.contains("tester@RMUXHOST:~$")
                    || transcript.contains("P1")
                    || transcript.contains("P2")
                    || transcript.contains("P3"))
            {
                break transcript;
            }
        }
        if std::time::Instant::now() >= deadline {
            break String::from_utf8(
                screen.capture_transcript(Default::default(), Default::default()),
            )?;
        }
    };
    let redraw_len = redraw_bytes.len();
    let redraw_debug = String::from_utf8_lossy(&redraw_bytes);
    let clear_count = redraw_debug.matches("\x1b[2J").count();
    let sort_count = redraw_debug.matches("sort: index").count();
    let clear_pos = redraw_debug.find("\x1b[2J");
    let first_sort_pos = redraw_debug.find("sort: index");
    let last_sort_pos = redraw_debug.rfind("sort: index");
    let mode_state = common::stdout(&harness.run(&[
        "list-panes",
        "-t",
        "alpha:0",
        "-F",
        "#{pane_index}:#{pane_active}:#{pane_in_mode}:#{pane_mode}:#{window_panes}",
    ])?);
    let captured_pane = common::stdout(&harness.run(&["capture-pane", "-p", "-t", "alpha:0"])?);

    assert!(
        mode_state
            .lines()
            .all(|line| line.ends_with(":0::3")),
        "killing a pane from choose-tree should leave tree-mode in every remaining pane and keep three panes, got {mode_state:?}"
    );
    assert!(
        !tree_after_kill.contains("sort: index")
            && !tree_after_kill.contains("┌ 0 (sort: index)")
            && !tree_after_kill.contains("└────────────────"),
        "confirming kill-pane from choose-tree should clear the stale choose-tree overlay, mode={mode_state:?}, capture={captured_pane:?}, redraw_len={redraw_len}, clear_count={clear_count}, sort_count={sort_count}, clear_pos={clear_pos:?}, first_sort_pos={first_sort_pos:?}, last_sort_pos={last_sort_pos:?}, got:\n{tree_after_kill}"
    );
    assert!(
        tree_after_kill.contains("tester@RMUXHOST:~$")
            || tree_after_kill.contains("P1")
            || tree_after_kill.contains("P2")
            || tree_after_kill.contains("P3"),
        "after clearing choose-tree, the live pane layout should be visible, mode={mode_state:?}, got:\n{tree_after_kill}"
    );
    let has_cropped_prompt = tree_after_kill.lines().any(|line| {
        line.starts_with("oPC:~$")
            || line.starts_with("er@RMUXHOST:~$")
            || line.starts_with("iner@RMUXHOST:~$")
            || line.contains("│oPC:~$")
            || line.contains("│er@RMUXHOST:~$")
            || line.contains("│iner@RMUXHOST:~$")
    });
    assert!(
        !has_cropped_prompt,
        "clearing choose-tree should remove the stale cropped preview, mode={mode_state:?}, got:\n{tree_after_kill}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_w_then_prefix_q_renders_display_panes_overlay() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-prefix-q")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["set", "-g", "display-panes-time", "5000"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "sort: index", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(100));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02q")?;
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    let mut overlay = String::new();
    loop {
        std::thread::sleep(Duration::from_millis(50));
        overlay.push_str(&String::from_utf8_lossy(&drain_attach_output_bytes(
            attach.master_mut(),
        )?));
        if overlay.contains("\x1b[41m")
            || overlay.contains("\x1b[44m")
            || overlay.contains("\x1b[42m")
            || overlay.contains("\x1b[45m")
        {
            break;
        }
        if std::time::Instant::now() >= deadline {
            break;
        }
    }
    assert!(
        overlay.contains("\x1b[41m")
            || overlay.contains("\x1b[44m")
            || overlay.contains("\x1b[42m")
            || overlay.contains("\x1b[45m"),
        "prefix q inside choose-tree should render the display-panes overlay, got: {overlay:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn compact_prefix_wq_closes_choose_tree_when_keys_arrive_in_one_burst() -> Result<(), Box<dyn Error>>
{
    let harness = CliHarness::new("attach-flow-choose-tree-compact-close")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "alpha", "-n", "beta"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02wq")?;
    wait_for_mode_tree_exit(&harness, "alpha:0.0")?;

    let output = harness.run(&[
        "display-message",
        "-p",
        "-t",
        "alpha:0.0",
        "#{window_name}|#{pane_in_mode}|#{pane_mode}",
    ])?;
    assert_mode_tree_exited_with_shell_name(&output);

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn compact_prefix_tq_exits_clock_mode_when_keys_arrive_in_one_burst() -> Result<(), Box<dyn Error>>
{
    let harness = CliHarness::new("attach-flow-clock-compact-close")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02tq")?;
    wait_for_mode_tree_exit(&harness, "alpha:0.0")?;

    let output = harness.run(&[
        "display-message",
        "-p",
        "-t",
        "alpha:0.0",
        "#{window_name}|#{pane_in_mode}|#{pane_mode}",
    ])?;
    assert_mode_tree_exited_with_shell_name(&output);

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_w_then_prefix_q_timeout_restores_choose_tree_overlay() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-prefix-q-timeout")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["set", "-g", "display-panes-time", "200"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let initial = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut initial_bytes = initial.into_bytes();
    initial_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);

    let mut screen = Screen::new(
        ScreenTerminalSize {
            cols: 100,
            rows: 30,
        },
        0,
    );
    let mut parser = InputParser::new();
    let _ = capture_attach_transcript(&mut screen, &mut parser, &initial_bytes)?;

    let active_pane = active_pane_target(&harness, "alpha")?;
    attach.send_bytes(b"\x02w")?;
    let tree_output = read_until_contains(attach.master_mut(), "sort: index", IO_TIMEOUT)?;
    let mut tree_bytes = tree_output.into_bytes();
    tree_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);
    let _ = capture_attach_transcript(&mut screen, &mut parser, &tree_bytes)?;
    wait_for_mode_tree_enter(&harness, &active_pane)?;
    let baseline_tree = wait_for_attach_transcript_matching(
        &mut attach,
        &mut screen,
        &mut parser,
        IO_TIMEOUT,
        "choose-tree baseline",
        |content| {
            content.contains("sort: index")
                && content.contains("(0) - alpha: 1 windows (attached)")
                && content.contains("└─> + 0: [rmux]*Z")
                && content.contains("│ 0 │")
                && content.contains("│ 3 │")
        },
    )?;
    let baseline_tree_content = transcript_without_status_line(&baseline_tree);
    assert!(
        baseline_tree_content.contains("sort: index"),
        "choose-tree baseline should contain the mode-tree header, got:\n{baseline_tree_content}"
    );

    attach.send_bytes(b"\x02q")?;
    let overlay = read_until_contains_all(attach.master_mut(), &["\x1b[?25l", "x"], IO_TIMEOUT)?;
    let mut overlay_bytes = overlay.into_bytes();
    overlay_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);
    let _ = capture_attach_transcript(&mut screen, &mut parser, &overlay_bytes)?;

    let restored_tree = wait_for_attach_transcript_matching(
        &mut attach,
        &mut screen,
        &mut parser,
        IO_TIMEOUT,
        "choose-tree overlay after display-panes timeout",
        |content| {
            content.contains("sort: index")
                && content.contains("(0) - alpha: 1 windows (attached)")
                && content.contains("└─> + 0: [rmux]*Z")
                && content.contains("│ 0 │")
                && content.contains("│ 3 │")
        },
    )?;
    let restored_tree_content = transcript_without_status_line(&restored_tree);
    assert!(
        restored_tree_content.contains("sort: index")
            && restored_tree_content.contains("(0) - alpha: 1 windows (attached)")
            && restored_tree_content.contains("└─> + 0: [rmux]*Z")
            && restored_tree_content.contains("│ 0 │")
            && restored_tree_content.contains("│ 3 │"),
        "display-panes timeout inside choose-tree should restore the choose-tree overlay\n--- baseline ---\n{baseline_tree_content}\n--- restored ---\n{restored_tree_content}"
    );

    attach.send_bytes(b"\x1b\x1b")?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_w_marks_the_host_pane_as_tmux_mode_for_status_formats() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-pane-mode")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "alpha", "-n", "beta"])?);
    assert_success(&harness.run(&["select-window", "-t", "alpha:0"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "[alpha]", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "alpha: 2 windows", IO_TIMEOUT)?;
    drain_attach_output(attach.master_mut())?;

    let output = harness.run(&[
        "display-message",
        "-p",
        "-t",
        "alpha:0.0",
        "#{window_name}|#{pane_in_mode}|#{pane_mode}",
    ])?;
    assert_eq!(common::stdout(&output), "[rmux]|1|tree-mode\n");
    let windows = harness.run(&[
        "list-windows",
        "-t",
        "alpha",
        "-F",
        "#{window_index}:#{window_name}",
    ])?;
    assert!(
        common::stdout(&windows)
            .lines()
            .any(|line| line == "0:[rmux]"),
        "choose-tree should update the stored automatic window name, got: {:?}",
        common::stdout(&windows)
    );

    attach.send_bytes(b"\x1b\x1b")?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn choose_tree_q_restores_the_four_pane_layout_on_the_real_attached_client(
) -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-choose-tree-restore")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(100, 30))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let initial = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut initial_bytes = initial.into_bytes();
    initial_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);

    let mut screen = Screen::new(
        ScreenTerminalSize {
            cols: 100,
            rows: 30,
        },
        0,
    );
    let mut parser = InputParser::new();
    let _ = capture_attach_transcript(&mut screen, &mut parser, &initial_bytes)?;

    attach.send_bytes(b"\x02%\x02%\x02\"")?;
    let pane_count_deadline = std::time::Instant::now() + IO_TIMEOUT;
    loop {
        let output = harness.run(&["list-panes", "-t", "alpha", "-F", "#{pane_index}"])?;
        if common::stdout(&output).lines().count() == 4 {
            break;
        }
        if std::time::Instant::now() >= pane_count_deadline {
            return Err("timed out waiting for four panes after prefix % % \"".into());
        }
        std::thread::sleep(Duration::from_millis(20));
    }
    let pane_indexes =
        common::stdout(&harness.run(&["list-panes", "-t", "alpha", "-F", "#{pane_index}"])?)
            .lines()
            .map(str::trim)
            .filter(|line| !line.is_empty())
            .map(str::to_owned)
            .collect::<Vec<_>>();
    let _ = apply_quiescent_attach_output(
        &mut attach,
        &mut screen,
        &mut parser,
        Duration::from_millis(300),
    )?;
    for pane_index in pane_indexes.iter().take(3) {
        let target = format!("alpha:0.{pane_index}");
        let marker = format!("P{pane_index}");
        send_shell_marker(&harness, &target, &marker)?;
    }
    let marker = pane_indexes
        .iter()
        .take(3)
        .nth(2)
        .map(|pane_index| format!("P{pane_index}"))
        .ok_or("missing pane indexes after split sequence")?;
    let marker_refs = pane_indexes
        .iter()
        .take(3)
        .map(|pane_index| format!("P{pane_index}"))
        .collect::<Vec<_>>();
    let marker_ref_slices = marker_refs.iter().map(String::as_str).collect::<Vec<_>>();
    let marker_output = read_until_contains(attach.master_mut(), &marker, IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut layout_bytes = marker_output.into_bytes();
    layout_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);
    let _ = capture_attach_transcript(&mut screen, &mut parser, &layout_bytes)?;
    let _ = wait_for_attach_transcript_matching(
        &mut attach,
        &mut screen,
        &mut parser,
        IO_TIMEOUT,
        "four pane marker baseline",
        |content| {
            marker_ref_slices
                .iter()
                .all(|marker| content.contains(marker))
        },
    )?;
    assert!(
        common::stdout(&harness.run(&["list-panes", "-t", "alpha", "-F", "#{pane_index}"])?)
            .lines()
            .count()
            == 4,
        "expected four panes after the interactive split sequence"
    );
    let baseline_geometry = list_pane_geometry(&harness, "alpha")?;

    let active_pane = active_pane_target(&harness, "alpha")?;
    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "windows", IO_TIMEOUT)?;
    wait_for_mode_tree_enter(&harness, &active_pane)?;
    std::thread::sleep(Duration::from_millis(100));
    let tree_bytes = drain_attach_output_bytes(attach.master_mut())?;
    let _ = capture_attach_transcript(&mut screen, &mut parser, &tree_bytes)?;

    attach.send_bytes(b"q")?;
    let mut after_q_bytes =
        wait_for_mode_tree_exit_collecting_output(&harness, &mut attach, &active_pane)?;
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    let restored = loop {
        after_q_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);
        let transcript = capture_attach_transcript(&mut screen, &mut parser, &after_q_bytes)?;
        after_q_bytes.clear();
        let panes = transcript_without_status_line(&transcript);
        if marker_ref_slices
            .iter()
            .all(|marker| panes.contains(marker))
            && !panes.contains("sort: index")
        {
            break transcript;
        }
        if std::time::Instant::now() >= deadline {
            panic!("choose-tree q did not settle back to panes before timeout, got:\n{panes}");
        }
        std::thread::sleep(Duration::from_millis(50));
        after_q_bytes = drain_attach_output_bytes(attach.master_mut())?;
    };

    let restored_panes = transcript_without_status_line(&restored);
    let restored_geometry = list_pane_geometry(&harness, "alpha")?;
    assert_eq!(
        restored_geometry, baseline_geometry,
        "choose-tree q should restore the exact pane geometry"
    );
    assert!(
        marker_ref_slices
            .iter()
            .all(|marker| restored_panes.contains(marker))
            && !restored_panes.contains("sort: index"),
        "choose-tree q should restore pane content instead of mode-tree content\n--- restored ---\n{restored_panes}"
    );

    let settled = apply_quiescent_attach_output(
        &mut attach,
        &mut screen,
        &mut parser,
        Duration::from_millis(1200),
    )?;
    let settled_panes = transcript_without_status_line(&settled);
    let settled_geometry = list_pane_geometry(&harness, "alpha")?;
    assert_eq!(
        settled_geometry, baseline_geometry,
        "choose-tree q should not alter pane geometry after queued redraws settle"
    );
    assert!(
        !settled_panes.contains("sort: index")
            && !settled_panes.contains("┌ 0 (sort: index)")
            && !settled_panes.contains("└─> + 0: [rmux]*Z"),
        "choose-tree q should not leave stale mode-tree content after queued redraws settle\n--- settled ---\n{settled_panes}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn choose_tree_q_after_resize_restores_the_resized_four_pane_layout() -> Result<(), Box<dyn Error>>
{
    let harness = CliHarness::new("attach-flow-choose-tree-resize-restore")?;
    let mut daemon = harness.start_hidden_daemon()?;

    let initial_size = TerminalSize::new(100, 30);
    let resized_size = TerminalSize::new(120, 40);
    let initial_screen_size = ScreenTerminalSize {
        cols: 100,
        rows: 30,
    };
    let resized_screen_size = ScreenTerminalSize {
        cols: 120,
        rows: 40,
    };

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["new-session", "-d", "-s", "beta"])?);

    let mut alpha = AttachedSession::spawn(&harness, "alpha", initial_size)?;
    alpha.wait_for_raw_mode(IO_TIMEOUT)?;
    let alpha_initial = read_until_contains(alpha.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut alpha_initial_bytes = alpha_initial.into_bytes();
    alpha_initial_bytes.extend(drain_attach_output_bytes(alpha.master_mut())?);

    let mut alpha_screen = Screen::new(initial_screen_size, 0);
    let mut alpha_parser = InputParser::new();
    let _ = capture_attach_transcript(&mut alpha_screen, &mut alpha_parser, &alpha_initial_bytes)?;

    alpha.send_bytes(b"\x02%\x02%\x02\"")?;
    let alpha_indexes = wait_for_pane_count(&harness, "alpha", 4)?;
    let _ = apply_quiescent_attach_output(
        &mut alpha,
        &mut alpha_screen,
        &mut alpha_parser,
        Duration::from_millis(300),
    )?;
    for pane_index in &alpha_indexes {
        assert_success(&harness.run(&[
            "send-keys",
            "-t",
            &format!("alpha:0.{pane_index}"),
            "export PS1='p$ '",
            "Enter",
            "clear",
            "Enter",
            &format!("echo A{pane_index}"),
            "Enter",
        ])?);
    }
    alpha.send_bytes(b"\x02r")?;
    let alpha_marker = alpha_indexes
        .last()
        .map(|pane_index| format!("A{pane_index}"))
        .ok_or("missing alpha pane indexes after split sequence")?;
    let alpha_markers = alpha_indexes
        .iter()
        .map(|pane_index| format!("A{pane_index}"))
        .collect::<Vec<_>>();
    let alpha_layout_output = read_until_contains(alpha.master_mut(), &alpha_marker, IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut alpha_layout_bytes = alpha_layout_output.into_bytes();
    alpha_layout_bytes.extend(drain_attach_output_bytes(alpha.master_mut())?);
    let _ = capture_attach_transcript(&mut alpha_screen, &mut alpha_parser, &alpha_layout_bytes)?;

    let alpha_layout_before_resize = window_layout(&harness, "alpha")?;
    let alpha_active_pane = active_pane_target(&harness, "alpha")?;
    alpha.send_bytes(b"\x02w")?;
    let _ = read_until_contains(alpha.master_mut(), "windows", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(100));
    let tree_bytes = drain_attach_output_bytes(alpha.master_mut())?;
    let _ = capture_attach_transcript(&mut alpha_screen, &mut alpha_parser, &tree_bytes)?;

    alpha.resize(resized_size)?;
    let _alpha_layout_after_resize =
        wait_for_layout_change(&harness, "alpha", &alpha_layout_before_resize)?;
    alpha_screen.resize(resized_screen_size);
    std::thread::sleep(Duration::from_millis(300));
    let alpha_resize_output = read_until_contains(alpha.master_mut(), &alpha_marker, IO_TIMEOUT)?;
    let mut alpha_resize_bytes = alpha_resize_output.into_bytes();
    alpha_resize_bytes.extend(drain_attach_output_bytes(alpha.master_mut())?);
    let _ = capture_attach_transcript(&mut alpha_screen, &mut alpha_parser, &alpha_resize_bytes)?;
    std::thread::sleep(Duration::from_millis(1000));
    drain_attach_output(alpha.master_mut())?;
    alpha.send_bytes(b"q")?;
    let mut pending_after_q =
        wait_for_mode_tree_exit_collecting_output(&harness, &mut alpha, &alpha_active_pane)?;
    let alpha_marker_refs = alpha_markers.iter().map(String::as_str).collect::<Vec<_>>();
    let alpha_command_refs = alpha_indexes
        .iter()
        .map(|pane_index| format!("p$ echo A{pane_index}"))
        .collect::<Vec<_>>();
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    let alpha_restored = loop {
        pending_after_q.extend(drain_attach_output_bytes(alpha.master_mut())?);
        assert!(
            !pending_after_q.is_empty(),
            "dismissing choose-tree after a resize should emit a redraw"
        );
        let transcript =
            capture_attach_transcript(&mut alpha_screen, &mut alpha_parser, &pending_after_q)?;
        pending_after_q.clear();
        let panes = transcript_without_status_line(&transcript);
        if alpha_marker_refs
            .iter()
            .all(|marker| panes.contains(marker))
            && alpha_command_refs
                .iter()
                .all(|command| panes.contains(command))
            && !panes.contains("sort: index")
        {
            break transcript;
        }
        if std::time::Instant::now() >= deadline {
            panic!(
                "choose-tree q after resize did not settle back to panes before timeout, got:\n{panes}"
            );
        }
        std::thread::sleep(Duration::from_millis(50));
        pending_after_q = drain_attach_output_bytes(alpha.master_mut())?;
    };

    let mut beta = AttachedSession::spawn(&harness, "beta", initial_size)?;
    beta.wait_for_raw_mode(IO_TIMEOUT)?;
    let beta_initial = read_until_contains(beta.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut beta_initial_bytes = beta_initial.into_bytes();
    beta_initial_bytes.extend(drain_attach_output_bytes(beta.master_mut())?);

    let mut beta_screen = Screen::new(initial_screen_size, 0);
    let mut beta_parser = InputParser::new();
    let _ = capture_attach_transcript(&mut beta_screen, &mut beta_parser, &beta_initial_bytes)?;

    beta.send_bytes(b"\x02%\x02%\x02\"")?;
    let beta_indexes = wait_for_pane_count(&harness, "beta", 4)?;
    let _ = apply_quiescent_attach_output(
        &mut beta,
        &mut beta_screen,
        &mut beta_parser,
        Duration::from_millis(300),
    )?;
    for pane_index in &beta_indexes {
        assert_success(&harness.run(&[
            "send-keys",
            "-t",
            &format!("beta:0.{pane_index}"),
            "export PS1='p$ '",
            "Enter",
            "clear",
            "Enter",
            &format!("echo A{pane_index}"),
            "Enter",
        ])?);
    }
    beta.send_bytes(b"\x02r")?;
    let beta_marker = beta_indexes
        .last()
        .map(|pane_index| format!("A{pane_index}"))
        .ok_or("missing beta pane indexes after split sequence")?;
    let beta_markers = beta_indexes
        .iter()
        .map(|pane_index| format!("A{pane_index}"))
        .collect::<Vec<_>>();
    let beta_layout_output = read_until_contains(beta.master_mut(), &beta_marker, IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut beta_layout_bytes = beta_layout_output.into_bytes();
    beta_layout_bytes.extend(drain_attach_output_bytes(beta.master_mut())?);
    let _ = capture_attach_transcript(&mut beta_screen, &mut beta_parser, &beta_layout_bytes)?;

    let beta_layout_before_resize = window_layout(&harness, "beta")?;
    beta.resize(resized_size)?;
    let _beta_layout_after_resize =
        wait_for_layout_change(&harness, "beta", &beta_layout_before_resize)?;
    beta_screen.resize(resized_screen_size);
    std::thread::sleep(Duration::from_millis(1000));
    let beta_marker_refs = beta_markers.iter().map(String::as_str).collect::<Vec<_>>();
    let beta_command_refs = beta_indexes
        .iter()
        .map(|pane_index| format!("p$ echo A{pane_index}"))
        .collect::<Vec<_>>();
    let deadline = std::time::Instant::now() + IO_TIMEOUT;
    let beta_expected = loop {
        let beta_resize_bytes = drain_attach_output_bytes(beta.master_mut())?;
        if !beta_resize_bytes.is_empty() {
            let transcript =
                capture_attach_transcript(&mut beta_screen, &mut beta_parser, &beta_resize_bytes)?;
            let panes = transcript_without_status_line(&transcript);
            if beta_marker_refs.iter().all(|marker| panes.contains(marker))
                && beta_command_refs
                    .iter()
                    .all(|command| panes.contains(command))
                && !panes.contains("[beta]")
            {
                break transcript;
            }
        }
        if std::time::Instant::now() >= deadline {
            let transcript = beta_screen.capture_transcript(Default::default(), Default::default());
            let panes = String::from_utf8_lossy(&transcript);
            panic!("plain resized layout did not settle before timeout, got:\n{panes}");
        }
        std::thread::sleep(Duration::from_millis(50));
    };

    assert_eq!(
        list_pane_geometry(&harness, "alpha")?,
        list_pane_geometry(&harness, "beta")?,
        "choose-tree resize should not change the underlying pane geometry"
    );
    let alpha_panes = transcript_without_status_line(&alpha_restored);
    let beta_panes = transcript_without_status_line(&beta_expected);
    assert_eq!(
        alpha_panes, beta_panes,
        "choose-tree q after resize should restore the same resized layout as a plain resize\n--- expected ---\n{beta_panes}\n--- actual ---\n{alpha_panes}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let alpha_status = alpha.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(alpha_status.code(), Some(0));
    alpha.assert_restored()?;

    assert_success(&harness.run(&["kill-session", "-t", "beta"])?);
    let beta_status = beta.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(beta_status.code(), Some(0));
    beta.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn choose_tree_preview_gutter_uses_tmux_margin_when_columns_overflow() -> Result<(), Box<dyn Error>>
{
    let harness = CliHarness::new("attach-flow-choose-tree-preview-gutter")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let initial = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut initial_bytes = initial.into_bytes();
    initial_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);

    let mut screen = Screen::new(ScreenTerminalSize { cols: 80, rows: 24 }, 0);
    let mut parser = InputParser::new();
    let _ = capture_attach_transcript(&mut screen, &mut parser, &initial_bytes)?;

    attach.send_bytes(b"\x02%\x02%\x02\"")?;
    let pane_count_deadline = std::time::Instant::now() + IO_TIMEOUT;
    loop {
        let output = harness.run(&["list-panes", "-t", "alpha", "-F", "#{pane_index}"])?;
        if common::stdout(&output).lines().count() == 4 {
            break;
        }
        if std::time::Instant::now() >= pane_count_deadline {
            return Err("timed out waiting for four panes after prefix % % \"".into());
        }
        std::thread::sleep(Duration::from_millis(20));
    }
    let pane_indexes =
        common::stdout(&harness.run(&["list-panes", "-t", "alpha", "-F", "#{pane_index}"])?)
            .lines()
            .map(str::trim)
            .filter(|line| !line.is_empty())
            .map(str::to_owned)
            .collect::<Vec<_>>();
    let _ = apply_quiescent_attach_output(
        &mut attach,
        &mut screen,
        &mut parser,
        Duration::from_millis(300),
    )?;
    for pane_index in pane_indexes.iter().take(3) {
        let target = format!("alpha:0.{pane_index}");
        let marker = format!("P{pane_index}");
        send_shell_marker(&harness, &target, &marker)?;
    }
    let marker = pane_indexes
        .iter()
        .take(3)
        .nth(2)
        .map(|pane_index| format!("P{pane_index}"))
        .ok_or("missing pane indexes after split sequence")?;
    let marker_output = read_until_contains(attach.master_mut(), &marker, IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    let mut marker_bytes = marker_output.into_bytes();
    marker_bytes.extend(drain_attach_output_bytes(attach.master_mut())?);
    let _ = capture_attach_transcript(&mut screen, &mut parser, &marker_bytes)?;
    let marker_refs = pane_indexes
        .iter()
        .take(3)
        .map(|pane_index| format!("P{pane_index}"))
        .collect::<Vec<_>>();
    let marker_ref_slices = marker_refs.iter().map(String::as_str).collect::<Vec<_>>();
    let _ = wait_for_attach_transcript_matching(
        &mut attach,
        &mut screen,
        &mut parser,
        IO_TIMEOUT,
        "pane markers before choose-tree preview",
        |content| {
            marker_ref_slices
                .iter()
                .all(|marker| content.contains(marker))
        },
    )?;

    attach.send_bytes(b"\x02w")?;
    let _ = read_until_contains(attach.master_mut(), "sort: index", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(100));
    let tree_bytes = drain_attach_output_bytes(attach.master_mut())?;
    let _ = capture_attach_transcript(&mut screen, &mut parser, &tree_bytes)?;
    let tree = wait_for_attach_transcript_matching(
        &mut attach,
        &mut screen,
        &mut parser,
        IO_TIMEOUT,
        "choose-tree preview with left chevron",
        |content| {
            content.contains("sort: index") && content.lines().any(line_has_isolated_left_chevron)
        },
    )?;
    let Some(chevron_line) = tree
        .lines()
        .find(|line| line_has_isolated_left_chevron(line))
    else {
        return Err(format!(
            "choose-tree preview did not render an isolated left chevron:\n{tree}"
        )
        .into());
    };
    assert!(
        !chevron_line.contains("│<") && !chevron_line.contains("<│"),
        "choose-tree preview gutter should not glue the left chevron to the box border, got:\n{tree}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_x_kills_the_current_pane_on_the_real_attached_client() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-prefix-x")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["select-pane", "-t", "alpha:0.1"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    attach.send_bytes(b"\x02x")?;
    let prompt = read_until_contains(attach.master_mut(), "kill-pane", IO_TIMEOUT)?;
    assert!(
        prompt.contains("kill-pane 1? (y/n)"),
        "prefix x should open the kill-pane confirmation prompt, got: {prompt:?}"
    );

    attach.send_bytes(b"y")?;
    std::thread::sleep(Duration::from_millis(250));
    drain_attach_output(attach.master_mut())?;

    let panes = harness.run(&["list-panes", "-t", "alpha:0", "-F", "#{pane_index}"])?;
    let pane_lines = common::stdout(&panes)
        .lines()
        .filter(|line| !line.is_empty())
        .count();
    assert_eq!(
        pane_lines, 1,
        "confirming prefix x should kill the active pane"
    );

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_ctrl_arrow_resizes_the_real_attached_pane() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-prefix-ctrl-arrow")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "leftcase"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "leftcase:0.0"])?);
    assert_success(&harness.run(&["select-pane", "-t", "leftcase:0.1"])?);

    let mut left_attach = AttachedSession::spawn(&harness, "leftcase", TerminalSize::new(80, 24))?;
    left_attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(left_attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(left_attach.master_mut())?;

    let before_left = list_pane_widths(&harness, "leftcase:0")?;
    left_attach.send_bytes(b"\x02\x1b[1;5D")?;
    std::thread::sleep(Duration::from_millis(250));
    drain_attach_output(left_attach.master_mut())?;
    let after_left = list_pane_widths(&harness, "leftcase:0")?;
    assert!(
        after_left[0].1 < before_left[0].1 && after_left[1].1 > before_left[1].1,
        "prefix Ctrl-Left should expand the active right pane leftwards, before={before_left:?} after={after_left:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "leftcase"])?);
    let left_status = left_attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(left_status.code(), Some(0));
    left_attach.assert_restored()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "rightcase"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "rightcase:0.0"])?);
    assert_success(&harness.run(&["select-pane", "-t", "rightcase:0.0"])?);

    let mut right_attach =
        AttachedSession::spawn(&harness, "rightcase", TerminalSize::new(80, 24))?;
    right_attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(right_attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(right_attach.master_mut())?;

    let before_right = list_pane_widths(&harness, "rightcase:0")?;
    right_attach.send_bytes(b"\x02\x1b[1;5C")?;
    std::thread::sleep(Duration::from_millis(250));
    drain_attach_output(right_attach.master_mut())?;
    let after_right = list_pane_widths(&harness, "rightcase:0")?;
    assert!(
        after_right[0].1 > before_right[0].1 && after_right[1].1 < before_right[1].1,
        "prefix Ctrl-Right should expand the active left pane rightwards, before={before_right:?} after={after_right:?}"
    );

    assert_success(&harness.run(&["kill-session", "-t", "rightcase"])?);
    let right_status = right_attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(right_status.code(), Some(0));
    right_attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}

#[test]
fn prefix_meta_digits_select_layouts_on_the_real_attached_client() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("attach-flow-prefix-meta-digits")?;
    let mut daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha:0.0"])?);

    let mut attach = AttachedSession::spawn(&harness, "alpha", TerminalSize::new(80, 24))?;
    attach.wait_for_raw_mode(IO_TIMEOUT)?;
    let _ = read_until_contains(attach.master_mut(), "tester@RMUXHOST", IO_TIMEOUT)?;
    std::thread::sleep(Duration::from_millis(200));
    drain_attach_output(attach.master_mut())?;

    for (bytes, expected_layout, starting_layout) in [
        (b"\x02\x1b1".as_slice(), "even-horizontal", "tiled"),
        (b"\x02\x1b2".as_slice(), "even-vertical", "tiled"),
        (b"\x02\x1b3".as_slice(), "main-horizontal", "tiled"),
        (b"\x02\x1b4".as_slice(), "main-vertical", "tiled"),
        (b"\x02\x1b5".as_slice(), "tiled", "even-horizontal"),
    ] {
        assert_success(&harness.run(&["select-layout", "-t", "alpha:0", expected_layout])?);
        let expected_dump = window_layout(&harness, "alpha")?;
        std::thread::sleep(Duration::from_millis(100));
        drain_attach_output(attach.master_mut())?;

        assert_success(&harness.run(&["select-layout", "-t", "alpha:0", starting_layout])?);
        std::thread::sleep(Duration::from_millis(300));
        drain_attach_output(attach.master_mut())?;
        attach.send_bytes(bytes)?;
        let deadline = std::time::Instant::now() + IO_TIMEOUT;
        let actual_dump = loop {
            drain_attach_output(attach.master_mut())?;
            let actual_dump = window_layout(&harness, "alpha")?;
            if actual_dump == expected_dump || std::time::Instant::now() >= deadline {
                break actual_dump;
            }
            std::thread::sleep(Duration::from_millis(50));
        };
        assert_eq!(
            actual_dump, expected_dump,
            "prefix meta digit {:?} should match select-layout {expected_layout}, got {actual_dump:?}",
            bytes
        );
    }

    assert_success(&harness.run(&["kill-session", "-t", "alpha"])?);
    let status = attach.wait_for_exit(IO_TIMEOUT)?;
    assert_eq!(status.code(), Some(0));
    attach.assert_restored()?;

    terminate_child(daemon.child_mut())?;
    Ok(())
}
