#![forbid(unsafe_code)]

//! Core widgets for FrankenTUI.
//!
//! This crate provides the [`Widget`] and [`StatefulWidget`] traits, along with
//! a collection of ready-to-use widgets for building terminal UIs.
//!
//! # Widget Trait Design
//!
//! Widgets render into a [`Frame`] rather than directly into a [`Buffer`]. The Frame
//! provides access to several subsystems beyond the cell grid:
//!
//! - **`frame.buffer`** - The cell grid for drawing characters and styles
//! - **`frame.hit_grid`** - Optional mouse hit testing (for interactive widgets)
//! - **`frame.cursor_position`** - Cursor placement (for input widgets)
//! - **`frame.cursor_visible`** - Cursor visibility control
//! - **`frame.degradation`** - Performance budget hints (for adaptive rendering)
//!
//! # Role in FrankenTUI
//! `ftui-widgets` is the standard widget library. It provides the reusable
//! building blocks (tables, lists, inputs, graphs, etc.) that most apps will
//! render inside their `view()` functions.
//!
//! # How it fits in the system
//! Widgets render into `ftui-render::Frame` using `ftui-style` for appearance
//! and `ftui-text` for text measurement and wrapping. The runtime drives these
//! widgets by calling your model's `view()` on each frame.
//!
//! # Widget Categories
//!
//! Widgets fall into four categories based on which Frame features they use:
//!
//! ## Category A: Simple Buffer-Only Widgets
//!
//! Most widgets only need buffer access. These are the simplest to implement:
//!
//! ```ignore
//! impl Widget for MyWidget {
//!     fn render(&self, area: Rect, frame: &mut Frame) {
//!         // Just write to the buffer
//!         frame.buffer.set(area.x, area.y, Cell::from_char('X'));
//!     }
//! }
//! ```
//!
//! Examples: [`block::Block`], [`paragraph::Paragraph`], [`rule::Rule`], [`StatusLine`]
//!
//! ## Category B: Interactive Widgets with Hit Testing
//!
//! Widgets that handle mouse clicks register hit regions:
//!
//! ```ignore
//! impl Widget for ClickableList {
//!     fn render(&self, area: Rect, frame: &mut Frame) {
//!         // Draw items...
//!         for (i, item) in self.items.iter().enumerate() {
//!             let row_area = Rect::new(area.x, area.y + i as u16, area.width, 1);
//!             // Draw item to buffer...
//!
//!             // Register hit region for mouse interaction
//!             if let Some(id) = self.hit_id {
//!                 frame.register_hit(row_area, id, HitRegion::Content, i as u64);
//!             }
//!         }
//!     }
//! }
//! ```
//!
//! Examples: [`list::List`], [`table::Table`], [`scrollbar::Scrollbar`]
//!
//! ## Category C: Input Widgets with Cursor Control
//!
//! Text input widgets need to position the cursor:
//!
//! ```ignore
//! impl Widget for TextInput {
//!     fn render(&self, area: Rect, frame: &mut Frame) {
//!         // Draw the input content...
//!
//!         // Position cursor when focused
//!         if self.focused {
//!             let cursor_x = area.x + self.cursor_offset as u16;
//!             frame.cursor_position = Some((cursor_x, area.y));
//!             frame.cursor_visible = true;
//!         }
//!     }
//! }
//! ```
//!
//! Examples: [`TextInput`](input::TextInput)
//!
//! ## Category D: Adaptive Widgets with Degradation Support
//!
//! Complex widgets can adapt their rendering based on performance budget:
//!
//! ```ignore
//! impl Widget for FancyProgressBar {
//!     fn render(&self, area: Rect, frame: &mut Frame) {
//!         let deg = frame.buffer.degradation;
//!
//!         if !deg.render_decorative() {
//!             // Skip decorative elements at reduced budgets
//!             return;
//!         }
//!
//!         if deg.apply_styling() {
//!             // Use full styling and effects
//!         } else {
//!             // Use simplified ASCII rendering
//!         }
//!     }
//! }
//! ```
//!
//! Examples: [`ProgressBar`](progress::ProgressBar), [`Spinner`](spinner::Spinner)
//!
//! # Essential vs Decorative Widgets
//!
//! The [`Widget::is_essential`] method indicates whether a widget should always render,
//! even at `EssentialOnly` degradation level:
//!
//! - **Essential**: Text inputs, primary content, status information
//! - **Decorative**: Borders, scrollbars, spinners, visual separators
//!
//! [`Frame`]: ftui_render::frame::Frame
//! [`Buffer`]: ftui_render::buffer::Buffer

