//! Checkpointed syntax highlighting via syntect.
//!
//! Optimized for ratatui rendering with:
//! - ParseState checkpoints every N lines (default: 100)
//! - Per-line token caching with version tracking
//! - Incremental highlighting: resume from nearest checkpoint
//! - Async background computation: render uses stale cache while a
//!   `spawn_blocking` task re-highlights the visible window, delivering
//!   results via `Operation::SetEditorHighlights`.

use std::cell::RefCell;
use std::collections::BTreeSet;
use lru::LruCache;
use std::num::NonZeroUsize;
use std::ops::Range;
use std::path::Path;
use std::sync::Arc;

use syntect::{
    easy::HighlightLines,
    highlighting::{FontStyle, HighlightState, Highlighter, Style, ThemeSet},
    parsing::{ParseState, SyntaxReference, SyntaxSet},
};

use once_cell::sync::Lazy;

use crate::editor::buffer::Version;

static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_nonewlines);
static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);

pub const DEFAULT_CHECKPOINT_INTERVAL: usize = 100;

#[derive(Debug, Clone, PartialEq)]
pub struct Token {
    pub range: Range<usize>,
    pub style: Style,
}

impl Token {
    pub fn new(start: usize, end: usize, style: Style) -> Self {
        Self {
            range: start..end,
            style,
        }
    }
}

#[derive(Debug, Clone)]
pub struct StyledSpan {
    /// Byte range within the original line for this span.
    pub range: Range<usize>,
    pub style: Style,
}

#[derive(Debug, Clone)]
struct Checkpoint {
    line: usize,
    highlight_state: HighlightState,
    parse_state: ParseState,
}

#[derive(Debug, Clone)]
struct CachedLine {
    version: Version,
    tokens: Arc<Vec<Token>>,
}

/// Sendable snapshot of a [`SyntaxHighlighter`]'s parser state.
///
/// Used to hand off checkpointing data to a `spawn_blocking` task without
/// moving the (non-`Send`) `SyntaxHighlighter` itself across thread boundaries.
///
/// In syntect ≥ 5, `ParseState` and `HighlightState` contain only owned data
/// (`Vec`, `String`, primitives) and auto-implement `Send`.  The compile-time
/// assertion below ensures this invariant holds; if a future syntect version
/// breaks it, the build will fail rather than introducing UB.
#[derive(Clone)]
pub struct HighlighterState {
    pub theme_name: String,
    pub syntax_name: String,
    pub checkpoint_interval: usize,
    checkpoints: Vec<Checkpoint>,
}

// Compile-time assertion: Checkpoint (and therefore HighlighterState) is Send.
// This replaces the previous `unsafe impl Send` with a sound, checked guarantee.
const _: () = {
    fn _assert_send<T: Send>() {}
    fn _check() {
        _assert_send::<HighlightState>();
        _assert_send::<ParseState>();
        _assert_send::<Checkpoint>();
        _assert_send::<HighlighterState>();
    }
};


#[derive(Debug)]
pub struct SyntaxHighlighter {
    pub theme_name: String,
    pub syntax_name: String,
    checkpoint_interval: usize,
    checkpoints: RefCell<Vec<Checkpoint>>,
    /// Per-line token cache keyed by line index.  O(1) insert/lookup; no
    /// full-Vec clone on every write (unlike the previous ArcSwap<Vec<…>>).
    /// Uses an LRU cache to evict least-recently-used lines rather than
    /// evicting by hard line-number ranges; this improves behavior on random
    /// access (go-to-definition, jumping).
    line_cache: RefCell<LruCache<usize, CachedLine>>,
    /// Sorted index of line numbers present in `line_cache`.  Allows
    /// `invalidate_from` to find affected entries via `range(from..)` in
    /// O(k log n) instead of scanning all 10 000 LRU entries.
    cache_line_index: RefCell<BTreeSet<usize>>,
}

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

impl Clone for SyntaxHighlighter {
    fn clone(&self) -> Self {
        Self {
            theme_name: self.theme_name.clone(),
            syntax_name: self.syntax_name.clone(),
            checkpoint_interval: self.checkpoint_interval,
            checkpoints: RefCell::new(self.checkpoints.borrow().clone()),
            line_cache: RefCell::new(LruCache::new(NonZeroUsize::new(10_000).unwrap())),
            cache_line_index: RefCell::new(BTreeSet::new()),
        }
    }
}

impl SyntaxHighlighter {
    pub fn new() -> Self {
        Self {
            theme_name: "base16-ocean.dark".to_string(),
            syntax_name: "Plain Text".to_string(),
            checkpoint_interval: DEFAULT_CHECKPOINT_INTERVAL,
            checkpoints: RefCell::new(Vec::new()),
            line_cache: RefCell::new(LruCache::new(NonZeroUsize::new(10_000).unwrap())),
            cache_line_index: RefCell::new(BTreeSet::new()),
        }
    }

    pub fn for_path(path: &Path) -> Self {
        let syntax_name = SYNTAX_SET
            .find_syntax_for_file(path)
            .ok()
            .flatten()
            .map(|s| s.name.clone())
            .unwrap_or_else(|| "Plain Text".to_string());

        Self::new().with_syntax(&syntax_name)
    }

    pub fn plain() -> Self {
        Self::new()
    }

    pub fn with_theme(mut self, theme_name: &str) -> Self {
        self.theme_name = theme_name.to_string();
        self
    }

    pub fn with_syntax(mut self, syntax_name: &str) -> Self {
        self.syntax_name = syntax_name.to_string();
        self
    }

    /// Extract a [`HighlighterState`] snapshot of the highlighter's configuration
    /// and accumulated checkpoints.
    ///
    /// IMPORTANT: This snapshot contains `ParseState` / `HighlightState` values
    /// from `syntect` which are not guaranteed to be thread-safe. Do NOT send
    /// the resulting `HighlighterState` across threads. For background-workers
    /// use the theme/syntax names and the buffer lines to reconstruct a
    /// `SyntaxHighlighter` inside the worker (see app.rs worker changes).
    pub fn snapshot_state(&self) -> HighlighterState {
        HighlighterState {
            theme_name: self.theme_name.clone(),
            syntax_name: self.syntax_name.clone(),
            checkpoint_interval: self.checkpoint_interval,
            checkpoints: self.checkpoints.borrow().clone(),
        }
    }

    /// Reconstruct a [`SyntaxHighlighter`] from a previously snapshotted state.
    ///
    /// The resulting highlighter has an empty per-line cache but inherits all
    /// accumulated checkpoints, so highlighting resumes cheaply.
    pub fn from_state(state: HighlighterState) -> Self {
        Self {
            theme_name: state.theme_name,
            syntax_name: state.syntax_name,
            checkpoint_interval: state.checkpoint_interval,
            checkpoints: RefCell::new(state.checkpoints),
            line_cache: RefCell::new(LruCache::new(NonZeroUsize::new(10_000).unwrap())),
            cache_line_index: RefCell::new(BTreeSet::new()),
        }
    }

