//! Text width / wrapping / slicing / ANSI utilities (port of `src/ui/utils.ts`).
//!
//! Grapheme-cluster-aware terminal column width ([`visible_width`]), ANSI-aware
//! word wrap ([`wrap_text_with_ansi`]), truncate-with-ellipsis
//! ([`truncate_to_width`]), column slicing ([`slice_by_column`] /
//! [`slice_with_width`] / [`extract_segments`]) for overlay compositing, and an
//! SGR state tracker ([`AnsiStateTracker`]) that re-emits styling across line
//! breaks.
//!
//! Parity-critical (must match byte-for-byte): the pure-ASCII fast path, tab →
//! three spaces, the `measure_cluster_width` emoji/east-asian rules (the
//! `segment.length > 2` check is a **UTF-16 code-unit** count), the ANSI strip
//! grammar, `truncate_to_width`'s `result + "\x1b[0m" + ellipsis`, the wrap's
//! cross-newline SGR carry + `\x1b[24m` underline-off line-ends, and the fixed
//! `get_active_codes` re-emit order (bold,dim,italic,underline,blink,inverse,
//! hidden,strikethrough,fg,bg).

use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::VecDeque;

use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;

// ===========================================================================
// Grapheme / width facades (≈ GraphemeHandler / TextWidthCalculator).
// ===========================================================================

/// One grapheme cluster, plus access to the shared segmenter
/// (≈ TS `GraphemeHandler`). Backed by `unicode-segmentation`.
pub struct GraphemeHandler;

impl GraphemeHandler {
    /// Iterate the grapheme clusters of `text`.
    pub fn iter<'a>(&self, text: &'a str) -> impl Iterator<Item = &'a str> {
        // `true` = extended grapheme clusters, matching Intl.Segmenter's default.
        UnicodeSegmentation::graphemes(text, true)
    }
}

/// Thin width facade over [`visible_width`] with a clearable cache
/// (≈ TS `TextWidthCalculator`).
pub struct TextWidthCalculator;

impl TextWidthCalculator {
    pub fn get_width(&self, text: &str) -> usize {
        visible_width(text)
    }
    pub fn clear_cache(&self) {
        WIDTH_CACHE.with(|cache| {
            let mut c = cache.borrow_mut();
            c.0.clear();
            c.1.clear();
        });
    }
}

// ===========================================================================
// East-asian width + cluster measurement (port of measureClusterWidth).
// ===========================================================================

/// Mirror of `get-east-asian-width`'s default (`ambiguousAsWide: false`): every
/// printable codepoint maps to 1, with Wide/Fullwidth forms mapping to 2.
///
/// The TS `eastAsianWidth(cp)` never returns 0 — combining marks and other
/// zero-width content are filtered upstream by the zero-width / leading
/// non-printing handling in [`measure_cluster_width`]. We therefore coerce
/// `unicode-width`'s `None`/`Some(0)` results up to 1 so the base codepoint is
/// counted exactly as the TS port counts it.
fn east_asian_width(ch: char) -> usize {
    match UnicodeWidthChar::width(ch) {
        Some(2) => 2,
        _ => 1,
    }
}

/// A grapheme cluster takes no terminal columns when it is made up entirely of
/// Default_Ignorable / Control / Mark / Surrogate scalar values (port of the TS
/// `zeroWidthRegex` test). Lone surrogates cannot appear in a Rust `&str`, so
/// they are unreachable here but listed for documentation parity.
fn is_zero_width_cluster(segment: &str) -> bool {
    let mut empty = true;
    for ch in segment.chars() {
        empty = false;
        let zero = is_default_ignorable(ch) || ch.is_control() || is_mark(ch);
        if !zero {
            return false;
        }
    }
    !empty
}

/// Default_Ignorable_Code_Point (covers the common ranges used in terminal
/// text: the soft hyphen, the format/joiner block, variation selectors, and the
/// zero-width-no-break-space). This is the same property the TS regex matches.
fn is_default_ignorable(ch: char) -> bool {
    matches!(
        ch as u32,
        0x00AD
            | 0x034F
            | 0x061C
            | 0x115F..=0x1160
            | 0x17B4..=0x17B5
            | 0x180B..=0x180F
            | 0x200B..=0x200F
            | 0x202A..=0x202E
            | 0x2060..=0x206F
            | 0x3164
            | 0xFE00..=0xFE0F
            | 0xFEFF
            | 0xFFA0
            | 0xFFF0..=0xFFF8
            | 0x1BCA0..=0x1BCA3
            | 0x1D173..=0x1D17A
            | 0xE0000..=0xE0FFF
    )
}

/// Unicode general-category Mark (Mn / Mc / Me): combining marks. Recognised via
/// `unicode-width` reporting zero columns for combining scalars, plus the
/// well-known combining-diacritical blocks for robustness.
fn is_mark(ch: char) -> bool {
    let cp = ch as u32;
    // Combining diacritical / enclosing / common combining blocks.
    matches!(
        cp,
        0x0300..=0x036F   // Combining Diacritical Marks
            | 0x0483..=0x0489
            | 0x0591..=0x05BD
            | 0x05BF
            | 0x05C1..=0x05C2
            | 0x0610..=0x061A
            | 0x064B..=0x065F
            | 0x0670
            | 0x06D6..=0x06DC
            | 0x06DF..=0x06E4
            | 0x06E7..=0x06E8
            | 0x06EA..=0x06ED
            | 0x0711
            | 0x0730..=0x074A
            | 0x1AB0..=0x1AFF // Combining Diacritical Marks Extended
            | 0x1DC0..=0x1DFF // Combining Diacritical Marks Supplement
            | 0x20D0..=0x20FF // Combining Diacritical Marks for Symbols
            | 0xFE20..=0xFE2F // Combining Half Marks
    )
}

/// Cheap pre-screen for whether a grapheme might be an RGI emoji (port of TS
/// `looksLikeEmojiCandidate`). The `segment.len() > 2` test in TS counts UTF-16
/// code units, which we reproduce with `encode_utf16().count()`.
fn looks_like_emoji_candidate(segment: &str) -> bool {
    let cp = match segment.chars().next() {
        Some(c) => c as u32,
        None => return false,
    };
    (0x1F000..=0x1FBFF).contains(&cp)
        || (0x2300..=0x23FF).contains(&cp)
        || (0x2600..=0x27BF).contains(&cp)
        || (0x2B50..=0x2B55).contains(&cp)
        || segment.contains('\u{FE0F}') // VS16, the emoji-presentation selector
        || segment.encode_utf16().count() > 2 // ZWJ joins, skin-tone modifiers, etc.
}

/// Confirm an emoji-candidate cluster is actually a (likely) RGI emoji. The
/// Rust `regex` crate has no `\p{RGI_Emoji}`, so — exactly like the TS
/// `uFallback` branch (`\p{Extended_Pictographic}`) — we treat a cluster whose
/// base scalar sits in the pictographic ranges (or which carries VS16 / is a
/// multi-scalar ZWJ/modifier sequence) as an emoji. Width-2 follows.
fn is_rgi_emoji(segment: &str) -> bool {
    let cp = match segment.chars().next() {
        Some(c) => c as u32,
        None => return false,
    };
    let base_pictographic = (0x1F000..=0x1FBFF).contains(&cp)
        || (0x2600..=0x27BF).contains(&cp)
        || (0x2B00..=0x2BFF).contains(&cp)
        || (0x2300..=0x23FF).contains(&cp)
        || matches!(
            cp,
            0x203C | 0x2049 | 0x2122 | 0x2139 | 0x3030 | 0x303D | 0x3297 | 0x3299
        );
    base_pictographic || segment.contains('\u{FE0F}') || segment.chars().count() > 1
}

/// Work out how many terminal columns a lone grapheme cluster occupies (port of
/// TS `measureClusterWidth`).
fn measure_cluster_width(segment: &str) -> usize {
    // Clusters that take up no space at all.
    if is_zero_width_cluster(segment) {
        return 0;
    }

    // Run the pre-screen first; only confirmed RGI emoji reach the regex.
    if looks_like_emoji_candidate(segment) && is_rgi_emoji(segment) {
        return 2;
    }

    // Strip any leading non-printing marks to find the first visible codepoint.
    let base = strip_leading_non_printing(segment);
    let mut chars = base.chars();
    let cp = match chars.next() {
        Some(c) => c,
        None => return 0,
    };

    let mut width = east_asian_width(cp);

    // Account for any trailing half/full-width forms tacked on after the base.
    // The TS guard is `segment.length > 1` (UTF-16 units) and iterates
    // `segment.slice(1)` — i.e. it skips the *first UTF-16 unit* of the original
    // cluster, not the stripped base. We mirror that precisely.
    if segment.encode_utf16().count() > 1 {
        for ch in chars_after_first_utf16_unit(segment) {
            let c = ch as u32;
            if (0xFF00..=0xFFEF).contains(&c) {
                width += east_asian_width(ch);
            }
        }
    }

    width
}