pub mod adaptive_radix;
pub mod align;
/// Badge widget (status/priority pills).
pub mod badge;
/// Block widget with borders, titles, and padding.
pub mod block;
pub mod borders;
pub mod cached;
pub mod choreography;
pub mod columns;
pub mod command_palette;
pub mod constraint_overlay;
#[cfg(feature = "debug-overlay")]
pub mod debug_overlay;
/// Galaxy-brain decision card widget with progressive-disclosure transparency.
pub mod decision_card;
/// Reusable diagnostic logging and telemetry substrate for JSONL diagnostics.
pub mod diagnostics;
/// Drag-and-drop protocol: [`Draggable`](drag::Draggable) sources, [`DropTarget`](drag::DropTarget) targets, and [`DragPayload`](drag::DragPayload).
pub mod drag;
/// Drift-triggered fallback visualization with per-domain confidence sparklines.
pub mod drift_visualization;
/// Elias-Fano encoding for monotone integer sequences (succinct prefix sums).
pub mod elias_fano;
pub mod emoji;
pub mod error_boundary;
/// Fenwick tree (Binary Indexed Tree) for O(log n) prefix sum queries.
pub mod fenwick;
pub mod file_picker;
/// Focus management: navigation graph for keyboard-driven focus traversal.
pub mod focus;
pub mod group;
/// Bayesian height prediction with conformal bounds for virtualized lists.
pub mod height_predictor;
pub mod help;
pub mod help_registry;
/// Utility-based keybinding hint ranking with Bayesian posteriors.
pub mod hint_ranker;
/// Undo/redo history panel widget for displaying command history.
pub mod history_panel;
pub mod input;
/// UI Inspector overlay for debugging widget trees and hit-test regions.
pub mod inspector;
pub mod json_view;
pub mod keyboard_drag;
pub mod layout;
pub mod layout_debugger;
pub mod list;
pub mod log_ring;
pub mod log_viewer;
pub mod louds;
/// Intrinsic sizing support for content-aware layout.
pub mod measurable;
/// Measure cache for memoizing widget measure results.
pub mod measure_cache;
pub mod modal;
/// Shared mouse event result type for widget mouse handling.
pub mod mouse;
/// Notification queue for managing multiple toast notifications.
pub mod notification_queue;
pub mod padding;
pub mod paginator;
pub mod panel;
/// Multi-line styled text paragraph widget.
pub mod paragraph;
pub mod popover;
pub mod pretty;
pub mod progress;
/// Receipt verifier panel — renders signed-decision-receipt verifier verdicts.
pub mod receipt_verifier_panel;
pub mod rule;
pub mod scrollbar;
pub mod sparkline;
pub mod spinner;
/// Opt-in persistable state trait for widgets.
pub mod stateful;
pub mod status_line;
pub mod stopwatch;
/// Table widget with rows, columns, and selection.
pub mod table;
pub mod tabs;
pub mod textarea;
pub mod timer;
/// Toast widget for transient notifications.
pub mod toast;
pub mod tree;
/// Undo support for widgets.
pub mod undo_support;
/// Inline validation error display widget.
pub mod validation_error;
pub mod virtualized;
pub mod voi_debug_overlay;

#[cfg(all(test, feature = "tracing"))]
pub(crate) mod tracing_test_support {
    use std::sync::{Mutex, MutexGuard, OnceLock};

    /// Serialize tests that install tracing subscribers and rebuild the
    /// callsite interest cache.
    pub(crate) struct TraceTestGuard {
        _lock: MutexGuard<'static, ()>,
    }

    impl Drop for TraceTestGuard {
        fn drop(&mut self) {
            tracing::callsite::rebuild_interest_cache();
        }
    }

    pub(crate) fn acquire() -> TraceTestGuard {
        static TRACE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

        // Preserve serialization even if an earlier tracing assertion panicked.
        let lock = TRACE_TEST_LOCK
            .get_or_init(|| Mutex::new(()))
            .lock()
            .unwrap_or_else(|e| e.into_inner());
        tracing::callsite::rebuild_interest_cache();
        TraceTestGuard { _lock: lock }
    }
}

pub use align::{Align, VerticalAlignment};
pub use badge::Badge;
pub use cached::{CacheKey, CachedWidget, CachedWidgetState, FnKey, HashKey, NoCacheKey};
pub use columns::{Column, Columns};
pub use constraint_overlay::{ConstraintOverlay, ConstraintOverlayStyle};
#[cfg(feature = "debug-overlay")]
pub use debug_overlay::{
    DebugOverlay, DebugOverlayOptions, DebugOverlayState, DebugOverlayStateful,
    DebugOverlayStatefulState,
};
pub use decision_card::DecisionCard;
pub use group::Group;
pub use help_registry::{HelpContent, HelpId, HelpRegistry, Keybinding};
pub use history_panel::{HistoryEntry, HistoryPanel, HistoryPanelMode};
pub use layout_debugger::{LayoutConstraints, LayoutDebugger, LayoutRecord};
pub use log_ring::LogRing;
pub use log_viewer::{LogViewer, LogViewerState, LogWrapMode, SearchConfig, SearchMode};
pub use paginator::{Paginator, PaginatorMode};
pub use panel::Panel;
pub use sparkline::Sparkline;
pub use status_line::{StatusItem, StatusLine};
pub use tabs::{Tab, Tabs, TabsState};
pub use virtualized::{
    HeightCache, ItemHeight, RenderItem, Virtualized, VirtualizedList, VirtualizedListState,
    VirtualizedStorage,
};
pub use voi_debug_overlay::{
    VoiDebugOverlay, VoiDecisionSummary, VoiLedgerEntry, VoiObservationSummary, VoiOverlayData,
    VoiOverlayStyle, VoiPosteriorSummary,
};

// Toast notification widget
pub use toast::{
    KeyEvent as ToastKeyEvent, Toast, ToastAction, ToastAnimationConfig, ToastAnimationPhase,
    ToastAnimationState, ToastConfig, ToastContent, ToastEasing, ToastEntranceAnimation,
    ToastEvent, ToastExitAnimation, ToastIcon, ToastId, ToastPosition, ToastState, ToastStyle,
};

// Notification queue manager
pub use notification_queue::{
    NotificationPriority, NotificationQueue, QueueAction, QueueConfig, QueueStats,
};

// Re-export accessibility trait and types for widget implementations.
pub use ftui_a11y::Accessible;

