#![forbid(unsafe_code)]

//! Text input widget.
//!
//! A single-line text input field with cursor management, scrolling, selection,
//! word-level operations, and styling. Grapheme-cluster aware for correct Unicode handling.

use ftui_core::event::{Event, ImeEvent, ImePhase, KeyCode, KeyEvent, KeyEventKind, Modifiers};
use ftui_core::geometry::Rect;
use ftui_render::cell::{Cell, CellContent};
use ftui_render::frame::Frame;
use ftui_style::Style;
use ftui_text::grapheme_width;
use unicode_segmentation::UnicodeSegmentation;

use crate::undo_support::{TextEditOperation, TextInputUndoExt, UndoSupport, UndoWidgetId};
use crate::{Widget, clear_text_area};

/// A single-line text input widget.
#[derive(Debug, Clone, Default)]
pub struct TextInput {
    /// Unique ID for undo tracking.
    undo_id: UndoWidgetId,
    /// Text value.
    value: String,
    /// Cursor position (grapheme index).
    cursor: usize,
    /// Scroll offset (visual cells) for horizontal scrolling.
    scroll_cells: std::cell::Cell<usize>,
    /// Selection anchor (grapheme index). When set, selection spans from anchor to cursor.
    selection_anchor: Option<usize>,
    /// Active IME composition text (preedit), if any.
    ime_composition: Option<String>,
    /// Placeholder text.
    placeholder: String,
    /// Mask character for password mode.
    mask_char: Option<char>,
    /// Maximum length in graphemes (None = unlimited).
    max_length: Option<usize>,
    /// Base style.
    style: Style,
    /// Cursor style.
    cursor_style: Style,
    /// Placeholder style.
    placeholder_style: Style,
    /// Selection highlight style.
    selection_style: Style,
    /// Whether the input is focused (controls cursor output).
    focused: bool,
}

impl TextInput {
    /// Create a new empty text input.
    pub fn new() -> Self {
        Self::default()
    }

    // --- Builder methods ---

    /// Set the text value (builder).
    #[must_use]
    pub fn with_value(mut self, value: impl Into<String>) -> Self {
        self.value = value.into();
        self.cursor = self.value.graphemes(true).count();
        self.selection_anchor = None;
        self
    }

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

    /// Set password mode with mask character (builder).
    #[must_use]
    pub fn with_mask(mut self, mask: char) -> Self {
        self.mask_char = Some(mask);
        self
    }

    /// Set maximum length in graphemes (builder).
    #[must_use]
    pub fn with_max_length(mut self, max: usize) -> Self {
        self.max_length = Some(max);
        self
    }

    /// Set base style (builder).
    #[must_use]
    pub fn with_style(mut self, style: Style) -> Self {
        self.style = style;
        self
    }

    /// Set cursor style (builder).
    #[must_use]
    pub fn with_cursor_style(mut self, style: Style) -> Self {
        self.cursor_style = style;
        self
    }

    /// Set placeholder style (builder).
    #[must_use]
    pub fn with_placeholder_style(mut self, style: Style) -> Self {
        self.placeholder_style = style;
        self
    }

    /// Set selection style (builder).
    #[must_use]
    pub fn with_selection_style(mut self, style: Style) -> Self {
        self.selection_style = style;
        self
    }

    /// Set whether the input is focused (builder).
    #[must_use]
    pub fn with_focused(mut self, focused: bool) -> Self {
        self.focused = focused;
        self
    }

    // --- Value access ---

    /// Get the current value.
    pub fn value(&self) -> &str {
        &self.value
    }

    /// Set the value, clamping cursor to valid range.
    pub fn set_value(&mut self, value: impl Into<String>) {
        self.value = value.into();
        let max = self.grapheme_count();
        self.cursor = self.cursor.min(max);
        self.scroll_cells.set(0);
        self.selection_anchor = None;
    }

    /// Clear all text.
    pub fn clear(&mut self) {
        self.value.clear();
        self.cursor = 0;
        self.scroll_cells.set(0);
        self.selection_anchor = None;
    }

    /// Get the cursor position (grapheme index).
    #[inline]
    pub fn cursor(&self) -> usize {
        self.cursor
    }

    /// Check if the input is focused.
    #[inline]
    pub fn focused(&self) -> bool {
        self.focused
    }

    /// Set focus state.
    pub fn set_focused(&mut self, focused: bool) {
        self.focused = focused;
    }

    /// Get the cursor screen position relative to a render area.
    ///
    /// Returns `(x, y)` where x is the column and y is the row.
    /// Useful for `Frame::set_cursor()`.
    pub fn cursor_position(&self, area: Rect) -> (u16, u16) {
        let cursor_visual = self.cursor_visual_pos();
        let effective_scroll = self.effective_scroll(area.width as usize);
        let rel_x = cursor_visual.saturating_sub(effective_scroll);
        let x = area
            .x
            .saturating_add(rel_x as u16)
            .min(area.right().saturating_sub(1));
        (x, area.y)
    }

    /// Get selected text, if any.
    #[must_use]
    pub fn selected_text(&self) -> Option<&str> {
        let anchor = self.selection_anchor?;
        let (start, end) = self.selection_range(anchor);
        let byte_start = self.grapheme_byte_offset(start);
        let byte_end = self.grapheme_byte_offset(end);
        Some(&self.value[byte_start..byte_end])
    }

    /// Start an IME composition session.
    pub fn ime_start_composition(&mut self) {
        if self.ime_composition.is_none() {
            self.delete_selection();
        }
        self.ime_composition = Some(String::new());
        #[cfg(feature = "tracing")]
        self.trace_edit("ime_start");
    }

    /// Update active IME preedit text.
    ///
    /// Starts composition automatically if none is active.
    pub fn ime_update_composition(&mut self, preedit: impl Into<String>) {
        if self.ime_composition.is_none() {
            self.delete_selection();
        }
        self.ime_composition = Some(preedit.into());
        #[cfg(feature = "tracing")]
        self.trace_edit("ime_update");
    }

    /// Commit active IME preedit text into the input value.
    ///
    /// Returns `true` if a composition session existed (even if empty).
    pub fn ime_commit_composition(&mut self) -> bool {
        let Some(preedit) = self.ime_composition.take() else {
            return false;
        };

        if !preedit.is_empty() {
            self.insert_text(&preedit);
        }

        #[cfg(feature = "tracing")]
        self.trace_edit("ime_commit");

        true
    }

    /// Cancel the active IME composition session.
    ///
    /// Returns `true` if a composition session was active.
    pub fn ime_cancel_composition(&mut self) -> bool {
        let cancelled = self.ime_composition.take().is_some();
        #[cfg(feature = "tracing")]
        if cancelled {
            self.trace_edit("ime_cancel");
        }
        cancelled
    }

    /// Get active IME preedit text, if any.
    #[must_use]
    pub fn ime_composition(&self) -> Option<&str> {
        self.ime_composition.as_deref()
    }

    // --- Event handling ---

    /// Handle a terminal event.
    ///
    /// Returns `true` if the state changed.
    pub fn handle_event(&mut self, event: &Event) -> bool {
        let changed = match event {
            Event::Key(key)
                if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat =>
            {
                self.handle_key(key)
            }
            Event::Ime(ime) => self.handle_ime_event(ime),
            Event::Paste(paste) => {
                let had_selection = self.selection_anchor.is_some();

                // For replacement pastes under a max-length constraint, reject
                // oversized payloads before deleting the selection.
                if had_selection {
                    let clean_text = Self::sanitize_input_text(&paste.text);
                    if let Some(max) = self.max_length {
                        let selection_len = if let Some(anchor) = self.selection_anchor {
                            let (start, end) = self.selection_range(anchor);
                            end.saturating_sub(start)
                        } else {
                            0
                        };
                        let available =
                            max.saturating_sub(self.grapheme_count().saturating_sub(selection_len));
                        if clean_text.graphemes(true).count() > available {
                            return false;
                        }
                    }
                }

                self.delete_selection();
                self.insert_text(&paste.text);
                true
            }
            _ => false,
        };

        #[cfg(feature = "tracing")]
        if changed {
            self.trace_edit(Self::event_operation_name(event));
        }

        changed
    }

    fn handle_ime_event(&mut self, ime: &ImeEvent) -> bool {
        match ime.phase {
            ImePhase::Start => {
                self.ime_start_composition();
                true
            }
            ImePhase::Update => {
                self.ime_update_composition(&ime.text);
                true
            }
            ImePhase::Commit => {
                if self.ime_composition.is_some() {
                    self.ime_update_composition(&ime.text);
                    self.ime_commit_composition()
                } else if !ime.text.is_empty() {
                    self.insert_text(&ime.text);
                    true
                } else {
                    false
                }
            }
            ImePhase::Cancel => self.ime_cancel_composition(),
        }
    }

