//! Commit window — staged/unstaged files, diff view, branch management, commit.

use std::path::PathBuf;

use std::cell::{Cell, RefCell};

use ratatui::{
    Frame,
    layout::{Constraint, Direction, Layout, Position, Rect},
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, List, ListItem, Paragraph},
};

use crate::editor::buffer::Buffer;
use crate::interraction::hit_test;
use crate::input::{Key, KeyEvent, Modifiers, MouseButton, MouseEvent, MouseEventKind};
use crate::operation::{CommitOp, Event, Operation};
use crate::settings::Settings;
use crate::vcs::{DiffLine, GitMode, StatusEntry};
use crate::path_format::width;
use crate::views::View;
use crate::widgets::DiffWidget;
use crate::widgets::focus::FocusRing;
use crate::widgets::focusable::FocusOp;
use crate::widgets::input_field::InputField;
use crate::widgets::status_bar::StatusBarBuilder;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CommitTab {
    Commit,
    Diff,
}

/// Popup state for branch selector.
#[derive(Debug, Clone)]
pub(crate) enum BranchPopup {
    /// Selector showing available branches with a cursor index.
    Selector { cursor: usize },
}

// Commit-tab focus targets
const FOCUS_STAGED: &str = "staged";
const FOCUS_UNSTAGED: &str = "unstaged";
const FOCUS_MESSAGE: &str = "message";
const FOCUS_COMMIT_BTN: &str = "commit_btn";

// Diff-tab focus targets
const FOCUS_FILE_LIST: &str = "file_list";
const FOCUS_DIFF_LINES: &str = "diff_lines";

// ---------------------------------------------------------------------------
// CommitWindow
// ---------------------------------------------------------------------------

#[derive(Debug)]
pub struct CommitWindow {
    /// Repository root — read by app.rs when spawning git tasks.
    pub repo_path: PathBuf,

    pub(crate) tab: CommitTab,
    pub(crate) loading: bool,

    // Commit tab ─────────────────────────────────────────────────────────
    pub(crate) staged: Vec<StatusEntry>,
    pub(crate) unstaged: Vec<StatusEntry>,
    pub(crate) commit_focus: FocusRing,
    pub(crate) staged_cursor: usize,
    pub(crate) unstaged_cursor: usize,
    /// Multiline commit message with cursor tracking.
    pub(crate) message_buf: Buffer,
    pub(crate) branches: Vec<String>,
    pub(crate) current_branch: String,
    pub(crate) branch_input: Option<InputField>,
    /// Optional popup for branch selection.
    pub(crate) branch_popup: Option<BranchPopup>,

    // Diff tab ────────────────────────────────────────────────────────────
    pub(crate) diff_file: Option<PathBuf>,
    pub(crate) diff_staged: bool,

    /// Original lines from git2, kept for git staging operations.
    pub(crate) diff_lines_raw: Vec<DiffLine>,

    pub(crate) diff_widget: Option<DiffWidget>,

    pub(crate) diff_list_cursor: usize,
    pub(crate) diff_focus: FocusRing,

    // Layout rects (Cell for interior-mutability during &self render)
    rect_tab_bar: Cell<Rect>,
    rect_staged_list: Cell<Rect>,
    rect_unstaged_list: Cell<Rect>,
    rect_message: Cell<Rect>,
    rect_commit_button: Cell<Rect>,
    rect_cancel_button: Cell<Rect>,
    rect_diff_file_list: Cell<Rect>,
    rect_diff: Cell<Rect>,
    /// Last rendered area for this view (used to anchor popups above status bar).
    rect_view_area: Cell<Rect>,

    // Mapping from displayed row -> entry index (None for header rows). Populated
    // each render so mouse clicks can be translated to original entry indices.
    staged_items_map: RefCell<Vec<Option<usize>>>,
    unstaged_items_map: RefCell<Vec<Option<usize>>>,
    /// Mapping for the diff tab's file list (display row -> entry index)
    diff_items_map: RefCell<Vec<Option<usize>>>,

    pub(crate) status_msg: Option<String>,
    /// Whether commit view is in amend mode (editing the previous commit)
    pub(crate) amend_mode: bool,
    /// OID of the base commit being amended
    pub(crate) base_commit_oid: Option<String>,
    /// Current Git operation mode (Normal / Rebase / Merge).
    pub(crate) git_mode: GitMode,
    /// True when the working tree contains unresolved conflicts.
    pub(crate) has_conflicts: bool,
}

impl CommitWindow {
    pub fn new(repo_path: PathBuf) -> Self {
        Self {
            repo_path,
            tab: CommitTab::Commit,
            loading: true,
            staged: Vec::new(),
            unstaged: Vec::new(),
            commit_focus: FocusRing::new(vec![
                FOCUS_STAGED, FOCUS_UNSTAGED, FOCUS_MESSAGE, FOCUS_COMMIT_BTN,
            ]),
            staged_cursor: 0,
            unstaged_cursor: 0,
            message_buf: Buffer::from_lines(vec![String::new()], None),
            branches: Vec::new(),
            current_branch: String::new(),
            branch_input: None,
            branch_popup: None,
            diff_file: None,
            diff_staged: false,
            diff_lines_raw: Vec::new(),
            diff_widget: None,
            diff_list_cursor: 0,
            diff_focus: FocusRing::new(vec![FOCUS_FILE_LIST, FOCUS_DIFF_LINES]),
            rect_tab_bar: Cell::new(Rect::default()),
            rect_staged_list: Cell::new(Rect::default()),
            rect_unstaged_list: Cell::new(Rect::default()),
            rect_message: Cell::new(Rect::default()),
            rect_commit_button: Cell::new(Rect::default()),
            rect_cancel_button: Cell::new(Rect::default()),
            rect_diff_file_list: Cell::new(Rect::default()),
            rect_diff: Cell::new(Rect::default()),
            rect_view_area: Cell::new(Rect::default()),
            staged_items_map: RefCell::new(Vec::new()),
            unstaged_items_map: RefCell::new(Vec::new()),
            diff_items_map: RefCell::new(Vec::new()),
            status_msg: None,
            amend_mode: false,
            base_commit_oid: None,
            git_mode: GitMode::Normal,
            has_conflicts: false,
        }
    }

    // -----------------------------------------------------------------------
    // Cursor helpers
    // -----------------------------------------------------------------------

    fn clamp(idx: usize, len: usize) -> usize {
        if len == 0 { 0 } else { idx.min(len - 1) }
    }

    fn clamp_all(&mut self) {
        self.staged_cursor = Self::clamp(self.staged_cursor, self.staged.len());
        self.unstaged_cursor = Self::clamp(self.unstaged_cursor, self.unstaged.len());
        let n = self.staged.len() + self.unstaged.len();
        self.diff_list_cursor = Self::clamp(self.diff_list_cursor, n);
    }

    /// True when the message buffer contains no text.
    pub(crate) fn message_is_empty(&self) -> bool {
        let lines = self.message_buf.lines();
        lines.len() == 1 && lines[0].is_empty()
    }

    /// The commit message as a single string (lines joined by `\n`).
    pub(crate) fn message_text(&self) -> String {
        self.message_buf.lines().join("\n")
    }

    // -----------------------------------------------------------------------
    // Accessors used by app.rs
    // -----------------------------------------------------------------------

    /// Current commit-tab focus target as a `&str`.
    fn active_pane(&self) -> &str {
        self.commit_focus.current()
    }