// Shared mouse result type for widget mouse handling
pub use mouse::MouseResult;

// Measurable widget support for intrinsic sizing
pub use measurable::{MeasurableWidget, SizeConstraints};

// Measure cache for memoizing measure() results
pub use measure_cache::{CacheStats, MeasureCache, WidgetId};
pub use modal::{
    BackdropConfig, MODAL_HIT_BACKDROP, MODAL_HIT_CONTENT, Modal, ModalAction, ModalConfig,
    ModalPosition, ModalSizeConstraints, ModalState,
};

// UI Inspector for debugging
pub use inspector::{
    DiagnosticEntry, DiagnosticEventKind, DiagnosticLog, HitInfo, InspectorMode, InspectorOverlay,
    InspectorState, InspectorStyle, TelemetryCallback, TelemetryHooks, WidgetInfo,
    diagnostics_enabled, init_diagnostics, is_deterministic_mode, reset_event_counter,
    set_diagnostics_enabled,
};

// Focus management
pub use focus::{
    FocusEvent, FocusGraph, FocusGroup, FocusId, FocusIndicator, FocusIndicatorKind, FocusManager,
    FocusNode, FocusTrap, NavDirection,
};

// Drag-and-drop protocol (source + target)
pub use drag::{
    DragConfig, DragPayload, DragState, Draggable, DropPosition, DropResult, DropTarget,
};

// Stateful persistence trait
pub use stateful::{StateKey, Stateful, VersionedState};

// Widget persist state types for state-persistence
pub use list::ListPersistState;
pub use table::TablePersistState;
pub use tree::TreePersistState;
pub use virtualized::VirtualizedListPersistState;

// Undo support for widgets
pub use undo_support::{
    ListOperation, ListUndoExt, SelectionOperation, TableOperation, TableUndoExt,
    TextEditOperation, TextInputUndoExt, TreeOperation, TreeUndoExt, UndoSupport, UndoWidgetId,
    WidgetTextEditCmd,
};

// Inline validation error display
pub use validation_error::{
    ANIMATION_DURATION_MS, ERROR_BG_DEFAULT, ERROR_FG_DEFAULT, ERROR_ICON_DEFAULT,
    ValidationErrorDisplay, ValidationErrorState,
};

use ftui_core::geometry::Rect;
use ftui_render::buffer::Buffer;
use ftui_render::cell::Cell;
use ftui_render::frame::{Frame, WidgetSignal};
use ftui_style::Style;
use ftui_text::grapheme_width;

/// Generate a deterministic accessibility node ID from a widget's bounding rect.
///
/// Uses FNV-1a to hash the area coordinates. Stable across frames for widgets
/// rendered at the same position, enabling efficient A11yTree diffing.
#[must_use]
pub(crate) fn a11y_node_id(area: Rect) -> u64 {
    // FNV-1a 64-bit
    const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
    const FNV_PRIME: u64 = 1_099_511_628_211;
    let mut h = FNV_OFFSET;
    for byte in area
        .x
        .to_le_bytes()
        .iter()
        .chain(&area.y.to_le_bytes())
        .chain(&area.width.to_le_bytes())
        .chain(&area.height.to_le_bytes())
    {
        h ^= u64::from(*byte);
        h = h.wrapping_mul(FNV_PRIME);
    }
    h
}

/// A widget that can render itself into a [`Frame`].
///
/// # Frame vs Buffer
///
/// Widgets render into a `Frame` rather than directly into a `Buffer`. This provides:
///
/// - **Buffer access**: `frame.buffer` for drawing cells
/// - **Hit testing**: `frame.register_hit()` for mouse interaction
/// - **Cursor control**: `frame.cursor_position` for input widgets
/// - **Performance hints**: `frame.buffer.degradation` for adaptive rendering
///
/// # Implementation Guide
///
/// Most widgets only need buffer access:
///
/// ```ignore
/// fn render(&self, area: Rect, frame: &mut Frame) {
///     for y in area.y..area.bottom() {
///         for x in area.x..area.right() {
///             frame.buffer.set(x, y, Cell::from_char('.'));
///         }
///     }
/// }
/// ```
///
/// Interactive widgets should register hit regions when a `hit_id` is set.
/// Input widgets should set `frame.cursor_position` when focused.
///
/// # Degradation Levels
///
/// Check `frame.buffer.degradation` to adapt rendering:
///
/// - `Full`: All features enabled
/// - `SimpleBorders`: Skip fancy borders, use ASCII
/// - `NoStyling`: Skip colors and attributes
/// - `EssentialOnly`: Only render essential widgets
/// - `Skeleton`: Minimal placeholder rendering
///
/// [`Frame`]: ftui_render::frame::Frame
pub trait Widget {
    /// Render the widget into the frame at the given area.
    ///
    /// The `area` defines the bounding rectangle within which the widget
    /// should render. Widgets should respect the area bounds and not
    /// draw outside them (the buffer's scissor stack enforces this).
    fn render(&self, area: Rect, frame: &mut Frame);

    /// Whether this widget is essential and should always render.
    ///
    /// Essential widgets render even at `EssentialOnly` degradation level.
    /// Override this to return `true` for:
    ///
    /// - Text inputs (user needs to see what they're typing)
    /// - Primary content areas (main information display)
    /// - Critical status indicators
    ///
    /// Returns `false` by default, appropriate for decorative widgets.
    fn is_essential(&self) -> bool {
        false
    }
}

