//! The bottom-pane footer renders transient hints and context indicators.
//!
//! The footer is pure rendering: it formats `FooterProps` into `Line`s without mutating any state.
//! It intentionally does not decide *which* footer content should be shown; that is owned by the
//! `ChatComposer` (which selects a `FooterMode`) and by higher-level state machines like
//! `ChatWidget` (which decides when quit/interrupt is allowed).
//!
//! Some footer content is time-based rather than event-based, such as the "press again to quit"
//! hint. The owning widgets schedule redraws so time-based hints can expire even if the UI is
//! otherwise idle.
//!
//! Terminology used in this module:
//! - "status line" means the configurable contextual row built from `/statusline` items such as
//!   model, git branch, and context usage.
//! - "instructional footer" means a row that tells the user what to do next, such as quit
//!   confirmation, shortcut help, or queue hints.
//! - "contextual footer" means the footer is free to show ambient context instead of an
//!   instruction. In that state, the footer may render the configured status line, the active
//!   agent label, side-conversation state, or some combination of those.
//!
//! Single-line collapse overview:
//! 1. The composer decides the current `FooterMode` and hint flags, then calls
//!    `single_line_footer_layout` for the base single-line modes.
//! 2. `single_line_footer_layout` applies the width-based fallback rules:
//!    (If this description is hard to follow, just try it out by resizing
//!    your terminal width; these rules were built out of trial and error.)
//!    - Start with the fullest left-side hint plus the right-side context.
//!    - When the queue hint is active, prefer keeping that queue hint visible,
//!      even if it means dropping the right-side context earlier; the queue
//!      hint may also be shortened before it is removed.
//!    - When the queue hint is not active but the mode cycle hint is applicable,
//!      drop "? for shortcuts" before dropping "(shift+tab to cycle)".
//!    - If "(shift+tab to cycle)" cannot fit, also hide the right-side
//!      context to avoid too many state transitions in quick succession.
//!    - Finally, try a mode-only line (with and without context), and fall
//!      back to no left-side footer if nothing can fit.
//! 3. When collapse chooses a specific line, callers render it via
//!    `render_footer_line`. Otherwise, callers render the straightforward
//!    mode-to-text mapping via `render_footer_from_props`.
//!
//! In short: `single_line_footer_layout` chooses *what* best fits, and the two
//! render helpers choose whether to draw the chosen line or the default
//! `FooterProps` mapping.
use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::render::line_utils::prefix_lines;
use crate::status::format_tokens_compact;
use crate::ui_consts::FOOTER_INDENT_COLS;
use crossterm::event::KeyCode;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;

/// The rendering inputs for the footer area under the composer.
///
/// Callers are expected to construct `FooterProps` from higher-level state (`ChatComposer`,
/// `BottomPane`, and `ChatWidget`) and pass it to the footer render helpers
/// (`render_footer_from_props` or the single-line collapse logic). The footer
/// treats these values as authoritative and does not attempt to infer missing
/// state (for example, it does not query whether a task is running).
#[derive(Clone, Debug)]
pub(crate) struct FooterProps {
    pub(crate) mode: FooterMode,
    pub(crate) esc_backtrack_hint: bool,
    pub(crate) use_shift_enter_hint: bool,
    pub(crate) is_task_running: bool,
    pub(crate) queue_submissions: bool,
    pub(crate) collaboration_modes_enabled: bool,
    pub(crate) is_wsl: bool,
    /// Which key the user must press again to quit.
    ///
    /// This is rendered when `mode` is `FooterMode::QuitShortcutReminder`.
    pub(crate) quit_shortcut_key: KeyBinding,
    pub(crate) status_line_value: Option<Line<'static>>,
    pub(crate) status_line_enabled: bool,
    pub(crate) key_hints: FooterKeyHints,
    /// Active thread label shown when the footer is rendering contextual information instead of an
    /// instructional hint.
    ///
    /// When both this label and the configured status line are available, they are rendered on the
    /// same row separated by ` · `.
    pub(crate) active_agent_label: Option<String>,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CollaborationModeIndicator {
    Plan,
    #[allow(dead_code)] // Hidden by current mode filtering; kept for future UI re-enablement.
    PairProgramming,
    #[allow(dead_code)] // Hidden by current mode filtering; kept for future UI re-enablement.
    Execute,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum GoalStatusIndicator {
    Active { usage: Option<String> },
    Paused,
    Blocked,
    UsageLimited,
    BudgetLimited { usage: Option<String> },
    Complete { usage: Option<String> },
}

const MODE_CYCLE_HINT: &str = "shift+tab to cycle";
const FOOTER_CONTEXT_GAP_COLS: u16 = 1;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct FooterKeyHints {
    pub(crate) toggle_shortcuts: Option<KeyBinding>,
    pub(crate) queue: Option<KeyBinding>,
    pub(crate) insert_newline: Option<KeyBinding>,
    pub(crate) external_editor: Option<KeyBinding>,
    pub(crate) edit_previous: Option<KeyBinding>,
    pub(crate) show_transcript: Option<KeyBinding>,
    pub(crate) history_search: Option<KeyBinding>,
    pub(crate) reasoning_down: Option<KeyBinding>,
    pub(crate) reasoning_up: Option<KeyBinding>,
}

impl FooterKeyHints {
    #[cfg(test)]
    pub(crate) fn default_bindings() -> Self {
        Self {
            toggle_shortcuts: Some(key_hint::plain(KeyCode::Char('?'))),
            queue: Some(key_hint::plain(KeyCode::Tab)),
            insert_newline: Some(key_hint::ctrl(KeyCode::Char('j'))),
            external_editor: Some(key_hint::ctrl(KeyCode::Char('g'))),
            edit_previous: Some(key_hint::plain(KeyCode::Esc)),
            show_transcript: Some(key_hint::ctrl(KeyCode::Char('t'))),
            history_search: Some(key_hint::ctrl(KeyCode::Char('r'))),
            reasoning_down: Some(key_hint::alt(KeyCode::Char(','))),
            reasoning_up: Some(key_hint::alt(KeyCode::Char('.'))),
        }
    }
}

impl CollaborationModeIndicator {
    fn label(self, show_cycle_hint: bool) -> String {
        let suffix = if show_cycle_hint {
            format!(" ({MODE_CYCLE_HINT})")
        } else {
            String::new()
        };
        match self {
            CollaborationModeIndicator::Plan => format!("Plan mode{suffix}"),
            CollaborationModeIndicator::PairProgramming => {
                format!("Pair Programming mode{suffix}")
            }
            CollaborationModeIndicator::Execute => format!("Execute mode{suffix}"),
        }
    }

    fn styled_span(self, show_cycle_hint: bool) -> Span<'static> {
        let label = self.label(show_cycle_hint);
        match self {
            CollaborationModeIndicator::Plan => Span::from(label).magenta(),
            CollaborationModeIndicator::PairProgramming => Span::from(label).cyan(),
            CollaborationModeIndicator::Execute => Span::from(label).dim(),
        }
    }
}

/// Selects which footer content is rendered.
///
/// The current mode is owned by `ChatComposer`, which may override it based on transient state
/// (for example, showing `QuitShortcutReminder` only while its timer is active).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum FooterMode {
    /// Single-line incremental history search prompt shown while Ctrl+R search is active.
    HistorySearch,
    /// Transient "press again to quit" reminder (Ctrl+C/Ctrl+D).
    QuitShortcutReminder,
    /// Multi-line shortcut overlay shown after pressing `?`.
    ShortcutOverlay,
    /// Transient "press Esc again" hint shown after the first Esc while idle.
    EscHint,
    /// Base single-line footer when the composer is empty.
    ComposerEmpty,
    /// Base single-line footer when the composer contains a draft.
    ///
    /// The shortcuts hint is suppressed here; when a task is running, this
    /// mode can show the queue hint instead.
    ComposerHasDraft,
}

