#![forbid(unsafe_code)]

//! History panel widget for displaying undo/redo command history.
//!
//! Renders a styled list of command descriptions showing the undo/redo history
//! stack. The current position in the history is marked to indicate what will
//! be undone/redone next.
//!
//! # Example
//!
//! ```ignore
//! use ftui_widgets::history_panel::HistoryPanel;
//!
//! let panel = HistoryPanel::new()
//!     .with_undo_items(&["Insert text", "Delete word"])
//!     .with_redo_items(&["Paste"])
//!     .with_title("History");
//! ```

use crate::{Widget, clear_text_area, draw_text_span};
use ftui_core::geometry::Rect;
use ftui_render::frame::Frame;
use ftui_style::Style;
use ftui_text::wrap::display_width;

/// A single entry in the history panel.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HistoryEntry {
    /// Description of the command.
    pub description: String,
    /// Whether this entry is in the undo or redo stack.
    pub is_redo: bool,
}

impl HistoryEntry {
    /// Create a new history entry.
    #[must_use]
    pub fn new(description: impl Into<String>, is_redo: bool) -> Self {
        Self {
            description: description.into(),
            is_redo,
        }
    }
}

/// Display mode for the history panel.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HistoryPanelMode {
    /// Compact mode: shows only the most recent undo/redo items.
    #[default]
    Compact,
    /// Full mode: shows the complete history stack.
    Full,
}

/// History panel widget that displays undo/redo command history.
///
/// The panel shows commands in chronological order with the current position
/// marked. Commands above the marker can be undone, commands below can be redone.
#[derive(Debug, Clone)]
pub struct HistoryPanel {
    /// Title displayed at the top of the panel.
    title: String,
    /// Entries in the undo stack (oldest first).
    undo_items: Vec<String>,
    /// Entries in the redo stack (oldest first).
    redo_items: Vec<String>,
    /// Display mode.
    mode: HistoryPanelMode,
    /// Maximum items to show in compact mode.
    compact_limit: usize,
    /// Style for the title.
    title_style: Style,
    /// Style for undo items.
    undo_style: Style,
    /// Style for redo items (dimmed, as they are "future" commands).
    redo_style: Style,
    /// Style for the current position marker.
    marker_style: Style,
    /// Style for the panel background.
    bg_style: Style,
    /// Current position marker text.
    marker_text: String,
    /// Undo icon prefix.
    undo_icon: String,
    /// Redo icon prefix.
    redo_icon: String,
}

impl Default for HistoryPanel {
    fn default() -> Self {
        Self::new()
    }
}

impl HistoryPanel {
    /// Create a new history panel with no entries.
    #[must_use]
    pub fn new() -> Self {
        Self {
            title: "History".to_string(),
            undo_items: Vec::new(),
            redo_items: Vec::new(),
            mode: HistoryPanelMode::Compact,
            compact_limit: 5,
            title_style: Style::new().bold(),
            undo_style: Style::default(),
            redo_style: Style::new().dim(),
            marker_style: Style::new().bold(),
            bg_style: Style::default(),
            marker_text: "─── current ───".to_string(),
            undo_icon: "↶ ".to_string(),
            redo_icon: "↷ ".to_string(),
        }
    }

    /// Set the panel title.
    #[must_use]
    pub fn with_title(mut self, title: impl Into<String>) -> Self {
        self.title = title.into();
        self
    }

    /// Set the undo items (descriptions from oldest to newest).
    #[must_use]
    pub fn with_undo_items(mut self, items: &[impl AsRef<str>]) -> Self {
        self.undo_items = items.iter().map(|s| s.as_ref().to_string()).collect();
        self
    }

    /// Set the redo items (descriptions from oldest to newest).
    #[must_use]
    pub fn with_redo_items(mut self, items: &[impl AsRef<str>]) -> Self {
        self.redo_items = items.iter().map(|s| s.as_ref().to_string()).collect();
        self
    }

    /// Set the display mode.
    #[must_use]
    pub fn with_mode(mut self, mode: HistoryPanelMode) -> Self {
        self.mode = mode;
        self
    }