/// Budget-aware wrapper that registers widget signals and respects refresh budgets.
pub struct Budgeted<W> {
    widget_id: u64,
    signal: WidgetSignal,
    inner: W,
}

impl<W> Budgeted<W> {
    /// Wrap a widget with a stable identifier and default signal values.
    #[must_use]
    pub fn new(widget_id: u64, inner: W) -> Self {
        Self {
            widget_id,
            signal: WidgetSignal::new(widget_id),
            inner,
        }
    }

    /// Override the widget signal template.
    #[must_use]
    pub fn with_signal(mut self, mut signal: WidgetSignal) -> Self {
        signal.widget_id = self.widget_id;
        self.signal = signal;
        self
    }

    /// Access the wrapped widget.
    #[must_use]
    pub fn inner(&self) -> &W {
        &self.inner
    }
}

impl<W: Widget> Widget for Budgeted<W> {
    fn render(&self, area: Rect, frame: &mut Frame) {
        let mut signal = self.signal.clone();
        signal.widget_id = self.widget_id;
        signal.essential = self.inner.is_essential();
        signal.area_cells = area.width as u32 * area.height as u32;
        frame.register_widget_signal(signal);

        if frame.should_render_widget(self.widget_id, self.inner.is_essential()) {
            self.inner.render(area, frame);
        }
    }

    fn is_essential(&self) -> bool {
        self.inner.is_essential()
    }
}

impl<W: StatefulWidget + Widget> StatefulWidget for Budgeted<W> {
    type State = W::State;

    fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
        let mut signal = self.signal.clone();
        signal.widget_id = self.widget_id;
        signal.essential = self.inner.is_essential();
        signal.area_cells = area.width as u32 * area.height as u32;
        frame.register_widget_signal(signal);

        if frame.should_render_widget(self.widget_id, self.inner.is_essential()) {
            StatefulWidget::render(&self.inner, area, frame, state);
        }
    }
}

/// A widget that renders based on mutable state.
///
/// Use `StatefulWidget` when the widget needs to:
///
/// - Update scroll position during render
/// - Track selection state
/// - Cache computed layout information
/// - Synchronize view with external model
///
/// # Example
///
/// ```ignore
/// pub struct ListState {
///     pub selected: Option<usize>,
///     pub offset: usize,
/// }
///
/// impl StatefulWidget for List<'_> {
///     type State = ListState;
///
///     fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
///         // Adjust offset to keep selection visible
///         if let Some(sel) = state.selected {
///             if sel < state.offset {
///                 state.offset = sel;
///             }
///         }
///         // Render items starting from offset...
///     }
/// }
/// ```
///
/// # Stateful vs Stateless
///
/// Prefer stateless [`Widget`] when possible. Use `StatefulWidget` only when
/// the render pass genuinely needs to modify state (e.g., scroll adjustment).
pub trait StatefulWidget {
    /// The state type associated with this widget.
    type State;

    /// Render the widget into the frame, potentially modifying state.
    ///
    /// State modifications should be limited to:
    /// - Scroll offset adjustments
    /// - Selection clamping
    /// - Layout caching
    fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State);
}

/// Merge a [`Style`] into a cell, preserving existing properties for unset fields.
///
/// - **Foreground / Background:** Only overwritten when the style explicitly sets
///   the field (`Some`).  Background colours with alpha < 255 are composited via
///   Porter-Duff SourceOver so semi-transparent overlays blend correctly.
/// - **Attributes:** New flags are OR-ed on top of existing flags (never cleared).
pub(crate) fn apply_style(cell: &mut Cell, style: Style) {
    if let Some(fg) = style.fg {
        cell.fg = fg;
    }
    if let Some(bg) = style.bg {
        match bg.a() {
            0 => {}                          // Fully transparent: no-op
            255 => cell.bg = bg,             // Fully opaque: replace
            _ => cell.bg = bg.over(cell.bg), // Composite src-over-dst
        }
    }
    if let Some(attrs) = style.attrs {
        let cell_flags: ftui_render::cell::StyleFlags = attrs.into();
        cell.attrs = cell.attrs.merged_flags(cell_flags);
    }
}

/// Apply a style to all cells in a rectangular area using **merge** semantics.
///
/// Only fields that are explicitly set in `style` (i.e. `Some`) are applied;
/// unset fields leave the existing cell values intact.  This is the correct
/// behaviour for selection / highlight overlays that specify only a background
/// colour — per-cell foreground colours from earlier text rendering are preserved.
///
/// - **Background:** Alpha-aware compositing (Porter-Duff SourceOver).
/// - **Attributes:** OR-ed on top of existing flags (never cleared).
pub(crate) fn set_style_area(buf: &mut Buffer, area: Rect, style: Style) {
    if style.is_empty() {
        return;
    }
    let clipped = area.intersection(&buf.current_scissor());
    if clipped.is_empty() {
        return;
    }

    let opacity = buf.current_opacity();
    let fg = style.fg.map(|fg| fg.with_opacity(opacity));
    let bg = style.bg.map(|bg| bg.with_opacity(opacity));
    let attrs = style.attrs.map(ftui_render::cell::StyleFlags::from);
    for y in clipped.y..clipped.bottom() {
        let Some(row) = buf.row_cells_mut_span(y, clipped.x, clipped.right()) else {
            continue;
        };
        for cell in row {
            if let Some(fg) = fg {
                cell.fg = fg;
            }
            if let Some(bg) = bg {
                match bg.a() {
                    0 => {}                          // Fully transparent: no-op
                    255 => cell.bg = bg,             // Fully opaque: replace
                    _ => cell.bg = bg.over(cell.bg), // Composite src-over-dst
                }
            }
            if let Some(attrs) = attrs {
                cell.attrs = cell.attrs.merged_flags(attrs);
            }
        }
    }
}