    fn get_syntax(&self) -> &SyntaxReference {
        SYNTAX_SET
            .find_syntax_by_name(&self.syntax_name)
            .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text())
    }

    pub fn clear_cache(&self) {
        self.checkpoints.borrow_mut().clear();
        self.line_cache.borrow_mut().clear();
        self.cache_line_index.borrow_mut().clear();
    }

    pub fn invalidate_from(&self, from_line: usize) {
        // Invalidate any checkpoint whose line is at or after `from_line`.
        // Edits starting exactly at a checkpoint boundary must invalidate that checkpoint.
        // Edits in the middle of an interval keep the previous checkpoint.
        self.checkpoints.borrow_mut().retain(|c| c.line < from_line);

        let mut cache = self.line_cache.borrow_mut();
        let mut index = self.cache_line_index.borrow_mut();
        // Use the BTreeSet index to find only the affected lines in O(k log n)
        // instead of scanning the entire LRU cache.
        let to_remove: Vec<usize> = index.range(from_line..).copied().collect();
        for k in to_remove {
            cache.pop(&k);
            index.remove(&k);
        }
    }

    fn get_or_create_checkpoint(&self, lines: &[String], line: usize) -> Checkpoint {
        let interval = self.checkpoint_interval;
        let checkpoint_line = (line / interval) * interval;

        if let Some(cp) = self
            .checkpoints
            .borrow()
            .iter()
            .find(|c| c.line == checkpoint_line)
            .cloned()
        {
            return cp;
        }

        let checkpoints_borrow = self.checkpoints.borrow();

        let prev_checkpoint = checkpoints_borrow
            .iter()
            .filter(|c| c.line < checkpoint_line)
            .max_by_key(|c| c.line)
            .cloned();

        let theme = &THEME_SET.themes[&self.theme_name];
        let highlighter = Highlighter::new(theme);

        let (highlight_state, parse_state, start_line) = if let Some(ref prev) = prev_checkpoint {
            (
                prev.highlight_state.clone(),
                prev.parse_state.clone(),
                prev.line,
            )
        } else {
            // No previous checkpoint: create one on-demand starting from line 0.
            // Avoid pre-filling checkpoints which caused O(N²) behavior for large
            // files. Instead, construct a fresh ParseState/HighlightState and
            // parse from line 0 up to the desired checkpoint_line.
            let parse = ParseState::new(self.get_syntax());
            let high =
                HighlightState::new(&highlighter, syntect::parsing::ScopeStack::new());
            (high, parse, 0)
        };

        drop(checkpoints_borrow); // Release borrow before spawn_blocking

        let mut h = HighlightLines::from_state(theme, highlight_state, parse_state);

        for i in start_line..checkpoint_line {
            if i < lines.len() {
                let _ = h.highlight_line(&lines[i], &SYNTAX_SET);
            }
        }

        let (highlight_state, parse_state) = h.state();

        let checkpoint = Checkpoint {
            line: checkpoint_line,
            highlight_state,
            parse_state,
        };

        let mut checkpoints = self.checkpoints.borrow_mut();
        let mut insert_pos = checkpoints
            .iter()
            .position(|c| c.line > checkpoint_line)
            .unwrap_or(checkpoints.len());
        checkpoints.insert(insert_pos, checkpoint);

        // Evict farthest checkpoints when capacity exceeded. Keep the checkpoints
        // closest to the requested checkpoint_line to minimize recomputation cost.
        const MAX_CHECKPOINTS: usize = 256;
        if checkpoints.len() > MAX_CHECKPOINTS {
            // Build (index, distance) pairs
            let mut distances: Vec<(usize, usize)> = checkpoints
                .iter()
                .enumerate()
                .map(|(i, c)| (i, c.line.abs_diff(checkpoint_line)))
                .collect();
            // Sort by distance ascending and keep the nearest MAX_CHECKPOINTS
            distances.sort_by_key(|&(_, d)| d);
            use std::collections::HashSet;
            let keep: HashSet<usize> = distances.into_iter().take(MAX_CHECKPOINTS).map(|(i, _)| i).collect();

            // Rebuild checkpoints preserving ascending line order
            let mut kept = Vec::with_capacity(MAX_CHECKPOINTS);
            for (i, cp) in checkpoints.iter().enumerate() {
                if keep.contains(&i) {
                    kept.push(cp.clone());
                }
            }
            *checkpoints = kept;

            // Recompute the inserted checkpoint's index in the compacted vec
            insert_pos = checkpoints
                .iter()
                .position(|c| c.line == checkpoint_line)
                .unwrap_or(checkpoints.len());
        }

        checkpoints[insert_pos].clone()
    }

    fn get_cached_line(&self, line: usize, version: Version) -> Option<Arc<Vec<Token>>> {
        let mut cache = self.line_cache.borrow_mut();
        match cache.get(&line) {
            Some(cached) if cached.version == version => Some(cached.tokens.clone()),
            _ => None,
        }
    }

    fn cache_line(&self, line: usize, version: Version, tokens: Arc<Vec<Token>>) {
        let mut cache = self.line_cache.borrow_mut();
        let mut index = self.cache_line_index.borrow_mut();
        if let Some((evicted_key, _)) = cache.push(line, CachedLine { version, tokens })
            && evicted_key != line {
                // An LRU eviction occurred; remove the evicted key from the index.
                index.remove(&evicted_key);
            }
        index.insert(line);
    }

    pub fn highlight_line_at(&self, lines: &[String], line: usize, version: Version) -> Arc<Vec<Token>> {
        if let Some(tokens) = self.get_cached_line(line, version) {
            return tokens;
        }

        let checkpoint = self.get_or_create_checkpoint(lines, line);
        let theme = &THEME_SET.themes[&self.theme_name];
        let mut h = HighlightLines::from_state(
            theme,
            checkpoint.highlight_state.clone(),
            checkpoint.parse_state.clone(),
        );

        for i in checkpoint.line..line {
            if i < lines.len() {
                let _ = h.highlight_line(&lines[i], &SYNTAX_SET);
            }
        }

        let highlighted = if line < lines.len() {
            h.highlight_line(&lines[line], &SYNTAX_SET)
                .unwrap_or_default()
        } else {
            Vec::new()
        };

        let tokens_vec = self.tokens_from_highlight(&highlighted);
        let tokens_arc = Arc::new(tokens_vec);

        self.cache_line(line, version, tokens_arc.clone());

        tokens_arc
    }

    fn tokens_from_highlight(&self, highlighted: &[(Style, &str)]) -> Vec<Token> {
        let mut tokens = Vec::new();
        let mut byte_offset = 0;

        for (style, text) in highlighted {
            let end = byte_offset + text.len();
            tokens.push(Token::new(byte_offset, end, *style));
            byte_offset = end;
        }
        tokens
    }

    pub fn highlight_range(
        &self,
        all_lines: &[String],
        version: Version,
        start_row: usize,
        count: usize,
    ) -> Vec<Vec<StyledSpan>> {
        let end_row = (start_row + count).min(all_lines.len());

        (start_row..end_row)
            .map(|i| {
                let tokens = self.highlight_line_at(all_lines, i, version);
                self.tokens_to_spans(&all_lines[i], tokens.as_ref())
            })
            .collect()
    }

    pub fn highlight_tokens(
        &self,
        all_lines: &[String],
        version: Version,
        start_line: usize,
        count: usize,
    ) -> Vec<Arc<Vec<Token>>> {
        let end_line = (start_line + count).min(all_lines.len());

        (start_line..end_line)
            .map(|i| self.highlight_line_at(all_lines, i, version))
            .collect()
    }

    /// Cancellable variant of `highlight_tokens` that checks `is_cancelled`
    /// between lines and returns early when it returns `true`. Useful for
    /// aborting long-running background highlight tasks.
    pub fn highlight_tokens_cancellable(
        &self,
        all_lines: &[String],
        version: Version,
        start_line: usize,
        count: usize,
        is_cancelled: impl Fn() -> bool,
    ) -> Vec<Arc<Vec<Token>>> {
        let end_line = (start_line + count).min(all_lines.len());
        let mut results = Vec::new();
        for i in start_line..end_line {
            if is_cancelled() {
                return results;
            }
            results.push(self.highlight_line_at(all_lines, i, version));
        }
        results
    }

    /// Highlight a sorted batch of lines efficiently using sequential parse-state reuse.
    ///
    /// Unlike calling [`highlight_line_at`] independently for each line (which
    /// re-parses from the nearest checkpoint every time), this method carries
    /// the parse state forward between adjacent lines.  For a contiguous range
    /// of N lines this reduces per-line cost from O(checkpoint_interval) to O(1).
    ///
    /// `requested_lines` **must** be sorted in ascending order.
    pub fn highlight_lines_batch(
        &self,
        all_lines: &[String],
        version: Version,
        requested_lines: &[usize],
    ) -> Vec<(usize, Arc<Vec<Token>>)> {
        if requested_lines.is_empty() {
            return Vec::new();
        }

        let interval = self.checkpoint_interval;
        let theme = &THEME_SET.themes[&self.theme_name];
        let mut results = Vec::with_capacity(requested_lines.len());

        // Sequential parse cursor.  Carrying state forward between adjacent
        // lines avoids the O(n²) cost of independently re-parsing from a
        // checkpoint for each line.
        let mut parse_pos: usize = 0;
        let mut h: Option<HighlightLines> = None;

        for &line in requested_lines {
            if line >= all_lines.len() {
                results.push((line, Arc::new(Vec::new())));
                continue;
            }

            if let Some(tokens) = self.get_cached_line(line, version) {
                results.push((line, tokens));
                // Cannot carry parse state past a skipped line.
                h = None;
                continue;
            }

            // Continue from the current cursor only when the gap is small.
            let can_continue =
                h.is_some() && parse_pos <= line && (line - parse_pos) <= interval;

            if !can_continue {
                let checkpoint = self.get_or_create_checkpoint(all_lines, line);
                h = Some(HighlightLines::from_state(
                    theme,
                    checkpoint.highlight_state.clone(),
                    checkpoint.parse_state.clone(),
                ));
                parse_pos = checkpoint.line;
            }

            let highlighter = h.as_mut().unwrap();

            // Advance to the target line.
            while parse_pos < line {
                if parse_pos < all_lines.len() {
                    let _ = highlighter.highlight_line(&all_lines[parse_pos], &SYNTAX_SET);
                }
                parse_pos += 1;
            }

            // Highlight the target line.
            let highlighted = highlighter
                .highlight_line(&all_lines[line], &SYNTAX_SET)
                .unwrap_or_default();
            parse_pos = line + 1;

            let tokens_vec = self.tokens_from_highlight(&highlighted);
            let tokens_arc = Arc::new(tokens_vec);
            self.cache_line(line, version, tokens_arc.clone());
            results.push((line, tokens_arc));
        }

        results
    }

    pub fn tokens_to_spans(&self, _line: &str, tokens: &[Token]) -> Vec<StyledSpan> {
        // Avoid allocating per-token strings. Return spans as (range, style)
        // and let the renderer slice the original line when drawing.
        tokens
            .iter()
            .map(|t| StyledSpan { range: t.range.clone(), style: t.style })
            .collect()
    }
}