    fn handle_key(&mut self, key: &KeyEvent) -> bool {
        let ctrl = key.modifiers.contains(Modifiers::CTRL);
        let shift = key.modifiers.contains(Modifiers::SHIFT);

        match key.code {
            KeyCode::Char(c) if !ctrl => {
                self.insert_char(c);
                true
            }
            // Ctrl+A: select all
            KeyCode::Char('a') if ctrl => {
                self.select_all();
                true
            }
            // Ctrl+W: delete word back
            KeyCode::Char('w') if ctrl => {
                self.delete_word_back();
                true
            }
            KeyCode::Backspace => {
                if self.selection_anchor.is_some() {
                    self.delete_selection();
                } else if ctrl {
                    self.delete_word_back();
                } else {
                    self.delete_char_back();
                }
                true
            }
            KeyCode::Delete => {
                if self.selection_anchor.is_some() {
                    self.delete_selection();
                } else if ctrl {
                    self.delete_word_forward();
                } else {
                    self.delete_char_forward();
                }
                true
            }
            KeyCode::Left => {
                if ctrl {
                    self.move_cursor_word_left(shift);
                } else if shift {
                    self.move_cursor_left_select();
                } else {
                    self.move_cursor_left();
                }
                true
            }
            KeyCode::Right => {
                if ctrl {
                    self.move_cursor_word_right(shift);
                } else if shift {
                    self.move_cursor_right_select();
                } else {
                    self.move_cursor_right();
                }
                true
            }
            KeyCode::Home => {
                if shift {
                    self.ensure_selection_anchor();
                } else {
                    self.selection_anchor = None;
                }
                self.cursor = 0;
                self.scroll_cells.set(0);
                true
            }
            KeyCode::End => {
                if shift {
                    self.ensure_selection_anchor();
                } else {
                    self.selection_anchor = None;
                }
                self.cursor = self.grapheme_count();
                true
            }
            _ => false,
        }
    }

    #[cfg(feature = "tracing")]
    fn trace_edit(&self, operation: &'static str) {
        let _span = tracing::debug_span!(
            "input.edit",
            operation,
            cursor_position = self.cursor,
            grapheme_count = self.grapheme_count(),
            has_selection = self.selection_anchor.is_some()
        )
        .entered();
    }

    #[cfg(feature = "tracing")]
    fn event_operation_name(event: &Event) -> &'static str {
        match event {
            Event::Key(key) => Self::key_operation_name(key),
            Event::Paste(_) => "paste",
            Event::Ime(ime) => match ime.phase {
                ImePhase::Start => "ime_start",
                ImePhase::Update => "ime_update",
                ImePhase::Commit => "ime_commit",
                ImePhase::Cancel => "ime_cancel",
            },
            Event::Resize { .. } => "resize",
            Event::Focus(_) => "focus",
            Event::Mouse(_) => "mouse",
            Event::Clipboard(_) => "clipboard",
            Event::Tick => "tick",
        }
    }

    #[cfg(feature = "tracing")]
    fn key_operation_name(key: &KeyEvent) -> &'static str {
        let ctrl = key.modifiers.contains(Modifiers::CTRL);
        let shift = key.modifiers.contains(Modifiers::SHIFT);

        match key.code {
            KeyCode::Char(_) if !ctrl => "insert_char",
            KeyCode::Char('a') if ctrl => "select_all",
            KeyCode::Char('w') if ctrl => "delete_word_back",
            KeyCode::Backspace if ctrl => "delete_word_back",
            KeyCode::Backspace => "delete_back",
            KeyCode::Delete if ctrl => "delete_word_forward",
            KeyCode::Delete => "delete_forward",
            KeyCode::Left if ctrl && shift => "move_word_left_select",
            KeyCode::Left if ctrl => "move_word_left",
            KeyCode::Left if shift => "move_left_select",
            KeyCode::Left => "move_left",
            KeyCode::Right if ctrl && shift => "move_word_right_select",
            KeyCode::Right if ctrl => "move_word_right",
            KeyCode::Right if shift => "move_right_select",
            KeyCode::Right => "move_right",
            KeyCode::Home if shift => "move_home_select",
            KeyCode::Home => "move_home",
            KeyCode::End if shift => "move_end_select",
            KeyCode::End => "move_end",
            _ => "key_other",
        }
    }

    // --- Editing operations ---

    fn sanitize_input_text(text: &str) -> String {
        // Map line breaks/tabs to spaces, filter other control chars
        text.chars()
            .map(|c| {
                if c == '\n' || c == '\r' || c == '\t' {
                    ' '
                } else {
                    c
                }
            })
            .filter(|c| !c.is_control())
            .collect()
    }

    /// Insert text at the current cursor position.
    ///
    /// This method:
    /// - Replaces newlines and tabs with spaces.
    /// - Filters out other control characters.
    /// - Respects `max_length` (truncating if necessary).
    /// - Efficiently inserts the result in one operation.
    pub fn insert_text(&mut self, text: &str) {
        self.delete_selection();

        let clean_text = Self::sanitize_input_text(text);

        if clean_text.is_empty() {
            return;
        }

        let current_count = self.grapheme_count();
        let avail = if let Some(max) = self.max_length {
            if current_count >= max {
                // Allow trying to insert 1 grapheme to see if it merges (combining char)
                1
            } else {
                max - current_count
            }
        } else {
            usize::MAX
        };

        // Calculate grapheme count of new text to see if we need to truncate
        let new_graphemes = clean_text.graphemes(true).count();
        let to_insert = if new_graphemes > avail {
            // Find byte index to truncate at
            let end_byte = clean_text
                .grapheme_indices(true)
                .map(|(i, _)| i)
                .nth(avail)
                .unwrap_or(clean_text.len());
            &clean_text[..end_byte]
        } else {
            clean_text.as_str()
        };

        if to_insert.is_empty() {
            return;
        }

        let byte_offset = self.grapheme_byte_offset(self.cursor);
        self.value.insert_str(byte_offset, to_insert);

        // Check if we exceeded max_length
        let new_total = self.grapheme_count();
        if let Some(max) = self.max_length
            && new_total > max
        {
            // Revert change
            self.value.drain(byte_offset..byte_offset + to_insert.len());
            return;
        }

        self.cursor = self
            .value
            .grapheme_indices(true)
            .take_while(|(i, _)| *i < byte_offset + to_insert.len())
            .count();
    }

    fn insert_char(&mut self, c: char) {
        // Strict control character filtering to prevent terminal corruption
        if c.is_control() {
            return;
        }

        self.delete_selection();

        let byte_offset = self.grapheme_byte_offset(self.cursor);
        self.value.insert(byte_offset, c);

        let new_count = self.grapheme_count();

        // Check constraints
        if let Some(max) = self.max_length
            && new_count > max
        {
            // Revert change
            let char_len = c.len_utf8();
            self.value.drain(byte_offset..byte_offset + char_len);
            return;
        }

        let char_len = c.len_utf8();
        self.cursor = self
            .value
            .grapheme_indices(true)
            .take_while(|(i, _)| *i < byte_offset + char_len)
            .count();
    }

    fn delete_char_back(&mut self) {
        if self.cursor > 0 {
            let byte_start = self.grapheme_byte_offset(self.cursor - 1);
            let byte_end = self.grapheme_byte_offset(self.cursor);
            self.value.drain(byte_start..byte_end);
            self.cursor -= 1;
            let gc = self.grapheme_count();
            if self.cursor > gc {
                self.cursor = gc;
            }
        }
    }

    fn delete_char_forward(&mut self) {
        let count = self.grapheme_count();
        if self.cursor < count {
            let byte_start = self.grapheme_byte_offset(self.cursor);
            let byte_end = self.grapheme_byte_offset(self.cursor + 1);
            self.value.drain(byte_start..byte_end);
            let gc = self.grapheme_count();
            if self.cursor > gc {
                self.cursor = gc;
            }
        }
    }

    fn delete_word_back(&mut self) {
        if self.cursor == 0 {
            return;
        }

        let old_cursor = self.cursor;
        // Use standard movement logic to find start of deletion
        self.move_cursor_word_left(false);
        let new_cursor = self.cursor;

        if new_cursor < old_cursor {
            let byte_start = self.grapheme_byte_offset(new_cursor);
            let byte_end = self.grapheme_byte_offset(old_cursor);
            self.value.drain(byte_start..byte_end);
            self.cursor = new_cursor;
            let gc = self.grapheme_count();
            if self.cursor > gc {
                self.cursor = gc;
            }
        }
    }

    fn delete_word_forward(&mut self) {
        let old_cursor = self.cursor;
        // Use standard movement logic to find end of deletion
        self.move_cursor_word_right(false);
        let new_cursor = self.cursor;
        // Reset cursor to start (deletion happens forward from here)
        self.cursor = old_cursor;

        if new_cursor > old_cursor {
            let byte_start = self.grapheme_byte_offset(old_cursor);
            let byte_end = self.grapheme_byte_offset(new_cursor);
            self.value.drain(byte_start..byte_end);
            let gc = self.grapheme_count();
            if self.cursor > gc {
                self.cursor = gc;
            }
        }
    }

    // --- Selection ---

    /// Select all text.
    pub fn select_all(&mut self) {
        self.selection_anchor = Some(0);
        self.cursor = self.grapheme_count();
    }

    /// Delete selected text. No-op if no selection.
    fn delete_selection(&mut self) {
        if let Some(anchor) = self.selection_anchor.take() {
            let (start, end) = self.selection_range(anchor);
            let byte_start = self.grapheme_byte_offset(start);
            let byte_end = self.grapheme_byte_offset(end);
            self.value.drain(byte_start..byte_end);
            self.cursor = start;
            let gc = self.grapheme_count();
            if self.cursor > gc {
                self.cursor = gc;
            }
        }
    }

    fn ensure_selection_anchor(&mut self) {
        if self.selection_anchor.is_none() {
            self.selection_anchor = Some(self.cursor);
        }
    }

    fn selection_range(&self, anchor: usize) -> (usize, usize) {
        if anchor <= self.cursor {
            (anchor, self.cursor)
        } else {
            (self.cursor, anchor)
        }
    }

    fn is_in_selection(&self, grapheme_idx: usize) -> bool {
        if let Some(anchor) = self.selection_anchor {
            let (start, end) = self.selection_range(anchor);
            grapheme_idx >= start && grapheme_idx < end
        } else {
            false
        }
    }

    // --- Cursor movement ---

    fn move_cursor_left(&mut self) {
        if let Some(anchor) = self.selection_anchor.take() {
            self.cursor = self.cursor.min(anchor);
        } else if self.cursor > 0 {
            self.cursor -= 1;
        }
    }

    fn move_cursor_right(&mut self) {
        if let Some(anchor) = self.selection_anchor.take() {
            self.cursor = self.cursor.max(anchor);
        } else if self.cursor < self.grapheme_count() {
            self.cursor += 1;
        }
    }

    fn move_cursor_left_select(&mut self) {
        self.ensure_selection_anchor();
        if self.cursor > 0 {
            self.cursor -= 1;
        }
    }

    fn move_cursor_right_select(&mut self) {
        self.ensure_selection_anchor();
        if self.cursor < self.grapheme_count() {
            self.cursor += 1;
        }
    }

    fn get_grapheme_class(g: &str) -> u8 {
        if g.chars().all(char::is_whitespace) {
            0
        } else if g.chars().any(char::is_alphanumeric) {
            1
        } else {
            2
        }
    }

    fn move_cursor_word_left(&mut self, select: bool) {
        if select {
            self.ensure_selection_anchor();
        } else {
            self.selection_anchor = None;
        }

        if self.cursor == 0 {
            return;
        }

        let byte_offset = self.grapheme_byte_offset(self.cursor);
        let before_cursor = &self.value[..byte_offset];
        let mut pos = self.cursor;

        let mut iter = before_cursor.graphemes(true).rev();

        while let Some(g) = iter.next() {
            if Self::get_grapheme_class(g) == 0 {
                pos = pos.saturating_sub(1);
            } else {
                pos = pos.saturating_sub(1);
                let target = Self::get_grapheme_class(g);
                for g_next in iter {
                    if Self::get_grapheme_class(g_next) == target {
                        pos = pos.saturating_sub(1);
                    } else {
                        break;
                    }
                }
                break;
            }
        }

        self.cursor = pos;
    }

    fn move_cursor_word_right(&mut self, select: bool) {
        if select {
            self.ensure_selection_anchor();
        } else {
            self.selection_anchor = None;
        }

        let mut iter = self.value.graphemes(true).peekable();
        for _ in 0..self.cursor {
            iter.next();
        }

        let mut pos = self.cursor;

        if let Some(&g) = iter.peek() {
            let start_class = Self::get_grapheme_class(g);
            if start_class != 0 {
                while let Some(&next_g) = iter.peek() {
                    if Self::get_grapheme_class(next_g) == start_class {
                        iter.next();
                        pos += 1;
                    } else {
                        break;
                    }
                }
            }
        }

        while let Some(&g) = iter.peek() {
            if Self::get_grapheme_class(g) == 0 {
                iter.next();
                pos += 1;
            } else {
                break;
            }
        }

        self.cursor = pos;
    }

    // --- Internal helpers ---

    fn grapheme_count(&self) -> usize {
        self.value.graphemes(true).count()
    }

    fn grapheme_byte_offset(&self, grapheme_idx: usize) -> usize {
        self.value
            .grapheme_indices(true)
            .nth(grapheme_idx)
            .map(|(i, _)| i)
            .unwrap_or(self.value.len())
    }

    fn grapheme_width(&self, g: &str) -> usize {
        if let Some(mask) = self.mask_char {
            let mut buf = [0u8; 4];
            let mask_str = mask.encode_utf8(&mut buf);
            grapheme_width(mask_str)
        } else {
            grapheme_width(g)
        }
    }

    fn prev_grapheme_width(&self) -> usize {
        if self.cursor == 0 {
            return 0;
        }
        self.value
            .graphemes(true)
            .nth(self.cursor - 1)
            .map(|g| self.grapheme_width(g))
            .unwrap_or(0)
    }

    fn cursor_visual_pos(&self) -> usize {
        let mut pos = 0;
        if !self.value.is_empty() {
            pos += self
                .value
                .graphemes(true)
                .take(self.cursor)
                .map(|g| self.grapheme_width(g))
                .sum::<usize>();
        }
        if let Some(ime) = &self.ime_composition {
            pos += ime
                .graphemes(true)
                .map(|g| self.grapheme_width(g))
                .sum::<usize>();
        }
        pos
    }

    fn effective_scroll(&self, viewport_width: usize) -> usize {
        let cursor_visual = self.cursor_visual_pos();
        let mut scroll = self.scroll_cells.get();
        if cursor_visual < scroll {
            scroll = cursor_visual;
        }
        if cursor_visual >= scroll + viewport_width {
            let candidate_scroll = cursor_visual - viewport_width + 1;
            // Ensure the character BEFORE the cursor is also fully visible
            // (prevent "hole" artifact for wide characters where start < scroll)
            let prev_width = self.prev_grapheme_width();
            let max_scroll_for_prev = cursor_visual.saturating_sub(prev_width);

            // Only enforce wide-char visibility if the viewport is wide enough to show
            // both the character and the cursor. Prioritize cursor visibility otherwise.
            if viewport_width > prev_width {
                scroll = candidate_scroll.min(max_scroll_for_prev);
            } else {
                scroll = candidate_scroll;
            }
        }

        // Sanitize: ensure scroll aligns with grapheme boundaries
        scroll = self.snap_scroll_to_grapheme_boundary(scroll, viewport_width);

        self.scroll_cells.set(scroll);
        scroll
    }

    fn snap_scroll_to_grapheme_boundary(&self, scroll: usize, viewport_width: usize) -> usize {
        let mut pos = 0;
        let cursor_visual = self.cursor_visual_pos();

        for g in self.value.graphemes(true) {
            let w = self.grapheme_width(g);
            let next_pos = pos + w;

            // If scroll is in the middle of this grapheme: [pos < scroll < next_pos]
            if pos < scroll && scroll < next_pos {
                // Try snapping to the start of the character to keep it visible.
                // Only allowed if the cursor remains visible on the right.
                // Cursor visibility condition: cursor_visual < new_scroll + viewport_width
                if cursor_visual <= pos + viewport_width {
                    return pos;
                } else {
                    // Cannot snap left without hiding cursor. Snap right (hide the character).
                    return next_pos;
                }
            }

            if next_pos > scroll {
                // Passed the scroll point, no split found.
                break;
            }
            pos = next_pos;
        }
        scroll
    }
}