    /// Current diff-tab focus target as a `&str`.
    fn active_diff_focus(&self) -> &str {
        self.diff_focus.current()
    }

    pub fn selected_file_path(&self) -> Option<PathBuf> {
        match self.active_pane() {
            FOCUS_STAGED => self.staged.get(self.staged_cursor).map(|e| e.path.clone()),
            FOCUS_UNSTAGED => self
                .unstaged
                .get(self.unstaged_cursor)
                .map(|e| e.path.clone()),
            _ => None,
        }
    }

    pub fn is_selected_staged(&self) -> bool {
        self.active_pane() == FOCUS_STAGED
    }

    /// Returns `(path, is_staged, selected_raw_indices, raw_diff_lines)` for git ops.
    pub fn diff_stage_info(&self) -> Option<(PathBuf, bool, Vec<usize>, Vec<DiffLine>)> {
        let widget = self.diff_widget.as_ref()?;
        if !widget.has_staged() {
            return None;
        }
        let pairs = widget.staged_lines(&self.diff_lines_raw);
        let indices: Vec<usize> = pairs.iter().map(|(i, _)| *i).collect();
        Some((
            self.diff_file.clone()?,
            self.diff_staged,
            indices,
            self.diff_lines_raw.clone(),
        ))
    }

    pub fn diff_jump_line(&self) -> usize {
        self.diff_widget
            .as_ref()
            .and_then(|w| w.selected_line_no())
            .unwrap_or(1)
            .saturating_sub(1)
    }

    /// Combined (staged + unstaged) file list for the diff tab: (path, is_staged).
    pub fn all_diff_files(&self) -> Vec<(PathBuf, bool)> {
        let mut files: Vec<(PathBuf, bool)> = self
            .staged
            .iter()
            .map(|e| (e.path.clone(), true))
            .chain(self.unstaged.iter().map(|e| (e.path.clone(), false)))
            .collect();
        files.sort_by(|a, b| a.0.cmp(&b.0));
        files
    }

    fn load_diff_op_for_index(&self, idx: usize) -> Option<Operation> {
        let files = self.all_diff_files();
        files.get(idx).map(|(path, staged)| {
            Operation::CommitLocal(CommitOp::LoadDiff {
                path: path.clone(),
                staged: *staged,
            })
        })
    }

    fn load_diff_for_selected(&self) -> Vec<Operation> {
        self.load_diff_op_for_index(self.diff_list_cursor).into_iter().collect()
    }
}

// ---------------------------------------------------------------------------
// View trait
// ---------------------------------------------------------------------------

impl View for CommitWindow {
    const KIND: crate::views::ViewKind = crate::views::ViewKind::Primary;

    fn save_state(&mut self, _app: &mut crate::app_state::AppState) {}

    fn status_bar(&self, _state: &crate::app_state::AppState, bar: &mut StatusBarBuilder) {
        let branch = if self.current_branch.is_empty() { "(detached)".to_string() } else { self.current_branch.clone() };
        bar.menu(
            format!("branch: {}", branch),
            crate::commands::CommandId::new_static("vcs", "open_branch_selector"),
        );
    }

    fn handle_key(&self, key: KeyEvent) -> Vec<Operation> {
        // Branch-name input mode takes priority.
        if let Some(ref bi) = self.branch_input {
            return match (key.modifiers, key.key) {
                (_, Key::Escape) => vec![Operation::CommitLocal(CommitOp::BranchInputCancel)],
                (_, Key::Enter) => vec![Operation::CommitLocal(CommitOp::BranchCreate)],
                _ => {
                    if let Some(field_op) = crate::widgets::input_field::key_to_op(key) {
                        let mut f = bi.clone();
                        f.apply(&field_op);
                        vec![Operation::CommitLocal(CommitOp::BranchChanged(
                            f.text().to_owned(),
                        ))]
                    } else {
                        vec![]
                    }
                }
            };
        }

        // Popup: branch selector takes priority when open.
        if let Some(BranchPopup::Selector { cursor }) = &self.branch_popup {
            return match key.key {
                Key::Escape => vec![Operation::CommitLocal(CommitOp::BranchSelectorCancel)],
                Key::Enter => {
                    let idx = *cursor;
                    if idx < self.branches.len() {
                        let name = self.branches[idx].clone();
                        vec![Operation::CommitLocal(CommitOp::BranchCheckout(name))]
                    } else {
                        vec![]
                    }
                }
                Key::ArrowUp => vec![Operation::NavigateUp],
                Key::ArrowDown => vec![Operation::NavigateDown],
                _ => vec![],
            };
        }

        match (key.modifiers, key.key) {
            // Universal ─────────────────────────────────────────────────────

            // Escape in rebase/merge mode aborts the operation.
            (_, Key::Escape) if self.git_mode == GitMode::Rebase => {
                vec![Operation::CommitLocal(CommitOp::AbortRebase)]
            }
            (_, Key::Escape) if self.git_mode == GitMode::Merge => {
                vec![Operation::CommitLocal(CommitOp::AbortMerge)]
            }

            (_, Key::Tab) => vec![Operation::CommitLocal(CommitOp::SwitchTab)],
            (m, Key::Char('s')) if m.contains(Modifiers::CTRL) => {
                if self.message_is_empty() {
                    vec![]
                } else if self.amend_mode {
                    vec![Operation::CommitLocal(CommitOp::ConfirmAmend)]
                } else {
                    vec![Operation::CommitLocal(CommitOp::Confirm)]
                }
            }

            // Diff tab ───────────────────────────────────────────────────────
            _ if self.tab == CommitTab::Diff => {
                let df = self.active_diff_focus();
                match (key.modifiers, key.key) {
                    (_, Key::ArrowLeft) if df == FOCUS_DIFF_LINES => {
                        vec![Operation::Focus(FocusOp::Left)]
                    }
                    (_, Key::ArrowRight) if df == FOCUS_FILE_LIST => {
                        vec![Operation::Focus(FocusOp::Right)]
                    }
                    // File list navigation
                    (_, Key::ArrowUp) if df == FOCUS_FILE_LIST => {
                        let target = self.diff_list_cursor.saturating_sub(1);
                        let mut ops = vec![Operation::NavigateUp];
                        if let Some(load) = self.load_diff_op_for_index(target) {
                            ops.push(load);
                        }
                        ops
                    }
                    (_, Key::ArrowDown) if df == FOCUS_FILE_LIST => {
                        let total = self.staged.len() + self.unstaged.len();
                        if total == 0 {
                            vec![]
                        } else {
                            let target = (self.diff_list_cursor + 1).min(total - 1);
                            let mut ops = vec![Operation::NavigateDown];
                            if let Some(load) = self.load_diff_op_for_index(target) {
                                ops.push(load);
                            }
                            ops
                        }
                    }
                    (_, Key::Enter) if df == FOCUS_FILE_LIST => {
                        let mut ops = self.load_diff_for_selected();
                        ops.push(Operation::Focus(FocusOp::Right));
                        ops
                    }
                    // Diff line navigation / actions
                    (_, Key::ArrowUp) => vec![Operation::NavigateUp],
                    (_, Key::ArrowDown) => vec![Operation::NavigateDown],
                    (_, Key::PageUp) => vec![Operation::NavigatePageUp],
                    (_, Key::PageDown) => vec![Operation::NavigatePageDown],
                    (_, Key::Char(' ')) => {
                        vec![Operation::CommitLocal(CommitOp::ToggleLineStage)]
                    }
                    (_, Key::Char('r')) => vec![Operation::CommitLocal(CommitOp::RevertLine)],
                    (_, Key::Char('e')) => {
                        if let (Some(path), row) = (self.diff_file.clone(), self.diff_jump_line())
                        {
                            vec![
                                Operation::OpenFile { path },
                                Operation::MoveCursor {
                                    path: None,
                                    cursor: crate::editor::Position::new(row, 0),
                                },
                            ]
                        } else {
                            vec![]
                        }
                    }
                    _ => vec![],
                }
            }

            // Commit tab — CommitButton pane ─────────────────────────────────
            _ if self.tab == CommitTab::Commit && self.active_pane() == FOCUS_COMMIT_BTN => {
                match (key.modifiers, key.key) {
                    (_, Key::Enter) | (_, Key::Char(' ')) => {
                        if !self.message_is_empty() {
                            if self.amend_mode {
                                vec![Operation::CommitLocal(CommitOp::ConfirmAmend)]
                            } else {
                                vec![Operation::CommitLocal(CommitOp::Confirm)]
                            }
                        } else {
                            vec![]
                        }
                    }
                    (_, Key::ArrowLeft)
                    | (_, Key::ArrowRight)
                    | (_, Key::ArrowUp)
                    | (_, Key::ArrowDown) => {
                        vec![Operation::Focus(FocusOp::Next)]
                    }
                    _ => vec![],
                }
            }

            // Commit tab — Message pane ──────────────────────────────────────
            _ if self.tab == CommitTab::Commit && self.active_pane() == FOCUS_MESSAGE => {
                match (key.modifiers, key.key) {
                    (_, Key::ArrowLeft) => vec![Operation::CommitLocal(CommitOp::MessageCursorLeft)],
                    (_, Key::ArrowRight) => vec![Operation::CommitLocal(CommitOp::MessageCursorRight)],
                    (_, Key::ArrowUp) => vec![Operation::CommitLocal(CommitOp::MessageCursorUp)],
                    (_, Key::ArrowDown) => vec![Operation::CommitLocal(CommitOp::MessageCursorDown)],
                    (_, Key::Enter) => vec![Operation::CommitLocal(CommitOp::MessageInsertNewline)],
                    (_, Key::Backspace) => vec![Operation::CommitLocal(CommitOp::MessageBackspace)],
                    (mods, Key::Char(c))
                        if !mods.contains(Modifiers::CTRL) && !mods.contains(Modifiers::ALT) =>
                    {
                        vec![Operation::CommitLocal(CommitOp::MessageInsertChar(c))]
                    }
                    _ => vec![],
                }
            }

            // Commit tab — file list panes ────────────────────────────────────
            _ if self.tab == CommitTab::Commit => match (key.modifiers, key.key) {
                (_, Key::ArrowUp) => vec![Operation::NavigateUp],
                (_, Key::ArrowDown) => vec![Operation::NavigateDown],
                (_, Key::PageUp) => vec![Operation::NavigatePageUp],
                (_, Key::PageDown) => vec![Operation::NavigatePageDown],
                (_, Key::ArrowLeft) | (_, Key::ArrowRight) => {
                    vec![Operation::Focus(FocusOp::Next)]
                }
                (_, Key::Char(' ')) => vec![Operation::CommitLocal(CommitOp::ToggleFileStage)],
                (_, Key::Char('n')) => {
                    vec![Operation::CommitLocal(CommitOp::BranchInputOpen)]
                }
                _ => vec![],
            },

            _ => vec![],
        }
    }