pub(crate) fn toggle_shortcut_mode(
    current: FooterMode,
    ctrl_c_hint: bool,
    is_empty: bool,
) -> FooterMode {
    if ctrl_c_hint && matches!(current, FooterMode::QuitShortcutReminder) {
        return current;
    }

    let base_mode = if is_empty {
        FooterMode::ComposerEmpty
    } else {
        FooterMode::ComposerHasDraft
    };

    match current {
        FooterMode::ShortcutOverlay | FooterMode::QuitShortcutReminder => base_mode,
        _ => FooterMode::ShortcutOverlay,
    }
}

pub(crate) fn esc_hint_mode(current: FooterMode, is_task_running: bool) -> FooterMode {
    if is_task_running {
        current
    } else {
        FooterMode::EscHint
    }
}

pub(crate) fn reset_mode_after_activity(current: FooterMode) -> FooterMode {
    match current {
        FooterMode::EscHint
        | FooterMode::ShortcutOverlay
        | FooterMode::QuitShortcutReminder
        | FooterMode::HistorySearch
        | FooterMode::ComposerHasDraft => FooterMode::ComposerEmpty,
        other => other,
    }
}

pub(crate) fn footer_height(props: &FooterProps) -> u16 {
    let show_shortcuts_hint = match props.mode {
        FooterMode::ComposerEmpty => true,
        FooterMode::ComposerHasDraft => false,
        FooterMode::HistorySearch
        | FooterMode::QuitShortcutReminder
        | FooterMode::ShortcutOverlay
        | FooterMode::EscHint => false,
    };
    let show_queue_hint = match props.mode {
        FooterMode::ComposerHasDraft => props.is_task_running,
        FooterMode::QuitShortcutReminder
        | FooterMode::HistorySearch
        | FooterMode::ComposerEmpty
        | FooterMode::ShortcutOverlay
        | FooterMode::EscHint => false,
    };
    footer_from_props_lines(
        props,
        /*collaboration_mode_indicator*/ None,
        /*show_cycle_hint*/ false,
        show_shortcuts_hint,
        show_queue_hint,
    )
    .len() as u16
}

/// Render a single precomputed footer line.
pub(crate) fn render_footer_line(area: Rect, buf: &mut Buffer, line: Line<'static>) {
    Paragraph::new(prefix_lines(
        vec![line],
        " ".repeat(FOOTER_INDENT_COLS).into(),
        " ".repeat(FOOTER_INDENT_COLS).into(),
    ))
    .render(area, buf);
}

/// Render footer content directly from `FooterProps`.
///
/// This is intentionally not part of the width-based collapse/fallback logic.
/// Transient instructional states (shortcut overlay, Esc hint, quit reminder)
/// prioritize "what to do next" instructions and currently suppress the
/// collaboration mode label entirely. When collapse logic has already chosen a
/// specific single line, prefer `render_footer_line`.
pub(crate) fn render_footer_from_props(
    area: Rect,
    buf: &mut Buffer,
    props: &FooterProps,
    collaboration_mode_indicator: Option<CollaborationModeIndicator>,
    show_cycle_hint: bool,
    show_shortcuts_hint: bool,
    show_queue_hint: bool,
) {
    Paragraph::new(prefix_lines(
        footer_from_props_lines(
            props,
            collaboration_mode_indicator,
            show_cycle_hint,
            show_shortcuts_hint,
            show_queue_hint,
        ),
        " ".repeat(FOOTER_INDENT_COLS).into(),
        " ".repeat(FOOTER_INDENT_COLS).into(),
    ))
    .render(area, buf);
}