/// Return the scalars of `segment` that follow its first UTF-16 code unit. For
/// BMP-leading clusters this is just "everything after the first char"; if the
/// cluster begins with an astral scalar (two UTF-16 units), the TS `slice(1)`
/// lands inside that surrogate pair, so the astral base char is dropped and we
/// resume at the second scalar. Matching that keeps the trailing-form scan in
/// lockstep with the TS implementation.
fn chars_after_first_utf16_unit(segment: &str) -> impl Iterator<Item = char> + '_ {
    let mut chars = segment.chars();
    // Consume the first scalar (it occupied 1 or 2 UTF-16 units; either way TS
    // resumes at the next scalar boundary).
    chars.next();
    chars
}

/// Strip leading Default_Ignorable / Control / Format / Mark scalars, mirroring
/// the TS `leadingNonPrintingRegex` replace. Returns a `&str` slice.
fn strip_leading_non_printing(segment: &str) -> &str {
    let mut idx = 0;
    for (i, ch) in segment.char_indices() {
        if is_default_ignorable(ch) || ch.is_control() || is_format(ch) || is_mark(ch) {
            idx = i + ch.len_utf8();
        } else {
            return &segment[i..];
        }
    }
    &segment[idx..]
}

/// Unicode general-category Format (Cf). Covers the common format controls that
/// the TS `\p{Format}` class strips (joiners, bidi controls, BOM, etc.).
fn is_format(ch: char) -> bool {
    matches!(
        ch as u32,
        0x00AD
            | 0x0600..=0x0605
            | 0x061C
            | 0x06DD
            | 0x070F
            | 0x200B..=0x200F
            | 0x202A..=0x202E
            | 0x2060..=0x2064
            | 0x2066..=0x206F
            | 0xFEFF
            | 0xFFF9..=0xFFFB
            | 0xE0001
            | 0xE0020..=0xE007F
    )
}

// ===========================================================================
// ANSI stripping.
// ===========================================================================

/// Strip ANSI escapes (port of the TS `AnsiStripper`). When no ESC byte is
/// present the input is returned verbatim.
fn ansi_strip(text: &str) -> String {
    if !text.contains('\u{1b}') {
        return text.to_string();
    }
    // Walk the string with the same recogniser used by extract_ansi_code so the
    // observable output matches the TS chain of regex replaces.
    let mut out = String::with_capacity(text.len());
    let s: &str = text;
    // We need byte indexing for extract_ansi_code; iterate char boundaries.
    let mut indices: Vec<usize> = s.char_indices().map(|(i, _)| i).collect();
    indices.push(s.len());
    let mut ci = 0usize;
    while ci < indices.len() - 1 {
        let pos = indices[ci];
        if let Some((_code, len)) = extract_ansi_code(s, pos) {
            // Skip the whole escape.
            let end = pos + len;
            // Advance ci to the char index at byte offset `end`.
            while ci < indices.len() && indices[ci] < end {
                ci += 1;
            }
            continue;
        }
        // Copy this char verbatim.
        let ch_end = indices[ci + 1];
        out.push_str(&s[pos..ch_end]);
        ci += 1;
    }
    out
}

// ===========================================================================
// Width cache + visible_width.
// ===========================================================================

const WIDTH_CACHE_SIZE: usize = 512;

thread_local! {
    /// Memoization for [`visible_width`] over non-ASCII strings, mirroring the
    /// TS module-level `Map` with FIFO eviction at `WIDTH_CACHE_SIZE`. A
    /// `VecDeque` tracks insertion order so the oldest key is evicted first,
    /// exactly like iterating `widthCache.keys().next()`.
    static WIDTH_CACHE: RefCell<(HashMap<String, usize>, VecDeque<String>)> =
        RefCell::new((HashMap::new(), VecDeque::new()));
}

/// Compute how many columns a string occupies when rendered to a terminal
/// (port of TS `visibleWidth`).
pub fn visible_width(s: &str) -> usize {
    if s.is_empty() {
        return 0;
    }

    // Shortcut: a string made only of printable ASCII is exactly its byte length
    // wide. We test on bytes since printable ASCII is single-byte.
    let mut is_pure_ascii = true;
    for &b in s.as_bytes() {
        if !(0x20..=0x7e).contains(&b) {
            is_pure_ascii = false;
            break;
        }
    }
    if is_pure_ascii {
        return s.len();
    }

    // Return a previously computed width if we have one.
    if let Some(cached) = WIDTH_CACHE.with(|c| c.borrow().0.get(s).copied()) {
        return cached;
    }

    // Pre-process: expand tabs to three spaces and drop any ANSI escapes.
    let mut clean = if s.contains('\t') {
        s.replace('\t', "   ")
    } else {
        s.to_string()
    };
    clean = ansi_strip(&clean);

    // Sum the width of every grapheme cluster.
    let mut width = 0usize;
    for segment in UnicodeSegmentation::graphemes(clean.as_str(), true) {
        width += measure_cluster_width(segment);
    }

    // Store the result, evicting the oldest entry once the cache is full.
    WIDTH_CACHE.with(|c| {
        let (map, order) = &mut *c.borrow_mut();
        if map.len() >= WIDTH_CACHE_SIZE
            && let Some(first_key) = order.pop_front()
        {
            map.remove(&first_key);
        }
        if map.insert(s.to_string(), width).is_none() {
            order.push_back(s.to_string());
        }
    });

    width
}

// ===========================================================================
// extract_ansi_code.
// ===========================================================================

/// If an ANSI escape begins exactly at `pos`, return `(code, byte_length)`,
/// else `None` (port of TS `extractAnsiCode`). CSI is terminated by
/// `m/G/K/H/J`; OSC/APC by BEL or `ESC \`.
///
/// `pos` is a byte offset and must fall on a char boundary (always true for the
/// scanners in this module, which only call it at ESC bytes or text starts).
pub fn extract_ansi_code(s: &str, pos: usize) -> Option<(String, usize)> {
    let bytes = s.as_bytes();
    if pos >= bytes.len() || bytes[pos] != 0x1b {
        return None;
    }

    let next = bytes.get(pos + 1).copied();

    // CSI form: ESC [ ... terminated by one of m/G/K/H/J.
    if next == Some(b'[') {
        let mut j = pos + 2;
        while j < bytes.len() && !matches!(bytes[j], b'm' | b'G' | b'K' | b'H' | b'J') {
            j += 1;
        }
        if j < bytes.len() {
            let code = s[pos..=j].to_string();
            return Some((code, j + 1 - pos));
        }
        return None;
    }

    // OSC form: ESC ] ... closed by BEL or by ST (ESC \).
    if next == Some(b']') {
        let mut j = pos + 2;
        while j < bytes.len() {
            if bytes[j] == 0x07 {
                let code = s[pos..=j].to_string();
                return Some((code, j + 1 - pos));
            }
            if bytes[j] == 0x1b && bytes.get(j + 1) == Some(&b'\\') {
                let code = s[pos..pos + (j + 2 - pos)].to_string();
                return Some((code, j + 2 - pos));
            }
            j += 1;
        }
        return None;
    }

    // APC form: ESC _ ... closed by BEL or by ST (ESC \).
    if next == Some(b'_') {
        let mut j = pos + 2;
        while j < bytes.len() {
            if bytes[j] == 0x07 {
                let code = s[pos..=j].to_string();
                return Some((code, j + 1 - pos));
            }
            if bytes[j] == 0x1b && bytes.get(j + 1) == Some(&b'\\') {
                let code = s[pos..j + 2].to_string();
                return Some((code, j + 2 - pos));
            }
            j += 1;
        }
        return None;
    }

    None
}

// ===========================================================================
// Whitespace / punctuation classification.
// ===========================================================================

/// Whether the char counts as whitespace (port of TS `isWhitespaceChar`, which
/// tests `/\s/`). JS `\s` matches ASCII whitespace plus the Unicode space
/// separators, the BOM, and the line/paragraph separators.
pub fn is_whitespace_char(ch: char) -> bool {
    matches!(ch, ' ' | '\t' | '\n' | '\r' | '\x0b' | '\x0c')
        || matches!(
            ch as u32,
            0x00A0
                | 0x1680
                | 0x2000..=0x200A
                | 0x2028..=0x2029
                | 0x202F
                | 0x205F
                | 0x3000
                | 0xFEFF
        )
}