    /// Set the compact mode limit.
    #[must_use]
    pub fn with_compact_limit(mut self, limit: usize) -> Self {
        self.compact_limit = limit;
        self
    }

    /// Set the title style.
    #[must_use]
    pub fn with_title_style(mut self, style: Style) -> Self {
        self.title_style = style;
        self
    }

    /// Set the undo items style.
    #[must_use]
    pub fn with_undo_style(mut self, style: Style) -> Self {
        self.undo_style = style;
        self
    }

    /// Set the redo items style.
    #[must_use]
    pub fn with_redo_style(mut self, style: Style) -> Self {
        self.redo_style = style;
        self
    }

    /// Set the marker style.
    #[must_use]
    pub fn with_marker_style(mut self, style: Style) -> Self {
        self.marker_style = style;
        self
    }

    /// Set the background style.
    #[must_use]
    pub fn with_bg_style(mut self, style: Style) -> Self {
        self.bg_style = style;
        self
    }

    /// Set the marker text.
    #[must_use]
    pub fn with_marker_text(mut self, text: impl Into<String>) -> Self {
        self.marker_text = text.into();
        self
    }

    /// Set the undo icon prefix.
    #[must_use]
    pub fn with_undo_icon(mut self, icon: impl Into<String>) -> Self {
        self.undo_icon = icon.into();
        self
    }

    /// Set the redo icon prefix.
    #[must_use]
    pub fn with_redo_icon(mut self, icon: impl Into<String>) -> Self {
        self.redo_icon = icon.into();
        self
    }

    /// Check if there are any history items.
    #[inline]
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.undo_items.is_empty() && self.redo_items.is_empty()
    }

    /// Get the total number of items.
    #[inline]
    #[must_use]
    pub fn len(&self) -> usize {
        self.undo_items.len() + self.redo_items.len()
    }

    /// Get the undo stack items.
    #[must_use]
    pub fn undo_items(&self) -> &[String] {
        &self.undo_items
    }

    /// Get the redo stack items.
    #[must_use]
    pub fn redo_items(&self) -> &[String] {
        &self.redo_items
    }

    /// Render the panel content.
    fn render_content(&self, area: Rect, frame: &mut Frame) {
        if area.width == 0 || area.height == 0 {
            return;
        }

        let deg = frame.buffer.degradation;
        if !deg.render_content() {
            return;
        }

        let title_style = if deg.apply_styling() {
            self.title_style
        } else {
            Style::default()
        };
        let undo_style = if deg.apply_styling() {
            self.undo_style
        } else {
            Style::default()
        };
        let redo_style = if deg.apply_styling() {
            self.redo_style
        } else {
            Style::default()
        };
        let marker_style = if deg.apply_styling() {
            self.marker_style
        } else {
            Style::default()
        };

        let max_x = area.right();
        let mut row: u16 = 0;

        // Title
        if row < area.height && !self.title.is_empty() {
            let y = area.y.saturating_add(row);
            draw_text_span(frame, area.x, y, &self.title, title_style, max_x);
            row += 1;

            // Blank line after title
            if row < area.height {
                row += 1;
            }
        }

        // Determine which items to show based on mode
        let (undo_to_show, redo_to_show) = match self.mode {
            HistoryPanelMode::Compact => {
                let half_limit = self.compact_limit / 2;
                let undo_start = self.undo_items.len().saturating_sub(half_limit);
                let redo_end = half_limit.min(self.redo_items.len());
                (&self.undo_items[undo_start..], &self.redo_items[..redo_end])
            }
            HistoryPanelMode::Full => (&self.undo_items[..], &self.redo_items[..]),
        };

        // Show ellipsis if there are hidden undo items
        if self.mode == HistoryPanelMode::Compact
            && undo_to_show.len() < self.undo_items.len()
            && row < area.height
        {
            let y = area.y.saturating_add(row);
            let hidden = self.undo_items.len() - undo_to_show.len();
            let text = format!("... ({} more)", hidden);
            draw_text_span(frame, area.x, y, &text, redo_style, max_x);
            row += 1;
        }

        // Undo items (oldest first, so they appear top-to-bottom chronologically)
        for desc in undo_to_show {
            if row >= area.height {
                break;
            }
            let y = area.y.saturating_add(row);
            let icon_end = draw_text_span(frame, area.x, y, &self.undo_icon, undo_style, max_x);
            draw_text_span(frame, icon_end, y, desc, undo_style, max_x);
            row += 1;
        }

        // Current position marker
        if row < area.height {
            let y = area.y.saturating_add(row);
            // Center the marker
            let marker_width = display_width(&self.marker_text);
            let available = area.width as usize;
            let pad_left = available.saturating_sub(marker_width) / 2;
            let x = area.x.saturating_add(pad_left as u16);
            draw_text_span(frame, x, y, &self.marker_text, marker_style, max_x);
            row += 1;
        }

        // Redo items (these are "future" commands that can be redone)
        for desc in redo_to_show {
            if row >= area.height {
                break;
            }
            let y = area.y.saturating_add(row);
            let icon_end = draw_text_span(frame, area.x, y, &self.redo_icon, redo_style, max_x);
            draw_text_span(frame, icon_end, y, desc, redo_style, max_x);
            row += 1;
        }

        // Show ellipsis if there are hidden redo items
        if self.mode == HistoryPanelMode::Compact
            && redo_to_show.len() < self.redo_items.len()
            && row < area.height
        {
            let y = area.y.saturating_add(row);
            let hidden = self.redo_items.len() - redo_to_show.len();
            let text = format!("... ({} more)", hidden);
            draw_text_span(frame, area.x, y, &text, redo_style, max_x);
        }
    }
}