    fn handle_mouse(&self, mouse: MouseEvent) -> Vec<Operation> {
        let col = mouse.column;
        let row = mouse.row;

        match mouse.kind {
            MouseEventKind::Down(MouseButton::Left) => {
                if hit_test((col, row), self.rect_tab_bar.get()) {
                    return vec![Operation::CommitLocal(CommitOp::SwitchTab)];
                }

                // Cancel/Abort button click
                let show_abort = self.amend_mode || self.git_mode != GitMode::Normal;
                if show_abort && hit_test((col, row), self.rect_cancel_button.get()) {
                    return match self.git_mode {
                        GitMode::Rebase => vec![Operation::CommitLocal(CommitOp::AbortRebase)],
                        GitMode::Merge  => vec![Operation::CommitLocal(CommitOp::AbortMerge)],
                        GitMode::Normal => vec![Operation::CommitLocal(CommitOp::CancelAmend)],
                    };
                }

                // Commit button click
                if hit_test((col, row), self.rect_commit_button.get())
                    && !self.message_is_empty()
                    && !self.has_conflicts
                {
                    if self.amend_mode {
                        return vec![Operation::CommitLocal(CommitOp::ConfirmAmend)];
                    }
                    return vec![Operation::CommitLocal(CommitOp::Confirm)];
                }

                if self.tab == CommitTab::Commit {
                    if let Some(row_idx) = list_click_index(self.rect_staged_list.get(), row) {
                        let mapping = self.staged_items_map.borrow();
                        let target_idx_opt = mapping.get(row_idx).and_then(|&o| o).or_else(|| {
                            // Prefer next file row, otherwise previous
                            mapping.iter().skip(row_idx + 1).find_map(|&o| o)
                        }).or_else(|| {
                            mapping.iter().take(row_idx).rev().find_map(|&o| o)
                        });
                        if let Some(target_idx) = target_idx_opt {
                            return navigate_to(FOCUS_STAGED, self.active_pane(), target_idx, self.staged_cursor);
                        }
                    }
                    if let Some(row_idx) = list_click_index(self.rect_unstaged_list.get(), row) {
                        let mapping = self.unstaged_items_map.borrow();
                        let target_idx_opt = mapping.get(row_idx).and_then(|&o| o).or_else(|| {
                            mapping.iter().skip(row_idx + 1).find_map(|&o| o)
                        }).or_else(|| {
                            mapping.iter().take(row_idx).rev().find_map(|&o| o)
                        });
                        if let Some(target_idx) = target_idx_opt {
                            return navigate_to(FOCUS_UNSTAGED, self.active_pane(), target_idx, self.unstaged_cursor);
                        }
                    }
                    if self.rect_message.get().contains(Position::new(col, row))
                        && self.active_pane() != FOCUS_MESSAGE
                    {
                        return vec![Operation::Focus(FocusOp::Next)];
                    }
                }

                if self.tab == CommitTab::Diff {
                    if let Some(row_idx) = list_click_index(self.rect_diff_file_list.get(), row) {
                        let mapping = self.diff_items_map.borrow();
                        let target_idx_opt = mapping.get(row_idx).and_then(|&o| o).or_else(|| {
                            mapping.iter().skip(row_idx + 1).find_map(|&o| o)
                        }).or_else(|| {
                            mapping.iter().take(row_idx).rev().find_map(|&o| o)
                        });
                        if let Some(target_idx) = target_idx_opt {
                            let mut ops = Vec::new();
                            let cur = self.diff_list_cursor;
                            if target_idx > cur {
                                for _ in 0..(target_idx - cur) {
                                    ops.push(Operation::NavigateDown);
                                }
                            } else {
                                for _ in 0..(cur - target_idx) {
                                    ops.push(Operation::NavigateUp);
                                }
                            }
                            if let Some(load) = self.load_diff_op_for_index(target_idx) {
                                ops.push(load);
                            }
                            return ops;
                        }
                    }
                    if let Some(click_pos) = list_click_index(self.rect_diff.get(), row)
                        && let Some(widget) = &self.diff_widget
                    {
                        let inner_h = self.rect_diff.get().height.saturating_sub(2) as usize;
                        let cur = widget.cursor_flat_idx();
                        let scroll = if cur >= inner_h { cur + 1 - inner_h } else { 0 };
                        let target =
                            (scroll + click_pos).min(widget.visible_count().saturating_sub(1));
                        let mut ops = Vec::new();
                        if self.active_diff_focus() != FOCUS_DIFF_LINES {
                            ops.push(Operation::Focus(FocusOp::Right));
                        }
                        if target > cur {
                            for _ in 0..(target - cur) {
                                ops.push(Operation::NavigateDown);
                            }
                        } else {
                            for _ in 0..(cur - target) {
                                ops.push(Operation::NavigateUp);
                            }
                        }
                        if !ops.is_empty() {
                            return ops;
                        }
                    }
                }

                vec![]
            }

            MouseEventKind::ScrollUp => match self.tab {
                CommitTab::Commit => vec![Operation::NavigateUp],
                CommitTab::Diff => {
                    // When in diff tab, navigate and also load the new selection.
                    let target = self.diff_list_cursor.saturating_sub(1);
                    let mut ops = vec![Operation::NavigateUp];
                    if let Some(load) = self.load_diff_op_for_index(target) {
                        ops.push(load);
                    }
                    ops
                },
            },
            MouseEventKind::ScrollDown => match self.tab {
                CommitTab::Commit => vec![Operation::NavigateDown],
                CommitTab::Diff => {
                    let total = self.staged.len() + self.unstaged.len();
                    if total == 0 {
                        vec![]
                    } else {
                        let target = (self.diff_list_cursor + 1).min(total - 1);
                        let mut ops = vec![Operation::NavigateDown];
                        if let Some(load) = self.load_diff_op_for_index(target) {
                            ops.push(load);
                        }
                        ops
                    }
                },
            },

            _ => vec![],
        }
    }