/// Punctuation set from the TS `PUNCTUATION_REGEX`:
/// `[(){}[]<>.,;:'"!?+\-=*/\\|&%^$#@~\`]`.
pub fn is_punctuation_char(ch: char) -> bool {
    matches!(
        ch,
        '(' | ')'
            | '{'
            | '}'
            | '['
            | ']'
            | '<'
            | '>'
            | '.'
            | ','
            | ';'
            | ':'
            | '\''
            | '"'
            | '!'
            | '?'
            | '+'
            | '-'
            | '='
            | '*'
            | '/'
            | '\\'
            | '|'
            | '&'
            | '%'
            | '^'
            | '$'
            | '#'
            | '@'
            | '~'
            | '`'
    )
}

// ===========================================================================
// AnsiStateTracker (port of the TS class).
// ===========================================================================

/// Remembers which SGR attributes are in force so styling can be re-emitted
/// after a line break (port of TS `AnsiStateTracker`). The `get_active_codes`
/// re-emit order is load-bearing for wrap byte-parity.
pub struct AnsiStateTracker {
    bold: bool,
    dim: bool,
    italic: bool,
    underline: bool,
    blink: bool,
    inverse: bool,
    hidden: bool,
    strikethrough: bool,
    fg: Option<String>,
    bg: Option<String>,
}

impl AnsiStateTracker {
    pub fn new() -> Self {
        Self {
            bold: false,
            dim: false,
            italic: false,
            underline: false,
            blink: false,
            inverse: false,
            hidden: false,
            strikethrough: false,
            fg: None,
            bg: None,
        }
    }

    /// Fold a single SGR code into the tracked state.
    pub fn process(&mut self, ansi_code: &str) {
        if !ansi_code.ends_with('m') {
            return;
        }

        // Pull out whatever sits between `\x1b[` and the terminating `m`.
        let params = match ansi_code
            .strip_prefix('\u{1b}')
            .and_then(|r| r.strip_prefix('['))
            .and_then(|r| r.strip_suffix('m'))
        {
            Some(p) => p,
            None => return,
        };
        // Only digits and semicolons are valid; anything else means no match
        // (mirroring the TS regex `[\d;]*`).
        if !params.chars().all(|c| c.is_ascii_digit() || c == ';') {
            return;
        }

        if params.is_empty() || params == "0" {
            self.reset();
            return;
        }

        let parts: Vec<&str> = params.split(';').collect();
        let mut i = 0;
        while i < parts.len() {
            let code: i64 = match parts[i].parse() {
                Ok(n) => n,
                Err(_) => {
                    i += 1;
                    continue;
                }
            };

            let consumed = self.try_consume_color_code(&parts, i, code);
            if consumed > 0 {
                i += consumed;
                continue;
            }

            self.apply_standard_code(code);
            i += 1;
        }
    }

    fn try_consume_color_code(&mut self, parts: &[&str], index: usize, code: i64) -> usize {
        if code != 38 && code != 48 {
            return 0;
        }

        if parts.get(index + 1) == Some(&"5") && parts.get(index + 2).is_some() {
            let color_code = format!("{};{};{}", parts[index], parts[index + 1], parts[index + 2]);
            if code == 38 {
                self.fg = Some(color_code);
            } else {
                self.bg = Some(color_code);
            }
            return 3;
        }

        if parts.get(index + 1) == Some(&"2") && parts.get(index + 4).is_some() {
            let color_code = format!(
                "{};{};{};{};{}",
                parts[index],
                parts[index + 1],
                parts[index + 2],
                parts[index + 3],
                parts[index + 4]
            );
            if code == 38 {
                self.fg = Some(color_code);
            } else {
                self.bg = Some(color_code);
            }
            return 5;
        }

        0
    }

    fn apply_standard_code(&mut self, code: i64) {
        match code {
            0 => self.reset(),
            1 => self.bold = true,
            2 => self.dim = true,
            3 => self.italic = true,
            4 => self.underline = true,
            5 => self.blink = true,
            7 => self.inverse = true,
            8 => self.hidden = true,
            9 => self.strikethrough = true,
            21 => self.bold = false,
            22 => {
                self.bold = false;
                self.dim = false;
            }
            23 => self.italic = false,
            24 => self.underline = false,
            25 => self.blink = false,
            27 => self.inverse = false,
            28 => self.hidden = false,
            29 => self.strikethrough = false,
            39 => self.fg = None,
            49 => self.bg = None,
            _ => {
                if (30..=37).contains(&code) || (90..=97).contains(&code) {
                    self.fg = Some(code.to_string());
                } else if (40..=47).contains(&code) || (100..=107).contains(&code) {
                    self.bg = Some(code.to_string());
                }
            }
        }
    }

    /// Clear all tracked state.
    pub fn reset(&mut self) {
        self.bold = false;
        self.dim = false;
        self.italic = false;
        self.underline = false;
        self.blink = false;
        self.inverse = false;
        self.hidden = false;
        self.strikethrough = false;
        self.fg = None;
        self.bg = None;
    }

    /// Alias for [`AnsiStateTracker::reset`] so the instance can be reused.
    pub fn clear(&mut self) {
        self.reset();
    }

    /// Re-emit the active attributes as an SGR string in the fixed order.
    pub fn get_active_codes(&self) -> String {
        let mut codes: Vec<String> = Vec::new();
        if self.bold {
            codes.push("1".to_string());
        }
        if self.dim {
            codes.push("2".to_string());
        }
        if self.italic {
            codes.push("3".to_string());
        }
        if self.underline {
            codes.push("4".to_string());
        }
        if self.blink {
            codes.push("5".to_string());
        }
        if self.inverse {
            codes.push("7".to_string());
        }
        if self.hidden {
            codes.push("8".to_string());
        }
        if self.strikethrough {
            codes.push("9".to_string());
        }
        if let Some(fg) = &self.fg {
            codes.push(fg.clone());
        }
        if let Some(bg) = &self.bg {
            codes.push(bg.clone());
        }

        if codes.is_empty() {
            return String::new();
        }
        format!("\u{1b}[{}m", codes.join(";"))
    }

    /// Whether any attribute is currently active.
    pub fn has_active_codes(&self) -> bool {
        self.bold
            || self.dim
            || self.italic
            || self.underline
            || self.blink
            || self.inverse
            || self.hidden
            || self.strikethrough
            || self.fg.is_some()
            || self.bg.is_some()
    }

    /// The escape needed to switch off attributes that would bleed into padding
    /// (in practice just underline → `\x1b[24m`); empty when none.
    pub fn get_line_end_reset(&self) -> String {
        if self.underline {
            return "\u{1b}[24m".to_string();
        }
        String::new()
    }
}

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

/// Feed `text` (interleaved ANSI codes + visible chars) through `tracker`,
/// folding every SGR code into its state (port of TS `mergeTextIntoTracker`).
fn merge_text_into_tracker(text: &str, tracker: &mut AnsiStateTracker) {
    let mut i = 0;
    while i < text.len() {
        if let Some((code, len)) = extract_ansi_code(text, i) {
            tracker.process(&code);
            i += len;
        } else {
            // Advance by one char (byte index must stay on a boundary).
            let ch_len = text[i..].chars().next().map(|c| c.len_utf8()).unwrap_or(1);
            i += ch_len;
        }
    }
}

// ===========================================================================
// AnsiCodeManager / TextWrapper facades.
// ===========================================================================

/// Mutable-borrow facade over an [`AnsiStateTracker`] (≈ TS `AnsiCodeManager`).
pub struct AnsiCodeManager {
    tracker: AnsiStateTracker,
}

impl AnsiCodeManager {
    pub fn new() -> Self {
        Self {
            tracker: AnsiStateTracker::new(),
        }
    }
    pub fn process(&mut self, ansi_code: &str) {
        self.tracker.process(ansi_code);
    }
    pub fn to_ansi_string(&self) -> String {
        self.tracker.get_active_codes()
    }
    pub fn reset(&mut self) {
        self.tracker.reset();
    }
}

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

/// Word-wrap facade over [`wrap_text_with_ansi`] (≈ TS `TextWrapper`).
pub struct TextWrapper;

impl TextWrapper {
    pub fn wrap(&self, text: &str, width: usize) -> Vec<String> {
        wrap_text_with_ansi(text, width)
    }
}