impl Widget for TextInput {
    fn render(&self, area: Rect, frame: &mut Frame) {
        #[cfg(feature = "tracing")]
        let _span = tracing::debug_span!(
            "widget_render",
            widget = "TextInput",
            x = area.x,
            y = area.y,
            w = area.width,
            h = area.height
        )
        .entered();

        if area.width < 1 || area.height < 1 {
            return;
        }

        let deg = frame.buffer.degradation;

        // TextInput is essential — always render content, but skip styling
        // at NoStyling+. At Skeleton, still render the raw text value.
        // We explicitly DO NOT check deg.render_content() here because this widget is essential.
        let base_style = if deg.apply_styling() {
            self.style
        } else {
            Style::default()
        };
        clear_text_area(frame, area, base_style);

        // Use arena allocation when available to avoid per-frame heap churn.
        let arena = frame.arena;
        let graphemes_heap;
        let graphemes: &[&str] = if let Some(a) = arena {
            a.alloc_iter(self.value.graphemes(true))
        } else {
            graphemes_heap = self.value.graphemes(true).collect::<Vec<_>>();
            &graphemes_heap
        };
        let show_placeholder =
            self.value.is_empty() && self.ime_composition.is_none() && !self.placeholder.is_empty();

        let viewport_width = area.width as usize;
        let cursor_visual_pos = self.cursor_visual_pos();
        let effective_scroll = self.effective_scroll(viewport_width);

        // Render content
        let mut visual_x: usize = 0;
        let y = area.y;

        if show_placeholder {
            let placeholder_style = if deg.apply_styling() {
                self.placeholder_style
            } else {
                Style::default()
            };
            for g in self.placeholder.graphemes(true) {
                let w = self.grapheme_width(g);
                if w == 0 {
                    continue;
                }

                // Fully scrolled out (left)
                if visual_x + w <= effective_scroll {
                    visual_x += w;
                    continue;
                }

                // Partially scrolled out (left) - skip drawing
                if visual_x < effective_scroll {
                    visual_x += w;
                    continue;
                }

                let rel_x = visual_x - effective_scroll;

                // Fully clipped (right)
                if rel_x >= viewport_width {
                    break;
                }

                // Partially clipped (right) - skip drawing
                if rel_x + w > viewport_width {
                    break;
                }

                let mut cell = if g.chars().count() > 1 || w > 1 {
                    let id = frame.intern_with_width(g, w as u8);
                    Cell::new(CellContent::from_grapheme(id))
                } else if let Some(c) = g.chars().next() {
                    Cell::from_char(c)
                } else {
                    visual_x += w;
                    continue;
                };
                crate::apply_style(&mut cell, placeholder_style);

                frame
                    .buffer
                    .set(area.x.saturating_add(rel_x as u16), y, cell);
                visual_x += w;
            }
        } else {
            let mut display_spans: Vec<(&str, Style, bool)> = Vec::new();
            for (gi, g) in graphemes.iter().enumerate() {
                if gi == self.cursor
                    && let Some(ime) = &self.ime_composition
                {
                    let ime_style = if deg.apply_styling() {
                        self.style
                    } else {
                        Style::default()
                    };
                    for ig in ime.graphemes(true) {
                        display_spans.push((ig, ime_style, true));
                    }
                }

                let cell_style = if !deg.apply_styling() {
                    Style::default()
                } else if self.is_in_selection(gi) {
                    self.selection_style
                } else {
                    self.style
                };
                display_spans.push((g, cell_style, false));
            }
            if self.cursor == graphemes.len()
                && let Some(ime) = &self.ime_composition
            {
                let ime_style = if deg.apply_styling() {
                    self.style
                } else {
                    Style::default()
                };
                for ig in ime.graphemes(true) {
                    display_spans.push((ig, ime_style, true));
                }
            }

            for (g, cell_style, is_ime) in display_spans {
                let w = self.grapheme_width(g);
                if w == 0 {
                    continue;
                }

                // Fully scrolled out (left)
                if visual_x + w <= effective_scroll {
                    visual_x += w;
                    continue;
                }

                // Partially scrolled out (left) - skip drawing
                if visual_x < effective_scroll {
                    visual_x += w;
                    continue;
                }

                let rel_x = visual_x - effective_scroll;

                // Fully clipped (right)
                if rel_x >= viewport_width {
                    break;
                }

                // Partially clipped (right) - skip drawing
                if rel_x + w > viewport_width {
                    break;
                }

                let mut cell = if let Some(mask) = self.mask_char {
                    Cell::from_char(mask)
                } else if g.chars().count() > 1 || w > 1 {
                    let id = frame.intern_with_width(g, w as u8);
                    Cell::new(CellContent::from_grapheme(id))
                } else {
                    Cell::from_char(g.chars().next().unwrap_or(' '))
                };
                crate::apply_style(&mut cell, cell_style);

                if is_ime && deg.apply_styling() {
                    use ftui_render::cell::StyleFlags;
                    let current_flags = cell.attrs.flags();
                    cell.attrs = cell.attrs.with_flags(current_flags | StyleFlags::UNDERLINE);
                }

                frame
                    .buffer
                    .set(area.x.saturating_add(rel_x as u16), y, cell);
                visual_x += w;
            }
        }

        if self.focused {
            // Set cursor style at cursor position
            let cursor_rel_x = cursor_visual_pos.saturating_sub(effective_scroll);
            if cursor_rel_x < viewport_width {
                let cursor_screen_x = area.x.saturating_add(cursor_rel_x as u16);
                if let Some(cell) = frame.buffer.get_mut(cursor_screen_x, y) {
                    if !deg.apply_styling() {
                        // At NoStyling, just use reverse video for cursor
                        use ftui_render::cell::StyleFlags;
                        let current_flags = cell.attrs.flags();
                        let new_flags = current_flags ^ StyleFlags::REVERSE;
                        cell.attrs = cell.attrs.with_flags(new_flags);
                    } else if self.cursor_style.is_empty() {
                        // Default: toggle reverse video for cursor visibility
                        use ftui_render::cell::StyleFlags;
                        let current_flags = cell.attrs.flags();
                        let new_flags = current_flags ^ StyleFlags::REVERSE;
                        cell.attrs = cell.attrs.with_flags(new_flags);
                    } else {
                        crate::apply_style(cell, self.cursor_style);
                    }
                }
            }

            frame.set_cursor(Some(self.cursor_position(area)));
            frame.set_cursor_visible(true);
        }
    }

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

// ============================================================================
// Accessibility
// ============================================================================

impl ftui_a11y::Accessible for TextInput {
    fn accessibility_nodes(&self, area: Rect) -> Vec<ftui_a11y::node::A11yNodeInfo> {
        use ftui_a11y::node::{A11yNodeInfo, A11yRole, A11yState};

        let id = crate::a11y_node_id(area);
        let name = if self.value.is_empty() {
            self.placeholder.clone()
        } else if self.mask_char.is_some() {
            // Don't expose masked values to screen readers.
            String::from("password field")
        } else {
            self.value.clone()
        };

        let state = A11yState {
            focused: self.focused,
            disabled: self.max_length == Some(0),
            ..A11yState::default()
        };

        let mut node = A11yNodeInfo::new(id, A11yRole::TextInput, area).with_state(state);
        if !name.is_empty() {
            node = node.with_name(name);
        }
        if self.mask_char.is_some() {
            node = node.with_description("password input");
        }

        vec![node]
    }
}

// ============================================================================
// Undo Support Implementation
// ============================================================================

/// Snapshot of TextInput state for undo.
#[derive(Debug, Clone)]
pub struct TextInputSnapshot {
    value: String,
    cursor: usize,
    selection_anchor: Option<usize>,
}

impl UndoSupport for TextInput {
    fn undo_widget_id(&self) -> UndoWidgetId {
        self.undo_id
    }