/// Clear a text area with styled spaces before rendering new content.
pub(crate) fn clear_text_area(frame: &mut Frame, area: Rect, style: Style) {
    if area.width == 0 || area.height == 0 {
        return;
    }

    let mut cell = Cell::from_char(' ');
    apply_style(&mut cell, style);
    frame.buffer.fill(area, cell);
}

/// Clear a single text row with styled spaces before rendering new content.
pub(crate) fn clear_text_row(frame: &mut Frame, area: Rect, style: Style) {
    clear_text_area(frame, Rect::new(area.x, area.y, area.width, 1), style);
}

/// Build a text cell that inherits existing visual styling from the buffer.
///
/// This preserves foreground/background/style flags applied by prior area-wide
/// overlays (for example selection/highlight passes) while intentionally
/// dropping any stale hyperlink ID before new text is written.
fn inherited_text_cell(
    frame: &Frame,
    x: u16,
    y: u16,
    content: ftui_render::cell::CellContent,
) -> Cell {
    let mut cell = frame.buffer.get(x, y).copied().unwrap_or_default();
    cell.content = content;
    cell.attrs = ftui_render::cell::CellAttrs::new(cell.attrs.flags(), 0);
    cell
}

/// Draw a text span into a frame at the given position.
///
/// Returns the x position after the last drawn character.
/// Stops at `max_x` (exclusive).
pub(crate) fn draw_text_span(
    frame: &mut Frame,
    mut x: u16,
    y: u16,
    content: &str,
    style: Style,
    max_x: u16,
) -> u16 {
    use unicode_segmentation::UnicodeSegmentation;

    for grapheme in content.graphemes(true) {
        if x >= max_x {
            break;
        }
        let w = grapheme_width(grapheme);
        if w == 0 {
            continue;
        }
        if x.saturating_add(w as u16) > max_x {
            break;
        }

        // Intern grapheme if needed
        let cell_content = if w > 1 || grapheme.chars().count() > 1 {
            let id = frame.intern_with_width(grapheme, w as u8);
            ftui_render::cell::CellContent::from_grapheme(id)
        } else if let Some(c) = grapheme.chars().next() {
            ftui_render::cell::CellContent::from_char(c)
        } else {
            continue;
        };

        let mut cell = inherited_text_cell(frame, x, y, cell_content);
        apply_style(&mut cell, style);

        // set_fast() skips scissor/opacity/compositing checks for common
        // single-width opaque cells; falls back to set() otherwise.
        frame.buffer.set_fast(x, y, cell);

        x = x.saturating_add(w as u16);
    }
    x
}

/// Draw a text span, optionally attaching a hyperlink.
#[allow(dead_code)]
pub(crate) fn draw_text_span_with_link(
    frame: &mut Frame,
    x: u16,
    y: u16,
    content: &str,
    style: Style,
    max_x: u16,
    link_url: Option<&str>,
) -> u16 {
    draw_text_span_scrolled(frame, x, y, content, style, max_x, 0, link_url)
}

/// Draw a text span with horizontal scrolling (skip first `scroll_x` visual cells).
#[allow(dead_code, clippy::too_many_arguments)]
pub(crate) fn draw_text_span_scrolled(
    frame: &mut Frame,
    mut x: u16,
    y: u16,
    content: &str,
    style: Style,
    max_x: u16,
    scroll_x: u16,
    link_url: Option<&str>,
) -> u16 {
    use unicode_segmentation::UnicodeSegmentation;

    // Register link if present
    let link_id = if let Some(url) = link_url {
        frame.register_link(url)
    } else {
        0
    };

    let mut visual_pos: u16 = 0;

    for grapheme in content.graphemes(true) {
        if x >= max_x {
            break;
        }
        let w = grapheme_width(grapheme);
        if w == 0 {
            continue;
        }

        let next_visual_pos = visual_pos.saturating_add(w as u16);

        // Check if this grapheme is visible
        if next_visual_pos <= scroll_x {
            // Fully scrolled out
            visual_pos = next_visual_pos;
            continue;
        }

        if visual_pos < scroll_x {
            // Partially scrolled out (e.g. wide char starting at scroll_x - 1)
            // We skip the whole character because we can't render half a cell.
            visual_pos = next_visual_pos;
            continue;
        }

        if x.saturating_add(w as u16) > max_x {
            break;
        }

        // Intern grapheme if needed
        let cell_content = if w > 1 || grapheme.chars().count() > 1 {
            let id = frame.intern_with_width(grapheme, w as u8);
            ftui_render::cell::CellContent::from_grapheme(id)
        } else if let Some(c) = grapheme.chars().next() {
            ftui_render::cell::CellContent::from_char(c)
        } else {
            continue;
        };

        let mut cell = inherited_text_cell(frame, x, y, cell_content);
        apply_style(&mut cell, style);

        // Apply link ID if present
        if link_id != 0 {
            cell.attrs = cell.attrs.with_link(link_id);
        }

        frame.buffer.set_fast(x, y, cell);

        x = x.saturating_add(w as u16);
        visual_pos = next_visual_pos;
    }
    x
}