// ===========================================================================
// Tokenization + wrapping (ports of tokenizeTextWithAnsi / wrapTextWithAnsi).
// ===========================================================================

/// Break text into word tokens, carrying ANSI codes along with the visible
/// characters they decorate (port of TS `tokenizeTextWithAnsi`).
fn tokenize_text_with_ansi(text: &str) -> Vec<String> {
    let mut tokens: Vec<String> = Vec::new();
    let mut current = String::new();
    let mut pending_ansi = String::new();
    let mut in_whitespace = false;
    let mut i = 0;

    while i < text.len() {
        if let Some((code, len)) = extract_ansi_code(text, i) {
            pending_ansi.push_str(&code);
            i += len;
            continue;
        }

        let ch = text[i..].chars().next().unwrap();
        let ch_len = ch.len_utf8();
        let char_is_space = ch == ' ';

        if char_is_space != in_whitespace && !current.is_empty() {
            tokens.push(std::mem::take(&mut current));
        }

        if !pending_ansi.is_empty() {
            current.push_str(&pending_ansi);
            pending_ansi.clear();
        }

        in_whitespace = char_is_space;
        current.push(ch);
        i += ch_len;
    }

    if !pending_ansi.is_empty() {
        current.push_str(&pending_ansi);
    }

    if !current.is_empty() {
        tokens.push(current);
    }

    tokens
}

/// Word-wrap text without losing ANSI styling (port of TS `wrapTextWithAnsi`).
///
/// Splits on `\n`, carries SGR state across literal newlines, re-emits active
/// codes as a per-line prefix from the 2nd line on, `trim_end`s each wrapped
/// line, and only emits `\x1b[24m` (underline-off) at line ends. Empty input →
/// `[""]`.
pub fn wrap_text_with_ansi(text: &str, width: usize) -> Vec<String> {
    if text.is_empty() {
        return vec![String::new()];
    }

    let input_lines: Vec<&str> = text.split('\n').collect();
    let mut result: Vec<String> = Vec::new();
    let mut tracker = AnsiStateTracker::new();

    for input_line in input_lines {
        let prefix = if !result.is_empty() {
            tracker.get_active_codes()
        } else {
            String::new()
        };
        let combined = format!("{prefix}{input_line}");
        result.extend(wrap_line_preserving_ansi(&combined, width));
        merge_text_into_tracker(input_line, &mut tracker);
    }

    if result.is_empty() {
        vec![String::new()]
    } else {
        result
    }
}

struct TokenWrapRunResult {
    wrapped: Vec<String>,
    current_line: String,
}

/// Walk `tokens`, greedily packing them into lines of visible width ≤ `width`
/// while keeping `tracker` in sync (port of TS `TokenWrapEngine.wrap`).
fn token_wrap(
    tokens: &[String],
    width: usize,
    tracker: &mut AnsiStateTracker,
) -> TokenWrapRunResult {
    let mut wrapped: Vec<String> = Vec::new();
    let mut current_line = String::new();
    let mut current_visible_length = 0usize;

    for token in tokens {
        let token_visible_length = visible_width(token);
        let is_whitespace = token.trim().is_empty();

        // A single non-space token wider than the limit must be chopped up.
        if token_visible_length > width && !is_whitespace {
            if !current_line.is_empty() {
                let line_end_reset = tracker.get_line_end_reset();
                if !line_end_reset.is_empty() {
                    current_line.push_str(&line_end_reset);
                }
                wrapped.push(std::mem::take(&mut current_line));
                current_visible_length = 0;
            }

            let broken = split_long_token(token, width, tracker);
            // push all but last
            if !broken.is_empty() {
                let last = broken.len() - 1;
                wrapped.extend_from_slice(&broken[..last]);
                current_line = broken[last].clone();
                current_visible_length = visible_width(&current_line);
            }
            continue;
        }

        let total_needed = current_visible_length + token_visible_length;
        if total_needed > width && current_visible_length > 0 {
            let mut line_to_wrap = current_line.trim_end().to_string();
            let line_end_reset = tracker.get_line_end_reset();
            if !line_end_reset.is_empty() {
                line_to_wrap.push_str(&line_end_reset);
            }
            wrapped.push(line_to_wrap);
            if is_whitespace {
                current_line = tracker.get_active_codes();
                current_visible_length = 0;
            } else {
                current_line = format!("{}{}", tracker.get_active_codes(), token);
                current_visible_length = token_visible_length;
            }
        } else {
            current_line.push_str(token);
            current_visible_length += token_visible_length;
        }

        merge_text_into_tracker(token, tracker);
    }

    TokenWrapRunResult {
        wrapped,
        current_line,
    }
}

/// Wrap a single (already newline-free) line, preserving ANSI styling (port of
/// TS `wrapLinePreservingAnsi`).
fn wrap_line_preserving_ansi(line: &str, width: usize) -> Vec<String> {
    if line.is_empty() {
        return vec![String::new()];
    }

    let visible_length = visible_width(line);
    if visible_length <= width {
        return vec![line.to_string()];
    }

    let mut tracker = AnsiStateTracker::new();
    let tokens = tokenize_text_with_ansi(line);
    let TokenWrapRunResult {
        mut wrapped,
        current_line,
    } = token_wrap(&tokens, width, &mut tracker);

    if !current_line.is_empty() {
        wrapped.push(current_line);
    }

    if wrapped.is_empty() {
        vec![String::new()]
    } else {
        wrapped
            .into_iter()
            .map(|seg| seg.trim_end().to_string())
            .collect()
    }
}

/// Hard-break a single token wider than `width` into ANSI-aware grapheme lines
/// (port of TS `splitLongToken`).
fn split_long_token(word: &str, width: usize, tracker: &mut AnsiStateTracker) -> Vec<String> {
    let mut lines: Vec<String> = Vec::new();
    let mut current_line = tracker.get_active_codes();
    let mut current_width = 0usize;

    // Pass one: pull the word apart into ANSI codes and visible graphemes.
    enum Seg {
        Ansi(String),
        Grapheme(String),
    }
    let mut segments: Vec<Seg> = Vec::new();

    let mut i = 0;
    while i < word.len() {
        if let Some((code, len)) = extract_ansi_code(word, i) {
            segments.push(Seg::Ansi(code));
            i += len;
        } else {
            // Advance to wherever the next ANSI code starts, or to the end.
            let mut end = i;
            while end < word.len() {
                if extract_ansi_code(word, end).is_some() {
                    break;
                }
                let ch_len = word[end..]
                    .chars()
                    .next()
                    .map(|c| c.len_utf8())
                    .unwrap_or(1);
                end += ch_len;
            }
            let text_portion = &word[i..end];
            for seg in UnicodeSegmentation::graphemes(text_portion, true) {
                segments.push(Seg::Grapheme(seg.to_string()));
            }
            i = end;
        }
    }

    // Pass two: assemble the wrapped lines.
    for seg in segments {
        match seg {
            Seg::Ansi(code) => {
                current_line.push_str(&code);
                tracker.process(&code);
            }
            Seg::Grapheme(grapheme) => {
                if grapheme.is_empty() {
                    continue;
                }
                let cluster_width = visible_width(&grapheme);

                if current_width + cluster_width > width {
                    let line_end_reset = tracker.get_line_end_reset();
                    if !line_end_reset.is_empty() {
                        current_line.push_str(&line_end_reset);
                    }
                    lines.push(std::mem::take(&mut current_line));
                    current_line = tracker.get_active_codes();
                    current_width = 0;
                }

                current_line.push_str(&grapheme);
                current_width += cluster_width;
            }
        }
    }

    if !current_line.is_empty() {
        lines.push(current_line);
    }

    if lines.is_empty() {
        vec![String::new()]
    } else {
        lines
    }
}

// ===========================================================================
// Background pad, truncate.
// ===========================================================================

/// Pad a line out to `width`, then run a background-color function over the
/// whole span so the colour fills every column (port of TS
/// `applyBackgroundToLine`).
pub fn apply_background_to_line(
    line: &str,
    width: usize,
    bg_fn: impl Fn(&str) -> String,
) -> String {
    let visible_len = visible_width(line);
    let padding_needed = width.saturating_sub(visible_len);
    let padding = " ".repeat(padding_needed);
    let with_padding = format!("{line}{padding}");
    bg_fn(&with_padding)
}