    fn handle_operation(&mut self, op: &Operation, _settings: &Settings) -> Option<Event> {
        // Handle shared focus ops.
        if let Operation::Focus(focus_op) = op {
            match self.tab {
                CommitTab::Commit => match focus_op {
                    FocusOp::Next => self.commit_focus.focus_next(),
                    FocusOp::Prev => self.commit_focus.focus_prev(),
                    _ => {}
                },
                CommitTab::Diff => match focus_op {
                    FocusOp::Next | FocusOp::Right => self.diff_focus.focus_next(),
                    FocusOp::Prev | FocusOp::Left => self.diff_focus.focus_prev(),
                    _ => {}
                },
            }
            return None;
        }

        let Operation::CommitLocal(commit_op) = op else {
            // Handle shared navigation ops before the CommitLocal early return.
            match op {
                Operation::NavigateUp => {
                    // If branch selector popup is open, move its cursor instead of list cursors.
                    if let Some(BranchPopup::Selector { cursor }) = &mut self.branch_popup {
                        if *cursor > 0 { *cursor -= 1; }
                    } else if self.tab == CommitTab::Commit {
                        let pane = self.active_pane();
                        if pane == FOCUS_STAGED {
                            self.staged_cursor = self.staged_cursor.saturating_sub(1);
                        } else if pane == FOCUS_UNSTAGED {
                            self.unstaged_cursor = self.unstaged_cursor.saturating_sub(1);
                        }
                    } else if self.tab == CommitTab::Diff {
                        if self.active_diff_focus() == FOCUS_FILE_LIST {
                            self.diff_list_cursor = self.diff_list_cursor.saturating_sub(1);
                        } else if let Some(w) = self.diff_widget.as_mut() {
                            w.move_up();
                        }
                    }
                }
                Operation::NavigateDown => {
                    // If branch selector popup is open, move its cursor instead of list cursors.
                    if let Some(BranchPopup::Selector { cursor }) = &mut self.branch_popup {
                        *cursor = (*cursor + 1).min(self.branches.len().saturating_sub(1));
                    } else if self.tab == CommitTab::Commit {
                        let pane = self.active_pane();
                        if pane == FOCUS_STAGED && !self.staged.is_empty() {
                            self.staged_cursor = (self.staged_cursor + 1).min(self.staged.len() - 1);
                        } else if pane == FOCUS_UNSTAGED && !self.unstaged.is_empty() {
                            self.unstaged_cursor = (self.unstaged_cursor + 1).min(self.unstaged.len() - 1);
                        }
                    } else if self.tab == CommitTab::Diff {
                        if self.active_diff_focus() == FOCUS_FILE_LIST {
                            let total = self.staged.len() + self.unstaged.len();
                            if total > 0 {
                                self.diff_list_cursor = (self.diff_list_cursor + 1).min(total - 1);
                            }
                        } else if let Some(w) = self.diff_widget.as_mut() {
                            w.move_down();
                        }
                    }
                }
                Operation::NavigatePageUp => {
                    if self.tab == CommitTab::Diff
                        && let Some(w) = self.diff_widget.as_mut()
                    {
                        w.page_up(20);
                    }
                }
                Operation::NavigatePageDown => {
                    if self.tab == CommitTab::Diff
                        && let Some(w) = self.diff_widget.as_mut()
                    {
                        w.page_down(20);
                    }
                }
                _ => {}
            }
            return None;
        };

        match commit_op {
            CommitOp::SwitchTab => {
                self.tab = match self.tab {
                    CommitTab::Commit => CommitTab::Diff,
                    CommitTab::Diff => CommitTab::Commit,
                };
                if self.tab == CommitTab::Diff {
                    let (path, is_staged) = if self.active_pane() == FOCUS_STAGED {
                        (
                            self.staged.get(self.staged_cursor).map(|e| e.path.clone()),
                            true,
                        )
                    } else {
                        (
                            self.unstaged
                                .get(self.unstaged_cursor)
                                .map(|e| e.path.clone()),
                            false,
                        )
                    };
                    if let Some(p) = path {
                        let changed_file = self.diff_file.as_ref() != Some(&p);
                        let changed_staged = self.diff_staged != is_staged;
                        if changed_file || changed_staged {
                            self.diff_file = Some(p);
                            self.diff_staged = is_staged;
                            self.diff_lines_raw.clear();
                            self.diff_widget = None;
                        }
                    }
                }
            }

            CommitOp::SwitchPane => {
                self.commit_focus.focus_next();
            }

            CommitOp::DiffFocusToggle => {
                self.diff_focus.focus_next();
            }

            CommitOp::MessageChanged(s) => {
                self.message_buf = Buffer::from_lines(
                    if s.is_empty() {
                        vec![String::new()]
                    } else {
                        s.lines().map(|l| l.to_string()).collect()
                    },
                    None,
                );
            }

            CommitOp::MessageInsertChar(c) => {
                self.message_buf.begin_transaction(crate::editor::history::ChangeKind::InsertText);
                self.message_buf.insert(&c.to_string());
                self.message_buf.end_transaction();
            }

            CommitOp::MessageInsertNewline => {
                self.message_buf.begin_transaction(crate::editor::history::ChangeKind::InsertText);
                self.message_buf.insert_newline();
                self.message_buf.end_transaction();
            }

            CommitOp::MessageBackspace => {
                self.message_buf.begin_transaction(crate::editor::history::ChangeKind::DeleteText);
                self.message_buf.delete_backward();
                self.message_buf.end_transaction();
            }

            CommitOp::MessageCursorLeft => {
                let pos = self.message_buf.offset_left(self.message_buf.cursor());
                self.message_buf.set_cursor(pos);
            }

            CommitOp::MessageCursorRight => {
                let pos = self.message_buf.offset_right(self.message_buf.cursor());
                self.message_buf.set_cursor(pos);
            }

            CommitOp::MessageCursorUp => {
                let pos = self.message_buf.offset_up(self.message_buf.cursor());
                self.message_buf.set_cursor(pos);
            }

            CommitOp::MessageCursorDown => {
                let pos = self.message_buf.offset_down(self.message_buf.cursor());
                self.message_buf.set_cursor(pos);
            }

            CommitOp::BranchInputOpen => {
                self.branch_input = Some(InputField::new("Branch"));
            }

            CommitOp::BranchInputCancel => {
                self.branch_input = None;
            }

            CommitOp::BranchChanged(s) => {
                if let Some(ref mut field) = self.branch_input {
                    field.set_text(s.clone());
                }
            }

            CommitOp::BranchSelectorOpen => {
                // Open the popup and position cursor on current branch when possible.
                let cursor = self
                    .branches
                    .iter()
                    .position(|b| b == &self.current_branch)
                    .unwrap_or(0);
                self.branch_popup = Some(BranchPopup::Selector { cursor });
            }

            CommitOp::BranchSelectorCancel => {
                self.branch_popup = None;
            }

            CommitOp::BranchCheckout(_name) => {
                // Popup closed; actual checkout work is performed in app.rs.
                self.branch_popup = None;
            }

            CommitOp::ToggleLineStage => {
                if let Some(w) = self.diff_widget.as_mut() {
                    w.toggle_stage_or_fold();
                }
            }

            CommitOp::StatusLoaded {
                staged,
                unstaged,
                branches,
                current_branch,
            } => {
                self.staged = staged.clone();
                self.unstaged = unstaged.clone();
                self.branches = branches.clone();
                self.current_branch = current_branch.clone();
                self.loading = false;
                self.clamp_all();
            }

            CommitOp::DiffLoaded {
                path,
                staged,
                lines,
            } => {
                self.diff_file = Some(path.clone());
                self.diff_staged = *staged;
                self.diff_lines_raw = lines.clone();
                self.diff_widget = Some(DiffWidget::new(lines));
                // Position diff_list_cursor on the loaded file.
                let files = self.all_diff_files();
                if let Some(pos) = files.iter().position(|(p, _)| p == path) {
                    self.diff_list_cursor = pos;
                }
            }

            CommitOp::GitOpSuccess(msg) => {
                self.status_msg = Some(msg.clone());
                self.loading = false;
            }

            CommitOp::GitOpError(msg) => {
                self.status_msg = Some(format!("Error: {}", msg));
                self.loading = false;
            }

            CommitOp::AmendModeSet(flag, oid) => {
                self.amend_mode = *flag;
                self.base_commit_oid = oid.clone();
                if *flag {
                    self.status_msg = Some("Amend mode".into());
                }
            }

            CommitOp::CancelAmend => {
                self.amend_mode = false;
                self.base_commit_oid = None;
                self.message_buf = Buffer::from_lines(vec![String::new()], None);
                self.status_msg = Some("Amend cancelled".into());
            }

            CommitOp::GitModeDetected { mode, has_conflicts, message } => {
                self.git_mode = *mode;
                self.has_conflicts = *has_conflicts;
                // Pre-fill the commit message when entering a rebase/merge step,
                // but only when the user hasn't typed anything yet.
                if *mode != GitMode::Normal && self.message_is_empty() && !message.is_empty() {
                    self.message_buf = Buffer::from_lines(
                        message.lines().map(|l| l.to_string()).collect(),
                        None,
                    );
                }
                if *has_conflicts {
                    self.status_msg = Some("Cannot commit: unresolved conflicts".into());
                } else if *mode == GitMode::Normal {
                    // Clear any stale mode message when returning to normal.
                    if let Some(ref msg) = self.status_msg
                        && msg.starts_with("Cannot commit") {
                            self.status_msg = None;
                        }
                }
                self.loading = false;
            }

            // Handled in app.rs
            CommitOp::ToggleFileStage
            | CommitOp::Confirm
            | CommitOp::ConfirmAmend
            | CommitOp::RevertLine
            | CommitOp::BranchCreate
            | CommitOp::LoadDiff { .. }
            | CommitOp::EnterAmendMode
            | CommitOp::AbortRebase
            | CommitOp::AbortMerge => {}
        }

        Some(Event::applied("commit", op.clone()))
    }