pub(crate) fn left_fits(area: Rect, left_width: u16) -> bool {
    let max_width = area.width.saturating_sub(FOOTER_INDENT_COLS as u16);
    left_width <= max_width
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SummaryHintKind {
    None,
    Shortcuts,
    QueueMessage,
    QueueShort,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct LeftSideState {
    hint: SummaryHintKind,
    show_cycle_hint: bool,
}

fn left_side_line(
    collaboration_mode_indicator: Option<CollaborationModeIndicator>,
    state: LeftSideState,
    key_hints: FooterKeyHints,
) -> Line<'static> {
    let mut line = Line::from("");
    match state.hint {
        SummaryHintKind::None => {}
        SummaryHintKind::Shortcuts => {
            if let Some(key) = key_hints.toggle_shortcuts {
                line.push_span(key);
                line.push_span(" for shortcuts".dim());
            }
        }
        SummaryHintKind::QueueMessage => {
            if let Some(key) = key_hints.queue {
                line.push_span(key);
                line.push_span(" to queue message".dim());
            }
        }
        SummaryHintKind::QueueShort => {
            if let Some(key) = key_hints.queue {
                line.push_span(key);
                line.push_span(" to queue".dim());
            }
        }
    };

    if let Some(collaboration_mode_indicator) = collaboration_mode_indicator {
        if !matches!(state.hint, SummaryHintKind::None) {
            line.push_span(" · ".dim());
        }
        line.push_span(collaboration_mode_indicator.styled_span(state.show_cycle_hint));
    }

    line
}

pub(crate) enum SummaryLeft {
    Default,
    Custom(Line<'static>),
    None,
}

/// Compute the single-line footer layout and whether the right-side context
/// indicator can be shown alongside it.
pub(crate) fn single_line_footer_layout(
    area: Rect,
    context_width: u16,
    collaboration_mode_indicator: Option<CollaborationModeIndicator>,
    show_cycle_hint: bool,
    show_shortcuts_hint: bool,
    show_queue_hint: bool,
    key_hints: FooterKeyHints,
) -> (SummaryLeft, bool) {
    let hint_kind = if show_queue_hint {
        SummaryHintKind::QueueMessage
    } else if show_shortcuts_hint {
        SummaryHintKind::Shortcuts
    } else {
        SummaryHintKind::None
    };
    let default_state = LeftSideState {
        hint: hint_kind,
        show_cycle_hint,
    };
    let default_line = left_side_line(collaboration_mode_indicator, default_state, key_hints);
    let default_width = default_line.width() as u16;
    if default_width > 0 && can_show_left_with_context(area, default_width, context_width) {
        return (SummaryLeft::Default, true);
    }

    let state_line = |state: LeftSideState| -> Line<'static> {
        if state == default_state {
            default_line.clone()
        } else {
            left_side_line(collaboration_mode_indicator, state, key_hints)
        }
    };
    let state_width = |state: LeftSideState| -> u16 { state_line(state).width() as u16 };
    // When the mode cycle hint is applicable (idle, non-queue mode), only show
    // the right-side context indicator if the "(shift+tab to cycle)" variant
    // can also fit.
    let context_requires_cycle_hint = show_cycle_hint && !show_queue_hint;

    if show_queue_hint {
        // In queue mode, prefer dropping context before dropping the queue hint.
        let queue_states = [
            default_state,
            LeftSideState {
                hint: SummaryHintKind::QueueMessage,
                show_cycle_hint: false,
            },
            LeftSideState {
                hint: SummaryHintKind::QueueShort,
                show_cycle_hint: false,
            },
        ];

        // Pass 1: keep the right-side context indicator if any queue variant
        // can fit alongside it. We skip adjacent duplicates because
        // `default_state` can already be the no-cycle queue variant.
        let mut previous_state: Option<LeftSideState> = None;
        for state in queue_states {
            if previous_state == Some(state) {
                continue;
            }
            previous_state = Some(state);
            let width = state_width(state);
            if width > 0 && can_show_left_with_context(area, width, context_width) {
                if state == default_state {
                    return (SummaryLeft::Default, true);
                }
                return (SummaryLeft::Custom(state_line(state)), true);
            }
        }

        // Pass 2: if context cannot fit, drop it before dropping the queue
        // hint. Reuse the same dedupe so we do not try equivalent states twice.
        let mut previous_state: Option<LeftSideState> = None;
        for state in queue_states {
            if previous_state == Some(state) {
                continue;
            }
            previous_state = Some(state);
            let width = state_width(state);
            if width > 0 && left_fits(area, width) {
                if state == default_state {
                    return (SummaryLeft::Default, false);
                }
                return (SummaryLeft::Custom(state_line(state)), false);
            }
        }
    } else if collaboration_mode_indicator.is_some() {
        if show_cycle_hint {
            // First fallback: drop shortcut hint but keep the cycle
            // hint on the mode label if it can fit.
            let cycle_state = LeftSideState {
                hint: SummaryHintKind::None,
                show_cycle_hint: true,
            };
            let cycle_width = state_width(cycle_state);
            if cycle_width > 0 && can_show_left_with_context(area, cycle_width, context_width) {
                return (SummaryLeft::Custom(state_line(cycle_state)), true);
            }
            if cycle_width > 0 && left_fits(area, cycle_width) {
                return (SummaryLeft::Custom(state_line(cycle_state)), false);
            }
        }

        // Next fallback: mode label only. If the cycle hint is applicable but
        // cannot fit, we also suppress context so the right side does not
        // outlive "(shift+tab to cycle)" on the left.
        let mode_only_state = LeftSideState {
            hint: SummaryHintKind::None,
            show_cycle_hint: false,
        };
        let mode_only_width = state_width(mode_only_state);
        if !context_requires_cycle_hint
            && mode_only_width > 0
            && can_show_left_with_context(area, mode_only_width, context_width)
        {
            return (
                SummaryLeft::Custom(state_line(mode_only_state)),
                true, // show_context
            );
        }
        if mode_only_width > 0 && left_fits(area, mode_only_width) {
            return (
                SummaryLeft::Custom(state_line(mode_only_state)),
                false, // show_context
            );
        }
    }

    // Final fallback: if queue variants (or other earlier states) could not fit
    // at all, drop every hint and try to show just the mode label.
    if let Some(collaboration_mode_indicator) = collaboration_mode_indicator {
        let mode_only_state = LeftSideState {
            hint: SummaryHintKind::None,
            show_cycle_hint: false,
        };
        // Compute the width without going through `state_line` so we do not
        // depend on `default_state` (which may still be a queue variant).
        let mode_only_width = left_side_line(
            Some(collaboration_mode_indicator),
            mode_only_state,
            key_hints,
        )
        .width() as u16;
        if !context_requires_cycle_hint
            && can_show_left_with_context(area, mode_only_width, context_width)
        {
            return (
                SummaryLeft::Custom(left_side_line(
                    Some(collaboration_mode_indicator),
                    mode_only_state,
                    key_hints,
                )),
                true, // show_context
            );
        }
        if left_fits(area, mode_only_width) {
            return (
                SummaryLeft::Custom(left_side_line(
                    Some(collaboration_mode_indicator),
                    mode_only_state,
                    key_hints,
                )),
                false, // show_context
            );
        }
    }

    (SummaryLeft::None, true)
}

pub(crate) fn mode_indicator_line(
    indicator: Option<CollaborationModeIndicator>,
    show_cycle_hint: bool,
) -> Option<Line<'static>> {
    indicator.map(|indicator| Line::from(vec![indicator.styled_span(show_cycle_hint)]))
}

pub(crate) fn goal_status_indicator_line(
    indicator: Option<&GoalStatusIndicator>,
) -> Option<Line<'static>> {
    let indicator = indicator?;
    let label = match indicator {
        GoalStatusIndicator::Active { usage } => {
            if let Some(usage) = usage {
                format!("Pursuing goal ({usage})")
            } else {
                "Pursuing goal".to_string()
            }
        }
        GoalStatusIndicator::Paused => "Goal paused (/goal resume)".to_string(),
        GoalStatusIndicator::Blocked => "Goal blocked (/goal resume)".to_string(),
        GoalStatusIndicator::UsageLimited => "Goal hit usage limits (/goal resume)".to_string(),
        GoalStatusIndicator::BudgetLimited { usage } => {
            if let Some(usage) = usage {
                format!("Goal unmet ({usage})")
            } else {
                "Goal abandoned".to_string()
            }
        }
        GoalStatusIndicator::Complete { usage } => {
            if let Some(usage) = usage {
                format!("Goal achieved ({usage})")
            } else {
                "Goal achieved".to_string()
            }
        }
    };

    Some(Line::from(vec![Span::from(label).magenta()]))
}

pub(crate) fn status_line_right_indicator_line(
    collaboration_mode_indicator: Option<CollaborationModeIndicator>,
    goal_status_indicator: Option<&GoalStatusIndicator>,
    ide_context_active: bool,
    show_cycle_hint: bool,
) -> Option<Line<'static>> {
    let primary_indicator = mode_indicator_line(collaboration_mode_indicator, show_cycle_hint)
        .or_else(|| goal_status_indicator_line(goal_status_indicator));
    let ide_context_indicator = ide_context_active.then(|| Line::from(vec!["IDE context".cyan()]));
    let mut line: Option<Line<'static>> = None;

    for indicator in [primary_indicator, ide_context_indicator]
        .into_iter()
        .flatten()
    {
        if let Some(line) = line.as_mut() {
            line.push_span(" · ".dim());
            for span in indicator.spans {
                line.push_span(span);
            }
        } else {
            line = Some(indicator);
        }
    }

    line
}

pub(crate) fn side_conversation_context_line(label: &str) -> Line<'static> {
    if let Some(rest) = label.strip_prefix("Side ") {
        Line::from(vec!["Side".magenta().bold(), format!(" {rest}").magenta()])
    } else {
        Line::from(label.to_string()).magenta()
    }
}

fn right_aligned_x(area: Rect, content_width: u16) -> Option<u16> {
    if area.is_empty() {
        return None;
    }

    let right_padding = FOOTER_INDENT_COLS as u16;
    let max_width = area.width.saturating_sub(right_padding);
    if content_width == 0 || max_width == 0 {
        return None;
    }

    if content_width >= max_width {
        return Some(area.x.saturating_add(right_padding));
    }

    Some(
        area.x
            .saturating_add(area.width)
            .saturating_sub(content_width)
            .saturating_sub(right_padding),
    )
}