/// Shorten text so its visible width stays within `max_width`, appending
/// `ellipsis` (default `"..."`) when truncation happens, optionally padding out
/// to exactly `max_width` (port of TS `truncateToWidth`). On truncation the
/// output is `result + "\x1b[0m" + ellipsis`; when the ellipsis itself is wider
/// than `max_width`, returns `ellipsis[..max_width]` (by visible columns).
pub fn truncate_to_width(text: &str, max_width: usize, ellipsis: &str, pad: bool) -> String {
    let text_visible_width = visible_width(text);

    if text_visible_width <= max_width {
        return if pad {
            format!("{text}{}", " ".repeat(max_width - text_visible_width))
        } else {
            text.to_string()
        };
    }

    let ellipsis_width = visible_width(ellipsis);
    // TS `maxWidth - ellipsisWidth` can go negative; we detect that as
    // `target_width <= 0` via signed arithmetic.
    let target_width_signed = max_width as i64 - ellipsis_width as i64;

    if target_width_signed <= 0 {
        // TS `ellipsis.substring(0, maxWidth)` slices by UTF-16 code units; for
        // the ASCII ellipses used here that equals the first `max_width` chars.
        return substring_utf16(ellipsis, max_width);
    }
    let target_width = target_width_signed as usize;

    // Split into ANSI codes and grapheme clusters, preserving their order.
    enum Seg {
        Ansi(String),
        Grapheme(String),
    }
    let mut segments: Vec<Seg> = Vec::new();
    let mut i = 0;
    while i < text.len() {
        if let Some((code, len)) = extract_ansi_code(text, i) {
            segments.push(Seg::Ansi(code));
            i += len;
        } else {
            let mut end = i;
            while end < text.len() {
                if extract_ansi_code(text, end).is_some() {
                    break;
                }
                let ch_len = text[end..]
                    .chars()
                    .next()
                    .map(|c| c.len_utf8())
                    .unwrap_or(1);
                end += ch_len;
            }
            let text_portion = &text[i..end];
            for seg in UnicodeSegmentation::graphemes(text_portion, true) {
                segments.push(Seg::Grapheme(seg.to_string()));
            }
            i = end;
        }
    }

    let mut result = String::new();
    let mut current_width = 0usize;

    for seg in segments {
        match seg {
            Seg::Ansi(code) => result.push_str(&code),
            Seg::Grapheme(grapheme) => {
                if grapheme.is_empty() {
                    continue;
                }
                let cluster_width = visible_width(&grapheme);
                if current_width + cluster_width > target_width {
                    break;
                }
                result.push_str(&grapheme);
                current_width += cluster_width;
            }
        }
    }

    let truncated = format!("{result}\u{1b}[0m{ellipsis}");
    if pad {
        let truncated_width = visible_width(&truncated);
        let pad_n = max_width.saturating_sub(truncated_width);
        format!("{truncated}{}", " ".repeat(pad_n))
    } else {
        truncated
    }
}

/// `str.substring(0, n)` semantics over UTF-16 code units (the ellipses passed
/// here are ASCII, so this also equals the first `n` bytes for them).
fn substring_utf16(s: &str, n: usize) -> String {
    let mut out = String::new();
    let mut units = 0usize;
    for ch in s.chars() {
        let ch_units = ch.encode_utf16(&mut [0u16; 2]).len();
        if units + ch_units > n {
            break;
        }
        out.push(ch);
        units += ch_units;
    }
    out
}

// ===========================================================================
// Column slicing: slice_by_column / slice_with_width / extract_segments.
// ===========================================================================

/// Cut out a span of visible columns from a line, coping with ANSI codes and
/// double-width chars (port of TS `sliceByColumn`). `strict` excludes a wide
/// char straddling the far edge.
pub fn slice_by_column(line: &str, start_col: usize, length: usize, strict: bool) -> String {
    slice_with_width(line, start_col, length, strict).text
}

/// The text + its visible width, produced by a column slice
/// (return shape of [`slice_with_width`]).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SliceWithWidth {
    pub text: String,
    pub width: usize,
}

#[derive(Clone, Copy, PartialEq)]
enum AnsiScanState {
    Text,
    AnsiCsi,
    AnsiOsc,
}

/// Like [`slice_by_column`] but also reports the visible width of the result
/// (port of TS `sliceWithWidth`). Uses the same explicit state machine.
pub fn slice_with_width(
    line: &str,
    start_col: usize,
    length: usize,
    strict: bool,
) -> SliceWithWidth {
    if length == 0 {
        return SliceWithWidth {
            text: String::new(),
            width: 0,
        };
    }

    let stop_col = start_col + length;

    let mut picked: Vec<String> = Vec::new();
    let mut picked_width = 0usize;
    let mut column = 0usize;
    let mut carried_style = String::new();

    let bytes = line.as_bytes();
    let mut state = AnsiScanState::Text;
    let mut esc_start = 0usize;
    let mut text_buf_start = 0usize;
    let mut reached_end = false;

    // Helper to flush a plain-text byte range as grapheme clusters.
    // Returns true if the caller should stop scanning.
    let flush_text_buf = |end_exclusive: usize,
                          text_buf_start: &mut usize,
                          column: &mut usize,
                          picked: &mut Vec<String>,
                          picked_width: &mut usize,
                          carried_style: &mut String|
     -> bool {
        if *text_buf_start >= end_exclusive {
            return false;
        }
        let chunk = &line[*text_buf_start..end_exclusive];
        *text_buf_start = end_exclusive;
        for segment in UnicodeSegmentation::graphemes(chunk, true) {
            let cell_width = measure_cluster_width(segment);
            let inside = *column >= start_col && *column < stop_col;
            let accepted = inside && (!strict || *column + cell_width <= stop_col);
            if accepted {
                if !carried_style.is_empty() {
                    picked.push(std::mem::take(carried_style));
                }
                picked.push(segment.to_string());
                *picked_width += cell_width;
            }
            *column += cell_width;
            if *column >= stop_col {
                return true;
            }
        }
        false
    };

    let emit_escape =
        |code: String, column: usize, picked: &mut Vec<String>, carried_style: &mut String| {
            let inside = column >= start_col && column < stop_col;
            if inside {
                picked.push(code);
            } else if column < start_col {
                carried_style.push_str(&code);
            }
        };

    let mut cursor = 0usize;
    while cursor < bytes.len() {
        let ch = bytes[cursor];

        match state {
            AnsiScanState::Text => {
                if ch == 0x1b {
                    let next = bytes.get(cursor + 1).copied();
                    if next == Some(b'[') {
                        if flush_text_buf(
                            cursor,
                            &mut text_buf_start,
                            &mut column,
                            &mut picked,
                            &mut picked_width,
                            &mut carried_style,
                        ) {
                            reached_end = true;
                            break;
                        }
                        state = AnsiScanState::AnsiCsi;
                        esc_start = cursor;
                        cursor += 2;
                        continue;
                    }
                    if next == Some(b']') || next == Some(b'_') {
                        if flush_text_buf(
                            cursor,
                            &mut text_buf_start,
                            &mut column,
                            &mut picked,
                            &mut picked_width,
                            &mut carried_style,
                        ) {
                            reached_end = true;
                            break;
                        }
                        state = AnsiScanState::AnsiOsc;
                        esc_start = cursor;
                        cursor += 2;
                        continue;
                    }
                }
                // Advance by a full char to stay on UTF-8 boundaries.
                cursor += char_len_at(bytes, cursor);
            }
            AnsiScanState::AnsiCsi => {
                if matches!(ch, b'm' | b'G' | b'K' | b'H' | b'J') {
                    emit_escape(
                        line[esc_start..cursor + 1].to_string(),
                        column,
                        &mut picked,
                        &mut carried_style,
                    );
                    text_buf_start = cursor + 1;
                    state = AnsiScanState::Text;
                }
                cursor += 1;
            }
            AnsiScanState::AnsiOsc => {
                if ch == 0x07 {
                    emit_escape(
                        line[esc_start..cursor + 1].to_string(),
                        column,
                        &mut picked,
                        &mut carried_style,
                    );
                    text_buf_start = cursor + 1;
                    state = AnsiScanState::Text;
                    cursor += 1;
                } else if ch == 0x1b && bytes.get(cursor + 1) == Some(&b'\\') {
                    emit_escape(
                        line[esc_start..cursor + 2].to_string(),
                        column,
                        &mut picked,
                        &mut carried_style,
                    );
                    text_buf_start = cursor + 2;
                    state = AnsiScanState::Text;
                    cursor += 2;
                } else {
                    cursor += char_len_at(bytes, cursor);
                }
            }
        }
    }

    if !reached_end && state == AnsiScanState::Text {
        flush_text_buf(
            line.len(),
            &mut text_buf_start,
            &mut column,
            &mut picked,
            &mut picked_width,
            &mut carried_style,
        );
    }

    SliceWithWidth {
        text: picked.concat(),
        width: picked_width,
    }
}