    fn create_snapshot(&self) -> Box<dyn std::any::Any + Send> {
        Box::new(TextInputSnapshot {
            value: self.value.clone(),
            cursor: self.cursor,
            selection_anchor: self.selection_anchor,
        })
    }

    fn restore_snapshot(&mut self, snapshot: &dyn std::any::Any) -> bool {
        if let Some(snap) = snapshot.downcast_ref::<TextInputSnapshot>() {
            self.value = snap.value.clone();
            self.cursor = snap.cursor;
            self.selection_anchor = snap.selection_anchor;
            self.scroll_cells.set(0); // Reset scroll on restore
            true
        } else {
            false
        }
    }
}

impl TextInputUndoExt for TextInput {
    fn text_value(&self) -> &str {
        &self.value
    }

    fn set_text_value(&mut self, value: &str) {
        self.value = value.to_string();
        let max = self.grapheme_count();
        self.cursor = self.cursor.min(max);
        self.selection_anchor = None;
    }

    fn cursor_position(&self) -> usize {
        self.cursor
    }

    fn set_cursor_position(&mut self, pos: usize) {
        let max = self.grapheme_count();
        self.cursor = pos.min(max);
    }

    fn insert_text_at(&mut self, position: usize, text: &str) {
        let byte_offset = self.grapheme_byte_offset(position);
        self.value.insert_str(byte_offset, text);
        let inserted_graphemes = text.graphemes(true).count();
        if self.cursor >= position {
            self.cursor += inserted_graphemes;
        }
    }

    fn delete_text_range(&mut self, start: usize, end: usize) {
        if start >= end {
            return;
        }
        let byte_start = self.grapheme_byte_offset(start);
        let byte_end = self.grapheme_byte_offset(end);
        self.value.drain(byte_start..byte_end);
        let deleted_count = end - start;
        if self.cursor > end {
            self.cursor -= deleted_count;
        } else if self.cursor > start {
            self.cursor = start;
        }
        let gc = self.grapheme_count();
        if self.cursor > gc {
            self.cursor = gc;
        }
    }
}

impl TextInput {
    /// Create an undo command for the given text edit operation.
    ///
    /// This creates a command that can be added to a [`HistoryManager`] for undo/redo support.
    /// The command includes callbacks that will be called when the operation is undone or redone.
    ///
    /// # Example
    ///
    /// ```ignore
    /// let mut input = TextInput::new();
    /// let old_value = input.value().to_string();
    ///
    /// // Perform the edit
    /// input.set_value("new text");
    ///
    /// // Create undo command
    /// if let Some(cmd) = input.create_text_edit_command(TextEditOperation::SetValue {
    ///     old_value,
    ///     new_value: "new text".to_string(),
    /// }) {
    ///     history.push(cmd);
    /// }
    /// ```
    ///
    /// [`HistoryManager`]: ftui_runtime::undo::HistoryManager
    #[must_use]
    pub fn create_text_edit_command(
        &self,
        operation: TextEditOperation,
    ) -> Option<crate::undo_support::WidgetTextEditCmd> {
        Some(crate::undo_support::WidgetTextEditCmd::new(
            self.undo_id,
            operation,
        ))
    }