    fn render(&self, frame: &mut Frame, area: Rect, theme: &crate::theme::Theme) {
        let chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Length(1), Constraint::Min(0)])
            .split(area);

        self.rect_tab_bar.set(chunks[0]);

        let tab_bg = theme.bg_active();
        let active_tab_fg = theme.fg_active();
        let inactive_tab_fg = theme.fg_dim();

        let tab_style = |active: bool| {
            if active {
                Style::default()
                    .fg(active_tab_fg)
                    .bg(tab_bg)
                    .add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
            } else {
                Style::default().fg(inactive_tab_fg).bg(tab_bg)
            }
        };

        let loading_span = if self.loading {
            Span::styled("  Loading…", Style::default().fg(theme.level_warn()).bg(tab_bg))
        } else {
            Span::raw("")
        };

        let tab_line = Line::from(vec![
            Span::styled("  ", Style::default().bg(tab_bg)),
            Span::styled("Commit", tab_style(self.tab == CommitTab::Commit)),
            Span::styled("   ", Style::default().bg(tab_bg)),
            Span::styled("Diff & Lines", tab_style(self.tab == CommitTab::Diff)),
            Span::styled("  ", Style::default().bg(tab_bg)),
            loading_span,
        ]);
        frame.render_widget(Block::default().style(Style::new().bg(tab_bg)), chunks[0]);
        frame.render_widget(Paragraph::new(tab_line), chunks[0]);

        // Mode badge for rebase / merge — rendered on the right side of the tab bar.
        if self.git_mode != GitMode::Normal {
            let (badge_text, badge_bg) = match self.git_mode {
                GitMode::Rebase => ("  REBASE  ", Color::Rgb(140, 80, 0)),
                GitMode::Merge  => ("  MERGE   ", Color::Rgb(60, 100, 0)),
                GitMode::Normal => unreachable!(),
            };
            let badge_w = badge_text.len() as u16;
            if badge_w < chunks[0].width {
                let badge_x = chunks[0].x + chunks[0].width.saturating_sub(badge_w);
                let badge_rect = Rect { x: badge_x, y: chunks[0].y, width: badge_w, height: 1 };
                frame.render_widget(
                    Paragraph::new(Span::styled(
                        badge_text,
                        Style::new().fg(Color::White).bg(badge_bg).add_modifier(Modifier::BOLD),
                    )),
                    badge_rect,
                );
            }
        }

        match self.tab {
            CommitTab::Commit => self.render_commit_tab(frame, chunks[1], theme),
            CommitTab::Diff => self.render_diff_tab(frame, chunks[1], theme),
        }
    }
}