pub(crate) fn max_left_width_for_right(area: Rect, right_width: u16) -> Option<u16> {
    let context_x = right_aligned_x(area, right_width)?;
    let left_start = area.x + FOOTER_INDENT_COLS as u16;

    // minimal one column gap between left and right
    let gap = FOOTER_CONTEXT_GAP_COLS;

    if context_x <= left_start + gap {
        return Some(0);
    }

    Some(context_x.saturating_sub(left_start + gap))
}

pub(crate) fn can_show_left_with_context(area: Rect, left_width: u16, context_width: u16) -> bool {
    let Some(context_x) = right_aligned_x(area, context_width) else {
        return true;
    };
    if left_width == 0 {
        return true;
    }
    let left_extent = FOOTER_INDENT_COLS as u16 + left_width + FOOTER_CONTEXT_GAP_COLS;
    left_extent <= context_x.saturating_sub(area.x)
}

pub(crate) fn render_context_right(area: Rect, buf: &mut Buffer, line: &Line<'static>) {
    if area.is_empty() {
        return;
    }

    let context_width = line.width() as u16;
    let Some(mut x) = right_aligned_x(area, context_width) else {
        return;
    };
    let y = area.y + area.height.saturating_sub(1);
    let max_x = area.x.saturating_add(area.width);

    for span in &line.spans {
        if x >= max_x {
            break;
        }
        let span_width = span.width() as u16;
        if span_width == 0 {
            continue;
        }
        let remaining = max_x.saturating_sub(x);
        let draw_width = span_width.min(remaining);
        buf.set_span(x, y, span, draw_width);
        x = x.saturating_add(span_width);
    }
}

pub(crate) fn inset_footer_hint_area(mut area: Rect) -> Rect {
    if area.width > 2 {
        area.x += 2;
        area.width = area.width.saturating_sub(2);
    }
    area
}

pub(crate) fn render_footer_hint_items(area: Rect, buf: &mut Buffer, items: &[(String, String)]) {
    if items.is_empty() {
        return;
    }

    footer_hint_items_line(items).render(inset_footer_hint_area(area), buf);
}

/// Map `FooterProps` to footer lines without width-based collapse.
///
/// This is the canonical FooterMode-to-text mapping. It powers transient,
/// instructional states (shortcut overlay, Esc hint, quit reminder) and also
/// the default rendering for base states when collapse is not applied (or when
/// `single_line_footer_layout` returns `SummaryLeft::Default`). Collapse and
/// fallback decisions live in `single_line_footer_layout`; this function only
/// formats the chosen/default content.
fn footer_from_props_lines(
    props: &FooterProps,
    collaboration_mode_indicator: Option<CollaborationModeIndicator>,
    show_cycle_hint: bool,
    show_shortcuts_hint: bool,
    show_queue_hint: bool,
) -> Vec<Line<'static>> {
    let key_hints = props.key_hints;
    // Passive footer context can come from the configurable status line, the
    // active agent label, or both combined.
    if let Some(status_line) = passive_footer_status_line(props) {
        return vec![status_line];
    }
    match props.mode {
        FooterMode::QuitShortcutReminder => {
            vec![quit_shortcut_reminder_line(props.quit_shortcut_key)]
        }
        FooterMode::HistorySearch => vec![Line::from("reverse-i-search: ").dim()],
        FooterMode::ComposerEmpty => {
            let state = LeftSideState {
                hint: if show_shortcuts_hint {
                    SummaryHintKind::Shortcuts
                } else {
                    SummaryHintKind::None
                },
                show_cycle_hint,
            };
            vec![left_side_line(
                collaboration_mode_indicator,
                state,
                key_hints,
            )]
        }
        FooterMode::ShortcutOverlay => {
            let state = ShortcutsState {
                use_shift_enter_hint: props.use_shift_enter_hint,
                esc_backtrack_hint: props.esc_backtrack_hint,
                is_task_running: props.is_task_running,
                queue_submissions: props.queue_submissions,
                is_wsl: props.is_wsl,
                collaboration_modes_enabled: props.collaboration_modes_enabled,
                key_hints,
            };
            shortcut_overlay_lines(state)
        }
        FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)],
        FooterMode::ComposerHasDraft => {
            let state = LeftSideState {
                hint: if show_queue_hint {
                    SummaryHintKind::QueueMessage
                } else if show_shortcuts_hint {
                    SummaryHintKind::Shortcuts
                } else {
                    SummaryHintKind::None
                },
                show_cycle_hint,
            };
            vec![left_side_line(
                collaboration_mode_indicator,
                state,
                key_hints,
            )]
        }
    }
}

/// Returns the contextual footer row when the footer is not busy showing an instructional hint.
///
/// The returned line may contain the configured status line, the currently viewed agent label, or
/// both combined. Active instructional states such as quit reminders, shortcut overlays, and queue
/// prompts deliberately return `None` so those call-to-action hints stay visible.
pub(crate) fn passive_footer_status_line(props: &FooterProps) -> Option<Line<'static>> {
    if !shows_passive_footer_line(props) {
        return None;
    }

    let mut line = if props.status_line_enabled {
        props.status_line_value.clone()
    } else {
        None
    };

    if let Some(active_agent_label) = props.active_agent_label.as_ref() {
        if let Some(existing) = line.as_mut() {
            existing.spans.push(" · ".dim());
            existing.spans.push(active_agent_label.clone().dim());
        } else {
            line = Some(Line::from(active_agent_label.clone()).dim());
        }
    }

    line
}

/// Whether the current footer mode allows contextual information to replace instructional hints.
///
/// In practice this means the composer is idle, or it has a draft but is not currently running a
/// task, so the footer can spend the row on ambient context instead of "what to do next" text.
pub(crate) fn shows_passive_footer_line(props: &FooterProps) -> bool {
    match props.mode {
        FooterMode::ComposerEmpty => true,
        FooterMode::ComposerHasDraft => !props.is_task_running,
        FooterMode::HistorySearch
        | FooterMode::QuitShortcutReminder
        | FooterMode::ShortcutOverlay
        | FooterMode::EscHint => false,
    }
}

/// Whether callers should reserve the dedicated status-line layout for a contextual footer row.
///
/// The dedicated layout exists for the configurable `/statusline` row. An agent label by itself
/// can be rendered by the standard footer flow, so this only becomes `true` when the status line
/// feature is enabled and the current mode allows contextual footer content.
pub(crate) fn uses_passive_footer_status_layout(props: &FooterProps) -> bool {
    props.status_line_enabled && shows_passive_footer_line(props)
}

pub(crate) fn footer_line_width(
    props: &FooterProps,
    collaboration_mode_indicator: Option<CollaborationModeIndicator>,
    show_cycle_hint: bool,
    show_shortcuts_hint: bool,
    show_queue_hint: bool,
) -> u16 {
    footer_from_props_lines(
        props,
        collaboration_mode_indicator,
        show_cycle_hint,
        show_shortcuts_hint,
        show_queue_hint,
    )
    .last()
    .map(|line| line.width() as u16)
    .unwrap_or(0)
}

pub(crate) fn footer_hint_items_width(items: &[(String, String)]) -> u16 {
    if items.is_empty() {
        return 0;
    }
    footer_hint_items_line(items).width() as u16
}