/// Before/after spans and their widths, produced by [`extract_segments`]
/// (port of the TS `{ before, beforeWidth, after, afterWidth }` shape).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Segments {
    pub before: String,
    pub before_width: usize,
    pub after: String,
    pub after_width: usize,
}

/// In one traversal, pull out the text before an overlay region and the text
/// after it, carrying styling established before the overlay into the "after"
/// run (port of TS `extractSegments`). Feeds overlay compositing.
pub fn extract_segments(
    line: &str,
    before_end: usize,
    after_start: usize,
    after_len: usize,
    strict_after: bool,
) -> Segments {
    let after_end = after_start + after_len;
    let has_after = after_len > 0;
    let finish_col = if has_after { after_end } else { before_end };

    let mut before_parts: Vec<String> = Vec::new();
    let mut before_width = 0usize;
    let mut after_parts: Vec<String> = Vec::new();
    let mut after_width = 0usize;
    let mut held_before_style = String::new();
    let mut after_opened = false;

    // The pooled tracker in TS is a module-level singleton; here a fresh local
    // tracker has identical observable behaviour (it is `clear()`-ed per call).
    let mut style_tracker = AnsiStateTracker::new();

    let mut column = 0usize;
    let bytes = line.as_bytes();
    let mut state = AnsiScanState::Text;
    let mut esc_start = 0usize;
    let mut text_buf_start = 0usize;
    let mut reached_end = false;

    let mut cursor = 0usize;
    while cursor < bytes.len() {
        let ch = bytes[cursor];

        match state {
            AnsiScanState::Text => {
                if ch == 0x1b {
                    let next = bytes.get(cursor + 1).copied();
                    if next == Some(b'[') {
                        if flush_text_buf_extract(
                            line,
                            cursor,
                            &mut text_buf_start,
                            &mut column,
                            before_end,
                            after_start,
                            after_end,
                            finish_col,
                            strict_after,
                            &mut before_parts,
                            &mut before_width,
                            &mut after_parts,
                            &mut after_width,
                            &mut held_before_style,
                            &mut after_opened,
                            &style_tracker,
                        ) {
                            reached_end = true;
                            break;
                        }
                        state = AnsiScanState::AnsiCsi;
                        esc_start = cursor;
                        cursor += 2;
                        continue;
                    }
                    if next == Some(b']') || next == Some(b'_') {
                        if flush_text_buf_extract(
                            line,
                            cursor,
                            &mut text_buf_start,
                            &mut column,
                            before_end,
                            after_start,
                            after_end,
                            finish_col,
                            strict_after,
                            &mut before_parts,
                            &mut before_width,
                            &mut after_parts,
                            &mut after_width,
                            &mut held_before_style,
                            &mut after_opened,
                            &style_tracker,
                        ) {
                            reached_end = true;
                            break;
                        }
                        state = AnsiScanState::AnsiOsc;
                        esc_start = cursor;
                        cursor += 2;
                        continue;
                    }
                }
                cursor += char_len_at(bytes, cursor);
            }
            AnsiScanState::AnsiCsi => {
                if matches!(ch, b'm' | b'G' | b'K' | b'H' | b'J') {
                    emit_escape_extract(
                        line[esc_start..cursor + 1].to_string(),
                        column,
                        before_end,
                        after_start,
                        after_end,
                        after_opened,
                        &mut held_before_style,
                        &mut after_parts,
                        &mut style_tracker,
                    );
                    text_buf_start = cursor + 1;
                    state = AnsiScanState::Text;
                }
                cursor += 1;
            }
            AnsiScanState::AnsiOsc => {
                if ch == 0x07 {
                    emit_escape_extract(
                        line[esc_start..cursor + 1].to_string(),
                        column,
                        before_end,
                        after_start,
                        after_end,
                        after_opened,
                        &mut held_before_style,
                        &mut after_parts,
                        &mut style_tracker,
                    );
                    text_buf_start = cursor + 1;
                    state = AnsiScanState::Text;
                    cursor += 1;
                } else if ch == 0x1b && bytes.get(cursor + 1) == Some(&b'\\') {
                    emit_escape_extract(
                        line[esc_start..cursor + 2].to_string(),
                        column,
                        before_end,
                        after_start,
                        after_end,
                        after_opened,
                        &mut held_before_style,
                        &mut after_parts,
                        &mut style_tracker,
                    );
                    text_buf_start = cursor + 2;
                    state = AnsiScanState::Text;
                    cursor += 2;
                } else {
                    cursor += char_len_at(bytes, cursor);
                }
            }
        }
    }

    if !reached_end && state == AnsiScanState::Text {
        flush_text_buf_extract(
            line,
            line.len(),
            &mut text_buf_start,
            &mut column,
            before_end,
            after_start,
            after_end,
            finish_col,
            strict_after,
            &mut before_parts,
            &mut before_width,
            &mut after_parts,
            &mut after_width,
            &mut held_before_style,
            &mut after_opened,
            &style_tracker,
        );
    }

    Segments {
        before: before_parts.concat(),
        before_width,
        after: after_parts.concat(),
        after_width,
    }
}

#[allow(clippy::too_many_arguments)]
fn flush_text_buf_extract(
    line: &str,
    end_exclusive: usize,
    text_buf_start: &mut usize,
    column: &mut usize,
    before_end: usize,
    after_start: usize,
    after_end: usize,
    finish_col: usize,
    strict_after: bool,
    before_parts: &mut Vec<String>,
    before_width: &mut usize,
    after_parts: &mut Vec<String>,
    after_width: &mut usize,
    held_before_style: &mut String,
    after_opened: &mut bool,
    style_tracker: &AnsiStateTracker,
) -> bool {
    if *text_buf_start >= end_exclusive {
        return false;
    }
    let chunk = &line[*text_buf_start..end_exclusive];
    *text_buf_start = end_exclusive;
    for segment in UnicodeSegmentation::graphemes(chunk, true) {
        let cell_width = measure_cluster_width(segment);
        let in_before = *column < before_end;
        let in_after = *column >= after_start && *column < after_end;
        if in_before {
            if !held_before_style.is_empty() {
                before_parts.push(std::mem::take(held_before_style));
            }
            before_parts.push(segment.to_string());
            *before_width += cell_width;
        } else if in_after && (!strict_after || *column + cell_width <= after_end) {
            if !*after_opened {
                after_parts.push(style_tracker.get_active_codes());
                *after_opened = true;
            }
            after_parts.push(segment.to_string());
            *after_width += cell_width;
        }
        *column += cell_width;
        if *column >= finish_col {
            return true;
        }
    }
    false
}

#[allow(clippy::too_many_arguments)]
fn emit_escape_extract(
    code: String,
    column: usize,
    before_end: usize,
    after_start: usize,
    after_end: usize,
    after_opened: bool,
    held_before_style: &mut String,
    after_parts: &mut Vec<String>,
    style_tracker: &mut AnsiStateTracker,
) {
    style_tracker.process(&code);
    let in_before = column < before_end;
    let in_after = column >= after_start && column < after_end;
    if in_before {
        held_before_style.push_str(&code);
    } else if after_opened && in_after {
        after_parts.push(code);
    }
}

/// Byte length of the UTF-8 char starting at `bytes[i]` (1 for invalid lead).
fn char_len_at(bytes: &[u8], i: usize) -> usize {
    let b = bytes[i];
    if b < 0x80 {
        1
    } else if b >> 5 == 0b110 {
        2
    } else if b >> 4 == 0b1110 {
        3
    } else if b >> 3 == 0b11110 {
        4
    } else {
        1
    }
}