    /// Get the undo widget ID.
    ///
    /// This can be used to associate undo commands with this widget instance.
    #[must_use]
    pub fn undo_id(&self) -> UndoWidgetId {
        self.undo_id
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[cfg(feature = "tracing")]
    use std::sync::{Arc, Mutex};

    #[cfg(feature = "tracing")]
    use tracing::Subscriber;
    #[cfg(feature = "tracing")]
    use tracing_subscriber::Layer;
    #[cfg(feature = "tracing")]
    use tracing_subscriber::layer::{Context, SubscriberExt};

    #[allow(dead_code)]
    fn cell_at(frame: &Frame, x: u16, y: u16) -> Cell {
        frame
            .buffer
            .get(x, y)
            .copied()
            .unwrap_or_else(|| panic!("test cell should exist at ({x},{y})"))
    }

    fn raw_row_text(frame: &Frame, y: u16, width: u16) -> String {
        (0..width)
            .map(|x| {
                frame
                    .buffer
                    .get(x, y)
                    .and_then(|cell| cell.content.as_char())
                    .unwrap_or(' ')
            })
            .collect()
    }

    #[cfg(feature = "tracing")]
    #[derive(Debug, Default)]
    struct InputTraceState {
        span_count: usize,
        has_cursor_position_field: bool,
        cursor_positions: Vec<usize>,
        operations: Vec<String>,
    }

    #[cfg(feature = "tracing")]
    struct InputTraceCapture {
        state: Arc<Mutex<InputTraceState>>,
    }

    #[cfg(feature = "tracing")]
    impl<S> Layer<S> for InputTraceCapture
    where
        S: Subscriber + for<'lookup> tracing_subscriber::registry::LookupSpan<'lookup>,
    {
        fn on_new_span(
            &self,
            attrs: &tracing::span::Attributes<'_>,
            _id: &tracing::Id,
            _ctx: Context<'_, S>,
        ) {
            if attrs.metadata().name() != "input.edit" {
                return;
            }

            #[derive(Default)]
            struct InputEditVisitor {
                cursor_position: Option<usize>,
                operation: Option<String>,
            }

            impl tracing::field::Visit for InputEditVisitor {
                fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
                    if field.name() == "cursor_position" {
                        self.cursor_position = usize::try_from(value).ok();
                    }
                }

                fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
                    if field.name() == "cursor_position" {
                        self.cursor_position = usize::try_from(value).ok();
                    }
                }

                fn record_debug(
                    &mut self,
                    field: &tracing::field::Field,
                    value: &dyn std::fmt::Debug,
                ) {
                    if field.name() == "operation" {
                        self.operation = Some(format!("{value:?}").trim_matches('"').to_owned());
                    }
                }

                fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
                    if field.name() == "operation" {
                        self.operation = Some(value.to_owned());
                    }
                }
            }

            let fields = attrs.metadata().fields();
            let mut visitor = InputEditVisitor::default();
            attrs.record(&mut visitor);