// ---------------------------------------------------------------------------
// Rendering helpers
// ---------------------------------------------------------------------------

impl CommitWindow {
    fn render_commit_tab(&self, frame: &mut Frame, area: Rect, theme: &crate::theme::Theme) {
        // Remember the rendered area so popups can be anchored relative to it.
        self.rect_view_area.set(area);

        // When there are conflicts, reserve one row for the conflict warning banner.
        let conflict_banner_h: u16 = if self.has_conflicts { 1 } else { 0 };
        let inner_area = Rect {
            y: area.y + conflict_banner_h,
            height: area.height.saturating_sub(conflict_banner_h),
            ..area
        };

        if self.has_conflicts {
            let warn_bg = Color::Rgb(100, 30, 30);
            let banner_rect = Rect { height: 1, ..area };
            frame.render_widget(Block::default().style(Style::new().bg(warn_bg)), banner_rect);
            frame.render_widget(
                Paragraph::new(Span::styled(
                    "  \u{26a0} Unresolved conflicts — resolve conflicts and stage files before committing",
                    Style::new().fg(Color::White).bg(warn_bg).add_modifier(Modifier::BOLD),
                )),
                banner_rect,
            );
        }

        // Compute dynamic height for the commit message area, but make the
        // file lists expand to fill available space. Message area stays as a
        // fixed number of lines equal to the message length (clamped).
        let total_h = inner_area.height as usize;
        let footer_h = 1 + 1; // branch + status rows
        // Reserve a modest minimum for file lists; they will expand to fill.
        let file_lists_min = 4_usize;
        let desired_msg_lines = self.message_buf.lines().len().max(1);
        // Ensure the message area doesn't consume all space — leave at least
        // `file_lists_min` for the file lists region.
        let max_msg_space = total_h.saturating_sub(file_lists_min + footer_h);
        let msg_h = desired_msg_lines.min(max_msg_space.max(1));

        let rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Min(file_lists_min as u16), // file lists (expandable)
                Constraint::Length(msg_h as u16),        // commit message + button
                Constraint::Length(1), // branch panel
                Constraint::Length(1), // status bar
            ])
            .split(inner_area);

        // ── File lists ───────────────────────────────────────────────────
        let file_cols = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
            .split(rows[0]);

        // Store rects (outer, including title row)
        self.rect_staged_list.set(file_cols[0]);
        self.rect_unstaged_list.set(file_cols[1]);

        // Files that appear in both staged and unstaged are partially staged.
        let unstaged_paths: std::collections::HashSet<&PathBuf> =
            self.unstaged.iter().map(|e| &e.path).collect();
        let partial_paths: std::collections::HashSet<&PathBuf> = self
            .staged
            .iter()
            .map(|e| &e.path)
            .filter(|p| unstaged_paths.contains(p))
            .collect();

        let cur_pane = self.active_pane();
        let (staged_items, staged_map) = crate::widgets::file_tree_list::build_tree_list_items_with_map_from_status(
            &self.staged,
            self.staged_cursor,
            cur_pane == FOCUS_STAGED,
            &partial_paths,
        );
        // Store mapping for mouse hit tests
        *self.staged_items_map.borrow_mut() = staged_map;

        let empty_partial = std::collections::HashSet::new();
        let (unstaged_items, unstaged_map) = crate::widgets::file_tree_list::build_tree_list_items_with_map_from_status(
            &self.unstaged,
            self.unstaged_cursor,
            cur_pane == FOCUS_UNSTAGED,
            &empty_partial,
        );
        *self.unstaged_items_map.borrow_mut() = unstaged_map;

        // Render panes with bg + title row
        for (col_area, pane_id, title, items) in [
            (file_cols[0], FOCUS_STAGED, "  Staged", staged_items),
            (file_cols[1], FOCUS_UNSTAGED, "  Unstaged", unstaged_items),
        ] {
            let is_active = cur_pane == pane_id;
            let pane_bg = if is_active { theme.bg_active() } else { theme.bg_inactive() };
            let title_fg = if is_active { theme.accent() } else { theme.fg_dim() };
            frame.render_widget(Block::default().style(Style::new().bg(pane_bg)), col_area);
            let title_area = Rect { height: 1, ..col_area };
            frame.render_widget(
                Paragraph::new(Span::styled(
                    title,
                    Style::new().fg(title_fg).bg(pane_bg).add_modifier(Modifier::BOLD),
                )),
                title_area,
            );
            let content_area = Rect {
                y: col_area.y + 1,
                height: col_area.height.saturating_sub(1),
                ..col_area
            };
            frame.render_widget(List::new(items), content_area);
        }

        // ── Commit message + button ──────────────────────────────────────
        let msg_cols = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Min(0), Constraint::Length(10), Constraint::Length(10)])
            .split(rows[1]);

        self.rect_message.set(msg_cols[0]);
        self.rect_commit_button.set(msg_cols[1]);
        self.rect_cancel_button.set(msg_cols[2]);

        let msg_bg = if cur_pane == FOCUS_MESSAGE { theme.bg_active() } else { theme.bg_inactive() };
        let msg_focused = cur_pane == FOCUS_MESSAGE;

        frame.render_widget(Block::default().style(Style::new().bg(msg_bg)), msg_cols[0]);

        if self.message_is_empty() && !msg_focused {
            frame.render_widget(
                Paragraph::new(Span::styled(
                    "  (type a commit message)",
                    Style::new().fg(theme.fg_dim()).bg(msg_bg),
                )),
                msg_cols[0],
            );
        } else {
            let lines = self.message_buf.lines();
            let cursor = self.message_buf.cursor();
            let fg = theme.fg_active();
            let mut ratatui_lines: Vec<Line> = Vec::with_capacity(lines.len());

            for (row_idx, line) in lines.iter().enumerate() {
                if msg_focused && row_idx == cursor.line {
                    // Embed block cursor in this line
                    let col = cursor.column.min(line.len());
                    let before = format!("  {}", &line[..col]);
                    let cursor_char = line[col..].chars().next().unwrap_or(' ');
                    let after_start = col + cursor_char.len_utf8().min(line.len() - col);
                    let after = if after_start < line.len() { &line[after_start..] } else { "" };
                    ratatui_lines.push(Line::from(vec![
                        Span::styled(before, Style::new().fg(fg).bg(msg_bg)),
                        Span::styled(
                            cursor_char.to_string(),
                            Style::new().fg(Color::Black).bg(Color::White).add_modifier(Modifier::BOLD),
                        ),
                        Span::styled(after.to_string(), Style::new().fg(fg).bg(msg_bg)),
                    ]));
                } else {
                    ratatui_lines.push(Line::from(Span::styled(
                        format!("  {}", line),
                        Style::new().fg(fg).bg(msg_bg),
                    )));
                }
            }
            frame.render_widget(Paragraph::new(ratatui_lines), msg_cols[0]);
        }

        let can_commit = !self.message_is_empty() && !self.has_conflicts;
        let btn_fg = if cur_pane == FOCUS_COMMIT_BTN {
            theme.selection_bg()
        } else if can_commit {
            theme.accent()
        } else {
            theme.fg_dim()
        };
        let btn_bg = if cur_pane == FOCUS_COMMIT_BTN { theme.accent() } else { theme.bg_inactive() };
        // Commit / Continue Rebase / Commit Merge button
        frame.render_widget(Block::default().style(Style::new().bg(btn_bg)), msg_cols[1]);
        let commit_label = match self.git_mode {
            GitMode::Rebase => " Continue",
            GitMode::Merge  => " Merge",
            GitMode::Normal => if self.amend_mode { " Amend" } else { " Commit" },
        };
        frame.render_widget(
            Paragraph::new(Span::styled(
                commit_label,
                Style::new().fg(btn_fg).bg(btn_bg).add_modifier(Modifier::BOLD),
            )),
            msg_cols[1],
        );
        // Cancel/Abort button — visible in amend mode or rebase/merge mode.
        frame.render_widget(Block::default().style(Style::new().bg(btn_bg)), msg_cols[2]);
        let show_abort = self.amend_mode || self.git_mode != GitMode::Normal;
        if show_abort {
            let abort_label = match self.git_mode {
                GitMode::Rebase => " Abort",
                GitMode::Merge  => " Abort",
                GitMode::Normal => " Cancel",
            };
            let abort_fg = theme.fg_dim();
            frame.render_widget(
                Paragraph::new(Span::styled(
                    abort_label,
                    Style::new().fg(abort_fg).bg(btn_bg).add_modifier(Modifier::BOLD),
                )),
                msg_cols[2],
            );
        }

        // ── Branch panel ─────────────────────────────────────────────────
        let branch_bg = theme.bg_inactive();
        let branch_text = if let Some(ref input) = self.branch_input {
            format!("  Branch: {}   New: {}_", self.current_branch, input.text())
        } else if self.amend_mode {
            format!("  [AMEND] Branch: {}", self.current_branch)
        } else {
            format!("  Branch: {}", self.current_branch)
        };
        frame.render_widget(Block::default().style(Style::new().bg(branch_bg)), rows[2]);
        frame.render_widget(
            Paragraph::new(Span::styled(branch_text, Style::new().fg(theme.fg_dim()).bg(branch_bg))),
            rows[2],
        );

        // ── Status ───────────────────────────────────────────────────────
        if let Some(ref msg) = self.status_msg {
            frame.render_widget(
                Paragraph::new(msg.as_str()).style(Style::default().fg(theme.level_warn())),
                rows[3],
            );
        }

        // Render branch selector popup (anchored above the view's bottom) if open.
        if let Some(BranchPopup::Selector { cursor }) = &self.branch_popup {
            self.render_branch_selector_popup(frame, area, *cursor);
        }
    }

    fn render_branch_selector_popup(&self, frame: &mut Frame, area: Rect, cursor: usize) {
        let n = self.branches.len();
        if n == 0 {
            return;
        }
        let height = (n as u16 + 2).min(area.height);
        let width = self
            .branches
            .iter()
            .map(|b| b.chars().count())
            .max()
            .unwrap_or(10) as u16
            + 4;
        let width = width.min(area.width).max(14);
        // Anchor to bottom-left of the view area (just above the global status bar).
        let y = area.bottom().saturating_sub(height);
        let popup_rect = Rect { x: area.x + 1, y, width, height };
        let branch_names: Vec<&str> = self.branches.iter().map(|s| s.as_str()).collect();
        let menu = crate::widgets::button::Menu::new(&branch_names).cursor(cursor);
        frame.render_widget(menu, popup_rect);
    }

    fn render_diff_tab(&self, frame: &mut Frame, area: Rect, theme: &crate::theme::Theme) {
        let cols = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(25), Constraint::Percentage(75)])
            .split(area);

        self.rect_diff_file_list.set(cols[0]);

        // ── Left: file list ──────────────────────────────────────────────
        let file_list_active = self.active_diff_focus() == FOCUS_FILE_LIST;
        let left_bg = if file_list_active { theme.bg_active() } else { theme.bg_inactive() };
        let left_title_fg = if file_list_active { theme.accent() } else { theme.fg_dim() };
        frame.render_widget(Block::default().style(Style::new().bg(left_bg)), cols[0]);
        let left_title_area = Rect { height: 1, ..cols[0] };
        frame.render_widget(
            Paragraph::new(Span::styled(
                "  Files",
                Style::new().fg(left_title_fg).bg(left_bg).add_modifier(Modifier::BOLD),
            )),
            left_title_area,
        );
        let left_content_area = Rect {
            y: cols[0].y + 1,
            height: cols[0].height.saturating_sub(1),
            ..cols[0]
        };

        let files = self.all_diff_files();
        let (file_items, file_map) = crate::widgets::file_tree_list::build_tree_list_items_with_map_from_paths_staged(
            &files,
            self.diff_list_cursor,
            file_list_active,
            theme.selection_bg(),
            theme.selection_fg(),
            theme.accent(),
            theme.fg_dim(),
        );
        // Store mapping for mouse handling
        *self.diff_items_map.borrow_mut() = file_map;
        frame.render_widget(List::new(file_items), left_content_area);

        // ── Right: diff ──────────────────────────────────────────────────
        let diff_active = self.active_diff_focus() == FOCUS_DIFF_LINES;
        let right_bg = if diff_active { theme.bg_active() } else { theme.bg_inactive() };
        frame.render_widget(Block::default().style(Style::new().bg(right_bg)), cols[1]);

        let right_rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints([
                Constraint::Length(1), // file path header
                Constraint::Min(1),    // diff lines
                Constraint::Length(1), // status
            ])
            .split(cols[1]);

        // Header: format filename first with smart path elision constrained to the header width.
        let header_line = match &self.diff_file {
            Some(p) => {
                let label = if self.diff_staged { "(staged)" } else { "(unstaged)" };

                // Base bg style to avoid repeated bg() calls
                let base_bg = Style::default().bg(right_bg);
                let filename_style = base_bg.fg(theme.accent()).add_modifier(Modifier::BOLD);
                let path_style = base_bg.fg(theme.fg_dim());
                let dim_style = base_bg.fg(theme.fg_dim()).add_modifier(Modifier::DIM);
                let sep_style = base_bg.fg(theme.fg_dim()).add_modifier(Modifier::DIM);

                let cfg = crate::path_format::StyleConfig {
                    filename: filename_style,
                    path: path_style,
                    dim: dim_style,
                    separator: sep_style,
                };

                // Reserve leading spaces and trailing label using display width
                let header_w = right_rows[0].width as usize;
                let leading_w = width("  ");
                let label_w = width(label);
                let reserved = leading_w + label_w;
                let avail = header_w.saturating_sub(reserved);

                // Build spans: leading padding, formatted path, trailing label
                let mut spans = Vec::new();
                spans.push(Span::raw("  "));
                spans.extend(crate::path_format::format_path_spans(p, avail, cfg));
                spans.push(Span::styled(format!(" {}", label), base_bg.fg(theme.fg_dim())));
                Line::from(spans)
            }
            None => Line::from(Span::styled(
                "  (select a file from the list)",
                Style::default().fg(theme.fg_dim()).bg(right_bg),
            )),
        };
        frame.render_widget(Paragraph::new(header_line), right_rows[0]);

        self.rect_diff.set(right_rows[1]);
        let inner_area = right_rows[1];

        let active = diff_active;
        match &self.diff_widget {
            None => {
                let msg = if self.diff_file.is_none() {
                    "(no file selected)"
                } else {
                    "(no diff — file may be binary or has no changes)"
                };
                frame.render_widget(
                    Paragraph::new(Span::styled(msg, Style::default().fg(theme.fg_dim()))),
                    inner_area,
                );
            }
            Some(w) => {
                w.render_into(inner_area, frame.buffer_mut(), active);
            }
        }

        if let Some(ref msg) = self.status_msg {
            frame.render_widget(
                Paragraph::new(msg.as_str()).style(Style::default().fg(theme.level_warn())),
                right_rows[2],
            );
        }
    }
}