// ===========================================================================
// Tests (mirror the TS behaviors of src/ui/utils.ts).
// ===========================================================================

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

    // -------------------------------------------------------------------
    // visible_width.
    // -------------------------------------------------------------------

    #[test]
    fn empty_string_is_zero_width() {
        assert_eq!(visible_width(""), 0);
    }

    #[test]
    fn pure_ascii_is_byte_length() {
        assert_eq!(visible_width("hello"), 5);
        assert_eq!(visible_width("hello world!"), 12);
        assert_eq!(visible_width(" "), 1);
        assert_eq!(visible_width("~"), 1);
    }

    #[test]
    fn tabs_expand_to_three_spaces() {
        // Pure-ASCII fast path is bypassed because of the non-printable tab byte;
        // a tab counts as 3 columns.
        assert_eq!(visible_width("a\tb"), 1 + 3 + 1);
        assert_eq!(visible_width("\t"), 3);
    }

    #[test]
    fn wide_cjk_is_two_columns_each() {
        // Each fullwidth ideograph occupies two terminal columns.
        assert_eq!(visible_width("中"), 2);
        assert_eq!(visible_width("中文"), 4);
        assert_eq!(visible_width("a中b"), 1 + 2 + 1);
    }

    #[test]
    fn fullwidth_forms_are_two_columns() {
        // Fullwidth exclamation mark (U+FF01).
        assert_eq!(visible_width("！"), 2);
    }

    #[test]
    fn combining_marks_add_no_width() {
        // "e" + combining acute accent renders in one column.
        assert_eq!(visible_width("e\u{0301}"), 1);
    }

    #[test]
    fn zero_width_space_is_zero() {
        assert_eq!(visible_width("\u{200B}"), 0);
    }

    #[test]
    fn ansi_codes_do_not_count_toward_width() {
        // SGR styling around the text must not be measured.
        assert_eq!(visible_width("\u{1b}[31mred\u{1b}[0m"), 3);
        assert_eq!(visible_width("\u{1b}[1;32mhi\u{1b}[0m"), 2);
    }

    #[test]
    fn osc8_hyperlink_wrappers_are_stripped() {
        let s = "\u{1b}]8;;http://x\u{07}link\u{1b}]8;;\u{07}";
        assert_eq!(visible_width(s), 4);
    }

    #[test]
    fn emoji_is_two_columns() {
        // A basic pictographic emoji.
        assert_eq!(visible_width("😀"), 2);
        // VS16-presentation emoji.
        assert_eq!(visible_width("\u{2764}\u{FE0F}"), 2);
    }

    #[test]
    fn width_cache_returns_consistent_results() {
        let s = "中文混合abc";
        let first = visible_width(s);
        let second = visible_width(s);
        assert_eq!(first, second);
        TextWidthCalculator.clear_cache();
        assert_eq!(visible_width(s), first);
    }

    // -------------------------------------------------------------------
    // extract_ansi_code.
    // -------------------------------------------------------------------

    #[test]
    fn extract_ansi_csi_sgr() {
        let s = "\u{1b}[31mX";
        let got = extract_ansi_code(s, 0).unwrap();
        assert_eq!(got.0, "\u{1b}[31m");
        assert_eq!(got.1, 5);
    }

    #[test]
    fn extract_ansi_returns_none_off_escape() {
        assert!(extract_ansi_code("abc", 0).is_none());
        assert!(extract_ansi_code("abc", 1).is_none());
    }

    #[test]
    fn extract_ansi_osc_bel_terminated() {
        let s = "\u{1b}]8;;url\u{07}rest";
        let got = extract_ansi_code(s, 0).unwrap();
        assert_eq!(got.0, "\u{1b}]8;;url\u{07}");
        assert_eq!(got.1, "\u{1b}]8;;url\u{07}".len());
    }

    #[test]
    fn extract_ansi_osc_st_terminated() {
        let s = "\u{1b}]0;title\u{1b}\\rest";
        let got = extract_ansi_code(s, 0).unwrap();
        assert_eq!(got.0, "\u{1b}]0;title\u{1b}\\");
    }

    #[test]
    fn extract_ansi_apc_terminated() {
        let s = "\u{1b}_payload\u{07}x";
        let got = extract_ansi_code(s, 0).unwrap();
        assert_eq!(got.0, "\u{1b}_payload\u{07}");
    }

    #[test]
    fn extract_ansi_unterminated_csi_is_none() {
        assert!(extract_ansi_code("\u{1b}[31", 0).is_none());
    }

    // -------------------------------------------------------------------
    // is_whitespace_char / is_punctuation_char.
    // -------------------------------------------------------------------

    #[test]
    fn whitespace_classification() {
        assert!(is_whitespace_char(' '));
        assert!(is_whitespace_char('\t'));
        assert!(is_whitespace_char('\n'));
        assert!(is_whitespace_char('\u{00A0}'));
        assert!(!is_whitespace_char('a'));
        assert!(!is_whitespace_char('-'));
    }

    #[test]
    fn punctuation_classification() {
        for c in [
            '(', ')', '.', ',', '!', '?', '-', '/', '\\', '@', '~', '`', '#',
        ] {
            assert!(is_punctuation_char(c), "expected {c:?} to be punctuation");
        }
        assert!(!is_punctuation_char('a'));
        assert!(!is_punctuation_char(' '));
        assert!(!is_punctuation_char('0'));
    }

    // -------------------------------------------------------------------
    // AnsiStateTracker.
    // -------------------------------------------------------------------

    #[test]
    fn tracker_records_and_reemits_in_fixed_order() {
        let mut t = AnsiStateTracker::new();
        // Apply bold + underline + a 256-color fg out of order; re-emit order is
        // fixed: bold(1), underline(4), fg.
        t.process("\u{1b}[4m");
        t.process("\u{1b}[1m");
        t.process("\u{1b}[38;5;200m");
        assert_eq!(t.get_active_codes(), "\u{1b}[1;4;38;5;200m");
        assert!(t.has_active_codes());
    }

    #[test]
    fn tracker_reset_via_zero() {
        let mut t = AnsiStateTracker::new();
        t.process("\u{1b}[1m");
        assert!(t.has_active_codes());
        t.process("\u{1b}[0m");
        assert!(!t.has_active_codes());
        assert_eq!(t.get_active_codes(), "");
    }

    #[test]
    fn tracker_empty_params_resets() {
        let mut t = AnsiStateTracker::new();
        t.process("\u{1b}[1m");
        t.process("\u{1b}[m");
        assert!(!t.has_active_codes());
    }

    #[test]
    fn tracker_bold_dim_off_codes() {
        let mut t = AnsiStateTracker::new();
        t.process("\u{1b}[1m");
        t.process("\u{1b}[2m");
        // 22 turns off both bold and dim.
        t.process("\u{1b}[22m");
        assert!(!t.has_active_codes());
    }

    #[test]
    fn tracker_truecolor_fg() {
        let mut t = AnsiStateTracker::new();
        t.process("\u{1b}[38;2;10;20;30m");
        assert_eq!(t.get_active_codes(), "\u{1b}[38;2;10;20;30m");
    }

    #[test]
    fn tracker_standard_colors() {
        let mut t = AnsiStateTracker::new();
        t.process("\u{1b}[31m"); // red fg
        t.process("\u{1b}[44m"); // blue bg
        assert_eq!(t.get_active_codes(), "\u{1b}[31;44m");
        t.process("\u{1b}[39m"); // default fg
        assert_eq!(t.get_active_codes(), "\u{1b}[44m");
        t.process("\u{1b}[49m"); // default bg
        assert!(!t.has_active_codes());
    }

    #[test]
    fn tracker_line_end_reset_only_for_underline() {
        let mut t = AnsiStateTracker::new();
        assert_eq!(t.get_line_end_reset(), "");
        t.process("\u{1b}[1m");
        assert_eq!(t.get_line_end_reset(), "");
        t.process("\u{1b}[4m");
        assert_eq!(t.get_line_end_reset(), "\u{1b}[24m");
    }

    #[test]
    fn tracker_ignores_non_m_codes() {
        let mut t = AnsiStateTracker::new();
        t.process("\u{1b}[2K"); // erase line, not SGR
        assert!(!t.has_active_codes());
    }

    #[test]
    fn code_manager_facade() {
        let mut m = AnsiCodeManager::new();
        m.process("\u{1b}[1m");
        assert_eq!(m.to_ansi_string(), "\u{1b}[1m");
        m.reset();
        assert_eq!(m.to_ansi_string(), "");
    }

    // -------------------------------------------------------------------
    // wrap_text_with_ansi.
    // -------------------------------------------------------------------

    #[test]
    fn wrap_empty_is_single_empty_line() {
        assert_eq!(wrap_text_with_ansi("", 10), vec!["".to_string()]);
    }

    #[test]
    fn wrap_short_line_unchanged() {
        assert_eq!(wrap_text_with_ansi("hello", 10), vec!["hello".to_string()]);
    }

    #[test]
    fn wrap_breaks_on_word_boundaries() {
        let out = wrap_text_with_ansi("the quick brown fox", 9);
        // "the quick" is 9 cols; "brown fox" is the remainder.
        assert!(out.iter().all(|l| visible_width(l) <= 9));
        assert!(out.len() >= 2);
        assert_eq!(out[0], "the quick");
    }

    #[test]
    fn wrap_preserves_explicit_newlines() {
        let out = wrap_text_with_ansi("a\nb", 10);
        assert_eq!(out, vec!["a".to_string(), "b".to_string()]);
    }

    #[test]
    fn wrap_trims_trailing_spaces() {
        let out = wrap_text_with_ansi("hello world foo", 11);
        for line in &out {
            assert_eq!(line.trim_end(), line.as_str());
        }
    }

    #[test]
    fn wrap_long_token_is_hard_broken() {
        let out = wrap_text_with_ansi("abcdefghij", 4);
        assert!(out.iter().all(|l| visible_width(l) <= 4));
        let joined: String = out.concat();
        assert_eq!(joined, "abcdefghij");
    }

    #[test]
    fn wrap_carries_underline_reset_at_line_end() {
        // Underlined long token must end intermediate lines with \x1b[24m.
        let out = wrap_text_with_ansi("\u{1b}[4mabcdefgh\u{1b}[0m", 4);
        assert!(out.len() >= 2);
        assert!(out[0].ends_with("\u{1b}[24m"));
    }

    #[test]
    fn wrapper_facade_matches_function() {
        let a = TextWrapper.wrap("one two three", 7);
        let b = wrap_text_with_ansi("one two three", 7);
        assert_eq!(a, b);
    }

    // -------------------------------------------------------------------
    // apply_background_to_line.
    // -------------------------------------------------------------------

    #[test]
    fn background_pads_then_colors() {
        let out = apply_background_to_line("hi", 5, |s| format!("[{s}]"));
        // padded to width 5: "hi   " then wrapped by bg fn.
        assert_eq!(out, "[hi   ]");
    }

    #[test]
    fn background_no_padding_when_already_wide() {
        let out = apply_background_to_line("hello", 3, |s| s.to_string());
        assert_eq!(out, "hello");
    }

    // -------------------------------------------------------------------
    // truncate_to_width.
    // -------------------------------------------------------------------

    #[test]
    fn truncate_noop_when_within_width() {
        assert_eq!(truncate_to_width("hello", 10, "...", false), "hello");
    }

    #[test]
    fn truncate_pads_when_requested() {
        assert_eq!(truncate_to_width("hi", 5, "...", true), "hi   ");
    }

    #[test]
    fn truncate_appends_ellipsis_with_reset() {
        // "hello world" capped at 8: target = 8-3 = 5 visible cols of text.
        let out = truncate_to_width("hello world", 8, "...", false);
        assert_eq!(out, "hello\u{1b}[0m...");
        // The visible width never exceeds maxWidth.
        assert!(visible_width(&out) <= 8);
    }

    #[test]
    fn truncate_ellipsis_wider_than_max() {
        // maxWidth 2, ellipsis "..." (3 wide): target <= 0 path.
        assert_eq!(truncate_to_width("hello", 2, "...", false), "..");
    }

    #[test]
    fn truncate_pad_after_truncation() {
        let out = truncate_to_width("hello world", 8, "...", true);
        assert_eq!(visible_width(&out), 8);
    }

    #[test]
    fn truncate_respects_ansi_not_counted() {
        let out = truncate_to_width("\u{1b}[31mhello world\u{1b}[0m", 8, "...", false);
        // The color code is preserved but not counted; ends with reset+ellipsis.
        assert!(out.ends_with("\u{1b}[0m..."));
        assert!(out.starts_with("\u{1b}[31m"));
    }

    // -------------------------------------------------------------------
    // slice_by_column / slice_with_width.
    // -------------------------------------------------------------------

    #[test]
    fn slice_zero_length_is_empty() {
        let r = slice_with_width("hello", 0, 0, false);
        assert_eq!(r.text, "");
        assert_eq!(r.width, 0);
    }

    #[test]
    fn slice_plain_ascii_range() {
        assert_eq!(slice_by_column("hello world", 0, 5, false), "hello");
        assert_eq!(slice_by_column("hello world", 6, 5, false), "world");
        assert_eq!(slice_by_column("hello world", 6, 100, false), "world");
    }

    #[test]
    fn slice_reports_width() {
        let r = slice_with_width("hello", 1, 3, false);
        assert_eq!(r.text, "ell");
        assert_eq!(r.width, 3);
    }

    #[test]
    fn slice_wide_chars_strict_excludes_straddler() {
        // "中" is 2 cols at column 0. A length-1 slice in strict mode excludes it.
        let strict = slice_with_width("中b", 0, 1, true);
        assert_eq!(strict.text, "");
        assert_eq!(strict.width, 0);
        // Non-strict includes the wide char even though it straddles the edge.
        let lax = slice_with_width("中b", 0, 1, false);
        assert_eq!(lax.text, "中");
        assert_eq!(lax.width, 2);
    }

    #[test]
    fn slice_preserves_ansi_inside_window() {
        // The color code sits at column 0; slicing from col 0 keeps it.
        let r = slice_by_column("\u{1b}[31mred\u{1b}[0m", 0, 3, false);
        assert!(r.contains("red"));
        assert!(r.contains("\u{1b}[31m"));
    }

    #[test]
    fn slice_carries_left_margin_style() {
        // Style established left of the window is carried onto the first kept char.
        let line = "\u{1b}[31mABCDEF\u{1b}[0m";
        let r = slice_by_column(line, 2, 2, false);
        // Should carry the red code and contain "CD".
        assert!(r.contains("CD"));
        assert!(r.contains("\u{1b}[31m"));
    }

    // -------------------------------------------------------------------
    // extract_segments.
    // -------------------------------------------------------------------

    #[test]
    fn extract_segments_plain_before_after() {
        // before = cols [0,3), after = cols [5, 5+3)
        let seg = extract_segments("abcdefghij", 3, 5, 3, false);
        assert_eq!(seg.before, "abc");
        assert_eq!(seg.before_width, 3);
        assert_eq!(seg.after, "fgh");
        assert_eq!(seg.after_width, 3);
    }

    #[test]
    fn extract_segments_no_after_when_len_zero() {
        let seg = extract_segments("abcdefghij", 4, 6, 0, false);
        assert_eq!(seg.before, "abcd");
        assert_eq!(seg.before_width, 4);
        assert_eq!(seg.after, "");
        assert_eq!(seg.after_width, 0);
    }

    #[test]
    fn extract_segments_inherits_style_into_after() {
        // Red is established at col 0 (in the before span). The after run must
        // open with the inherited active code.
        let line = "\u{1b}[31mabcdefghij\u{1b}[0m";
        let seg = extract_segments(line, 3, 5, 3, false);
        assert_eq!(seg.before_width, 3);
        assert!(seg.before.contains("abc"));
        assert!(seg.before.starts_with("\u{1b}[31m"));
        // The after run inherits red.
        assert!(seg.after.starts_with("\u{1b}[31m"));
        assert!(seg.after.contains("fgh"));
        assert_eq!(seg.after_width, 3);
    }

    #[test]
    fn extract_segments_strict_after_excludes_wide_straddler() {
        // "中" occupies cols [0,2). after window [1,1+1) -> strict excludes the
        // wide char that crosses afterEnd.
        let seg = extract_segments("中b", 0, 1, 1, true);
        // before is empty (before_end = 0). after strict: 中 starts at col 0 not in
        // after; b is at col 2 not in [1,2). So after is empty.
        assert_eq!(seg.after, "");
    }

    // -------------------------------------------------------------------
    // GraphemeHandler.
    // -------------------------------------------------------------------

    #[test]
    fn grapheme_handler_iterates_clusters() {
        let h = GraphemeHandler;
        let clusters: Vec<&str> = h.iter("a中b").collect();
        assert_eq!(clusters, vec!["a", "中", "b"]);
        // A base + combining mark is a single cluster.
        let combo: Vec<&str> = h.iter("e\u{0301}").collect();
        assert_eq!(combo, vec!["e\u{0301}"]);
    }

    // -------------------------------------------------------------------
    // measure_cluster_width direct (boundary behaviors).
    // -------------------------------------------------------------------

    #[test]
    fn measure_cluster_width_boundaries() {
        assert_eq!(measure_cluster_width("a"), 1);
        assert_eq!(measure_cluster_width("中"), 2);
        assert_eq!(measure_cluster_width("\u{200B}"), 0); // zero-width space
        assert_eq!(measure_cluster_width("e\u{0301}"), 1); // base + combining
        assert_eq!(measure_cluster_width("😀"), 2); // emoji
    }
}