fn footer_hint_items_line(items: &[(String, String)]) -> Line<'static> {
    let mut spans = Vec::with_capacity(items.len() * 4);
    for (idx, (key, label)) in items.iter().enumerate() {
        spans.push(" ".into());
        spans.push(key.clone().bold());
        spans.push(format!(" {label}").into());
        if idx + 1 != items.len() {
            spans.push("   ".into());
        }
    }
    Line::from(spans)
}

#[derive(Clone, Copy, Debug)]
struct ShortcutsState {
    use_shift_enter_hint: bool,
    esc_backtrack_hint: bool,
    is_task_running: bool,
    queue_submissions: bool,
    is_wsl: bool,
    collaboration_modes_enabled: bool,
    key_hints: FooterKeyHints,
}

fn quit_shortcut_reminder_line(key: KeyBinding) -> Line<'static> {
    Line::from(vec![key.into(), " again to quit".into()]).dim()
}

fn esc_hint_line(esc_backtrack_hint: bool) -> Line<'static> {
    let esc = key_hint::plain(KeyCode::Esc);
    if esc_backtrack_hint {
        Line::from(vec![esc.into(), " again to edit previous message".into()]).dim()
    } else {
        Line::from(vec![
            esc.into(),
            " ".into(),
            esc.into(),
            " to edit previous message".into(),
        ])
        .dim()
    }
}

fn shortcut_overlay_lines(state: ShortcutsState) -> Vec<Line<'static>> {
    let mut commands = Line::from("");
    let mut shell_commands = Line::from("");
    let mut newline = Line::from("");
    let mut queue_message_tab = Line::from("");
    let mut file_paths = Line::from("");
    let mut paste_image = Line::from("");
    let mut external_editor = Line::from("");
    let mut edit_previous = Line::from("");
    let mut history_search = Line::from("");
    let mut quit = Line::from("");
    let mut show_transcript = Line::from("");
    let mut change_mode = Line::from("");
    let mut reasoning_down = Line::from("");
    let mut reasoning_up = Line::from("");

    for descriptor in SHORTCUTS {
        if let Some(text) = descriptor.overlay_entry(state) {
            match descriptor.id {
                ShortcutId::Commands => commands = text,
                ShortcutId::ShellCommands => shell_commands = text,
                ShortcutId::InsertNewline => newline = text,
                ShortcutId::QueueMessageTab => queue_message_tab = text,
                ShortcutId::FilePaths => file_paths = text,
                ShortcutId::PasteImage => paste_image = text,
                ShortcutId::ExternalEditor => external_editor = text,
                ShortcutId::EditPrevious => edit_previous = text,
                ShortcutId::HistorySearch => history_search = text,
                ShortcutId::Quit => quit = text,
                ShortcutId::ShowTranscript => show_transcript = text,
                ShortcutId::ChangeMode => change_mode = text,
                ShortcutId::ReasoningDown => reasoning_down = text,
                ShortcutId::ReasoningUp => reasoning_up = text,
            }
        }
    }

    let mut ordered = vec![
        commands,
        shell_commands,
        newline,
        queue_message_tab,
        file_paths,
        paste_image,
        external_editor,
        edit_previous,
        history_search,
        quit,
        reasoning_down,
        reasoning_up,
    ];
    if change_mode.width() > 0 {
        ordered.push(change_mode);
    }
    ordered.push(show_transcript);

    let mut lines = build_columns(ordered);
    lines.push(Line::from(""));
    lines.push(Line::from(vec![
        "customize shortcuts with ".into(),
        "/keymap".cyan(),
    ]));
    lines
}