// ---------------------------------------------------------------------------
// Tree-style file list builder
// ---------------------------------------------------------------------------

#[allow(dead_code)]
fn build_tree_list_items(
    entries: &[StatusEntry],
    cursor: usize,
    active: bool,
    partial_paths: &std::collections::HashSet<&PathBuf>,
) -> Vec<ListItem<'static>> {
    crate::widgets::file_tree_list::build_tree_list_items_from_status(entries, cursor, active, partial_paths)
}


// ---------------------------------------------------------------------------
// Mouse helpers
// ---------------------------------------------------------------------------

fn list_click_index(rect: Rect, row: u16) -> Option<usize> {
    let inner_top = rect.y + 1;
    let inner_bottom = rect.y + rect.height.saturating_sub(1);
    if row >= inner_top && row < inner_bottom {
        Some((row - inner_top) as usize)
    } else {
        None
    }
}

fn navigate_to(
    target_pane: &str,
    current_pane: &str,
    target_idx: usize,
    current_idx: usize,
) -> Vec<Operation> {
    let mut ops = Vec::new();
    if current_pane != target_pane {
        ops.push(Operation::Focus(FocusOp::Next));
    }
    if target_idx > current_idx {
        for _ in 0..(target_idx - current_idx) {
            ops.push(Operation::NavigateDown);
        }
    } else {
        for _ in 0..(current_idx - target_idx) {
            ops.push(Operation::NavigateUp);
        }
    }
    ops
}



// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::vcs::GitMode;

    fn make_cw() -> CommitWindow {
        CommitWindow::new(std::path::PathBuf::from("/tmp/test-repo"))
    }

    #[test]
    fn new_window_starts_in_normal_mode() {
        let cw = make_cw();
        assert_eq!(cw.git_mode, GitMode::Normal);
        assert!(!cw.has_conflicts);
    }

    #[test]
    fn git_mode_detected_sets_rebase_mode() {
        let mut cw = make_cw();
        let op = Operation::CommitLocal(CommitOp::GitModeDetected {
            mode: GitMode::Rebase,
            has_conflicts: false,
            message: "fix: something".into(),
        });
        cw.handle_operation(&op, &crate::settings::Settings::new(std::path::Path::new("/nonexistent/config.yaml")).unwrap_or_else(|_| crate::settings::Settings::new(std::path::Path::new(".")).unwrap()));
        assert_eq!(cw.git_mode, GitMode::Rebase);
        assert!(!cw.has_conflicts);
        // Message should be pre-filled when the buffer was empty.
        assert_eq!(cw.message_text(), "fix: something");
    }

    #[test]
    fn git_mode_detected_sets_merge_mode() {
        let mut cw = make_cw();
        let op = Operation::CommitLocal(CommitOp::GitModeDetected {
            mode: GitMode::Merge,
            has_conflicts: false,
            message: "Merge branch 'feature'".into(),
        });
        cw.handle_operation(&op, &crate::settings::Settings::new(std::path::Path::new("/nonexistent/config.yaml")).unwrap_or_else(|_| crate::settings::Settings::new(std::path::Path::new(".")).unwrap()));
        assert_eq!(cw.git_mode, GitMode::Merge);
        assert_eq!(cw.message_text(), "Merge branch 'feature'");
    }

    #[test]
    fn git_mode_detected_with_conflicts_sets_status_msg() {
        let mut cw = make_cw();
        let op = Operation::CommitLocal(CommitOp::GitModeDetected {
            mode: GitMode::Merge,
            has_conflicts: true,
            message: String::new(),
        });
        cw.handle_operation(&op, &crate::settings::Settings::new(std::path::Path::new("/nonexistent/config.yaml")).unwrap_or_else(|_| crate::settings::Settings::new(std::path::Path::new(".")).unwrap()));
        assert!(cw.has_conflicts);
        assert!(
            cw.status_msg.as_deref().unwrap_or("").contains("conflict"),
            "status_msg should mention conflicts"
        );
    }

    #[test]
    fn git_mode_detected_does_not_overwrite_existing_message() {
        let mut cw = make_cw();
        // Pre-type a message.
        cw.message_buf = crate::editor::buffer::Buffer::from_lines(
            vec!["already typed".to_string()],
            None,
        );
        let op = Operation::CommitLocal(CommitOp::GitModeDetected {
            mode: GitMode::Rebase,
            has_conflicts: false,
            message: "pre-filled from rebase".into(),
        });
        cw.handle_operation(&op, &crate::settings::Settings::new(std::path::Path::new("/nonexistent/config.yaml")).unwrap_or_else(|_| crate::settings::Settings::new(std::path::Path::new(".")).unwrap()));
        // User's message should be preserved.
        assert_eq!(cw.message_text(), "already typed");
    }

    #[test]
    fn git_mode_detected_normal_clears_conflict_status() {
        let mut cw = make_cw();
        cw.has_conflicts = true;
        cw.status_msg = Some("Cannot commit: unresolved conflicts".into());
        let op = Operation::CommitLocal(CommitOp::GitModeDetected {
            mode: GitMode::Normal,
            has_conflicts: false,
            message: String::new(),
        });
        cw.handle_operation(&op, &crate::settings::Settings::new(std::path::Path::new("/nonexistent/config.yaml")).unwrap_or_else(|_| crate::settings::Settings::new(std::path::Path::new(".")).unwrap()));
        assert!(!cw.has_conflicts);
        assert!(cw.status_msg.is_none(), "conflict status should be cleared");
    }

    #[test]
    fn escape_key_in_rebase_mode_emits_abort_rebase() {
        let mut cw = make_cw();
        cw.git_mode = GitMode::Rebase;
        let key = KeyEvent { key: Key::Escape, modifiers: crate::input::Modifiers::empty() };
        let ops = cw.handle_key(key);
        assert!(
            ops.iter().any(|o| matches!(o, Operation::CommitLocal(CommitOp::AbortRebase))),
            "Escape in rebase mode should emit AbortRebase"
        );
    }

    #[test]
    fn escape_key_in_merge_mode_emits_abort_merge() {
        let mut cw = make_cw();
        cw.git_mode = GitMode::Merge;
        let key = KeyEvent { key: Key::Escape, modifiers: crate::input::Modifiers::empty() };
        let ops = cw.handle_key(key);
        assert!(
            ops.iter().any(|o| matches!(o, Operation::CommitLocal(CommitOp::AbortMerge))),
            "Escape in merge mode should emit AbortMerge"
        );
    }

    #[test]
    fn escape_key_in_normal_mode_does_not_abort() {
        let cw = make_cw();
        let key = KeyEvent { key: Key::Escape, modifiers: crate::input::Modifiers::empty() };
        let ops = cw.handle_key(key);
        assert!(
            !ops.iter().any(|o| matches!(
                o,
                Operation::CommitLocal(CommitOp::AbortRebase)
                    | Operation::CommitLocal(CommitOp::AbortMerge)
            )),
            "Escape in normal mode should not emit abort ops"
        );
    }
}