/// Helper for allocation-free case-insensitive containment check.
pub(crate) fn contains_ignore_case(haystack: &str, needle_lower: &str) -> bool {
    if needle_lower.is_empty() {
        return true;
    }
    // Fast path for ASCII
    if haystack.is_ascii() && needle_lower.is_ascii() {
        let haystack_bytes = haystack.as_bytes();
        let needle_bytes = needle_lower.as_bytes();
        if needle_bytes.len() > haystack_bytes.len() {
            return false;
        }
        // Naive byte-by-byte scan is fast enough for short strings (UI labels)
        for i in 0..=haystack_bytes.len() - needle_bytes.len() {
            let mut match_found = true;
            for (j, &b) in needle_bytes.iter().enumerate() {
                if haystack_bytes[i + j].to_ascii_lowercase() != b {
                    match_found = false;
                    break;
                }
            }
            if match_found {
                return true;
            }
        }
        return false;
    }
    // Fallback for Unicode (allocates, but correct)
    haystack.to_lowercase().contains(needle_lower)
}

#[cfg(test)]
mod tests {
    use super::*;
    use ftui_render::cell::PackedRgba;
    use ftui_render::grapheme_pool::GraphemePool;

    #[test]
    fn apply_style_sets_fg() {
        let mut cell = Cell::default();
        let style = Style::new().fg(PackedRgba::rgb(255, 0, 0));
        apply_style(&mut cell, style);
        assert_eq!(cell.fg, PackedRgba::rgb(255, 0, 0));
    }

    #[test]
    fn apply_style_sets_bg() {
        let mut cell = Cell::default();
        let style = Style::new().bg(PackedRgba::rgb(0, 255, 0));
        apply_style(&mut cell, style);
        assert_eq!(cell.bg, PackedRgba::rgb(0, 255, 0));
    }

    #[test]
    fn apply_style_preserves_content() {
        let mut cell = Cell::from_char('Z');
        let style = Style::new().fg(PackedRgba::rgb(1, 2, 3));
        apply_style(&mut cell, style);
        assert_eq!(cell.content.as_char(), Some('Z'));
    }

    #[test]
    fn apply_style_empty_is_noop() {
        let original = Cell::default();
        let mut cell = Cell::default();
        apply_style(&mut cell, Style::default());
        assert_eq!(cell.fg, original.fg);
        assert_eq!(cell.bg, original.bg);
    }

    #[test]
    fn apply_style_bg_only_preserves_fg() {
        // Simulate: cell already has syntax-highlighted fg, selection overlay sets only bg.
        let mut cell = Cell::from_char('x').with_fg(PackedRgba::rgb(0, 200, 0));
        let selection = Style::new().bg(PackedRgba::rgb(0, 0, 180));
        apply_style(&mut cell, selection);
        // fg must survive — the selection style didn't set a fg.
        assert_eq!(cell.fg, PackedRgba::rgb(0, 200, 0));
        assert_eq!(cell.bg, PackedRgba::rgb(0, 0, 180));
    }

    #[test]
    fn apply_style_composites_alpha_bg() {
        let base_bg = PackedRgba::rgb(200, 0, 0);
        let mut cell = Cell::default().with_bg(base_bg);
        let overlay = PackedRgba::rgba(0, 0, 200, 128);
        apply_style(&mut cell, Style::new().bg(overlay));
        assert_eq!(cell.bg, overlay.over(base_bg));
    }

    #[test]
    fn apply_style_transparent_bg_is_noop() {
        let base_bg = PackedRgba::rgb(100, 100, 100);
        let mut cell = Cell::default().with_bg(base_bg);
        apply_style(&mut cell, Style::new().bg(PackedRgba::rgba(255, 0, 0, 0)));
        assert_eq!(cell.bg, base_bg);
    }

    #[test]
    fn apply_style_merges_attrs_not_replaces() {
        use ftui_render::cell::StyleFlags as CellFlags;
        // Cell starts with BOLD.
        let mut cell = Cell::default();
        cell.attrs = cell.attrs.with_flags(CellFlags::BOLD);
        // Overlay adds ITALIC — should NOT clear BOLD.
        let overlay = Style::new().italic();
        apply_style(&mut cell, overlay);
        assert!(cell.attrs.has_flag(CellFlags::BOLD), "BOLD must survive");
        assert!(
            cell.attrs.has_flag(CellFlags::ITALIC),
            "ITALIC must be added"
        );
    }