fn build_columns(entries: Vec<Line<'static>>) -> Vec<Line<'static>> {
    if entries.is_empty() {
        return Vec::new();
    }

    const COLUMNS: usize = 2;
    const COLUMN_PADDING: [usize; COLUMNS] = [4, 4];
    const COLUMN_GAP: usize = 4;

    let rows = entries.len().div_ceil(COLUMNS);
    let target_len = rows * COLUMNS;
    let mut entries = entries;
    if entries.len() < target_len {
        entries.extend(std::iter::repeat_n(
            Line::from(""),
            target_len - entries.len(),
        ));
    }

    let mut column_widths = [0usize; COLUMNS];

    for (idx, entry) in entries.iter().enumerate() {
        let column = idx % COLUMNS;
        column_widths[column] = column_widths[column].max(entry.width());
    }

    for (idx, width) in column_widths.iter_mut().enumerate() {
        *width += COLUMN_PADDING[idx];
    }

    entries
        .chunks(COLUMNS)
        .map(|chunk| {
            let mut line = Line::from("");
            for (col, entry) in chunk.iter().enumerate() {
                line.extend(entry.spans.clone());
                if col < COLUMNS - 1 {
                    let target_width = column_widths[col];
                    let padding = target_width.saturating_sub(entry.width()) + COLUMN_GAP;
                    line.push_span(Span::from(" ".repeat(padding)));
                }
            }
            line.dim()
        })
        .collect()
}

pub(crate) fn context_window_line(percent: Option<i64>, used_tokens: Option<i64>) -> Line<'static> {
    if let Some(percent) = percent {
        let percent = percent.clamp(0, 100);
        return Line::from(vec![Span::from(format!("{percent}% context left")).dim()]);
    }

    if let Some(tokens) = used_tokens {
        let used_fmt = format_tokens_compact(tokens);
        return Line::from(vec![Span::from(format!("{used_fmt} used")).dim()]);
    }

    Line::from(vec![Span::from("100% context left").dim()])
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ShortcutId {
    Commands,
    ShellCommands,
    InsertNewline,
    QueueMessageTab,
    FilePaths,
    PasteImage,
    ExternalEditor,
    EditPrevious,
    HistorySearch,
    Quit,
    ShowTranscript,
    ChangeMode,
    ReasoningDown,
    ReasoningUp,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ShortcutBinding {
    key: KeyBinding,
    condition: DisplayCondition,
}

impl ShortcutBinding {
    fn matches(&self, state: ShortcutsState) -> bool {
        self.condition.matches(state)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum DisplayCondition {
    Always,
    WhenShiftEnterHint,
    WhenNotShiftEnterHint,
    WhenUnderWSL,
    WhenCollaborationModesEnabled,
}

impl DisplayCondition {
    fn matches(self, state: ShortcutsState) -> bool {
        match self {
            DisplayCondition::Always => true,
            DisplayCondition::WhenShiftEnterHint => state.use_shift_enter_hint,
            DisplayCondition::WhenNotShiftEnterHint => !state.use_shift_enter_hint,
            DisplayCondition::WhenUnderWSL => state.is_wsl,
            DisplayCondition::WhenCollaborationModesEnabled => state.collaboration_modes_enabled,
        }
    }
}

struct ShortcutDescriptor {
    id: ShortcutId,
    bindings: &'static [ShortcutBinding],
    prefix: &'static str,
    label: &'static str,
}

impl ShortcutDescriptor {
    fn binding_for(&self, state: ShortcutsState) -> Option<&'static ShortcutBinding> {
        self.bindings.iter().find(|binding| binding.matches(state))
    }

    fn overlay_entry(&self, state: ShortcutsState) -> Option<Line<'static>> {
        let key = match self.id {
            ShortcutId::InsertNewline => state.key_hints.insert_newline,
            ShortcutId::QueueMessageTab => state.key_hints.queue,
            ShortcutId::ExternalEditor => state.key_hints.external_editor,
            ShortcutId::EditPrevious => state.key_hints.edit_previous,
            ShortcutId::ShowTranscript => state.key_hints.show_transcript,
            ShortcutId::HistorySearch => state.key_hints.history_search,
            ShortcutId::ReasoningDown => state.key_hints.reasoning_down,
            ShortcutId::ReasoningUp => state.key_hints.reasoning_up,
            ShortcutId::Commands
            | ShortcutId::ShellCommands
            | ShortcutId::FilePaths
            | ShortcutId::PasteImage
            | ShortcutId::Quit
            | ShortcutId::ChangeMode => self.binding_for(state).map(|binding| binding.key),
        }?;
        let mut line = Line::from(vec![self.prefix.into(), key.into()]);
        match self.id {
            ShortcutId::QueueMessageTab => {
                if state.is_task_running || state.queue_submissions {
                    line.push_span(" to queue message");
                } else {
                    line.push_span(" to submit message");
                }
            }
            ShortcutId::EditPrevious => {
                if state.esc_backtrack_hint {
                    line.push_span(" again to edit previous message");
                } else {
                    line.extend(vec![
                        " ".into(),
                        key.into(),
                        " to edit previous message".into(),
                    ]);
                }
            }
            ShortcutId::Quit => {
                if state.is_task_running {
                    line.push_span(" to interrupt");
                } else {
                    line.push_span(" to exit");
                }
            }
            _ => line.push_span(self.label),
        };
        Some(line)
    }
}

const SHORTCUTS: &[ShortcutDescriptor] = &[
    ShortcutDescriptor {
        id: ShortcutId::Commands,
        bindings: &[ShortcutBinding {
            key: key_hint::plain(KeyCode::Char('/')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " for commands",
    },
    ShortcutDescriptor {
        id: ShortcutId::ShellCommands,
        bindings: &[ShortcutBinding {
            key: key_hint::plain(KeyCode::Char('!')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " for shell commands",
    },
    ShortcutDescriptor {
        id: ShortcutId::InsertNewline,
        bindings: &[
            ShortcutBinding {
                key: key_hint::shift(KeyCode::Enter),
                condition: DisplayCondition::WhenShiftEnterHint,
            },
            ShortcutBinding {
                key: key_hint::ctrl(KeyCode::Char('j')),
                condition: DisplayCondition::WhenNotShiftEnterHint,
            },
        ],
        prefix: "",
        label: " for newline",
    },
    ShortcutDescriptor {
        id: ShortcutId::QueueMessageTab,
        bindings: &[ShortcutBinding {
            key: key_hint::plain(KeyCode::Tab),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " to queue message",
    },
    ShortcutDescriptor {
        id: ShortcutId::FilePaths,
        bindings: &[ShortcutBinding {
            key: key_hint::plain(KeyCode::Char('@')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " for file paths",
    },
    ShortcutDescriptor {
        id: ShortcutId::PasteImage,
        // Show Ctrl+Alt+V when running under WSL (terminals often intercept plain
        // Ctrl+V); otherwise fall back to Ctrl+V.
        bindings: &[
            ShortcutBinding {
                key: key_hint::ctrl_alt(KeyCode::Char('v')),
                condition: DisplayCondition::WhenUnderWSL,
            },
            ShortcutBinding {
                key: key_hint::ctrl(KeyCode::Char('v')),
                condition: DisplayCondition::Always,
            },
        ],
        prefix: "",
        label: " to paste images",
    },
    ShortcutDescriptor {
        id: ShortcutId::ExternalEditor,
        bindings: &[ShortcutBinding {
            key: key_hint::ctrl(KeyCode::Char('g')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " to edit in external editor",
    },
    ShortcutDescriptor {
        id: ShortcutId::EditPrevious,
        bindings: &[ShortcutBinding {
            key: key_hint::plain(KeyCode::Esc),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: "",
    },
    ShortcutDescriptor {
        id: ShortcutId::HistorySearch,
        bindings: &[ShortcutBinding {
            key: key_hint::ctrl(KeyCode::Char('r')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " search history",
    },
    ShortcutDescriptor {
        id: ShortcutId::Quit,
        bindings: &[ShortcutBinding {
            key: key_hint::ctrl(KeyCode::Char('c')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " to exit",
    },
    ShortcutDescriptor {
        id: ShortcutId::ShowTranscript,
        bindings: &[ShortcutBinding {
            key: key_hint::ctrl(KeyCode::Char('t')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " to view transcript",
    },
    ShortcutDescriptor {
        id: ShortcutId::ChangeMode,
        bindings: &[ShortcutBinding {
            key: key_hint::shift(KeyCode::Tab),
            condition: DisplayCondition::WhenCollaborationModesEnabled,
        }],
        prefix: "",
        label: " to change mode",
    },
    ShortcutDescriptor {
        id: ShortcutId::ReasoningDown,
        bindings: &[ShortcutBinding {
            key: key_hint::alt(KeyCode::Char(',')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " reasoning down",
    },
    ShortcutDescriptor {
        id: ShortcutId::ReasoningUp,
        bindings: &[ShortcutBinding {
            key: key_hint::alt(KeyCode::Char('.')),
            condition: DisplayCondition::Always,
        }],
        prefix: "",
        label: " reasoning up",
    },
];

#[cfg(test)]
mod tests {
    use super::*;
    use crate::line_truncation::truncate_line_with_ellipsis_if_overflow;
    use crate::test_backend::VT100Backend;
    use insta::assert_snapshot;
    use pretty_assertions::assert_eq;
    use ratatui::Terminal;
    use ratatui::backend::Backend;
    use ratatui::backend::TestBackend;

    fn snapshot_footer(name: &str, props: FooterProps) {
        snapshot_footer_with_mode_indicator(
            name, /*width*/ 80, &props, /*collaboration_mode_indicator*/ None,
        );
    }

    fn snapshot_footer_with_context(
        name: &str,
        props: FooterProps,
        percent: Option<i64>,
        used_tokens: Option<i64>,
    ) {
        snapshot_footer_with_mode_indicator_and_context(
            name,
            /*width*/ 80,
            &props,
            /*collaboration_mode_indicator*/ None,
            context_window_line(percent, used_tokens),
        );
    }

    fn draw_footer_frame<B: Backend>(
        terminal: &mut Terminal<B>,
        height: u16,
        props: &FooterProps,
        collaboration_mode_indicator: Option<CollaborationModeIndicator>,
        ide_context_active: bool,
        context_line: Line<'static>,
    ) {
        terminal
            .draw(|f| {
                let area = Rect::new(0, 0, f.area().width, height);
                let show_cycle_hint = !props.is_task_running;
                let show_shortcuts_hint = match props.mode {
                    FooterMode::ComposerEmpty => true,
                    FooterMode::ComposerHasDraft => false,
                    FooterMode::HistorySearch
                    | FooterMode::QuitShortcutReminder
                    | FooterMode::ShortcutOverlay
                    | FooterMode::EscHint => false,
                };
                let show_queue_hint = match props.mode {
                    FooterMode::ComposerHasDraft => props.is_task_running,
                    FooterMode::HistorySearch
                    | FooterMode::QuitShortcutReminder
                    | FooterMode::ComposerEmpty
                    | FooterMode::ShortcutOverlay
                    | FooterMode::EscHint => false,
                };
                let status_line_active = uses_passive_footer_status_layout(props);
                let passive_status_line = if status_line_active {
                    passive_footer_status_line(props)
                } else {
                    None
                };
                let left_mode_indicator = if status_line_active {
                    None
                } else {
                    collaboration_mode_indicator
                };
                let available_width = area.width.saturating_sub(FOOTER_INDENT_COLS as u16) as usize;
                let mut truncated_status_line = if status_line_active
                    && matches!(
                        props.mode,
                        FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft
                    ) {
                    passive_status_line.as_ref().map(|line| {
                        truncate_line_with_ellipsis_if_overflow(line.clone(), available_width)
                    })
                } else {
                    None
                };
                let mut left_width = if status_line_active {
                    truncated_status_line
                        .as_ref()
                        .map(|line| line.width() as u16)
                        .unwrap_or(0)
                } else {
                    footer_line_width(
                        props,
                        left_mode_indicator,
                        show_cycle_hint,
                        show_shortcuts_hint,
                        show_queue_hint,
                    )
                };
                let right_line = if status_line_active {
                    let full = status_line_right_indicator_line(
                        collaboration_mode_indicator,
                        /*goal_status_indicator*/ None,
                        ide_context_active,
                        show_cycle_hint,
                    );
                    let compact = status_line_right_indicator_line(
                        collaboration_mode_indicator,
                        /*goal_status_indicator*/ None,
                        ide_context_active,
                        /*show_cycle_hint*/ false,
                    );
                    let full_width = full.as_ref().map(|line| line.width() as u16).unwrap_or(0);
                    if can_show_left_with_context(area, left_width, full_width) {
                        full
                    } else {
                        compact
                    }
                } else {
                    Some(context_line.clone())
                };
                let right_width = right_line
                    .as_ref()
                    .map(|line| line.width() as u16)
                    .unwrap_or(0);
                if status_line_active
                    && let Some(max_left) = max_left_width_for_right(area, right_width)
                    && left_width > max_left
                    && let Some(line) = passive_status_line.as_ref().map(|line| {
                        truncate_line_with_ellipsis_if_overflow(line.clone(), max_left as usize)
                    })
                {
                    left_width = line.width() as u16;
                    truncated_status_line = Some(line);
                }
                let can_show_left_and_context =
                    can_show_left_with_context(area, left_width, right_width);
                if matches!(
                    props.mode,
                    FooterMode::ComposerEmpty | FooterMode::ComposerHasDraft
                ) {
                    if status_line_active {
                        if let Some(line) = truncated_status_line.clone() {
                            render_footer_line(area, f.buffer_mut(), line);
                        }
                        if can_show_left_and_context && let Some(line) = &right_line {
                            render_context_right(area, f.buffer_mut(), line);
                        }
                    } else {
                        let (summary_left, show_context) = single_line_footer_layout(
                            area,
                            right_width,
                            left_mode_indicator,
                            show_cycle_hint,
                            show_shortcuts_hint,
                            show_queue_hint,
                            props.key_hints,
                        );
                        match summary_left {
                            SummaryLeft::Default => {
                                render_footer_from_props(
                                    area,
                                    f.buffer_mut(),
                                    props,
                                    left_mode_indicator,
                                    show_cycle_hint,
                                    show_shortcuts_hint,
                                    show_queue_hint,
                                );
                            }
                            SummaryLeft::Custom(line) => {
                                render_footer_line(area, f.buffer_mut(), line);
                            }
                            SummaryLeft::None => {}
                        }
                        if show_context && let Some(line) = &right_line {
                            render_context_right(area, f.buffer_mut(), line);
                        }
                    }
                } else {
                    render_footer_from_props(
                        area,
                        f.buffer_mut(),
                        props,
                        left_mode_indicator,
                        show_cycle_hint,
                        show_shortcuts_hint,
                        show_queue_hint,
                    );
                    let show_context = can_show_left_and_context
                        && !matches!(
                            props.mode,
                            FooterMode::EscHint
                                | FooterMode::HistorySearch
                                | FooterMode::QuitShortcutReminder
                                | FooterMode::ShortcutOverlay
                        );
                    if show_context && let Some(line) = &right_line {
                        render_context_right(area, f.buffer_mut(), line);
                    }
                }
            })
            .unwrap();
    }

    fn snapshot_footer_with_mode_indicator(
        name: &str,
        width: u16,
        props: &FooterProps,
        collaboration_mode_indicator: Option<CollaborationModeIndicator>,
    ) {
        snapshot_footer_with_mode_indicator_and_context(
            name,
            width,
            props,
            collaboration_mode_indicator,
            context_window_line(/*percent*/ None, /*used_tokens*/ None),
        );
    }

    fn snapshot_footer_with_mode_indicator_and_context(
        name: &str,
        width: u16,
        props: &FooterProps,
        collaboration_mode_indicator: Option<CollaborationModeIndicator>,
        context_line: Line<'static>,
    ) {
        let height = footer_height(props).max(1);
        let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
        draw_footer_frame(
            &mut terminal,
            height,
            props,
            collaboration_mode_indicator,
            /*ide_context_active*/ false,
            context_line,
        );
        assert_snapshot!(name, terminal.backend());
    }

    fn render_footer_with_mode_indicator_and_context(
        width: u16,
        props: &FooterProps,
        collaboration_mode_indicator: Option<CollaborationModeIndicator>,
        context_line: Line<'static>,
    ) -> String {
        let height = footer_height(props).max(1);
        let mut terminal = Terminal::new(VT100Backend::new(width, height)).expect("terminal");
        draw_footer_frame(
            &mut terminal,
            height,
            props,
            collaboration_mode_indicator,
            /*ide_context_active*/ false,
            context_line,
        );
        terminal.backend().vt100().screen().contents()
    }

    fn snapshot_footer_with_indicators(
        name: &str,
        width: u16,
        props: &FooterProps,
        collaboration_mode_indicator: Option<CollaborationModeIndicator>,
        ide_context_active: bool,
    ) {
        let height = footer_height(props).max(1);
        let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
        draw_footer_frame(
            &mut terminal,
            height,
            props,
            collaboration_mode_indicator,
            ide_context_active,
            context_window_line(/*percent*/ None, /*used_tokens*/ None),
        );
        assert_snapshot!(name, terminal.backend());
    }

    #[test]
    fn footer_snapshots() {
        snapshot_footer(
            "footer_shortcuts_default",
            FooterProps {
                mode: FooterMode::ComposerEmpty,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: false,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
        );

        snapshot_footer(
            "footer_shortcuts_shift_and_esc",
            FooterProps {
                mode: FooterMode::ShortcutOverlay,
                esc_backtrack_hint: true,
                use_shift_enter_hint: true,
                is_task_running: false,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints {
                    insert_newline: Some(key_hint::shift(KeyCode::Enter)),
                    ..FooterKeyHints::default_bindings()
                },
                active_agent_label: None,
            },
        );

        snapshot_footer(
            "footer_shortcuts_collaboration_modes_enabled",
            FooterProps {
                mode: FooterMode::ShortcutOverlay,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: false,
                queue_submissions: false,
                collaboration_modes_enabled: true,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
        );

        snapshot_footer(
            "footer_shortcuts_running",
            FooterProps {
                mode: FooterMode::ShortcutOverlay,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: true,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
        );

        snapshot_footer(
            "footer_ctrl_c_quit_idle",
            FooterProps {
                mode: FooterMode::QuitShortcutReminder,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: false,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
        );

        snapshot_footer(
            "footer_ctrl_c_quit_running",
            FooterProps {
                mode: FooterMode::QuitShortcutReminder,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: true,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
        );

        snapshot_footer(
            "footer_esc_hint_idle",
            FooterProps {
                mode: FooterMode::EscHint,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: false,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
        );

        snapshot_footer(
            "footer_esc_hint_primed",
            FooterProps {
                mode: FooterMode::EscHint,
                esc_backtrack_hint: true,
                use_shift_enter_hint: false,
                is_task_running: false,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
        );

        snapshot_footer_with_context(
            "footer_shortcuts_context_running",
            FooterProps {
                mode: FooterMode::ComposerEmpty,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: true,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
            Some(72),
            /*used_tokens*/ None,
        );

        snapshot_footer_with_context(
            "footer_context_tokens_used",
            FooterProps {
                mode: FooterMode::ComposerEmpty,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: false,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
            /*percent*/ None,
            Some(123_456),
        );

        snapshot_footer(
            "footer_composer_has_draft_queue_hint_enabled",
            FooterProps {
                mode: FooterMode::ComposerHasDraft,
                esc_backtrack_hint: false,
                use_shift_enter_hint: false,
                is_task_running: true,
                queue_submissions: false,
                collaboration_modes_enabled: false,
                is_wsl: false,
                quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
                status_line_value: None,
                status_line_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
                active_agent_label: None,
            },
        );

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: true,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: None,
            status_line_enabled: false,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        snapshot_footer_with_mode_indicator(
            "footer_mode_indicator_wide",
            /*width*/ 120,
            &props,
            Some(CollaborationModeIndicator::Plan),
        );

        snapshot_footer_with_mode_indicator(
            "footer_mode_indicator_narrow_overlap_hides",
            /*width*/ 50,
            &props,
            Some(CollaborationModeIndicator::Plan),
        );

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: true,
            queue_submissions: false,
            collaboration_modes_enabled: true,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: None,
            status_line_enabled: false,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        snapshot_footer_with_mode_indicator(
            "footer_mode_indicator_running_hides_hint",
            /*width*/ 120,
            &props,
            Some(CollaborationModeIndicator::Plan),
        );

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: false,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: Some(Line::from("Status line content".to_string())),
            status_line_enabled: true,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        snapshot_footer("footer_status_line_overrides_shortcuts", props);

        let props = FooterProps {
            mode: FooterMode::ComposerHasDraft,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: true,
            queue_submissions: false,
            collaboration_modes_enabled: false,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: Some(Line::from("Status line content".to_string())),
            status_line_enabled: true,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        snapshot_footer("footer_status_line_yields_to_queue_hint", props);

        let props = FooterProps {
            mode: FooterMode::ComposerHasDraft,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: false,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: Some(Line::from("Status line content".to_string())),
            status_line_enabled: true,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        snapshot_footer("footer_status_line_overrides_draft_idle", props);

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: true,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: None, // command timed out / empty
            status_line_enabled: true,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        snapshot_footer_with_mode_indicator_and_context(
            "footer_status_line_enabled_mode_right",
            /*width*/ 120,
            &props,
            Some(CollaborationModeIndicator::Plan),
            context_window_line(Some(50), /*used_tokens*/ None),
        );

        snapshot_footer_with_indicators(
            "footer_status_line_enabled_mode_and_ide_context_right",
            /*width*/ 120,
            &props,
            Some(CollaborationModeIndicator::Plan),
            /*ide_context_active*/ true,
        );

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: true,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: None,
            status_line_enabled: false,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        snapshot_footer_with_mode_indicator_and_context(
            "footer_status_line_disabled_context_right",
            /*width*/ 120,
            &props,
            Some(CollaborationModeIndicator::Plan),
            context_window_line(Some(50), /*used_tokens*/ None),
        );

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: false,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: None,
            status_line_enabled: true,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        // has status line and no collaboration mode
        snapshot_footer_with_mode_indicator_and_context(
            "footer_status_line_enabled_no_mode_right",
            /*width*/ 120,
            &props,
            /*collaboration_mode_indicator*/ None,
            context_window_line(Some(50), /*used_tokens*/ None),
        );

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: true,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: Some(Line::from(
                "Status line content that should truncate before the mode indicator".to_string(),
            )),
            status_line_enabled: true,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        snapshot_footer_with_mode_indicator_and_context(
            "footer_status_line_truncated_with_gap",
            /*width*/ 40,
            &props,
            Some(CollaborationModeIndicator::Plan),
            context_window_line(Some(50), /*used_tokens*/ None),
        );

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: false,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: None,
            status_line_enabled: false,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: Some("Robie [explorer]".to_string()),
        };

        snapshot_footer("footer_active_agent_label", props);

        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: false,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: Some(Line::from("Status line content".to_string())),
            status_line_enabled: true,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: Some("Robie [explorer]".to_string()),
        };

        snapshot_footer("footer_status_line_with_active_agent_label", props);
    }

    #[test]
    fn footer_status_line_truncates_to_keep_mode_indicator() {
        let props = FooterProps {
            mode: FooterMode::ComposerEmpty,
            esc_backtrack_hint: false,
            use_shift_enter_hint: false,
            is_task_running: false,
            queue_submissions: false,
            collaboration_modes_enabled: true,
            is_wsl: false,
            quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
            status_line_value: Some(Line::from(
                "Status line content that is definitely too long to fit alongside the mode label"
                    .to_string(),
            )),
            status_line_enabled: true,
            key_hints: FooterKeyHints::default_bindings(),
            active_agent_label: None,
        };

        let screen = render_footer_with_mode_indicator_and_context(
            /*width*/ 80,
            &props,
            Some(CollaborationModeIndicator::Plan),
            context_window_line(Some(50), /*used_tokens*/ None),
        );
        let collapsed = screen.split_whitespace().collect::<Vec<_>>().join(" ");
        assert!(
            collapsed.contains("Plan mode"),
            "mode indicator should remain visible"
        );
        assert!(
            !collapsed.contains("shift+tab to cycle"),
            "compact mode indicator should be used when space is tight"
        );
        assert!(
            screen.contains('…'),
            "status line should be truncated with ellipsis to keep mode indicator"
        );
    }

    #[test]
    fn paste_image_shortcut_prefers_ctrl_alt_v_under_wsl() {
        let descriptor = SHORTCUTS
            .iter()
            .find(|descriptor| descriptor.id == ShortcutId::PasteImage)
            .expect("paste image shortcut");

        let is_wsl = {
            #[cfg(target_os = "linux")]
            {
                crate::clipboard_paste::is_probably_wsl()
            }
            #[cfg(not(target_os = "linux"))]
            {
                false
            }
        };

        let expected_key = if is_wsl {
            key_hint::ctrl_alt(KeyCode::Char('v'))
        } else {
            key_hint::ctrl(KeyCode::Char('v'))
        };

        let actual_key = descriptor
            .binding_for(ShortcutsState {
                use_shift_enter_hint: false,
                esc_backtrack_hint: false,
                is_task_running: false,
                queue_submissions: false,
                is_wsl,
                collaboration_modes_enabled: false,
                key_hints: FooterKeyHints::default_bindings(),
            })
            .expect("shortcut binding")
            .key;

        assert_eq!(actual_key, expected_key);
    }
}