impl Widget for HistoryPanel {
    fn render(&self, area: Rect, frame: &mut Frame) {
        let deg = frame.buffer.degradation;
        if !deg.render_content() {
            clear_text_area(frame, area, Style::default());
            return;
        }

        // Fill background area
        let mut bg_cell = ftui_render::cell::Cell::from_char(' ');
        crate::apply_style(
            &mut bg_cell,
            if deg.apply_styling() {
                self.bg_style
            } else {
                Style::default()
            },
        );
        frame.buffer.fill(area, bg_cell);

        self.render_content(area, frame);
    }

    fn is_essential(&self) -> bool {
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ftui_render::budget::DegradationLevel;
    use ftui_render::frame::Frame;
    use ftui_render::grapheme_pool::GraphemePool;

    #[test]
    fn new_panel_is_empty() {
        let panel = HistoryPanel::new();
        assert!(panel.is_empty());
        assert_eq!(panel.len(), 0);
    }

    #[test]
    fn with_undo_items() {
        let panel = HistoryPanel::new().with_undo_items(&["Insert text", "Delete word"]);
        assert_eq!(panel.undo_items().len(), 2);
        assert_eq!(panel.undo_items()[0], "Insert text");
        assert_eq!(panel.len(), 2);
    }

    #[test]
    fn with_redo_items() {
        let panel = HistoryPanel::new().with_redo_items(&["Paste"]);
        assert_eq!(panel.redo_items().len(), 1);
        assert_eq!(panel.len(), 1);
    }

    #[test]
    fn with_both_stacks() {
        let panel = HistoryPanel::new()
            .with_undo_items(&["A", "B"])
            .with_redo_items(&["C"]);
        assert!(!panel.is_empty());
        assert_eq!(panel.len(), 3);
    }

    #[test]
    fn with_title() {
        let panel = HistoryPanel::new().with_title("My History");
        assert_eq!(panel.title, "My History");
    }

    #[test]
    fn with_mode() {
        let panel = HistoryPanel::new().with_mode(HistoryPanelMode::Full);
        assert_eq!(panel.mode, HistoryPanelMode::Full);
    }

    #[test]
    fn render_empty() {
        let panel = HistoryPanel::new();
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);
        panel.render(area, &mut frame); // Should not panic
    }

    #[test]
    fn render_with_items() {
        let panel = HistoryPanel::new()
            .with_undo_items(&["Insert text"])
            .with_redo_items(&["Delete word"]);

        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);
        panel.render(area, &mut frame);