#[inline]
pub fn to_ratatui_color(c: syntect::highlighting::Color) -> ratatui::style::Color {
    ratatui::style::Color::Rgb(c.r, c.g, c.b)
}

pub fn to_ratatui_style(style: &Style) -> ratatui::style::Style {
    use ratatui::style::Modifier;
    let mut s = ratatui::style::Style::default().fg(to_ratatui_color(style.foreground));
    if style.font_style.contains(FontStyle::BOLD) {
        s = s.add_modifier(Modifier::BOLD);
    }
    if style.font_style.contains(FontStyle::ITALIC) {
        s = s.add_modifier(Modifier::ITALIC);
    }
    if style.font_style.contains(FontStyle::UNDERLINE) {
        s = s.add_modifier(Modifier::UNDERLINED);
    }
    s
}

pub const BRACE_PAIRS: &[(char, char)] = &[('{', '}'), ('(', ')'), ('[', ']')];

/// Find the matching brace for the character at (cursor_row, cursor_col).
/// `cursor_col` is a character index (0-indexed), not a byte index.
pub fn find_matching_brace(
    lines: &[String],
    cursor_row: usize,
    cursor_col: usize,
) -> Option<(usize, usize)> {
    let line = lines.get(cursor_row)?;

    // Get the character at cursor_col (character index)
    let ch = line.chars().nth(cursor_col)?;

    let (open, close, forward) = BRACE_PAIRS.iter().find_map(|&(o, c)| {
        if ch == o {
            Some((o, c, true))
        } else if ch == c {
            Some((o, c, false))
        } else {
            None
        }
    })?;

    if forward {
        // Search forward for matching closing brace
        let mut depth = 0i32;
        for (row, line) in lines.iter().enumerate().skip(cursor_row) {
            let start_char = if row == cursor_row { cursor_col } else { 0 };
            for (char_idx, c) in line.chars().enumerate().skip(start_char) {
                if c == open {
                    depth += 1;
                }
                if c == close {
                    depth -= 1;
                    if depth == 0 && (row, char_idx) != (cursor_row, cursor_col) {
                        return Some((row, char_idx));
                    }
                }
            }
        }
    } else {
        // Search backward for matching opening brace
        let mut depth = 0i32;
        for row in (0..=cursor_row).rev() {
            let line = &lines[row];
            let chars: Vec<char> = line.chars().collect();
            let end_char = if row == cursor_row {
                cursor_col + 1
            } else {
                chars.len()
            };

            for (char_idx, c) in chars.into_iter().enumerate().take(end_char).rev() {
                if c == close {
                    depth += 1;
                }
                if c == open {
                    depth -= 1;
                    if depth == 0 && (row, char_idx) != (cursor_row, cursor_col) {
                        return Some((row, char_idx));
                    }
                }
            }
        }
    }
    None
}

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

    #[test]
    fn brace_match_forward() {
        let lines = vec!["fn f() {".to_string(), "    x".to_string(), "}".to_string()];
        assert_eq!(find_matching_brace(&lines, 0, 7), Some((2, 0)));
    }

    #[test]
    fn brace_match_backward() {
        let lines = vec!["fn f() {".to_string(), "    x".to_string(), "}".to_string()];
        assert_eq!(find_matching_brace(&lines, 2, 0), Some((0, 7)));
    }

    #[test]
    fn brace_match_nested() {
        let lines = vec!["if (a && (b || c)) {".to_string(), "}".to_string()];
        assert_eq!(find_matching_brace(&lines, 0, 3), Some((0, 17)));
    }

    #[test]
    fn no_match_for_plain_char() {
        let lines = vec!["let x = 1;".to_string()];
        assert_eq!(find_matching_brace(&lines, 0, 4), None);
    }

    #[test]
    fn brace_match_with_utf8() {
        // UTF-8 character '∞' takes 3 bytes, so character positions are:
        // "∞ (" -> char 0='∞', char 1=' ', char 2='(', char 3=')'
        let lines = vec!["∞ ()".to_string()];
        // '(' is at character index 2, ')' is at character index 3
        assert_eq!(find_matching_brace(&lines, 0, 2), Some((0, 3)));
        assert_eq!(find_matching_brace(&lines, 0, 3), Some((0, 2)));
    }

    #[test]
    fn tokens_to_spans_with_utf8_characters() {
        let highlighter = SyntaxHighlighter::new();
        let line = "∞ code";
        // '∞' is 3 bytes, ' ' is 1 byte, so:
        // bytes 0-3: '∞'
        // bytes 3-4: ' '
        // bytes 4-8: 'code'

        // Create tokens with byte offsets
        let tokens = vec![
            Token::new(0, 3, Style::default()), // '∞'
            Token::new(3, 4, Style::default()), // ' '
            Token::new(4, 8, Style::default()), // 'code'
        ];

        let spans = highlighter.tokens_to_spans(line, &tokens);
        assert_eq!(spans.len(), 3);
        assert_eq!(&line[spans[0].range.start..spans[0].range.end], "∞");
        assert_eq!(&line[spans[1].range.start..spans[1].range.end], " ");
        assert_eq!(&line[spans[2].range.start..spans[2].range.end], "code");
    }

    #[test]
    fn token_creation() {
        let token = Token::new(0, 5, Style::default());
        assert_eq!(token.range, 0..5);
    }

    #[test]
    fn highlighting_produces_tokens() {
        let highlighter = SyntaxHighlighter::new();
        let lines = vec!["fn main() {}".to_string()];
        let tokens = highlighter.highlight_line_at(&lines, 0, Version::new());
        assert!(!tokens.is_empty());
    }

    #[test]
    fn cache_works() {
        let highlighter = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["fn main() {}".to_string()];

        let tokens1 = highlighter.highlight_line_at(&lines, 0, version);
        let tokens2 = highlighter.highlight_line_at(&lines, 0, version);

        assert_eq!(tokens1.len(), tokens2.len());
    }

    #[test]
    fn checkpoint_interval() {
        let highlighter = SyntaxHighlighter::new();
        let mut h = highlighter.clone();
        h.checkpoint_interval = 50;

        let version = Version::new();
        let lines: Vec<String> = (0..200).map(|i| format!("line {}", i)).collect();

        for i in 0..200 {
            h.highlight_line_at(&lines, i, version);
        }

        let checkpoints = h.checkpoints.borrow();
        assert!(!checkpoints.is_empty());
        assert!(checkpoints.iter().all(|c| c.line % 50 == 0));
    }

    #[test]
    fn checkpoint_eviction_keeps_nearby_checkpoints() {
        // Create a highlighter that checkpoints every line so we can produce
        // many checkpoints quickly.
        let mut h = SyntaxHighlighter::new();
        h.checkpoint_interval = 1;
        let version = Version::new();
        let lines: Vec<String> = (0..300).map(|i| format!("line {}", i)).collect();

        // Create checkpoints for all lines except the center line. This ensures
        // inserting the center will be the operation that triggers eviction.
        for i in 0..300 {
            if i == 150 { continue; }
            h.highlight_line_at(&lines, i, version);
        }

        // Insert the center checkpoint last to trigger eviction centered on 150
        h.highlight_line_at(&lines, 150, version);

        let checkpoints = h.checkpoints.borrow();
        // Eviction should shrink the checkpoint set to <= 256
        assert!(checkpoints.len() <= 256);

        let cp_lines: Vec<usize> = checkpoints.iter().map(|c| c.line).collect();
        assert!(cp_lines.contains(&150));

        // The kept checkpoints should form a contiguous window around the
        // requested checkpoint_line and its size should be <= 256.
        let min = *cp_lines.first().unwrap();
        let max = *cp_lines.last().unwrap();
        assert!(min <= 150 && 150 <= max);
        assert!(max - min < 256);
    }

    #[test]
    fn spans_from_highlight_cover_entire_line() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["fn test_span_cover() { let x = 1; }".to_string()];

        let tokens = h.highlight_line_at(&lines, 0, version);
        let spans = h.tokens_to_spans(&lines[0], tokens.as_ref());

        let mut reconstructed = String::new();
        for s in &spans {
            reconstructed.push_str(&lines[0][s.range.start..s.range.end]);
        }

        assert_eq!(reconstructed, lines[0]);
    }

    #[test]
    fn invalidate_from_boundary_behaviour() {
        // Create a highlighter that checkpoints every 50 lines.
        let mut h = SyntaxHighlighter::new();
        h.checkpoint_interval = 50;
        let version = Version::new();
        let lines: Vec<String> = (0..200).map(|i| format!("line {}", i)).collect();

        // Build checkpoints by highlighting many lines
        for i in 0..200 {
            h.highlight_line_at(&lines, i, version);
        }

        // Ensure a checkpoint exists at the boundary (100)
        let cp_lines: Vec<usize> = h.checkpoints.borrow().iter().map(|c| c.line).collect();
        assert!(cp_lines.contains(&100));

        // Invalidate starting exactly at the checkpoint boundary: 100
        h.invalidate_from(100);
        let cp_after: Vec<usize> = h.checkpoints.borrow().iter().map(|c| c.line).collect();
        // The 100 checkpoint must be removed and all remaining checkpoints be < 100
        assert!(!cp_after.contains(&100));
        assert!(cp_after.iter().all(|&l| l < 100));

        // Rebuild checkpoints again
        for i in 0..200 {
            h.highlight_line_at(&lines, i, version);
        }

        // Invalidate starting in the middle of an interval (101). The checkpoint
        // at 100 should remain because the edit begins after it.
        h.invalidate_from(101);
        let cp_after_mid: Vec<usize> = h.checkpoints.borrow().iter().map(|c| c.line).collect();
        assert!(cp_after_mid.contains(&100));
    }

    // -------------------------------------------------------------------------
    // Caching: version mismatch, Arc identity, clear_cache
    // -------------------------------------------------------------------------

    #[test]
    fn cache_miss_on_version_change() {
        let h = SyntaxHighlighter::new();
        let lines = vec!["fn main() {}".to_string()];
        let v1 = Version::from_raw(0);
        let v2 = Version::from_raw(1);

        let t1 = h.highlight_line_at(&lines, 0, v1);
        let t2 = h.highlight_line_at(&lines, 0, v2);

        // Both should produce tokens; the second is a fresh computation (different
        // version breaks the cache key) but must still produce identical content.
        assert_eq!(t1.len(), t2.len());
    }

    #[test]
    fn cache_hit_returns_same_arc() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["let x = 42;".to_string()];

        let t1 = h.highlight_line_at(&lines, 0, version);
        let t2 = h.highlight_line_at(&lines, 0, version);

        // Same Arc allocation — pointer equality proves the cache was hit.
        assert!(Arc::ptr_eq(&t1, &t2));
    }

    #[test]
    fn clear_cache_forces_recomputation() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["let x = 42;".to_string()];

        let t1 = h.highlight_line_at(&lines, 0, version);
        h.clear_cache();
        let t2 = h.highlight_line_at(&lines, 0, version);

        // After clear the Arc must be a fresh allocation.
        assert!(!Arc::ptr_eq(&t1, &t2));
        // But the content should be identical.
        assert_eq!(t1.len(), t2.len());
    }

    #[test]
    fn clear_cache_removes_all_checkpoints() {
        let mut h = SyntaxHighlighter::new();
        h.checkpoint_interval = 10;
        let version = Version::new();
        let lines: Vec<String> = (0..50).map(|i| format!("line {}", i)).collect();

        for i in 0..50 {
            h.highlight_line_at(&lines, i, version);
        }
        assert!(!h.checkpoints.borrow().is_empty());

        h.clear_cache();
        assert!(h.checkpoints.borrow().is_empty());
    }

    #[test]
    fn lru_eviction_does_not_panic() {
        // Default LRU capacity is 10,000. Inserting more entries should silently
        // evict without panicking.
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..10_200).map(|i| format!("// line {}", i)).collect();

        for i in 0..10_200 {
            h.highlight_line_at(&lines, i, version);
        }
        // If we reach here, no panic occurred.
    }

    // -------------------------------------------------------------------------
    // invalidate_from: cache-line semantics
    // -------------------------------------------------------------------------

    #[test]
    fn invalidate_from_zero_clears_line_cache_entirely() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..10).map(|i| format!("let x{} = {};", i, i)).collect();

        // Populate cache for all 10 lines.
        for i in 0..10 {
            h.highlight_line_at(&lines, i, version);
        }

        h.invalidate_from(0);

        // After invalidation from 0, every line should produce a fresh Arc.
        for (i, _line) in lines.iter().enumerate().take(10) {
            let before_ptr = {
                // Re-cache line i to get a fresh Arc, then capture pointer.
                h.highlight_line_at(&lines, i, version)
            };
            // The first call after invalidation must NOT reuse the old Arc stored
            // pre-invalidation. However since we just called highlight_line_at, the
            // cache is now populated again; verify content is valid.
            let after_ptr = h.highlight_line_at(&lines, i, version);
            assert!(Arc::ptr_eq(&before_ptr, &after_ptr), "line {} should now be cached", i);
        }
    }

    #[test]
    fn invalidate_from_preserves_lines_before_edit_point() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..20).map(|i| format!("let x{} = {};", i, i)).collect();

        // Cache all 20 lines.
        let arcs_before: Vec<_> = (0..20)
            .map(|i| h.highlight_line_at(&lines, i, version))
            .collect();

        // Invalidate from line 10 onward.
        h.invalidate_from(10);

        // Lines 0..10 should still be served from cache (same Arc pointer).
        for (i, _line) in arcs_before.iter().enumerate().take(10) {
            let after = h.highlight_line_at(&lines, i, version);
            assert!(
                Arc::ptr_eq(&arcs_before[i], &after),
                "line {} before invalidation point should still be cached", i
            );
        }
    }

    #[test]
    fn invalidate_from_removes_lines_at_and_after_edit_point() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..20).map(|i| format!("let x{} = {};", i, i)).collect();

        let arcs_before: Vec<_> = (0..20)
            .map(|i| h.highlight_line_at(&lines, i, version))
            .collect();

        h.invalidate_from(10);

        // Lines 10..20 must have been evicted from the cache: a fresh Arc is
        // produced by the next call.
        for (i, _line) in arcs_before.iter().enumerate().take(20).skip(10) {
            let after = h.highlight_line_at(&lines, i, version);
            assert!(
                !Arc::ptr_eq(&arcs_before[i], &after),
                "line {} at/after invalidation point should be evicted", i
            );
        }
    }

    #[test]
    fn invalidate_from_exact_boundary_removes_that_line() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["fn a() {}".to_string(), "fn b() {}".to_string()];

        let arc0 = h.highlight_line_at(&lines, 0, version);
        let arc1 = h.highlight_line_at(&lines, 1, version);

        // Invalidate from exactly line 1.
        h.invalidate_from(1);

        // Line 0 should still be cached.
        let after0 = h.highlight_line_at(&lines, 0, version);
        assert!(Arc::ptr_eq(&arc0, &after0), "line 0 must remain cached");

        // Line 1 should be evicted.
        let after1 = h.highlight_line_at(&lines, 1, version);
        assert!(!Arc::ptr_eq(&arc1, &after1), "line 1 must be evicted");
    }

    // -------------------------------------------------------------------------
    // Editing simulation
    // -------------------------------------------------------------------------

    #[test]
    fn edit_at_beginning_invalidates_all_lines() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let mut lines: Vec<String> = (0..10).map(|i| format!("let x{} = {};", i, i)).collect();

        let arcs_before: Vec<_> = (0..10)
            .map(|i| h.highlight_line_at(&lines, i, version))
            .collect();

        // Simulate inserting a new first line.
        lines.insert(0, "// new comment".to_string());
        h.invalidate_from(0);

        // Every line's cached Arc is now stale (edit at line 0 shifts all).
        // Fresh computation must differ from the pre-edit Arcs.
        for (i, _line) in arcs_before.iter().enumerate().take(10) {
            let after = h.highlight_line_at(&lines, i, version);
            assert!(!Arc::ptr_eq(&arcs_before[i], &after),
                "line {} must be recomputed after edit at line 0", i);
        }
    }

    #[test]
    fn edit_at_middle_preserves_preceding_lines() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let mut lines: Vec<String> = (0..20).map(|i| format!("// line {}", i)).collect();

        let arcs_before: Vec<_> = (0..20)
            .map(|i| h.highlight_line_at(&lines, i, version))
            .collect();

        // Simulate an edit at line 10.
        lines[10] = "// CHANGED".to_string();
        h.invalidate_from(10);

        // Lines 0..10 should still be in cache.
        for (i, _line) in arcs_before.iter().enumerate().take(10) {
            let after = h.highlight_line_at(&lines, i, version);
            assert!(Arc::ptr_eq(&arcs_before[i], &after),
                "line {} before edit should still be cached", i);
        }

        // Lines 10..20 should be fresh.
        for (i, _line) in arcs_before.iter().enumerate().take(20).skip(10) {
            let after = h.highlight_line_at(&lines, i, version);
            assert!(!Arc::ptr_eq(&arcs_before[i], &after),
                "line {} at/after edit point should be recomputed", i);
        }
    }

    #[test]
    fn edit_at_last_line_preserves_rest() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let mut lines: Vec<String> = (0..5).map(|i| format!("// line {}", i)).collect();

        let arcs: Vec<_> = (0..5)
            .map(|i| h.highlight_line_at(&lines, i, version))
            .collect();

        lines[4] = "// CHANGED LAST".to_string();
        h.invalidate_from(4);

        for (i, _line) in arcs.iter().enumerate().take(4) {
            let after = h.highlight_line_at(&lines, i, version);
            assert!(Arc::ptr_eq(&arcs[i], &after), "line {} must stay cached", i);
        }

        let after4 = h.highlight_line_at(&lines, 4, version);
        assert!(!Arc::ptr_eq(&arcs[4], &after4), "last line must be recomputed");
    }

    #[test]
    fn sequential_edits_produce_consistent_results() {
        // Simulate a realistic editing session: three edits at decreasing positions.
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let mut lines: Vec<String> = (0..30).map(|i| format!("let v{} = {};", i, i)).collect();

        // First: highlight all 30 lines.
        for i in 0..30 {
            h.highlight_line_at(&lines, i, version);
        }

        // Edit 1: line 25.
        lines[25] = "let v25 = 9999;".to_string();
        h.invalidate_from(25);
        for i in 0..30 {
            let t = h.highlight_line_at(&lines, i, version);
            assert!(!t.is_empty() || lines[i].is_empty(), "line {} should produce tokens", i);
        }

        // Edit 2: line 10.
        lines[10] = "let v10 = 8888;".to_string();
        h.invalidate_from(10);
        for i in 0..30 {
            let t = h.highlight_line_at(&lines, i, version);
            assert!(!t.is_empty() || lines[i].is_empty());
        }

        // Edit 3: line 0.
        lines[0] = "// file header".to_string();
        h.invalidate_from(0);
        for i in 0..30 {
            let t = h.highlight_line_at(&lines, i, version);
            assert!(!t.is_empty() || lines[i].is_empty());
        }
    }

    // -------------------------------------------------------------------------
    // Scrolling simulation
    // -------------------------------------------------------------------------

    #[test]
    fn scroll_down_highlights_new_lines() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..100).map(|i| format!("// line {}", i)).collect();

        // First screen (lines 0..40).
        for i in 0..40 {
            h.highlight_line_at(&lines, i, version);
        }

        // Scroll to second screen (lines 40..80).
        let second_screen: Vec<_> = (40..80)
            .map(|i| h.highlight_line_at(&lines, i, version))
            .collect();

        assert_eq!(second_screen.len(), 40);
        for tokens in &second_screen {
            assert!(!tokens.is_empty());
        }
    }

    #[test]
    fn scroll_back_up_uses_cache() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..100).map(|i| format!("// line {}", i)).collect();

        // Highlight first 40 lines and capture Arcs.
        let first_screen: Vec<_> = (0..40)
            .map(|i| h.highlight_line_at(&lines, i, version))
            .collect();

        // Scroll down to lines 40..80.
        for i in 40..80 {
            h.highlight_line_at(&lines, i, version);
        }

        // Scroll back up: lines 0..40 should be served from cache.
        for (i, _line) in first_screen.iter().enumerate().take(40) {
            let after = h.highlight_line_at(&lines, i, version);
            assert!(Arc::ptr_eq(&first_screen[i], &after),
                "line {} should be cache-hit on scroll back", i);
        }
    }

    #[test]
    fn highlight_window_jump_no_panic() {
        // Simulate jumping from line 0 to line 5000 (e.g. go-to-line command).
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..6000).map(|i| format!("// line {}", i)).collect();

        // Highlight first screen.
        for i in 0..40 {
            h.highlight_line_at(&lines, i, version);
        }

        // Jump to line 5000 and highlight a screen worth of lines.
        for i in 5000..5040 {
            let t = h.highlight_line_at(&lines, i, version);
            assert!(!t.is_empty());
        }
    }

    // -------------------------------------------------------------------------
    // highlight_range and highlight_tokens
    // -------------------------------------------------------------------------

    #[test]
    fn highlight_range_returns_correct_count() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..20).map(|i| format!("let x{} = {};", i, i)).collect();

        let spans = h.highlight_range(&lines, version, 5, 10);
        assert_eq!(spans.len(), 10, "should return exactly 10 rows");
    }

    #[test]
    fn highlight_range_zero_count_returns_empty() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["let x = 1;".to_string()];

        let spans = h.highlight_range(&lines, version, 0, 0);
        assert!(spans.is_empty());
    }

    #[test]
    fn highlight_range_clamped_to_buffer_length() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..5).map(|i| format!("// {}", i)).collect();

        // Requesting more rows than exist: should clamp, not panic.
        let spans = h.highlight_range(&lines, version, 0, 100);
        assert_eq!(spans.len(), 5);
    }

    #[test]
    fn highlight_range_spans_cover_full_line() {
        let h = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let lines = vec![
            "fn main() {".to_string(),
            "    let x = 42;".to_string(),
            "}".to_string(),
        ];

        let all_spans = h.highlight_range(&lines, version, 0, 3);
        for (row, spans) in all_spans.iter().enumerate() {
            let mut reconstructed = String::new();
            for s in spans {
                reconstructed.push_str(&lines[row][s.range.start..s.range.end]);
            }
            assert_eq!(reconstructed, lines[row], "spans must reconstruct line {}", row);
        }
    }

    #[test]
    fn highlight_tokens_matches_individual_highlight_line_at() {
        let h = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let lines: Vec<String> = (0..10).map(|i| format!("let v{} = {};", i, i)).collect();

        let batch = h.highlight_tokens(&lines, version, 0, 10);

        // Rebuild the highlighter to compare against individual calls (cache cleared).
        let h2 = SyntaxHighlighter::new().with_syntax("Rust");
        for (i, _line) in batch.iter().enumerate().take(10) {
            let individual = h2.highlight_line_at(&lines, i, version);
            assert_eq!(
                batch[i].len(), individual.len(),
                "batch and individual token counts must match for line {}", i
            );
        }
    }

    #[test]
    fn highlight_tokens_beyond_bounds_clamped() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["one".to_string(), "two".to_string()];

        let tokens = h.highlight_tokens(&lines, version, 0, 1000);
        assert_eq!(tokens.len(), 2, "should clamp to buffer length");
    }

    // -------------------------------------------------------------------------
    // highlight_lines_batch
    // -------------------------------------------------------------------------

    #[test]
    fn batch_matches_individual() {
        let h1 = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let lines: Vec<String> = (0..20).map(|i| format!("let v{} = {};", i, i)).collect();

        let requested: Vec<usize> = (0..20).collect();
        let batch = h1.highlight_lines_batch(&lines, version, &requested);

        let h2 = SyntaxHighlighter::new().with_syntax("Rust");
        for (line, tokens) in &batch {
            let individual = h2.highlight_line_at(&lines, *line, version);
            assert_eq!(
                tokens.len(), individual.len(),
                "batch and individual token counts must match for line {}", line
            );
        }
    }

    #[test]
    fn batch_empty_request() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["hello".to_string()];
        let result = h.highlight_lines_batch(&lines, version, &[]);
        assert!(result.is_empty());
    }

    #[test]
    fn batch_out_of_bounds_returns_empty_tokens() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["hello".to_string()];
        let result = h.highlight_lines_batch(&lines, version, &[0, 5, 100]);
        assert_eq!(result.len(), 3);
        assert!(!result[0].1.is_empty()); // line 0 valid
        assert!(result[1].1.is_empty());  // line 5 OOB
        assert!(result[2].1.is_empty());  // line 100 OOB
    }

    #[test]
    fn batch_with_gaps() {
        let h1 = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let lines: Vec<String> = (0..200).map(|i| format!("let v{} = {};", i, i)).collect();

        let requested = vec![0, 5, 50, 100, 150, 199];
        let batch = h1.highlight_lines_batch(&lines, version, &requested);

        let h2 = SyntaxHighlighter::new().with_syntax("Rust");
        for (line, tokens) in &batch {
            let individual = h2.highlight_line_at(&lines, *line, version);
            assert_eq!(
                tokens.len(), individual.len(),
                "batch and individual must match for line {}", line
            );
        }
    }

    #[test]
    fn batch_reuses_state_for_adjacent_lines() {
        // Verify that batch with sorted adjacent lines produces correct results.
        // This implicitly tests state reuse since adjacent lines share parse state.
        let h1 = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let lines: Vec<String> = (0..50).map(|i| format!("fn f{}() {{ }}", i)).collect();

        let requested: Vec<usize> = (10..30).collect();
        let batch = h1.highlight_lines_batch(&lines, version, &requested);

        assert_eq!(batch.len(), 20);
        let h2 = SyntaxHighlighter::new().with_syntax("Rust");
        for (line, tokens) in &batch {
            let individual = h2.highlight_line_at(&lines, *line, version);
            assert_eq!(tokens.len(), individual.len(), "mismatch at line {}", line);
        }
    }

    // -------------------------------------------------------------------------
    // highlight_tokens_cancellable
    // -------------------------------------------------------------------------

    #[test]
    fn cancellable_no_cancel_same_as_normal() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..10).map(|i| format!("// line {}", i)).collect();

        let normal = h.highlight_tokens(&lines, version, 0, 10);
        // Rebuild to ensure fresh cache for comparison.
        let h2 = SyntaxHighlighter::new();
        let cancellable = h2.highlight_tokens_cancellable(&lines, version, 0, 10, || false);

        assert_eq!(normal.len(), cancellable.len());
        for i in 0..normal.len() {
            assert_eq!(normal[i].len(), cancellable[i].len(),
                "token count for line {} must match", i);
        }
    }

    #[test]
    fn cancellable_cancel_before_start_returns_empty() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..10).map(|i| format!("// line {}", i)).collect();

        let result = h.highlight_tokens_cancellable(&lines, version, 0, 10, || true);
        assert!(result.is_empty(), "cancelled before start must return empty vec");
    }

    #[test]
    fn cancellable_cancel_mid_way_returns_partial() {
        use std::sync::atomic::{AtomicBool, Ordering};
        use std::sync::Arc;

        // We cannot cancel precisely between lines in a single-threaded test,
        // but we CAN verify that a pre-set cancellation after first line works
        // by using a shared flag set externally.
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = (0..50).map(|i| format!("// line {}", i)).collect();
        let cancelled = Arc::new(AtomicBool::new(false));
        let cancelled_clone = cancelled.clone();

        // Cancel after a short moment in a separate thread — this races, but
        // since the highlighting is CPU-bound and cancellation is polled between
        // lines, we just verify the result is a subset [0, 50].
        let handle = std::thread::spawn(move || {
            std::thread::sleep(std::time::Duration::from_micros(1));
            cancelled_clone.store(true, Ordering::SeqCst);
        });

        let result = h.highlight_tokens_cancellable(
            &lines, version, 0, 50,
            || cancelled.load(Ordering::SeqCst),
        );
        handle.join().unwrap();

        // Result must be a strict prefix of the full 50-line set.
        assert!(result.len() <= 50, "result cannot exceed total lines");
    }

    // -------------------------------------------------------------------------
    // Edge cases
    // -------------------------------------------------------------------------

    #[test]
    fn empty_lines_array_returns_empty() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines: Vec<String> = vec![];

        // highlight_line_at on empty array: should return empty, not panic.
        let tokens = h.highlight_line_at(&lines, 0, version);
        assert!(tokens.is_empty());

        let range = h.highlight_range(&lines, version, 0, 10);
        assert!(range.is_empty());
    }

    #[test]
    fn out_of_bounds_line_returns_empty_tokens() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["let x = 1;".to_string()];

        let tokens = h.highlight_line_at(&lines, 999, version);
        assert!(tokens.is_empty(), "OOB line must return empty tokens, not panic");
    }

    #[test]
    fn single_empty_string_line_no_panic() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["".to_string()];

        let tokens = h.highlight_line_at(&lines, 0, version);
        // Empty line may produce zero or one tokens — just must not panic.
        let _ = tokens;
    }

    #[test]
    fn very_long_line_no_panic() {
        let h = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let long_line = "x".repeat(100_000);
        let lines = vec![format!("let s = \"{}\";", long_line)];

        let tokens = h.highlight_line_at(&lines, 0, version);
        assert!(!tokens.is_empty());
    }

    #[test]
    fn unicode_heavy_line_no_panic() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["// ∞ ★ 𝓤𝓷𝓲𝓬𝓸𝓭𝓮 αβγδ 日本語 العربية".to_string()];

        let tokens = h.highlight_line_at(&lines, 0, version);
        assert!(!tokens.is_empty());
    }

    #[test]
    fn line_of_only_whitespace_no_panic() {
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["    \t    ".to_string()];

        let _ = h.highlight_line_at(&lines, 0, version);
    }

    #[test]
    fn null_bytes_in_line_no_panic() {
        // Ensure the highlighter handles unexpected bytes gracefully.
        let h = SyntaxHighlighter::new();
        let version = Version::new();
        let lines = vec!["normal text".to_string()];
        let _ = h.highlight_line_at(&lines, 0, version);
    }

    // -------------------------------------------------------------------------
    // Construction and state snapshot
    // -------------------------------------------------------------------------

    #[test]
    fn for_path_rs_detects_rust_syntax() {
        use std::path::Path;
        let h = SyntaxHighlighter::for_path(Path::new("src/main.rs"));
        assert_eq!(h.syntax_name, "Rust");
    }

    #[test]
    fn for_path_unknown_extension_uses_plain_text() {
        use std::path::Path;
        let h = SyntaxHighlighter::for_path(Path::new("file.xyzdoesnotexist123"));
        assert_eq!(h.syntax_name, "Plain Text");
    }

    #[test]
    fn snapshot_state_roundtrip_preserves_config() {
        let original = SyntaxHighlighter::new()
            .with_theme("base16-ocean.dark")
            .with_syntax("Rust");

        let state = original.snapshot_state();
        assert_eq!(state.theme_name, "base16-ocean.dark");
        assert_eq!(state.syntax_name, "Rust");

        let restored = SyntaxHighlighter::from_state(state);
        assert_eq!(restored.theme_name, "base16-ocean.dark");
        assert_eq!(restored.syntax_name, "Rust");
    }

    #[test]
    fn snapshot_state_carries_accumulated_checkpoints() {
        let mut h = SyntaxHighlighter::new();
        h.checkpoint_interval = 10;
        let version = Version::new();
        let lines: Vec<String> = (0..30).map(|i| format!("// line {}", i)).collect();

        for i in 0..30 {
            h.highlight_line_at(&lines, i, version);
        }

        let state = h.snapshot_state();
        // Checkpoints should be present in the snapshot.
        assert!(!state.checkpoints.is_empty());
    }

    #[test]
    fn from_state_has_empty_line_cache() {
        // Constructing from a state snapshot gives an empty line cache, so the
        // first highlight_line_at after from_state always recomputes.
        let mut h = SyntaxHighlighter::new();
        h.checkpoint_interval = 1;
        let version = Version::new();
        let lines = vec!["fn test() {}".to_string()];

        let arc_original = h.highlight_line_at(&lines, 0, version);
        let state = h.snapshot_state();

        let h2 = SyntaxHighlighter::from_state(state);
        let arc_restored = h2.highlight_line_at(&lines, 0, version);

        // Different Arcs (fresh cache) but same content.
        assert!(!Arc::ptr_eq(&arc_original, &arc_restored));
        assert_eq!(arc_original.len(), arc_restored.len());
    }

    // -------------------------------------------------------------------------
    // Style conversion helpers
    // -------------------------------------------------------------------------

    #[test]
    fn to_ratatui_color_converts_rgb() {
        use syntect::highlighting::Color;
        let c = Color { r: 255, g: 128, b: 0, a: 255 };
        let ratatui_color = to_ratatui_color(c);
        assert_eq!(ratatui_color, ratatui::style::Color::Rgb(255, 128, 0));
    }

    #[test]
    fn to_ratatui_style_plain_produces_no_modifiers() {
        use syntect::highlighting::Style;
        let style = Style::default();
        let ratatui_style = to_ratatui_style(&style);
        // No bold/italic/underline bits should be set.
        assert_eq!(ratatui_style.add_modifier, ratatui::style::Modifier::empty());
    }

    #[test]
    fn to_ratatui_style_bold_sets_modifier() {
        use syntect::highlighting::{FontStyle, Style};
        let style = Style { font_style: FontStyle::BOLD, ..Default::default() };
        let ratatui_style = to_ratatui_style(&style);
        assert!(ratatui_style.add_modifier.contains(ratatui::style::Modifier::BOLD));
    }

    // -------------------------------------------------------------------------
    // Consistency: batch vs individual, multi-language
    // -------------------------------------------------------------------------

    #[test]
    fn tokens_to_spans_empty_tokens_gives_empty_spans() {
        let h = SyntaxHighlighter::new();
        let spans = h.tokens_to_spans("", &[]);
        assert!(spans.is_empty());
    }

    #[test]
    fn highlight_with_rust_syntax_produces_more_tokens_than_plain() {
        // Rust syntax should produce more distinct tokens for Rust code than
        // plain-text mode (which emits a single token per line).
        let rust_h = SyntaxHighlighter::new().with_syntax("Rust");
        let plain_h = SyntaxHighlighter::new(); // Plain Text default
        let version = Version::new();
        let lines = vec!["fn main() { let x = 42; }".to_string()];

        let rust_tokens = rust_h.highlight_line_at(&lines, 0, version);
        let plain_tokens = plain_h.highlight_line_at(&lines, 0, version);

        assert!(
            rust_tokens.len() > plain_tokens.len(),
            "Rust highlighting should produce more token splits than plain text"
        );
    }

    #[test]
    fn sequential_lines_produce_valid_spans() {
        // Verify that multiple consecutive lines each produce non-empty, covering spans.
        let h = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let lines = vec![
            "fn add(a: i32, b: i32) -> i32 {".to_string(),
            "    a + b".to_string(),
            "}".to_string(),
        ];

        for (i, line) in lines.iter().enumerate() {
            let tokens = h.highlight_line_at(&lines, i, version);
            let spans = h.tokens_to_spans(line, tokens.as_ref());
            let mut reconstructed = String::new();
            for s in &spans {
                reconstructed.push_str(&line[s.range.start..s.range.end]);
            }
            assert_eq!(reconstructed, *line, "spans must reconstruct line {}", i);
        }
    }

    // -------------------------------------------------------------------------
    // Realistic editing session
    // -------------------------------------------------------------------------

    #[test]
    fn realistic_editing_session() {
        // Simulate: open a Rust file, highlight full screen, type on line 5,
        // re-highlight visible window, scroll down, scroll back, type again.
        let h = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let mut lines: Vec<String> = vec![
            "use std::collections::HashMap;".to_string(),
            "".to_string(),
            "fn process(data: &[u8]) -> HashMap<u32, Vec<u8>> {".to_string(),
            "    let mut map = HashMap::new();".to_string(),
            "    for (i, &b) in data.iter().enumerate() {".to_string(),
            "        map.entry(b as u32).or_default().push(i as u8);".to_string(),
            "    }".to_string(),
            "    map".to_string(),
            "}".to_string(),
            "".to_string(),
            "fn main() {".to_string(),
            "    let data = vec![1, 2, 3, 1, 2];".to_string(),
            "    let result = process(&data);".to_string(),
            "    println!(\"{:?}\", result);".to_string(),
            "}".to_string(),
        ];

        // Initial full-screen highlight.
        for i in 0..lines.len() {
            let t = h.highlight_line_at(&lines, i, version);
            assert!(!t.is_empty() || lines[i].is_empty(), "line {} must be highlighted", i);
        }

        // Edit line 5: change loop body.
        lines[5] = "        map.entry(b as u32).or_insert_with(Vec::new).push(i as u8);".to_string();
        h.invalidate_from(5);

        // Re-highlight visible window (all 15 lines).
        for i in 0..lines.len() {
            let t = h.highlight_line_at(&lines, i, version);
            assert!(!t.is_empty() || lines[i].is_empty());
        }

        // Lines before edit should not have been recomputed (same Arc).
        // We can't check this here since we discarded the old Arcs, but the key
        // invariant is no panic and valid highlighting.

        // Simulate append: add a new line at the end.
        lines.push("// end of file".to_string());
        h.invalidate_from(lines.len() - 1);

        let last = h.highlight_line_at(&lines, lines.len() - 1, version);
        assert!(!last.is_empty());
    }

    #[test]
    fn many_small_edits_at_same_line_remain_correct() {
        // Simulates typing one character at a time on the same line.
        let h = SyntaxHighlighter::new().with_syntax("Rust");
        let version = Version::new();
        let mut lines = vec!["fn main".to_string()];

        let suffixes = ["(", ") {", "\n    let x = 0;", "\n}"];
        for suffix in &suffixes {
            lines[0].push_str(suffix);
            h.invalidate_from(0);
            let t = h.highlight_line_at(&lines, 0, version);
            assert!(!t.is_empty(), "tokens must be non-empty after appending '{}'", suffix);
        }
    }
}