            let mut state = self.state.lock().expect("trace state lock");
            state.span_count += 1;
            state.has_cursor_position_field |= fields.field("cursor_position").is_some();
            if let Some(cursor) = visitor.cursor_position {
                state.cursor_positions.push(cursor);
            }
            if let Some(operation) = visitor.operation {
                state.operations.push(operation);
            }
        }
    }

    #[allow(dead_code)]
    #[test]
    fn test_empty_input() {
        let input = TextInput::new();
        assert!(input.value().is_empty());
        assert_eq!(input.cursor(), 0);
        assert!(input.selected_text().is_none());
    }

    #[test]
    fn test_with_value() {
        let mut input = TextInput::new().with_value("hello");
        input.set_focused(true);
        assert_eq!(input.value(), "hello");
        assert_eq!(input.cursor(), 5);
    }

    #[test]
    fn test_set_value() {
        let mut input = TextInput::new().with_value("hello world");
        input.cursor = 11;
        input.set_value("hi");
        assert_eq!(input.value(), "hi");
        assert_eq!(input.cursor(), 2);
    }

    #[test]
    fn test_clear() {
        let mut input = TextInput::new().with_value("hello");
        input.set_focused(true);
        input.clear();
        assert!(input.value().is_empty());
        assert_eq!(input.cursor(), 0);
    }

    #[test]
    fn test_insert_char() {
        let mut input = TextInput::new();
        input.insert_char('a');
        input.insert_char('b');
        input.insert_char('c');
        assert_eq!(input.value(), "abc");
        assert_eq!(input.cursor(), 3);
    }

    #[test]
    fn test_insert_char_mid() {
        let mut input = TextInput::new().with_value("ac");
        input.cursor = 1;
        input.insert_char('b');
        assert_eq!(input.value(), "abc");
        assert_eq!(input.cursor(), 2);
    }

    #[test]
    fn test_max_length() {
        let mut input = TextInput::new().with_max_length(3);
        for c in "abcdef".chars() {
            input.insert_char(c);
        }
        assert_eq!(input.value(), "abc");
        assert_eq!(input.cursor(), 3);
    }

    #[test]
    fn test_delete_char_back() {
        let mut input = TextInput::new().with_value("hello");
        input.delete_char_back();
        assert_eq!(input.value(), "hell");
        assert_eq!(input.cursor(), 4);
    }

    #[test]
    fn test_delete_char_back_at_start() {
        let mut input = TextInput::new().with_value("hello");
        input.cursor = 0;
        input.delete_char_back();
        assert_eq!(input.value(), "hello");
    }

    #[test]
    fn test_delete_char_forward() {
        let mut input = TextInput::new().with_value("hello");
        input.cursor = 0;
        input.delete_char_forward();
        assert_eq!(input.value(), "ello");
        assert_eq!(input.cursor(), 0);
    }

    #[test]
    fn test_delete_char_forward_at_end() {
        let mut input = TextInput::new().with_value("hello");
        input.delete_char_forward();
        assert_eq!(input.value(), "hello");
    }

    #[test]
    fn test_cursor_left_right() {
        let mut input = TextInput::new().with_value("hello");
        assert_eq!(input.cursor(), 5);
        input.move_cursor_left();
        assert_eq!(input.cursor(), 4);
        input.move_cursor_left();
        assert_eq!(input.cursor(), 3);
        input.move_cursor_right();
        assert_eq!(input.cursor(), 4);
    }

    #[test]
    fn test_cursor_bounds() {
        let mut input = TextInput::new().with_value("hi");
        input.cursor = 0;
        input.move_cursor_left();
        assert_eq!(input.cursor(), 0);
        input.cursor = 2;
        input.move_cursor_right();
        assert_eq!(input.cursor(), 2);
    }

    #[test]
    fn test_word_movement_left() {
        let mut input = TextInput::new().with_value("hello world test");
        // "hello world test"
        //                 ^ (16)
        input.move_cursor_word_left(false);
        assert_eq!(input.cursor(), 12); // "hello world |test"

        input.move_cursor_word_left(false);
        assert_eq!(input.cursor(), 6); // "hello |world test"

        input.move_cursor_word_left(false);
        assert_eq!(input.cursor(), 0); // "|hello world test"
    }

    #[test]
    fn test_word_movement_right() {
        let mut input = TextInput::new().with_value("hello world test");
        input.cursor = 0;
        // "|hello world test"

        input.move_cursor_word_right(false);
        assert_eq!(input.cursor(), 6); // "hello |world test"

        input.move_cursor_word_right(false);
        assert_eq!(input.cursor(), 12); // "hello world |test"

        input.move_cursor_word_right(false);
        assert_eq!(input.cursor(), 16); // "hello world test|"
    }

    #[test]
    fn test_word_movement_skips_punctuation() {
        let mut input = TextInput::new().with_value("hello, world");
        input.cursor = 0;
        // "|hello, world"

        input.move_cursor_word_right(false);
        assert_eq!(input.cursor(), 5); // "hello|, world"

        input.move_cursor_word_right(false);
        assert_eq!(input.cursor(), 7); // "hello, |world"

        input.move_cursor_word_right(false);
        assert_eq!(input.cursor(), 12); // "hello, world|"

        input.move_cursor_word_left(false);
        assert_eq!(input.cursor(), 7); // "hello, |world"
    }

    #[test]
    fn test_delete_word_back() {
        let mut input = TextInput::new().with_value("hello world");
        // "hello world|"
        input.delete_word_back();
        assert_eq!(input.value(), "hello "); // Deleted "world"

        // "hello |" — word-left skips space and deletes the preceding word
        input.delete_word_back();
        assert_eq!(input.value(), ""); // Deleted "hello "
    }

    #[test]
    fn test_delete_word_forward() {
        let mut input = TextInput::new().with_value("hello world");
        input.cursor = 0;
        // "|hello world" — word-right skips "hello" then space
        input.delete_word_forward();
        assert_eq!(input.value(), "world"); // Deleted "hello "

        input.delete_word_forward();
        assert_eq!(input.value(), ""); // Deleted "world"
    }

    #[test]
    fn test_select_all() {
        let mut input = TextInput::new().with_value("hello");
        input.select_all();
        assert_eq!(input.selected_text(), Some("hello"));
    }

    #[test]
    fn test_delete_selection() {
        let mut input = TextInput::new().with_value("hello world");
        input.selection_anchor = Some(0);
        input.cursor = 5;
        input.delete_selection();
        assert_eq!(input.value(), " world");
        assert_eq!(input.cursor(), 0);
    }

    #[test]
    fn test_insert_replaces_selection() {
        let mut input = TextInput::new().with_value("hello");
        input.select_all();
        input.delete_selection();
        input.insert_char('x');
        assert_eq!(input.value(), "x");
    }

    #[test]
    fn test_unicode_grapheme_handling() {
        let mut input = TextInput::new();
        input.set_value("café");
        assert_eq!(input.grapheme_count(), 4);
        input.cursor = 4;
        input.delete_char_back();
        assert_eq!(input.value(), "caf");
    }

    #[test]
    fn test_multi_codepoint_grapheme_cursor_movement() {
        let mut input = TextInput::new().with_value("a👩‍💻b");
        assert_eq!(input.grapheme_count(), 3);
        assert_eq!(input.cursor(), 3);

        input.move_cursor_left();
        assert_eq!(input.cursor(), 2);
        input.move_cursor_left();
        assert_eq!(input.cursor(), 1);
        input.move_cursor_left();
        assert_eq!(input.cursor(), 0);

        input.move_cursor_right();
        assert_eq!(input.cursor(), 1);
        input.move_cursor_right();
        assert_eq!(input.cursor(), 2);
        input.move_cursor_right();
        assert_eq!(input.cursor(), 3);
    }

    #[test]
    fn test_delete_back_multi_codepoint_grapheme() {
        let mut input = TextInput::new().with_value("a👩‍💻b");
        input.cursor = 2; // after the emoji grapheme
        input.delete_char_back();
        assert_eq!(input.value(), "ab");
        assert_eq!(input.cursor(), 1);
        assert_eq!(input.grapheme_count(), 2);
    }

    #[test]
    fn test_ime_composition_start_update_commit() {
        let mut input = TextInput::new().with_value("ab");
        input.cursor = 1;

        input.ime_start_composition();
        assert_eq!(input.ime_composition(), Some(""));

        input.ime_update_composition("漢");
        assert_eq!(input.ime_composition(), Some("漢"));

        assert!(input.ime_commit_composition());
        assert_eq!(input.ime_composition(), None);
        assert_eq!(input.value(), "a漢b");
        assert_eq!(input.cursor(), 2);
    }

    #[test]
    fn test_ime_composition_cancel_keeps_value() {
        let mut input = TextInput::new().with_value("hello");
        input.ime_start_composition();
        input.ime_update_composition("👩‍💻");
        assert_eq!(input.ime_composition(), Some("👩‍💻"));
        assert!(input.ime_cancel_composition());
        assert_eq!(input.ime_composition(), None);
        assert_eq!(input.value(), "hello");
        assert_eq!(input.cursor(), 5);
    }

    #[test]
    fn test_ime_commit_without_session_is_noop() {
        let mut input = TextInput::new().with_value("abc");
        assert!(!input.ime_commit_composition());
        assert_eq!(input.value(), "abc");
        assert_eq!(input.cursor(), 3);
    }

    #[test]
    fn test_handle_event_ime_update_and_commit() {
        let mut input = TextInput::new().with_value("ab");
        input.cursor = 1;

        assert!(input.handle_event(&Event::Ime(ImeEvent::start())));
        assert!(input.handle_event(&Event::Ime(ImeEvent::update("漢"))));
        assert_eq!(input.ime_composition(), Some("漢"));
        assert!(input.handle_event(&Event::Ime(ImeEvent::commit("漢"))));
        assert_eq!(input.ime_composition(), None);
        assert_eq!(input.value(), "a漢b");
        assert_eq!(input.cursor(), 2);
    }

    #[test]
    fn test_handle_event_ime_cancel() {
        let mut input = TextInput::new().with_value("hello");
        input.cursor = 5;
        assert!(input.handle_event(&Event::Ime(ImeEvent::start())));
        assert!(input.handle_event(&Event::Ime(ImeEvent::update("👩‍💻"))));
        assert!(input.handle_event(&Event::Ime(ImeEvent::cancel())));
        assert_eq!(input.ime_composition(), None);
        assert_eq!(input.value(), "hello");
        assert_eq!(input.cursor(), 5);
    }

    #[test]
    fn test_flag_emoji_grapheme_delete_and_cursor() {
        let mut input = TextInput::new().with_value("a🇺🇸b");
        assert_eq!(input.grapheme_count(), 3);
        input.cursor = 2;
        input.delete_char_back();
        assert_eq!(input.value(), "ab");
        assert_eq!(input.cursor(), 1);
    }

    #[test]
    fn test_combining_grapheme_delete_and_cursor() {
        let mut input = TextInput::new().with_value("a\u{0301}b");
        assert_eq!(input.grapheme_count(), 2);
        input.cursor = 1;
        input.delete_char_back();
        assert_eq!(input.value(), "b");
        assert_eq!(input.cursor(), 0);
    }

    #[test]
    fn test_bidi_logical_cursor_movement_over_graphemes() {
        let mut input = TextInput::new().with_value("AאבB");
        assert_eq!(input.grapheme_count(), 4);

        input.move_cursor_left();
        assert_eq!(input.cursor(), 3);
        input.move_cursor_left();
        assert_eq!(input.cursor(), 2);
        input.move_cursor_left();
        assert_eq!(input.cursor(), 1);
        input.move_cursor_left();
        assert_eq!(input.cursor(), 0);

        input.move_cursor_right();
        assert_eq!(input.cursor(), 1);
        input.move_cursor_right();
        assert_eq!(input.cursor(), 2);
        input.move_cursor_right();
        assert_eq!(input.cursor(), 3);
        input.move_cursor_right();
        assert_eq!(input.cursor(), 4);
    }

    #[test]
    fn test_handle_event_char() {
        let mut input = TextInput::new();
        let event = Event::Key(KeyEvent::new(KeyCode::Char('a')));
        assert!(input.handle_event(&event));
        assert_eq!(input.value(), "a");
    }

    #[test]
    fn test_handle_event_backspace() {
        let mut input = TextInput::new().with_value("ab");
        let event = Event::Key(KeyEvent::new(KeyCode::Backspace));
        assert!(input.handle_event(&event));
        assert_eq!(input.value(), "a");
    }

    #[test]
    fn test_handle_event_ctrl_a() {
        let mut input = TextInput::new().with_value("hello");
        let event = Event::Key(KeyEvent::new(KeyCode::Char('a')).with_modifiers(Modifiers::CTRL));
        assert!(input.handle_event(&event));
        assert_eq!(input.selected_text(), Some("hello"));
    }

    #[test]
    fn test_handle_event_ctrl_backspace() {
        let mut input = TextInput::new().with_value("hello world");
        let event = Event::Key(KeyEvent::new(KeyCode::Backspace).with_modifiers(Modifiers::CTRL));
        assert!(input.handle_event(&event));
        assert_eq!(input.value(), "hello ");
    }

    #[test]
    fn test_handle_event_home_end() {
        let mut input = TextInput::new().with_value("hello");
        input.cursor = 3;
        let home = Event::Key(KeyEvent::new(KeyCode::Home));
        assert!(input.handle_event(&home));
        assert_eq!(input.cursor(), 0);
        let end = Event::Key(KeyEvent::new(KeyCode::End));
        assert!(input.handle_event(&end));
        assert_eq!(input.cursor(), 5);
    }

    #[test]
    fn test_shift_left_creates_selection() {
        let mut input = TextInput::new().with_value("hello");
        let event = Event::Key(KeyEvent::new(KeyCode::Left).with_modifiers(Modifiers::SHIFT));
        assert!(input.handle_event(&event));
        assert_eq!(input.cursor(), 4);
        assert_eq!(input.selection_anchor, Some(5));
        assert_eq!(input.selected_text(), Some("o"));
    }

    #[test]
    fn test_cursor_position() {
        let input = TextInput::new().with_value("hello");
        let area = Rect::new(10, 5, 20, 1);
        let (x, y) = input.cursor_position(area);
        assert_eq!(x, 15);
        assert_eq!(y, 5);
    }

    #[test]
    fn test_cursor_position_empty() {
        let input = TextInput::new();
        let area = Rect::new(0, 0, 80, 1);
        let (x, y) = input.cursor_position(area);
        assert_eq!(x, 0);
        assert_eq!(y, 0);
    }

    #[test]
    fn test_password_mask() {
        let input = TextInput::new().with_mask('*').with_value("secret");
        assert_eq!(input.value(), "secret");
        assert_eq!(input.cursor_visual_pos(), 6);
    }

    #[test]
    fn test_render_basic() {
        use ftui_render::frame::Frame;
        use ftui_render::grapheme_pool::GraphemePool;

        let input = TextInput::new().with_value("hi");
        let area = Rect::new(0, 0, 10, 1);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 1, &mut pool);
        input.render(area, &mut frame);
        let cell_h = cell_at(&frame, 0, 0);
        assert_eq!(cell_h.content.as_char(), Some('h'));
        let cell_i = cell_at(&frame, 1, 0);
        assert_eq!(cell_i.content.as_char(), Some('i'));
    }

    #[test]
    fn test_render_sets_cursor_when_focused() {
        use ftui_render::frame::Frame;
        use ftui_render::grapheme_pool::GraphemePool;

        let input = TextInput::new().with_value("hi").with_focused(true);
        let area = Rect::new(0, 0, 10, 1);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 1, &mut pool);
        input.render(area, &mut frame);

        assert_eq!(frame.cursor_position, Some((2, 0)));
        assert!(frame.cursor_visible);
    }

    #[test]
    fn test_render_does_not_set_cursor_when_unfocused() {
        use ftui_render::frame::Frame;
        use ftui_render::grapheme_pool::GraphemePool;

        let input = TextInput::new().with_value("hi");
        let area = Rect::new(0, 0, 10, 1);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 1, &mut pool);
        input.render(area, &mut frame);

        assert!(frame.cursor_position.is_none());
    }

    #[test]
    fn test_render_grapheme_uses_pool() {
        use ftui_render::frame::Frame;
        use ftui_render::grapheme_pool::GraphemePool;

        let grapheme = "👩‍💻";
        let input = TextInput::new().with_value(grapheme);
        let area = Rect::new(0, 0, 6, 1);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(6, 1, &mut pool);
        input.render(area, &mut frame);

        let cell = cell_at(&frame, 0, 0);
        assert!(cell.content.is_grapheme());
        let width = grapheme_width(grapheme);
        if width > 1 {
            assert!(cell_at(&frame, 1, 0).is_continuation());
        }
    }

    #[test]
    fn test_render_shorter_value_clears_stale_suffix_and_owned_rows() {
        use ftui_render::frame::Frame;
        use ftui_render::grapheme_pool::GraphemePool;

        let area = Rect::new(0, 0, 8, 2);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(8, 2, &mut pool);

        TextInput::new()
            .with_value("abcdef")
            .render(area, &mut frame);
        TextInput::new().with_value("xy").render(area, &mut frame);

        assert_eq!(raw_row_text(&frame, 0, 8), "xy      ");
        assert_eq!(raw_row_text(&frame, 1, 8), "        ");
    }

    #[test]
    fn test_left_collapses_selection() {
        let mut input = TextInput::new().with_value("hello");
        input.selection_anchor = Some(1);
        input.cursor = 4;
        input.move_cursor_left();
        assert_eq!(input.cursor(), 1);
        assert!(input.selection_anchor.is_none());
    }

    #[test]
    fn test_right_collapses_selection() {
        let mut input = TextInput::new().with_value("hello");
        input.selection_anchor = Some(1);
        input.cursor = 4;
        input.move_cursor_right();
        assert_eq!(input.cursor(), 4);
        assert!(input.selection_anchor.is_none());
    }

    #[test]
    fn test_render_sets_frame_cursor() {
        use ftui_render::frame::Frame;
        use ftui_render::grapheme_pool::GraphemePool;

        let input = TextInput::new().with_value("hello").with_focused(true);
        let area = Rect::new(5, 3, 20, 1);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(30, 10, &mut pool);
        input.render(area, &mut frame);

        // Cursor should be positioned at the end of "hello" (5 chars)
        // area.x = 5, cursor_visual_pos = 5, effective_scroll = 0
        // So cursor_screen_x = 5 + 5 = 10
        assert_eq!(frame.cursor_position, Some((10, 3)));
    }

    #[test]
    fn test_render_cursor_mid_text() {
        use ftui_render::frame::Frame;
        use ftui_render::grapheme_pool::GraphemePool;

        let mut input = TextInput::new().with_value("hello").with_focused(true);
        input.cursor = 2; // After "he"
        let area = Rect::new(0, 0, 20, 1);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(20, 1, &mut pool);
        input.render(area, &mut frame);

        // Cursor after "he" = visual position 2
        assert_eq!(frame.cursor_position, Some((2, 0)));
    }

    // ========================================================================
    // Undo Support Tests
    // ========================================================================

    #[test]
    fn test_undo_widget_id_is_stable() {
        let input = TextInput::new();
        let id1 = input.undo_id();
        let id2 = input.undo_id();
        assert_eq!(id1, id2);
    }

    #[test]
    fn test_undo_widget_id_unique_per_instance() {
        let input1 = TextInput::new();
        let input2 = TextInput::new();
        assert_ne!(input1.undo_id(), input2.undo_id());
    }

    #[test]
    fn test_snapshot_and_restore() {
        let mut input = TextInput::new().with_value("hello");
        input.cursor = 3;
        input.selection_anchor = Some(1);

        let snapshot = input.create_snapshot();

        // Modify the input
        input.set_value("world");
        input.cursor = 5;
        input.selection_anchor = None;

        assert_eq!(input.value(), "world");
        assert_eq!(input.cursor(), 5);

        // Restore from snapshot
        assert!(input.restore_snapshot(snapshot.as_ref()));
        assert_eq!(input.value(), "hello");
        assert_eq!(input.cursor(), 3);
        assert_eq!(input.selection_anchor, Some(1));
    }

    #[test]
    fn test_text_input_undo_ext_insert() {
        let mut input = TextInput::new().with_value("hello");
        input.cursor = 2;

        input.insert_text_at(2, " world");
        // "hello" with " world" inserted at position 2 = "he" + " world" + "llo"
        assert_eq!(input.value(), "he worldllo");
        assert_eq!(input.cursor(), 8); // cursor moved by inserted text length (6 graphemes)
    }

    #[test]
    fn test_text_input_undo_ext_delete() {
        let mut input = TextInput::new().with_value("hello world");
        input.cursor = 8;

        input.delete_text_range(5, 11); // Delete " world"
        assert_eq!(input.value(), "hello");
        assert_eq!(input.cursor(), 5); // cursor clamped to end of remaining text
    }

    #[test]
    fn test_create_text_edit_command() {
        let input = TextInput::new().with_value("hello");
        let cmd = input.create_text_edit_command(TextEditOperation::Insert {
            position: 0,
            text: "hi".to_string(),
        });
        assert!(cmd.is_some());
        let cmd = cmd.expect("test command should exist");
        assert_eq!(cmd.widget_id(), input.undo_id());
        assert_eq!(cmd.description(), "Insert text");
    }

    #[test]
    fn test_paste_bulk_insert() {
        let mut input = TextInput::new().with_value("hello");
        input.cursor = 5;
        let event = Event::Paste(ftui_core::event::PasteEvent::bracketed(" world"));
        assert!(input.handle_event(&event));
        assert_eq!(input.value(), "hello world");
        assert_eq!(input.cursor(), 11);
    }

    #[test]
    fn test_paste_multi_grapheme_sequence() {
        let mut input = TextInput::new().with_value("hi");
        input.cursor = 2;
        let event = Event::Paste(ftui_core::event::PasteEvent::new("👩‍💻🔥", false));
        assert!(input.handle_event(&event));
        assert_eq!(input.value(), "hi👩‍💻🔥");
        assert_eq!(input.cursor(), 4);
    }

    #[test]
    fn test_paste_max_length() {
        let mut input = TextInput::new().with_value("abc").with_max_length(5);
        input.cursor = 3;
        // Paste "def" (3 chars). Should be truncated to "de" (2 chars) to fit max 5.
        let event = Event::Paste(ftui_core::event::PasteEvent::bracketed("def"));
        assert!(input.handle_event(&event));
        assert_eq!(input.value(), "abcde");
        assert_eq!(input.cursor(), 5);
    }

    #[test]
    fn test_paste_combining_merge() {
        let mut input = TextInput::new().with_value("e");
        input.cursor = 1;
        // Paste combining acute accent (U+0301). Should merge with 'e' -> 'é'.
        // Grapheme count stays 1. Cursor stays 1 (after the merged grapheme).
        let event = Event::Paste(ftui_core::event::PasteEvent::bracketed("\u{0301}"));
        assert!(input.handle_event(&event));
        assert_eq!(input.value(), "e\u{0301}");
        assert_eq!(input.grapheme_count(), 1);
        assert_eq!(input.cursor(), 1);
    }

    #[test]
    fn test_paste_combining_merge_mid_string() {
        let mut input = TextInput::new().with_value("ab");
        input.cursor = 1; // between a and b
        let event = Event::Paste(ftui_core::event::PasteEvent::bracketed("\u{0301}"));
        assert!(input.handle_event(&event));
        assert_eq!(input.value(), "a\u{0301}b");
        assert_eq!(input.grapheme_count(), 2);
        assert_eq!(input.cursor(), 1);
    }

    #[test]
    fn test_wide_char_scroll_visibility() {
        use ftui_render::frame::Frame;
        use ftui_render::grapheme_pool::GraphemePool;

        let wide_char = "\u{3000}"; // Ideographic space, Width 2
        let mut input = TextInput::new().with_value(wide_char).with_focused(true);
        input.cursor = 1; // After the char

        // Viewport width 2.
        // cursor_visual_pos = 2.
        // effective_scroll: 2 >= 0 + 2 -> scroll = 1.
        // Render: char at 0..2. 0 < 1 -> Skipped!
        // Expectation: We should see it.
        let area = Rect::new(0, 0, 2, 1);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(2, 1, &mut pool);
        input.render(area, &mut frame);

        let cell = cell_at(&frame, 0, 0);
        // If bug exists, this assertion will fail because cell is empty/default
        assert!(!cell.is_empty(), "Wide char should be visible");
    }

    #[test]
    fn test_wide_char_scroll_snapping() {
        // Verify that effective_scroll snaps to grapheme boundaries
        let _input = TextInput::new().with_value("a\u{3000}b"); // "a", "ID_SPACE", "b"
        // Widths: 1, 2, 1. Positions: 0, 1, 3. Total 4.

        // Force scroll to 2 (middle of wide char)
        // We can't directly set scroll_cells (private), but we can manipulate cursor/viewport
        // to force the logic.

        // Viewport width 2.
        // Move cursor to end (pos 4).
        // effective_scroll tries to show cursor.
        // cursor_visual = 4.
        // candidate = 4 - 2 + 1 = 3.
        // prev_width (b) = 1. max_scroll_for_prev = 4 - 1 = 3.
        // scroll = 3.
        // At scroll 3, we show "b" (pos 3).
        //
        // Let's try to force it to land on 2.
        // value: "\u{3000}a" (Wide, a).
        // Pos: 0, 2. Total 3.
        // Cursor at 2 (after wide char).
        // Viewport 1.
        // cursor_visual = 2.
        // candidate = 2 - 1 + 1 = 2.
        // prev_width = 2. max_scroll_for_prev = 2 - 2 = 0.
        // scroll = min(2, 0) = 0.
        // It snaps to 0!

        // Let's use the internal logic test if possible, or observe via render.

        let wide_char = "\u{3000}";
        let text = format!("a{wide_char}b");
        let mut input = TextInput::new().with_value(&text);

        // We simulate the logic by calling effective_scroll via render logic paths
        // or by unit testing the private method if we expose it?
        // No, we rely on behavior.

        // Case 1: Scroll lands on 2 (start of wide char). Valid.
        // Case 2: Scroll lands on 1 (start of 'a' is 0, wide is 1..3).
        // Scroll 1 means we skip 'a'. Wide char moves to x=0. Valid.

        // Wait, grapheme 1 starts at visual pos 1.
        // If scroll is 2. We skip 'a' (1) and half of wide char?
        // No. "a" width 1. "Wide" width 2.
        // Visual positions: "a"@[0], "Wide"@[1,2].
        // If scroll = 1. "a" is at -1. "Wide" is at 0,1. Visible.
        // If scroll = 2. "a" at -2. "Wide" at -1,0.
        //   Render loop: g="Wide", w=2. visual_x starts at 1.
        //   visual_x (1) < scroll (2). visual_x += 2 -> 3. Continue.
        //   Wide char is SKIPPED.
        //   We see nothing (or 'b').
        //   This is the "pop" artifact.

        // We want scroll to NOT be 2. It should be 1 or 3.

        // How to force scroll=2?
        // Cursor at 3 (after 'b'?).
        // 'b' is at 3. width 1.
        // If we want 'b' visible at x=0. Scroll must be 3.

        // We need a setup where candidate_scroll lands on 2.
        // Let cursor be at 4 (end of string).
        // Viewport 2.
        // candidate = 4 - 2 + 1 = 3.

        // Let cursor be at 3 (start of b).
        // candidate = 3 - 2 + 1 = 2.
        // If we force scroll=2.

        input.cursor = 3; // Before 'b', after wide char.
        let _area = Rect::new(0, 0, 2, 1); // Width 2
        // effective_scroll logic:
        // cursor_visual = 3.
        // candidate = 3 - 2 + 1 = 2.
        // prev_width (Wide) = 2. max_scroll_for_prev = 3 - 2 = 1.
        // min(2, 1) = 1.
        // The existing logic `max_scroll_for_prev` ALREADY protects the previous char!

        // So when does the bug happen?
        // When we are NOT near the cursor?
        // No, effective_scroll is driven by cursor.

        // What if we have multiple wide chars?
        // "\u{3000}\u{3000}" (Wide, Wide).
        // Widths: 2, 2. Pos: 0, 2. Total 4.
        // Cursor at 4. Viewport 3.
        // candidate = 4 - 3 + 1 = 2.
        // prev (Wide #2) width 2. max_for_prev = 4 - 2 = 2.
        // scroll = 2.
        // Render:
        //   Wide#1 @ 0..2. scroll=2. 0 < 2. Skipped. Correct.
        //   Wide#2 @ 2..4. scroll=2. 2 >= 2. Rendered at 0. Correct.

        // Wait, what if scroll lands *inside* a wide char?
        // Text: "a" (1) + "Wide" (2). Total 3.
        // Cursor at 3. Viewport 2.
        // candidate = 3 - 2 + 1 = 2.
        // prev (Wide) width 2. max_for_prev = 3 - 2 = 1.
        // scroll = 1.
        // Render:
        //   'a' @ 0. < 1. Skipped.
        //   Wide @ 1. >= 1. Rendered at 0.
        // Correct.

        // So `max_scroll_for_prev` protects the char *immediately* before the cursor.
        // But what about chars before THAT?
        // Text: "Wide1" (2) + "Wide2" (2).
        // Cursor at 4. Viewport 2.
        // scroll = 2.
        // Wide1 @ 0. < 2. Skipped.
        // Wide2 @ 2. >= 2. Rendered.

        // Is it possible for scroll to land on 1?
        // If we manually scroll? Input widget doesn't expose manual scroll.
        // It relies on cursor position.

        // What if viewport is 1?
        // Cursor at 4.
        // candidate = 4 - 1 + 1 = 4.
        // max_for_prev = 4 - 2 = 2.
        // scroll = 2.
        // Wide1 @ 0. Skipped.
        // Wide2 @ 2. Rendered at 0. (Clipped to width 1).

        // It seems `max_scroll_for_prev` handles the "hole" issue for the active character.
        // But the user issue mentioned "partial scrolling".

        // Let's assume the fix `snap_scroll_to_grapheme_boundary` is robust regardless.
        // It prevents ANY scroll value from landing inside a grapheme.
    }

    #[cfg(feature = "tracing")]
    #[test]
    fn tracing_input_edit_span_tracks_cursor_positions() {
        let state = Arc::new(Mutex::new(InputTraceState::default()));
        let _trace_test_guard = crate::tracing_test_support::acquire();
        let subscriber = tracing_subscriber::registry().with(InputTraceCapture {
            state: Arc::clone(&state),
        });
        let _guard = tracing::subscriber::set_default(subscriber);
        tracing::callsite::rebuild_interest_cache();

        let mut input = TextInput::new().with_value("ab");
        tracing::callsite::rebuild_interest_cache();
        assert!(input.handle_event(&Event::Key(KeyEvent::new(KeyCode::Char('c')))));
        tracing::callsite::rebuild_interest_cache();
        assert!(input.handle_event(&Event::Key(KeyEvent::new(KeyCode::Left))));
        tracing::callsite::rebuild_interest_cache();
        assert!(input.handle_event(&Event::Key(KeyEvent::new(KeyCode::Backspace))));

        tracing::callsite::rebuild_interest_cache();
        let snapshot = state.lock().expect("trace state lock");
        assert!(
            snapshot.span_count >= 3,
            "expected at least 3 input.edit spans, got {}",
            snapshot.span_count
        );
        assert!(
            snapshot.has_cursor_position_field,
            "input.edit span missing cursor_position field"
        );
        assert_eq!(
            snapshot.cursor_positions,
            vec![3, 2, 1],
            "expected cursor positions after insert/left/backspace"
        );
        assert!(
            snapshot.operations.starts_with(&[
                "insert_char".to_string(),
                "move_left".to_string(),
                "delete_back".to_string()
            ]),
            "unexpected operations: {:?}",
            snapshot.operations
        );
    }
}

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

    #[test]
    fn test_scroll_snap_left_cursor_visibility() {
        // Test the scenario: [Char A][Char B][Char C]
        // Viewport width 1.
        // Scroll is at B.
        // Move cursor to A (left).
        // Cursor visual pos = 0.
        // effective_scroll must become 0.

        let mut input = TextInput::new().with_value("ABC");
        input.cursor = 1; // At B

        // Force internal scroll to 1 (B) by rendering with narrow viewport
        // We can't set private scroll_cells directly, but we can simulate state
        let area = Rect::new(0, 0, 1, 1);
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(1, 1, &mut pool);
        input.render(area, &mut frame); // Scroll should be 1

        // Now move left
        input.move_cursor_left(); // Cursor at A (0)

        // Render again
        input.render(area, &mut frame);

        // Cell should be A
        let cell = frame.buffer.get(0, 0).unwrap();
        assert_eq!(cell.content.as_char(), Some('A'));
    }

    #[test]
    fn test_max_length_replacement_failure() {
        // "abc", max 3. Select "b". Insert "de".
        // Should delete "b" -> "ac".
        // Try insert "de" -> "adec" (len 4).
        // Should revert insert -> "ac".

        let mut input = TextInput::new().with_value("abc").with_max_length(3);
        input.selection_anchor = Some(1); // "b" start
        input.cursor = 2; // "b" end

        // Simulate paste "de"
        let event = Event::Paste(ftui_core::event::PasteEvent::new("de", false));
        input.handle_event(&event);

        // Oversized pastes replace nothing and leave selection intact.
        assert_eq!(input.value(), "abc");
        assert_eq!(input.cursor(), 2);
        assert_eq!(input.selection_anchor, Some(1));
    }
}