        // Verify title appears
        let cell = frame.buffer.get(0, 0).unwrap();
        assert_eq!(cell.content.as_char(), Some('H')); // "History"
    }

    #[test]
    fn render_zero_area() {
        let panel = HistoryPanel::new().with_undo_items(&["Test"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 0, 0);
        panel.render(area, &mut frame); // Should not panic
    }

    #[test]
    fn compact_limit() {
        let items: Vec<_> = (0..10).map(|i| format!("Item {}", i)).collect();
        let panel = HistoryPanel::new()
            .with_mode(HistoryPanelMode::Compact)
            .with_compact_limit(4)
            .with_undo_items(&items);

        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 20, &mut pool);
        let area = Rect::new(0, 0, 30, 20);
        panel.render(area, &mut frame); // Should show only last 2 undo items
    }

    #[test]
    fn is_not_essential() {
        let panel = HistoryPanel::new();
        assert!(!panel.is_essential());
    }

    #[test]
    fn render_no_styling_drops_configured_styles() {
        let fg = ftui_render::cell::PackedRgba::rgb(255, 0, 0);
        let bg = ftui_render::cell::PackedRgba::rgb(0, 40, 80);
        let panel = HistoryPanel::new()
            .with_undo_items(&["Insert text"])
            .with_title_style(Style::new().fg(fg).bold())
            .with_undo_style(Style::new().fg(fg))
            .with_marker_style(Style::new().fg(fg).italic())
            .with_bg_style(Style::new().bg(bg));
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        frame.buffer.degradation = DegradationLevel::NoStyling;
        let area = Rect::new(0, 0, 30, 10);

        panel.render(area, &mut frame);

        let title_cell = frame.buffer.get(0, 0).unwrap();
        let background_cell = frame.buffer.get(15, 5).unwrap();
        let default_text = ftui_render::cell::Cell::from_char('H');
        let default_bg = ftui_render::cell::Cell::from_char(' ');

        assert_eq!(title_cell.content.as_char(), Some('H'));
        assert_eq!(title_cell.fg, default_text.fg);
        assert_eq!(title_cell.bg, default_text.bg);
        assert_eq!(title_cell.attrs, default_text.attrs);
        assert_eq!(background_cell.fg, default_bg.fg);
        assert_eq!(background_cell.bg, default_bg.bg);
        assert_eq!(background_cell.attrs, default_bg.attrs);
    }

    #[test]
    fn render_skeleton_is_noop() {
        let panel = HistoryPanel::new().with_undo_items(&["Insert text"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);
        panel.render(area, &mut frame);

        frame.buffer.degradation = DegradationLevel::Skeleton;
        panel.render(area, &mut frame);

        let cell = frame.buffer.get(0, 0).unwrap();
        assert_eq!(cell.content.as_char(), Some(' '));
        let default_cell = ftui_render::cell::Cell::from_char(' ');
        assert_eq!(cell.fg, default_cell.fg);
        assert_eq!(cell.bg, default_cell.bg);
        assert_eq!(cell.attrs, default_cell.attrs);
    }

    #[test]
    fn render_shorter_history_clears_stale_rows() {
        let long = HistoryPanel::new()
            .with_undo_items(&["Insert text", "Delete line", "Paste block"])
            .with_redo_items(&["Redo thing"]);
        let short = HistoryPanel::new().with_undo_items(&["Insert"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);

        long.render(area, &mut frame);
        short.render(area, &mut frame);

        for y in 6..10u16 {
            for x in 0..30u16 {
                assert_eq!(frame.buffer.get(x, y).unwrap().content.as_char(), Some(' '));
            }
        }
    }

    #[test]
    fn default_impl() {
        let panel = HistoryPanel::default();
        assert!(panel.is_empty());
    }

    #[test]
    fn with_icons() {
        let panel = HistoryPanel::new()
            .with_undo_icon("<< ")
            .with_redo_icon(">> ");
        assert_eq!(panel.undo_icon, "<< ");
        assert_eq!(panel.redo_icon, ">> ");
    }

    #[test]
    fn with_marker_text() {
        let panel = HistoryPanel::new().with_marker_text("=== NOW ===");
        assert_eq!(panel.marker_text, "=== NOW ===");
    }

    #[test]
    fn history_entry_new() {
        let entry = HistoryEntry::new("Delete line", false);
        assert_eq!(entry.description, "Delete line");
        assert!(!entry.is_redo);

        let redo = HistoryEntry::new("Paste", true);
        assert!(redo.is_redo);
    }

    #[test]
    fn history_panel_mode_default_is_compact() {
        assert_eq!(HistoryPanelMode::default(), HistoryPanelMode::Compact);
    }

    #[test]
    fn with_compact_limit_setter() {
        let panel = HistoryPanel::new().with_compact_limit(10);
        assert_eq!(panel.compact_limit, 10);
    }

    #[test]
    fn history_entry_equality() {
        let a = HistoryEntry::new("X", false);
        let b = HistoryEntry::new("X", false);
        let c = HistoryEntry::new("X", true);
        assert_eq!(a, b);
        assert_ne!(a, c);
    }

    #[test]
    fn full_mode_renders_all_items() {
        let items: Vec<_> = (0..10).map(|i| format!("Item {i}")).collect();
        let panel = HistoryPanel::new()
            .with_mode(HistoryPanelMode::Full)
            .with_undo_items(&items);

        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 30, &mut pool);
        let area = Rect::new(0, 0, 30, 30);
        panel.render(area, &mut frame); // Should not panic in full mode
    }

    // ── Edge-case tests (bd-2yn6z) ──────────────────────────

    #[test]
    fn style_setters_applied() {
        let style = Style::new().italic();
        let panel = HistoryPanel::new()
            .with_title_style(style)
            .with_undo_style(style)
            .with_redo_style(style)
            .with_marker_style(style)
            .with_bg_style(style);
        assert_eq!(panel.title_style, style);
        assert_eq!(panel.undo_style, style);
        assert_eq!(panel.redo_style, style);
        assert_eq!(panel.marker_style, style);
        assert_eq!(panel.bg_style, style);
    }

    #[test]
    fn clone_preserves_all_fields() {
        let panel = HistoryPanel::new()
            .with_title("T")
            .with_undo_items(&["A"])
            .with_redo_items(&["B"])
            .with_mode(HistoryPanelMode::Full)
            .with_compact_limit(3)
            .with_marker_text("NOW")
            .with_undo_icon("U ")
            .with_redo_icon("R ");
        let cloned = panel.clone();
        assert_eq!(cloned.title, "T");
        assert_eq!(cloned.undo_items, vec!["A"]);
        assert_eq!(cloned.redo_items, vec!["B"]);
        assert_eq!(cloned.mode, HistoryPanelMode::Full);
        assert_eq!(cloned.compact_limit, 3);
        assert_eq!(cloned.marker_text, "NOW");
        assert_eq!(cloned.undo_icon, "U ");
        assert_eq!(cloned.redo_icon, "R ");
    }

    #[test]
    fn debug_format() {
        let panel = HistoryPanel::new();
        let dbg = format!("{:?}", panel);
        assert!(dbg.contains("HistoryPanel"));
        assert!(dbg.contains("History"));

        let entry = HistoryEntry::new("X", true);
        let dbg_e = format!("{:?}", entry);
        assert!(dbg_e.contains("HistoryEntry"));
        assert!(dbg_e.contains("is_redo: true"));

        let mode = HistoryPanelMode::Compact;
        assert!(format!("{:?}", mode).contains("Compact"));
    }

    #[test]
    fn history_entry_clone() {
        let a = HistoryEntry::new("Hello", false);
        let b = a.clone();
        assert_eq!(a, b);
        assert_eq!(b.description, "Hello");
    }

    #[test]
    fn history_panel_mode_copy_eq() {
        let a = HistoryPanelMode::Full;
        let b = a; // Copy
        assert_eq!(a, b);
        assert_ne!(a, HistoryPanelMode::Compact);
    }

    #[test]
    fn render_only_redo_no_undo() {
        let panel = HistoryPanel::new().with_redo_items(&["Redo1", "Redo2"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);
        panel.render(area, &mut frame);
        // Title on row 0, blank on row 1, marker on row 2, redo items on rows 3-4
        let cell = frame.buffer.get(0, 0).unwrap();
        assert_eq!(cell.content.as_char(), Some('H')); // "History"
    }

    #[test]
    fn render_empty_title() {
        let panel = HistoryPanel::new().with_title("").with_undo_items(&["A"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);
        panel.render(area, &mut frame);
        // With empty title, undo icon should start at row 0
        let cell = frame.buffer.get(0, 0).unwrap();
        // First char should be undo icon '↶'
        assert_ne!(cell.content.as_char(), Some('H'));
    }

    #[test]
    fn compact_both_stacks_overflow() {
        let undo: Vec<_> = (0..8).map(|i| format!("U{i}")).collect();
        let redo: Vec<_> = (0..8).map(|i| format!("R{i}")).collect();
        let panel = HistoryPanel::new()
            .with_mode(HistoryPanelMode::Compact)
            .with_compact_limit(4)
            .with_undo_items(&undo)
            .with_redo_items(&redo);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(40, 20, &mut pool);
        let area = Rect::new(0, 0, 40, 20);
        panel.render(area, &mut frame);
        // Should show: title, blank, "... (6 more)", 2 undo items, marker, 2 redo items, "... (6 more)"
    }

    #[test]
    fn compact_limit_zero() {
        let panel = HistoryPanel::new()
            .with_compact_limit(0)
            .with_undo_items(&["A", "B"])
            .with_redo_items(&["C"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);
        panel.render(area, &mut frame); // half_limit=0, no items shown
    }

    #[test]
    fn compact_limit_one_odd() {
        let panel = HistoryPanel::new()
            .with_compact_limit(1)
            .with_undo_items(&["A", "B", "C"])
            .with_redo_items(&["D", "E"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);
        // half_limit = 0 (1/2 = 0 in integer division), so nothing shown
        panel.render(area, &mut frame);
    }

    #[test]
    fn render_width_one() {
        let panel = HistoryPanel::new()
            .with_undo_items(&["LongItem"])
            .with_redo_items(&["AnotherLong"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 1, 10);
        panel.render(area, &mut frame); // Should not panic, content truncated
    }

    #[test]
    fn render_height_one() {
        let panel = HistoryPanel::new()
            .with_undo_items(&["A"])
            .with_redo_items(&["B"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 1);
        panel.render(area, &mut frame); // Only title fits
    }

    #[test]
    fn render_height_three_no_room_for_redo() {
        let panel = HistoryPanel::new()
            .with_undo_items(&["A"])
            .with_redo_items(&["B"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        // title(1) + blank(1) + undo(1) = 3, marker and redo don't fit
        let area = Rect::new(0, 0, 30, 3);
        panel.render(area, &mut frame);
    }

    #[test]
    fn bg_style_fills_area() {
        use ftui_render::cell::PackedRgba;
        let red = PackedRgba::rgb(255, 0, 0);
        let panel = HistoryPanel::new().with_bg_style(Style::new().bg(red));
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 5, &mut pool);
        let area = Rect::new(0, 0, 10, 5);
        panel.render(area, &mut frame);
        // All cells in area should have red background
        for y in 0..5u16 {
            for x in 0..10u16 {
                let cell = frame.buffer.get(x, y).unwrap();
                assert_eq!(cell.bg, red);
            }
        }
    }

    #[test]
    fn bg_style_none_does_not_fill() {
        use ftui_render::cell::PackedRgba;
        let panel = HistoryPanel::new();
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 5, &mut pool);
        let area = Rect::new(0, 0, 10, 5);
        panel.render(area, &mut frame);
        // Default bg should remain transparent
        let cell = frame.buffer.get(5, 3).unwrap();
        assert_eq!(cell.bg, PackedRgba::TRANSPARENT);
    }

    #[test]
    fn marker_centering_even_width() {
        let panel = HistoryPanel::new()
            .with_title("")
            .with_marker_text("XX")
            .with_undo_items(&["A"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(20, 10, &mut pool);
        let area = Rect::new(0, 0, 20, 10);
        panel.render(area, &mut frame);
        // "XX" is 2 chars wide, area is 20. pad_left = (20 - 2) / 2 = 9
        // marker starts at x=9
        let cell_before = frame.buffer.get(8, 1).unwrap();
        assert_ne!(cell_before.content.as_char(), Some('X'));
        let cell_start = frame.buffer.get(9, 1).unwrap();
        assert_eq!(cell_start.content.as_char(), Some('X'));
    }

    #[test]
    fn marker_wider_than_area() {
        let panel = HistoryPanel::new()
            .with_title("")
            .with_marker_text("VERY LONG MARKER TEXT THAT EXCEEDS");
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 5, &mut pool);
        let area = Rect::new(0, 0, 10, 5);
        // marker_width > available, pad_left = 0 (saturating_sub)
        panel.render(area, &mut frame);
    }

    #[test]
    fn overwrite_items_replaces() {
        let panel = HistoryPanel::new()
            .with_undo_items(&["Old1", "Old2"])
            .with_undo_items(&["New1"]);
        assert_eq!(panel.undo_items().len(), 1);
        assert_eq!(panel.undo_items()[0], "New1");
    }

    #[test]
    fn render_at_offset_area() {
        let panel = HistoryPanel::new()
            .with_undo_items(&["A"])
            .with_redo_items(&["B"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 20, &mut pool);
        let area = Rect::new(5, 5, 20, 10);
        panel.render(area, &mut frame);
        // Title should be at (5, 5)
        let cell = frame.buffer.get(5, 5).unwrap();
        assert_eq!(cell.content.as_char(), Some('H'));
        // (0, 0) should not have been written by the panel
        let origin = frame.buffer.get(0, 0).unwrap();
        assert_ne!(origin.content.as_char(), Some('H'));
    }

    #[test]
    fn empty_undo_icon_and_redo_icon() {
        let panel = HistoryPanel::new()
            .with_undo_icon("")
            .with_redo_icon("")
            .with_undo_items(&["A"])
            .with_redo_items(&["B"]);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        let area = Rect::new(0, 0, 30, 10);
        panel.render(area, &mut frame);
    }

    #[test]
    fn full_mode_no_ellipsis() {
        let undo: Vec<_> = (0..10).map(|i| format!("U{i}")).collect();
        let redo: Vec<_> = (0..10).map(|i| format!("R{i}")).collect();
        let panel = HistoryPanel::new()
            .with_mode(HistoryPanelMode::Full)
            .with_undo_items(&undo)
            .with_redo_items(&redo);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 30, &mut pool);
        let area = Rect::new(0, 0, 30, 30);
        panel.render(area, &mut frame);
        // In full mode, all items should show without ellipsis
    }

    #[test]
    fn compact_undo_only_with_overflow() {
        let items: Vec<_> = (0..10).map(|i| format!("Item{i}")).collect();
        let panel = HistoryPanel::new()
            .with_mode(HistoryPanelMode::Compact)
            .with_compact_limit(4)
            .with_undo_items(&items);
        // half_limit = 2, shows last 2 of 10 undo items + ellipsis
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 15, &mut pool);
        let area = Rect::new(0, 0, 30, 15);
        panel.render(area, &mut frame);
    }

    #[test]
    fn compact_redo_only_with_overflow() {
        let items: Vec<_> = (0..10).map(|i| format!("Item{i}")).collect();
        let panel = HistoryPanel::new()
            .with_mode(HistoryPanelMode::Compact)
            .with_compact_limit(4)
            .with_redo_items(&items);
        // half_limit = 2, shows first 2 of 10 redo items + ellipsis
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 15, &mut pool);
        let area = Rect::new(0, 0, 30, 15);
        panel.render(area, &mut frame);
    }

    #[test]
    fn history_entry_from_string_type() {
        let entry = HistoryEntry::new(String::from("Owned"), false);
        assert_eq!(entry.description, "Owned");
    }
}