    #[test]
    fn set_style_area_bg_only_preserves_per_cell_fg() {
        // A 3-cell buffer where each cell has a distinct fg.
        let mut buf = Buffer::new(3, 1);
        buf.set(0, 0, Cell::from_char('R').with_fg(PackedRgba::RED));
        buf.set(1, 0, Cell::from_char('G').with_fg(PackedRgba::GREEN));
        buf.set(2, 0, Cell::from_char('B').with_fg(PackedRgba::BLUE));

        // Selection highlight sets only bg.
        let highlight = Style::new().bg(PackedRgba::rgb(40, 40, 40));
        set_style_area(&mut buf, Rect::new(0, 0, 3, 1), highlight);

        // All fg colours must be preserved.
        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::RED);
        assert_eq!(buf.get(1, 0).unwrap().fg, PackedRgba::GREEN);
        assert_eq!(buf.get(2, 0).unwrap().fg, PackedRgba::BLUE);
        // bg should be the highlight colour.
        assert_eq!(buf.get(0, 0).unwrap().bg, PackedRgba::rgb(40, 40, 40));
    }

    #[test]
    fn set_style_area_merges_attrs_not_replaces() {
        use ftui_render::cell::StyleFlags as CellFlags;
        let mut buf = Buffer::new(1, 1);
        let mut cell = Cell::from_char('X');
        cell.attrs = cell.attrs.with_flags(CellFlags::BOLD);
        buf.set(0, 0, cell);

        set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().italic());

        let result = buf.get(0, 0).unwrap();
        assert!(result.attrs.has_flag(CellFlags::BOLD), "BOLD must survive");
        assert!(
            result.attrs.has_flag(CellFlags::ITALIC),
            "ITALIC must be added"
        );
    }

    #[test]
    fn set_style_area_applies_to_all_cells() {
        let mut buf = Buffer::new(3, 2);
        let area = Rect::new(0, 0, 3, 2);
        let style = Style::new().bg(PackedRgba::rgb(10, 20, 30));
        set_style_area(&mut buf, area, style);

        for y in 0..2 {
            for x in 0..3 {
                assert_eq!(
                    buf.get(x, y).unwrap().bg,
                    PackedRgba::rgb(10, 20, 30),
                    "cell ({x},{y}) should have style applied"
                );
            }
        }
    }

    #[test]
    fn set_style_area_composites_alpha_bg_over_existing_bg() {
        let mut buf = Buffer::new(1, 1);
        let base = PackedRgba::rgb(200, 0, 0);
        buf.set(0, 0, Cell::default().with_bg(base));

        let overlay = PackedRgba::rgba(0, 0, 200, 128);
        set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().bg(overlay));

        let expected = overlay.over(base);
        assert_eq!(buf.get(0, 0).unwrap().bg, expected);
    }

    #[test]
    fn set_style_area_partial_rect() {
        let mut buf = Buffer::new(5, 5);
        let area = Rect::new(1, 1, 2, 2);
        let style = Style::new().fg(PackedRgba::rgb(99, 99, 99));
        set_style_area(&mut buf, area, style);

        // Inside area should be styled
        assert_eq!(buf.get(1, 1).unwrap().fg, PackedRgba::rgb(99, 99, 99));
        assert_eq!(buf.get(2, 2).unwrap().fg, PackedRgba::rgb(99, 99, 99));

        // Outside area should be default
        assert_ne!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(99, 99, 99));
    }

    #[test]
    fn set_style_area_empty_style_is_noop() {
        let mut buf = Buffer::new(3, 3);
        buf.set(0, 0, Cell::from_char('A'));
        let original_fg = buf.get(0, 0).unwrap().fg;

        set_style_area(&mut buf, Rect::new(0, 0, 3, 3), Style::default());

        // Should not have changed
        assert_eq!(buf.get(0, 0).unwrap().fg, original_fg);
        assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('A'));
    }

    #[test]
    fn set_style_area_respects_scissor() {
        let mut buf = Buffer::new(3, 3);
        let style = Style::new().bg(PackedRgba::rgb(10, 20, 30));

        buf.push_scissor(Rect::new(1, 1, 1, 1));
        set_style_area(&mut buf, Rect::new(0, 0, 3, 3), style);

        assert_eq!(buf.get(1, 1).unwrap().bg, PackedRgba::rgb(10, 20, 30));
        assert_ne!(buf.get(0, 1).unwrap().bg, PackedRgba::rgb(10, 20, 30));
        assert_ne!(buf.get(1, 0).unwrap().bg, PackedRgba::rgb(10, 20, 30));
        assert_ne!(buf.get(2, 2).unwrap().bg, PackedRgba::rgb(10, 20, 30));
    }

    #[test]
    fn set_style_area_respects_opacity_stack() {
        let mut buf = Buffer::new(1, 1);
        let base_fg = PackedRgba::rgb(20, 30, 40);
        let base_bg = PackedRgba::rgb(50, 60, 70);
        buf.set(0, 0, Cell::from_char('X').with_fg(base_fg).with_bg(base_bg));

        let overlay_fg = PackedRgba::rgb(200, 100, 0);
        let overlay_bg = PackedRgba::rgb(0, 0, 200);
        buf.push_opacity(0.5);
        set_style_area(
            &mut buf,
            Rect::new(0, 0, 1, 1),
            Style::new().fg(overlay_fg).bg(overlay_bg),
        );

        let cell = buf.get(0, 0).unwrap();
        assert_eq!(cell.fg, overlay_fg.with_opacity(0.5));
        assert_eq!(cell.bg, overlay_bg.with_opacity(0.5).over(base_bg));
    }

    #[test]
    fn draw_text_span_basic() {
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 1, &mut pool);
        let end_x = draw_text_span(&mut frame, 0, 0, "ABC", Style::default(), 10);

        assert_eq!(end_x, 3);
        assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('A'));
        assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('B'));
        assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('C'));
    }

    #[test]
    fn draw_text_span_clipped_at_max_x() {
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 1, &mut pool);
        let end_x = draw_text_span(&mut frame, 0, 0, "ABCDEF", Style::default(), 3);

        assert_eq!(end_x, 3);
        assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('A'));
        assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('C'));
        // 'D' should not be drawn
        assert!(frame.buffer.get(3, 0).unwrap().is_empty());
    }

    #[test]
    fn draw_text_span_starts_at_offset() {
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 1, &mut pool);
        let end_x = draw_text_span(&mut frame, 5, 0, "XY", Style::default(), 10);

        assert_eq!(end_x, 7);
        assert_eq!(frame.buffer.get(5, 0).unwrap().content.as_char(), Some('X'));
        assert_eq!(frame.buffer.get(6, 0).unwrap().content.as_char(), Some('Y'));
        assert!(frame.buffer.get(4, 0).unwrap().is_empty());
    }

    #[test]
    fn draw_text_span_empty_string() {
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(5, 1, &mut pool);
        let end_x = draw_text_span(&mut frame, 0, 0, "", Style::default(), 5);
        assert_eq!(end_x, 0);
    }

    #[test]
    fn draw_text_span_applies_style() {
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(5, 1, &mut pool);
        let style = Style::new().fg(PackedRgba::rgb(255, 128, 0));
        draw_text_span(&mut frame, 0, 0, "A", style, 5);

        assert_eq!(
            frame.buffer.get(0, 0).unwrap().fg,
            PackedRgba::rgb(255, 128, 0)
        );
    }

    #[test]
    fn draw_text_span_preserves_existing_overlay_fg_and_bg() {
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(3, 1, &mut pool);
        frame.buffer.set(
            0,
            0,
            Cell::from_char('x').with_fg(PackedRgba::rgb(200, 40, 10)),
        );
        set_style_area(
            &mut frame.buffer,
            Rect::new(0, 0, 1, 1),
            Style::new().bg(PackedRgba::rgb(20, 30, 40)),
        );

        draw_text_span(&mut frame, 0, 0, "A", Style::default(), 1);

        let cell = frame.buffer.get(0, 0).unwrap();
        assert_eq!(cell.content.as_char(), Some('A'));
        assert_eq!(cell.fg, PackedRgba::rgb(200, 40, 10));
        assert_eq!(cell.bg, PackedRgba::rgb(20, 30, 40));
    }

    #[test]
    fn draw_text_span_drops_stale_link_id_but_keeps_style_flags() {
        use ftui_render::cell::{CellAttrs, StyleFlags as CellFlags};

        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(3, 1, &mut pool);
        frame.buffer.set(
            0,
            0,
            Cell::from_char('x').with_attrs(CellAttrs::new(CellFlags::UNDERLINE, 42)),
        );

        draw_text_span(&mut frame, 0, 0, "A", Style::default(), 1);

        let cell = frame.buffer.get(0, 0).unwrap();
        assert_eq!(cell.content.as_char(), Some('A'));
        assert!(cell.attrs.has_flag(CellFlags::UNDERLINE));
        assert_eq!(cell.attrs.link_id(), 0);
    }

    #[test]
    fn draw_text_span_max_x_at_start_draws_nothing() {
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(5, 1, &mut pool);
        let end_x = draw_text_span(&mut frame, 3, 0, "ABC", Style::default(), 3);
        assert_eq!(end_x, 3);
        assert!(frame.buffer.get(3, 0).unwrap().is_empty());
    }

    #[test]
    fn widget_is_essential_default_false() {
        struct DummyWidget;
        impl Widget for DummyWidget {
            fn render(&self, _: Rect, _: &mut Frame) {}
        }
        assert!(!DummyWidget.is_essential());
    }

    #[test]
    fn budgeted_new_and_inner() {
        struct TestW;
        impl Widget for TestW {
            fn render(&self, _: Rect, _: &mut Frame) {}
        }
        let b = Budgeted::new(42, TestW);
        assert_eq!(b.widget_id, 42);
        let _ = b.inner(); // Should not panic
    }

    #[test]
    fn budgeted_with_signal() {
        struct TestW;
        impl Widget for TestW {
            fn render(&self, _: Rect, _: &mut Frame) {}
        }
        let sig = WidgetSignal::new(99);
        let b = Budgeted::new(42, TestW).with_signal(sig);
        // with_signal should override the signal's widget_id to match
        assert_eq!(b.signal.widget_id, 42);
    }

    #[test]
    fn set_style_area_transparent_bg_is_noop() {
        let mut buf = Buffer::new(1, 1);
        let base = PackedRgba::rgb(100, 100, 100);
        buf.set(0, 0, Cell::default().with_bg(base));

        // Alpha=0 means fully transparent, should leave bg unchanged
        let transparent = PackedRgba::rgba(255, 0, 0, 0);
        set_style_area(
            &mut buf,
            Rect::new(0, 0, 1, 1),
            Style::new().bg(transparent),
        );
        assert_eq!(buf.get(0, 0).unwrap().bg, base);
    }

    #[test]
    fn set_style_area_opaque_bg_replaces() {
        let mut buf = Buffer::new(1, 1);
        buf.set(
            0,
            0,
            Cell::default().with_bg(PackedRgba::rgb(100, 100, 100)),
        );

        let opaque = PackedRgba::rgba(0, 255, 0, 255);
        set_style_area(&mut buf, Rect::new(0, 0, 1, 1), Style::new().bg(opaque));
        assert_eq!(buf.get(0, 0).unwrap().bg, opaque);
    }

    #[test]
    fn draw_text_span_scrolled_skips_chars() {
        let mut pool = GraphemePool::new();
        let mut frame = Frame::new(10, 1, &mut pool);
        // Scroll past first 2 chars of "ABCDE"
        let end_x =
            draw_text_span_scrolled(&mut frame, 0, 0, "ABCDE", Style::default(), 10, 2, None);

        assert_eq!(end_x, 3);
        assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('C'));
        assert_eq!(frame.buffer.get(1, 0).unwrap().content.as_char(), Some('D'));
        assert_eq!(frame.buffer.get(2, 0).unwrap().content.as_char(), Some('E'));
    }
}
