//! Auto-indentation.
//!
//! # Architecture
//!
//! For files with a tree-sitter grammar, the language's `indents.scm` is the
//! source of truth. Captures are interpreted via `QueryCursor`/`Node` APIs
//! (no ad-hoc parsing of the query file's literal strings).
//!
//! When tree-sitter cannot decide (typically because the parsed window
//! contains incomplete syntax — e.g. the user has typed `{` but not the
//! matching `}`), the fallback depends on the language family:
//!
//! - **Keyword-delimited languages** (Lua, Ruby, Bash, Pascal): copy the
//!   current line's indent. Layering byte heuristics on top would mis-indent
//!   them — `(` opens a function call, not a block. See `calculate_indent`.
//! - **C-family languages** (Rust, JS/TS, C/C++, Java, Go, Python, JSON,
//!   HTML, CSS, …): consult [`IndentCalculator::calculate_indent_pattern`] as
//!   a pragmatic last-resort heuristic, since its `{`/`[`/`(`/`:` triggers
//!   line up with those languages' block openers.
//!
//! For files **without** any tree-sitter grammar (`.txt`, `.ini`, `Dockerfile`,
//! `Makefile`, …), [`IndentCalculator::calculate_indent_no_language`] uses
//! the C-family pattern heuristic directly — without an AST there is nothing
//! better to do.
//!
//! # Performance
//! - Parses up to 2000 bytes before cursor (balances accuracy vs speed).
//! - Pattern matching is O(n) where n = lines scanned (typically < 100).
//! - Tree-sitter queries cached per-language.
//!
//! # Query Captures
//! - `@indent`: Increase indent after this node (e.g., `block`).
//! - `@dedent`: Decrease indent for this node (e.g., closing `}`, `end`,
//!   `fi`, `done`).
//!
//! # History
//! Issue #1425 generalised auto-indent's leading-whitespace handling. PR #1819
//! revealed that the previous "tree-sitter then unconditional C-family pattern
//! matching" pipeline cross-contaminated keyword-delimited languages. This
//! module's current shape — tree-sitter as the source of truth, pattern
//! fallback gated by language family — is the resolution.

use crate::model::buffer::Buffer;
use crate::primitives::highlighter::Language;
use fresh_languages::tree_sitter::{Parser, Query, QueryCursor, StreamingIterator};
use std::collections::HashMap;

/// Maximum bytes to parse before cursor for indent calculation
const MAX_PARSE_BYTES: usize = 2000;

/// Indent calculator using tree-sitter queries
pub struct IndentCalculator {
    /// Map of language to (parser, query).
    ///
    /// Only populated when the `tree-sitter` feature is enabled; without it
    /// `get_config` always returns `None` and indentation falls back to the
    /// pattern-based heuristics, leaving this map unused.
    #[cfg_attr(not(feature = "tree-sitter"), allow(dead_code))]
    configs: HashMap<&'static str, (Parser, Query)>,
}

impl IndentCalculator {
    /// Create a new indent calculator
    pub fn new() -> Self {
        Self {
            configs: HashMap::new(),
        }
    }

    /// Get or create parser and query for a language
    fn get_config(&mut self, language: &Language) -> Option<(&mut Parser, &Query)> {
        // Without the tree-sitter grammars compiled in, there is no parser to
        // build. Returning `None` makes `calculate_indent` fall back to the
        // pure-Rust pattern-based indentation heuristics.
        #[cfg(not(feature = "tree-sitter"))]
        {
            let _ = language;
            return None;
        }
        #[cfg(feature = "tree-sitter")]
        {
            // Parser language comes from the centralized accessor, which is
            // `None` for any grammar not compiled into this build. Most
            // languages are no longer bundled (they use syntect highlighting +
            // the regex indent-rules tier), so this bails to the caller's
            // fallback for them. See fresh_languages::Language::ts_language.
            let ts_language: fresh_languages::tree_sitter::Language = language.ts_language()?;
            let (lang_name, query_str) = match language {
                Language::Rust => ("rust", include_str!("../../queries/rust/indents.scm")),
                Language::Python => ("python", include_str!("../../queries/python/indents.scm")),
                Language::JavaScript => (
                    "javascript",
                    include_str!("../../queries/javascript/indents.scm"),
                ),
                Language::TypeScript => (
                    "typescript",
                    include_str!("../../queries/typescript/indents.scm"),
                ),
                Language::C => ("c", include_str!("../../queries/c/indents.scm")),
                Language::Cpp => ("cpp", include_str!("../../queries/cpp/indents.scm")),
                Language::Go => ("go", include_str!("../../queries/go/indents.scm")),
                Language::Java => ("java", include_str!("../../queries/java/indents.scm")),
                Language::HTML => ("html", include_str!("../../queries/html/indents.scm")),
                Language::CSS => ("css", include_str!("../../queries/css/indents.scm")),
                Language::Bash => ("bash", include_str!("../../queries/bash/indents.scm")),
                Language::Json => ("json", include_str!("../../queries/json/indents.scm")),
                Language::Jsonc => ("jsonc", include_str!("../../queries/json/indents.scm")),
                Language::Ruby => ("ruby", include_str!("../../queries/ruby/indents.scm")),
                Language::Php => ("php", include_str!("../../queries/php/indents.scm")),
                Language::Lua => ("lua", include_str!("../../queries/lua/indents.scm")),
                Language::CSharp => ("csharp", include_str!("../../queries/csharp/indents.scm")),
                Language::Pascal => ("pascal", include_str!("../../queries/pascal/indents.scm")),
                Language::Odin => ("odin", include_str!("../../queries/odin/indents.scm")),
                // Templ extends Go's grammar; Go's indent rules apply to the Go
                // portions of a templ file. The HTML/CSS portions fall back to
                // copy-current-line indent, good enough as an initial heuristic.
                Language::Templ => ("templ", include_str!("../../queries/go/indents.scm")),
            };

            // Check if we already have this config
            if !self.configs.contains_key(lang_name) {
                // Create parser
                let mut parser = Parser::new();
                if parser.set_language(&ts_language).is_err() {
                    tracing::error!("Failed to set language for {}", lang_name);
                    return None;
                }

                // Create query
                let query = match Query::new(&ts_language, query_str) {
                    Ok(q) => q,
                    Err(e) => {
                        tracing::error!("Failed to create query for {}: {:?}", lang_name, e);
                        return None;
                    }
                };

                self.configs.insert(lang_name, (parser, query));
            }

            // Return mutable references
            let (parser, query) = self.configs.get_mut(lang_name)?;
            Some((parser, query))
        }
    }

    /// Calculate indent for a new line at the given position
    ///
    /// Returns the number of spaces to indent, or None if auto-indent should be disabled.
    ///
    /// # Fallback policy by language family
    ///
    /// When `language` has tree-sitter support, tree-sitter (via the language's
    /// `indents.scm`) is the source of truth. If tree-sitter cannot decide
    /// (e.g. it returns `None` because the parsed window is incomplete), the
    /// fallback depends on whether the language uses keyword-delimited blocks
    /// (see [`uses_keyword_delimited_blocks`]):
    ///
    /// - **Keyword-delimited (Lua, Ruby, Bash, Pascal):** copy the current
    ///   line's indent and stop. The C-family byte heuristics in
    ///   [`calculate_indent_pattern`] would mis-indent these languages —
    ///   their `(` opens a function call, not a block, and their blocks are
    ///   opened by words (`function`, `def`, `do`, `then`, `begin`) that the
    ///   pattern matcher cannot recognise. See issue #1425 and PR #1819.
    /// - **C-family (Rust, JavaScript, TypeScript, C, C++, Java, Go, Python,
    ///   JSON, HTML, CSS, …):** consult [`calculate_indent_pattern`] as a
    ///   pragmatic last-resort heuristic. This keeps "user typed `{` and
    ///   pressed Enter before completing the closing `}`" working in the
    ///   common case where tree-sitter cannot parse the half-written buffer.
    pub fn calculate_indent(
        &mut self,
        buffer: &Buffer,
        position: usize,
        language: &Language,
        tab_size: usize,
    ) -> Option<usize> {
        // When the cursor is inside (or at the boundary of) an existing
        // non-empty line's leading whitespace, the auto-indent must equal
        // the cursor's column so that pressing Enter does not displace the
        // existing content. See #1425.
        if let Some(indent) = Self::indent_for_cursor_in_leading_ws(buffer, position, tab_size) {
            return Some(indent);
        }

        // Try tree-sitter-based indent
        if let Some(indent) =
            self.calculate_indent_tree_sitter(buffer, position, language, tab_size)
        {
            return Some(indent);
        }

        // No tree-sitter grammar (most languages aren't bundled) or it couldn't
        // decide: consult the per-language regex rules tier, which knows each
        // language's openers/closers (Python `:`/`return`, Ruby `end`, …). This
        // path runs without scope masking (no highlighter here); the editor's
        // primary, masked rules pass happens earlier in `actions.rs`, so this is
        // mainly the fallback for languages whose grammar was dropped.
        if let Some(rules) = crate::primitives::indent_rules::rules_for_id(language.id()) {
            return Some(rules.calculate_indent(buffer, position, tab_size, |_| true));
        }

        // Tree-sitter could not decide. For keyword-delimited languages, copy
        // the current line's indent and stop — running `calculate_indent_pattern`
        // would mis-indent them (its `(`/`:` triggers don't match those
        // languages' grammars). For C-family languages, fall back to pattern
        // matching as a pragmatic last resort for the common
        // "buffer is mid-edit" case where tree-sitter has no useful structure.
        if Self::uses_keyword_delimited_blocks(language) {
            return Some(Self::get_current_line_indent(buffer, position, tab_size));
        }

        if let Some(indent) = Self::calculate_indent_pattern(buffer, position, tab_size) {
            return Some(indent);
        }

        Some(Self::get_current_line_indent(buffer, position, tab_size))
    }

    /// Whether the language opens blocks with keywords (e.g. `function … end`,
    /// `def … end`, `if … fi`, `begin … end`) rather than C-style braces.
    ///
    /// Used by [`calculate_indent`] to decide whether the C-family byte
    /// heuristics in [`calculate_indent_pattern`] are a safe last-resort
    /// fallback when tree-sitter cannot decide. For keyword-delimited
    /// languages, those heuristics produce wrong answers (notably treating
    /// `(` as an indent trigger when it opens a function call rather than a
    /// block) and must not be consulted.
    fn uses_keyword_delimited_blocks(language: &Language) -> bool {
        matches!(
            language,
            Language::Lua | Language::Ruby | Language::Bash | Language::Pascal
        )
    }

    /// Calculate indent without language/tree-sitter support
    /// Uses pattern matching and current line copying as fallback
    /// This is used for files without syntax highlighting (e.g., .txt files)
    pub fn calculate_indent_no_language(
        buffer: &Buffer,
        position: usize,
        tab_size: usize,
    ) -> usize {
        // See `calculate_indent` for the rationale (#1425).
        if let Some(indent) = Self::indent_for_cursor_in_leading_ws(buffer, position, tab_size) {
            return indent;
        }

        // Pattern-based indent (for incomplete syntax)
        if let Some(indent) = Self::calculate_indent_pattern(buffer, position, tab_size) {
            return indent;
        }

        // Final fallback: copy current line's indent
        Self::get_current_line_indent(buffer, position, tab_size)
    }

    /// If `position` is inside (or at the boundary of) the leading whitespace
    /// of a line that has non-whitespace content, return the cursor's column
    /// measured in indent units.
    ///
    /// At such a position, pressing Enter splits the line before (or in the
    /// middle of) the existing leading whitespace. To preserve the existing
    /// content's column on the new line below, the auto-indent inserted
    /// between the new `\n` and the remainder of the line must equal the
    /// cursor's column. Concretely:
    ///
    /// - cursor at col 0 of `unindented line`     → 0 (no displacement)
    /// - cursor at col 0 of `    indented_target` → 0 (the 4 spaces ride
    ///   over with the content untouched)
    /// - cursor at col 2 of `    indented_target` → 2 (line A keeps 2
    ///   spaces; 2 more spaces remain in front of `indented_target`)
    /// - cursor at col 4 of `    foo()` (just before `f`) → 4
    ///
    /// The rule is language-agnostic — it does not look at what character
    /// starts the content (word, `}`, `end`, `</tag>`, `fi`, …) — and it
    /// matches the behaviour of VS Code, Sublime Text, and similar editors.
    /// Returns `None` when the cursor is past any non-whitespace character on
    /// the line, or when the line has no content (empty / whitespace-only);
    /// in those cases the regular smart-indent logic takes over.
    fn indent_for_cursor_in_leading_ws(
        buffer: &Buffer,
        position: usize,
        tab_size: usize,
    ) -> Option<usize> {
        // Find start of the current line.
        let mut line_start = position;
        while line_start > 0 {
            if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
                break;
            }
            line_start = line_start.saturating_sub(1);
        }

        // Verify everything from line_start to position is whitespace and
        // accumulate the cursor's column in indent units.
        let mut col = 0;
        let mut pos = line_start;
        while pos < position {
            match Self::byte_at(buffer, pos) {
                Some(b' ') => col += 1,
                Some(b'\t') => col += tab_size,
                Some(b'\r') => {}
                _ => return None, // cursor is past content on this line
            }
            pos += 1;
        }

        // Require at least one non-whitespace character at or after the
        // cursor; otherwise this is a blank/whitespace-only line and the
        // existing logic already handles it correctly.
        let mut pos = position;
        while pos < buffer.len() {
            match Self::byte_at(buffer, pos) {
                Some(b'\n') => return None,
                Some(b' ') | Some(b'\t') | Some(b'\r') => pos += 1,
                Some(_) => return Some(col),
                None => return None,
            }
        }
        None
    }

    /// Calculate the correct indent for a closing delimiter being typed.
    ///
    /// # C-family limitation
    ///
    /// The pattern-matching fallback used here ([`calculate_dedent_pattern`])
    /// only understands C-family bracket nesting (`{}`, `[]`, `()`). It does
    /// **not** know about keyword-delimited blocks like Lua `function … end`,
    /// Ruby `def … end`, or Bash `if … fi`. Generalising the dedent algorithm
    /// to those languages requires tracking opener/closer pairs that are words
    /// rather than single characters — a separate, larger change. For now the
    /// public callers (auto-dedent on typing a closing delimiter) only fire on
    /// C-family delimiters anyway, so the limitation is contained.
    ///
    /// # Strategy: Tree-sitter with Pattern Fallback
    ///
    /// This function attempts to use tree-sitter first, but falls back to pattern matching
    /// when the syntax is incomplete (which is the common case during typing).
    ///
    /// ## Tree-sitter Path
    /// 1. Parse buffer content before cursor (up to 2000 bytes)
    /// 2. Count @indent nodes at cursor position vs reference line
    /// 3. Calculate dedent based on nesting level difference
    /// 4. **Problem**: Fails when syntax is incomplete (e.g., missing closing brace)
    ///
    /// ## Pattern Matching Fallback (see calculate_dedent_pattern)
    /// 1. Scan backwards line by line
    /// 2. Track nesting depth (closing delimiters increment, opening decrement)
    /// 3. Find first unmatched opening delimiter
    /// 4. Dedent to its indentation level
    ///
    /// # Example
    /// ```text
    /// if (1) {
    ///     if (2) {
    ///         hi
    ///     }      // inner closing at depth 1
    ///     more
    ///     <cursor typing }>  // should dedent to column 0, not 4
    /// ```
    ///
    /// Pattern matching correctly skips the matched inner block and finds the outer `if (1) {`.
    pub fn calculate_dedent_for_delimiter(
        &mut self,
        buffer: &Buffer,
        position: usize,
        _delimiter: char,
        language: &Language,
        tab_size: usize,
    ) -> Option<usize> {
        // Get parser and query for this language. When no grammar is bundled
        // (most languages), defer to the per-language regex rules tier, then to
        // the language-agnostic bracket scanner.
        let Some((parser, query)) = self.get_config(language) else {
            if let Some(rules) = crate::primitives::indent_rules::rules_for_id(language.id()) {
                if let Some(indent) = rules.calculate_dedent_for_delimiter(
                    buffer,
                    position,
                    _delimiter,
                    tab_size,
                    |_| true,
                ) {
                    return Some(indent);
                }
            }
            return Self::calculate_dedent_pattern(buffer, position, tab_size);
        };

        // Extract context before cursor (for parsing)
        let parse_start = position.saturating_sub(MAX_PARSE_BYTES);
        let parse_range = parse_start..position;

        if parse_range.is_empty() {
            return Some(0);
        }

        let source = buffer.slice_bytes(parse_range.clone());

        // Parse the source
        let tree = parser.parse(&source, None)?;
        let root = tree.root_node();

        // Find capture index for @indent
        let mut indent_capture_idx = None;
        for (i, name) in query.capture_names().iter().enumerate() {
            if *name == "indent" {
                indent_capture_idx = Some(i);
                break;
            }
        }

        let indent_capture_idx = indent_capture_idx?;

        let cursor_offset = position - parse_start;

        // Hybrid heuristic: find previous non-empty line as reference
        // This is the same approach used in calculate_indent_tree_sitter
        let (reference_line_indent, reference_line_offset) = {
            let mut search_pos = position;
            let mut reference_indent = 0;
            let mut reference_offset = cursor_offset;

            // Scan backwards through the buffer to find a non-empty line
            while search_pos > 0 {
                // Find start of current line
                let mut line_start = search_pos;
                while line_start > 0 {
                    if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
                        break;
                    }
                    line_start = line_start.saturating_sub(1);
                }

                // Check if this line has non-whitespace content
                let mut has_content = false;
                let mut line_indent = 0;
                let mut content_pos = line_start;
                let mut pos = line_start;
                while pos < search_pos {
                    match Self::byte_at(buffer, pos) {
                        Some(b' ') => line_indent += 1,
                        Some(b'\t') => line_indent += tab_size,
                        Some(b'\n') => break,
                        Some(_) => {
                            has_content = true;
                            content_pos = pos; // Remember where we found content
                            break;
                        }
                        None => break,
                    }
                    pos += 1;
                }

                if has_content {
                    // Found a non-empty line, use it as reference
                    reference_indent = line_indent;
                    // Use position of first non-whitespace character as reference
                    if content_pos >= parse_start {
                        reference_offset = content_pos - parse_start;
                    } else {
                        // Reference line is before parse window - use start of parse window
                        reference_offset = 0;
                    }
                    break;
                }

                // Move to previous line
                if line_start == 0 {
                    break;
                }
                search_pos = line_start.saturating_sub(1);
            }

            (reference_indent, reference_offset)
        };

        // Count @indent nodes at reference and cursor positions
        let mut reference_indent_count: i32 = 0;
        let mut cursor_indent_count: i32 = 0;

        let mut query_cursor = QueryCursor::new();
        let mut captures = query_cursor.captures(query, root, source.as_slice());

        while let Some((match_result, _)) = captures.next() {
            for capture in match_result.captures {
                if capture.index == indent_capture_idx as u32 {
                    let node = capture.node;
                    let node_start = node.start_byte();
                    let node_end = node.end_byte();

                    // Count @indent nodes at reference position
                    if node_start < reference_line_offset && reference_line_offset <= node_end {
                        reference_indent_count += 1;
                    }

                    // Count @indent nodes at cursor position
                    if node_start < cursor_offset && cursor_offset <= node_end {
                        cursor_indent_count += 1;
                    }
                }
            }
        }

        // Tree-sitter fallback: incomplete syntax produces ERROR nodes with no structure
        // This is the common case when typing (e.g., "if (true) {\n    hi\n    " is incomplete)
        // Pattern matching handles this gracefully by tracking delimiter nesting
        if cursor_indent_count == 0 && reference_indent_count == 0 {
            tracing::debug!("No @indent nodes found (incomplete syntax), using pattern fallback");
            return Self::calculate_dedent_pattern(buffer, position, tab_size);
        }

        // Tree-sitter path: Calculate relative indent based on @indent node counts
        // The closing delimiter should be at one level less than current nesting
        // Formula: reference_indent + (cursor_depth - reference_depth - 1) * tab_size
        // The -1 accounts for the closing delimiter dedenting one level
        let indent_delta = cursor_indent_count - reference_indent_count - 1;
        let final_indent =
            (reference_line_indent as i32 + (indent_delta * tab_size as i32)).max(0) as usize;

        tracing::debug!(
            "Tree-sitter dedent: reference_indent={}, cursor_depth={}, reference_depth={}, delta={}, final_indent={}",
            reference_line_indent,
            cursor_indent_count,
            reference_indent_count,
            indent_delta,
            final_indent
        );

        Some(final_indent)
    }

    /// Calculate dedent using pattern matching (fallback for incomplete syntax)
    ///
    /// This is the **primary dedent algorithm** used during typing, since tree-sitter
    /// cannot handle incomplete syntax.
    ///
    /// # Algorithm: Nesting Depth Tracking
    ///
    /// Scans backwards line by line, tracking nesting depth to skip over already-matched
    /// delimiter pairs. This ensures we find the **matching** opening delimiter, not just
    /// any opening delimiter.
    ///
    /// ## Depth Counter Logic
    /// - **Closing delimiter** (`}`, `]`, `)`) → increment depth
    ///   - Reason: We need to skip its matching opening delimiter
    /// - **Opening delimiter** (`{`, `[`, `(`) → check depth:
    ///   - If depth > 0: decrement and continue (this open is matched)
    ///   - If depth == 0: **found it!** This is the unmatched opening we're looking for
    ///
    /// ## Example Walkthrough
    /// ```text
    /// if (1) {           // ← target: we want to find this
    ///     if (2) {
    ///         hi
    ///     }              // matched pair
    ///     more
    ///     <cursor>       // typing } here
    /// ```
    ///
    /// Search backwards:
    /// 1. Line "    more" → not a delimiter, continue
    /// 2. Line "    }" → closing delimiter, depth = 1 (skip next opening)
    /// 3. Line "        hi" → not a delimiter, continue
    /// 4. Line "    if (2) {" → opening delimiter, but depth = 1, so decrement to 0, continue
    /// 5. Line "if (1) {" → opening delimiter, depth = 0, **match found!** Return indent = 0
    ///
    /// # Language Agnostic
    /// Works for any language using C-style delimiters: { } [ ] ( )
    fn calculate_dedent_pattern(
        buffer: &Buffer,
        position: usize,
        tab_size: usize,
    ) -> Option<usize> {
        let mut depth = 0;
        let mut search_pos = position;

        while search_pos > 0 {
            // Find start of line
            let mut line_start = search_pos;
            while line_start > 0 {
                if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
                    break;
                }
                line_start = line_start.saturating_sub(1);
            }

            // Get line content
            let line_bytes = buffer.slice_bytes(line_start..search_pos + 1);
            let last_non_ws = line_bytes
                .iter()
                .rev()
                .find(|&&b| b != b' ' && b != b'\t' && b != b'\r' && b != b'\n');

            if let Some(&last_char) = last_non_ws {
                // Calculate this line's indentation (count leading spaces/tabs)
                let mut line_indent = 0;
                let mut pos = line_start;
                while pos < search_pos {
                    match Self::byte_at(buffer, pos) {
                        Some(b' ') => line_indent += 1,
                        Some(b'\t') => line_indent += tab_size,
                        Some(b'\n') => break,
                        Some(_) => break, // Hit non-whitespace
                        None => break,
                    }
                    pos += 1;
                }

                // Apply nesting depth tracking based on last character
                match last_char {
                    // Closing delimiter: increment depth to skip its matching opening
                    b'}' | b']' | b')' => {
                        depth += 1;
                        tracing::debug!(
                            "Pattern dedent: found closing '{}', depth now {}",
                            last_char as char,
                            depth
                        );
                    }

                    // Opening delimiter: check if it's matched or unmatched
                    b'{' | b'[' | b'(' => {
                        if depth > 0 {
                            // Already matched by a closing delimiter we saw earlier
                            depth -= 1;
                            tracing::debug!(
                                "Pattern dedent: skipping matched '{}' (depth {}→{})",
                                last_char as char,
                                depth + 1,
                                depth
                            );
                        } else {
                            // Unmatched! This is the opening delimiter we're closing
                            tracing::debug!(
                                "Pattern dedent: found unmatched '{}' at indent {}",
                                last_char as char,
                                line_indent
                            );
                            return Some(line_indent);
                        }
                    }

                    // Content line: continue searching
                    _ => {
                        tracing::debug!(
                            "Pattern dedent: line ends with '{}', continuing",
                            last_char as char
                        );
                    }
                }
            }

            // Move to previous line
            if line_start == 0 {
                break;
            }
            search_pos = line_start.saturating_sub(1);
        }

        // No matching opening delimiter found - dedent to column 0
        Some(0)
    }

    /// Calculate indent using simple byte-level pattern matching.
    ///
    /// # Scope
    ///
    /// This function is the heuristic used:
    ///
    /// 1. By [`calculate_indent_no_language`] — files without a tree-sitter
    ///    grammar (`.txt`, `.ini`, `Dockerfile`, `Makefile`, …). Without an
    ///    AST there is nothing better to do.
    /// 2. By [`calculate_indent`] **only** for tree-sitter-backed C-family
    ///    languages (Rust, JS/TS, C/C++, Java, Go, Python, JSON, HTML, CSS,
    ///    PHP, C#, Odin) when tree-sitter cannot decide — typically because
    ///    the parsed window contains incomplete syntax (e.g. user just typed
    ///    `{` and has not yet typed the matching `}`).
    ///
    /// It is **not** consulted for keyword-delimited languages (Lua, Ruby,
    /// Bash, Pascal). For those, layering this byte heuristic on top of
    /// tree-sitter would produce wrong answers — most notably treating `(` as
    /// an indent trigger when in those languages `(` opens a function call,
    /// not a block.
    ///
    /// # C-family bias (intentional)
    ///
    /// The triggers `{`, `[`, `(`, `:` are baked in. Without a grammar there
    /// is nothing better to do — these are the most common delimiters across
    /// programming and structured-text formats. They line up with C-family
    /// languages' own block openers, which is why we still use this as a
    /// last-resort fallback for those. They are wrong for keyword-delimited
    /// languages, which is why [`calculate_indent`] suppresses this path for
    /// Lua/Ruby/Bash/Pascal. See issue #1425 and PR #1819.
    fn calculate_indent_pattern(
        buffer: &Buffer,
        position: usize,
        tab_size: usize,
    ) -> Option<usize> {
        if position == 0 {
            return None;
        }

        // Find start of the line we're currently on (before pressing Enter)
        let mut line_start = position;
        while line_start > 0 {
            if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
                break;
            }
            line_start = line_start.saturating_sub(1);
        }

        // Get the content of the current line (the one we're leaving)
        let line_bytes = buffer.slice_bytes(line_start..position);

        // Find the last non-whitespace character on current line
        let last_non_whitespace = line_bytes
            .iter()
            .rev()
            .find(|&&b| b != b' ' && b != b'\t' && b != b'\r');

        // Check if current line is empty (only whitespace)
        let current_line_is_empty = last_non_whitespace.is_none();

        // Hybrid heuristic: find previous non-empty line for reference
        let reference_indent = if !current_line_is_empty {
            // Current line has content - use its indent as reference
            Self::get_current_line_indent(buffer, position, tab_size)
        } else {
            // Current line is empty - find previous non-empty line and check for indent triggers
            let mut search_pos = if line_start > 0 {
                line_start - 1 // Position of \n before current line
            } else {
                0
            };

            let mut found_reference_indent = 0;
            while search_pos > 0 {
                // Find start of line
                let mut ref_line_start = search_pos;
                while ref_line_start > 0 {
                    if Self::byte_at(buffer, ref_line_start.saturating_sub(1)) == Some(b'\n') {
                        break;
                    }
                    ref_line_start = ref_line_start.saturating_sub(1);
                }

                // Check if this line has non-whitespace content
                let ref_line_bytes = buffer.slice_bytes(ref_line_start..search_pos + 1);
                let ref_last_non_ws = ref_line_bytes
                    .iter()
                    .rev()
                    .find(|&&b| b != b' ' && b != b'\t' && b != b'\r' && b != b'\n');

                if ref_last_non_ws.is_some() {
                    // Found a non-empty reference line - calculate its indent
                    let mut line_indent = 0;
                    let mut pos = ref_line_start;
                    while pos <= search_pos {
                        let byte_opt = Self::byte_at(buffer, pos);
                        match byte_opt {
                            Some(b' ') => line_indent += 1,
                            Some(b'\t') => line_indent += tab_size,
                            Some(b'\n') => break,
                            Some(_) => break, // Hit non-whitespace, done counting indent
                            None => break,
                        }
                        pos += 1;
                    }

                    found_reference_indent = line_indent;

                    // Check if reference line ends with indent trigger
                    if let Some(&last_char) = ref_last_non_ws {
                        match last_char {
                            b'{' | b'[' | b'(' => {
                                tracing::debug!(
                                    "Pattern match: reference line ends with '{}'",
                                    last_char as char
                                );
                                return Some(found_reference_indent + tab_size);
                            }
                            b':' => {
                                tracing::debug!("Pattern match: reference line ends with colon");
                                return Some(found_reference_indent + tab_size);
                            }
                            _ => {}
                        }
                    }
                    break;
                }

                // Move to previous line
                if ref_line_start == 0 {
                    break;
                }
                search_pos = ref_line_start.saturating_sub(1);
            }

            // Return the reference indent we found (or 0 if no non-empty line was found)
            found_reference_indent
        };

        // If current line ends with indent trigger, add to reference
        if let Some(&last_char) = last_non_whitespace {
            tracing::debug!("Pattern match: last char = '{}'", last_char as char);
            match last_char {
                b'{' | b'[' | b'(' => {
                    // Opening braces/brackets/parens: increase indent
                    tracing::debug!("Pattern match: found opening brace/bracket at end of line");
                    return Some(reference_indent + tab_size);
                }
                b':' => {
                    // Colon (for Python, YAML, etc.): increase indent
                    tracing::debug!("Pattern match: found colon at end of line");
                    return Some(reference_indent + tab_size);
                }
                _ => {
                    tracing::debug!("Pattern match: no indent trigger found");
                }
            }
        }

        // Current line is empty and has no indent trigger - use reference indent
        Some(reference_indent)
    }

    /// Calculate indent using tree-sitter queries
    fn calculate_indent_tree_sitter(
        &mut self,
        buffer: &Buffer,
        position: usize,
        language: &Language,
        tab_size: usize,
    ) -> Option<usize> {
        // Get parser and query
        let (parser, query) = self.get_config(language)?;

        // Extract context before cursor (for parsing)
        let parse_start = position.saturating_sub(MAX_PARSE_BYTES);
        let parse_range = parse_start..position;

        if parse_range.is_empty() {
            return None;
        }

        let source = buffer.slice_bytes(parse_range.clone());

        // Parse the source
        let tree = parser.parse(&source, None)?;
        let root = tree.root_node();

        // Find capture indices for @indent, @dedent and @dedent_after
        let mut indent_capture_idx = None;
        let mut dedent_capture_idx = None;
        // `@dedent_after` marks statements that end the current block's
        // straight-line flow (Python `return`/`pass`/`raise`/`break`/
        // `continue`). When the cursor is at the end of such a statement,
        // pressing Enter dedents the new line one level — matching VS Code,
        // PyCharm and neovim. Languages opt in by adding the capture to their
        // `indents.scm`; those that don't are unaffected.
        let mut dedent_after_capture_idx = None;
        for (i, name) in query.capture_names().iter().enumerate() {
            if *name == "indent" {
                indent_capture_idx = Some(i);
            } else if *name == "dedent" {
                dedent_capture_idx = Some(i);
            } else if *name == "dedent_after" {
                dedent_after_capture_idx = Some(i);
            }
        }

        // Query for indent/dedent captures
        let mut query_cursor = QueryCursor::new();

        // Count indent/dedent at cursor position
        // The cursor position in the parsed text is (position - parse_start)
        let cursor_offset = position - parse_start;

        let mut indent_delta = 0i32;
        let mut found_any_captures = false;

        // Find the line start to get the base column offset
        let mut line_start_offset = cursor_offset;
        while line_start_offset > 0 {
            if source.get(line_start_offset.saturating_sub(1)) == Some(&b'\n') {
                break;
            }
            line_start_offset = line_start_offset.saturating_sub(1);
        }

        // Find the previous non-empty line in the buffer to use as reference
        // This is the "hybrid heuristic" approach: calculate indent delta relative to actual code
        let (reference_line_indent, reference_line_offset) = {
            let mut search_pos = position;
            let mut reference_indent = 0;
            let mut reference_offset = cursor_offset;

            // Scan backwards through the buffer to find a non-empty line
            while search_pos > 0 {
                // Find start of current line
                let mut line_start = search_pos;
                while line_start > 0 {
                    if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
                        break;
                    }
                    line_start = line_start.saturating_sub(1);
                }

                // Check if this line has non-whitespace content
                let mut has_content = false;
                let mut line_indent = 0;
                let mut content_pos = line_start;
                let mut pos = line_start;
                while pos < search_pos {
                    match Self::byte_at(buffer, pos) {
                        Some(b' ') => line_indent += 1,
                        Some(b'\t') => line_indent += tab_size,
                        Some(b'\n') => break,
                        Some(_) => {
                            has_content = true;
                            content_pos = pos; // Remember where we found content
                            break;
                        }
                        None => break,
                    }
                    pos += 1;
                }

                if has_content {
                    // Found a non-empty line, use it as reference
                    reference_indent = line_indent;
                    // Use position of first non-whitespace character as reference
                    // This ensures we're measuring from inside the content, not at line boundaries
                    if content_pos >= parse_start {
                        reference_offset = content_pos - parse_start;
                    } else {
                        // Reference line is before parse window - use start of parse window
                        reference_offset = 0;
                    }
                    break;
                }

                // Move to previous line
                if line_start == 0 {
                    break;
                }
                search_pos = line_start.saturating_sub(1);
            }

            (reference_indent, reference_offset)
        };

        // Locate the last non-whitespace byte on the cursor's line (used below
        // to ask tree-sitter what kind of token sits at the line's end —
        // structural replacement for the old "is the byte `}`?" check).
        let last_nonws_offset = {
            let mut pos = cursor_offset;
            let mut found = None;
            while pos > line_start_offset {
                pos -= 1;
                match source.get(pos) {
                    Some(b' ') | Some(b'\t') | Some(b'\r') => continue,
                    Some(_) => {
                        found = Some(pos);
                        break;
                    }
                    None => break,
                }
            }
            found
        };

        // Calculate indent delta using hybrid heuristic:
        // Count @indent nodes at reference line and at cursor, then compute the difference.
        let mut reference_indent_count: i32 = 0;
        let mut cursor_indent_count: i32 = 0;
        // Tree-sitter analogue of the old "line ends with `{`/`:`/..." byte
        // rescue: an @indent node that opens on the current line and contains
        // the cursor means a block has just been opened — pressing Enter
        // should go one level deeper.
        let mut indent_opens_on_cursor_line = false;
        // Distinguishes "this line *opens* a new block" from "the cursor is on a
        // statement *inside* an already-open block". Languages whose body is its
        // own node (Python's `(block)`, captured as @indent separately from the
        // compound statement) start that body node at the block's first
        // statement — on the cursor's line — so `indent_opens_on_cursor_line`
        // alone fires even when we are merely continuing the block, producing a
        // spurious extra level (issue #2192). A freshly-opened block instead
        // surfaces as an *empty* @indent node sitting exactly at the cursor
        // (e.g. `def foo():` yields `block 10..10`; `if x:` yields the inner
        // `block 20..20`). Use that as the precise "a block was just opened"
        // signal.
        let mut empty_indent_node_at_cursor = false;
        // Tree-sitter analogue of the old "line ends with `}`" byte rescue:
        // if the last token on the cursor's line is itself captured as @dedent
        // (any closing delimiter the language declares — `}`, `end`, `fi`,
        // `done`, `until`, `</tag>`, …), the new line should match the
        // line's existing indent rather than re-counting nesting (which
        // produces asymmetric results when the `@dedent` token sits on the
        // boundary of an `@indent` node).
        let mut last_nonws_is_dedent_capture = false;
        // Set when the cursor's line is a flow-exit statement captured as
        // `@dedent_after` (see capture-index lookup above): the new line should
        // drop one level out of the block this statement closes.
        let mut cursor_line_is_dedent_after = false;

        // Manually iterate through matches to count indent/dedent captures
        let mut captures = query_cursor.captures(query, root, source.as_slice());
        while let Some((match_result, _)) = captures.next() {
            for capture in match_result.captures {
                let node = capture.node;
                let node_start = node.start_byte();
                let node_end = node.end_byte();

                // Count @indent nodes at reference position
                if let Some(idx) = indent_capture_idx {
                    if capture.index == idx as u32 {
                        // Reference line: count if reference position is inside this node
                        if node_start < reference_line_offset && reference_line_offset <= node_end {
                            reference_indent_count += 1;
                        }

                        // Cursor position: count if cursor is inside this node
                        // Also check: node must start on a previous line (not current line)
                        let node_on_previous_line = node_start < line_start_offset;
                        let cursor_inside_node =
                            node_start < cursor_offset && cursor_offset <= node_end;

                        if cursor_inside_node && node_on_previous_line {
                            cursor_indent_count += 1;
                            found_any_captures = true;
                        }

                        if cursor_inside_node && !node_on_previous_line {
                            indent_opens_on_cursor_line = true;
                        }

                        // An empty @indent node sitting exactly at the cursor is
                        // a block that has just been opened with no body yet.
                        if node_start == cursor_offset && node_end == cursor_offset {
                            empty_indent_node_at_cursor = true;
                        }
                    }
                }

                // Handle @dedent captures
                if let Some(idx) = dedent_capture_idx {
                    if capture.index == idx as u32 {
                        // Existing: dedent that begins exactly at the cursor position.
                        if cursor_offset == node_start && node_end > node_start {
                            indent_delta -= 1;
                            found_any_captures = true;
                        }

                        // Structural check for "line ends with closing token":
                        // does this @dedent capture cover the last non-ws byte
                        // on the cursor's line?
                        if let Some(last_pos) = last_nonws_offset {
                            if node_start <= last_pos && last_pos < node_end {
                                last_nonws_is_dedent_capture = true;
                            }
                        }
                    }
                }

                // Handle @dedent_after captures: the cursor's line is a
                // flow-exit statement when one of these nodes starts on this
                // line and covers the line's last non-whitespace token.
                // Requiring the node to start on the cursor's line avoids
                // dedenting in the middle of a multi-line statement.
                if let Some(idx) = dedent_after_capture_idx {
                    if capture.index == idx as u32 {
                        if let Some(last_pos) = last_nonws_offset {
                            if node_start >= line_start_offset
                                && node_start <= last_pos
                                && last_pos < node_end
                            {
                                cursor_line_is_dedent_after = true;
                            }
                        }
                    }
                }
            }
        }

        // Flow-exit statement: dedent one level relative to the statement's own
        // indent. Takes priority over the @dedent maintain branch below so that
        // `return foo()` (which ends in `)`, a @dedent token) dedents just like
        // `return 42`.
        if cursor_line_is_dedent_after {
            let line_indent = Self::get_current_line_indent(buffer, position, tab_size);
            return Some(line_indent.saturating_sub(tab_size));
        }

        // When the current line ends with a token captured as @dedent (`}`,
        // `end`, `fi`, `done`, …), keep the new line at the same indent as
        // the closing token. The grammar has already placed that token at the
        // correct column matching its opener; the next line continues at the
        // enclosing scope's indent.
        if last_nonws_is_dedent_capture {
            let line_indent = Self::get_current_line_indent(buffer, position, tab_size);
            tracing::debug!(
                "Cursor line ends with @dedent token, maintaining indent level: {}",
                line_indent
            );
            return Some(line_indent);
        }

        // Calculate delta: how many more @indent levels are we at cursor vs reference
        indent_delta += cursor_indent_count - reference_indent_count;

        // When the cursor is at the end of a line that has just opened an
        // @indent block (e.g. `def foo():` in Python, `function foo()` in Lua,
        // `if true; then` in Bash, `fn main() {` in Rust), the regular
        // counting under-counts at the cursor because the new node's
        // `node_start` is on the current line and the `node_on_previous_line`
        // filter excludes it. Detect this structurally: if a `@indent` node
        // opens on the cursor's line and contains the cursor, treat it as one
        // additional level of nesting.
        //
        // The rescue must only fire when the line genuinely *opens* a block, not
        // when the cursor is on a statement *inside* an already-open block whose
        // body node (e.g. Python's `(block)`) happens to start on this line. Two
        // signals confirm a real opener: an empty @indent node at the cursor (a
        // bodyless block just opened, e.g. after `def foo():` or `if x:`), or
        // the opener being at the outermost level (`cursor_indent_count == 0`,
        // e.g. Bash `if true; then`, where no separate body node is emitted).
        // Without this guard, pressing Enter after `\tmy_var = 42` inside a
        // function wrongly indented an extra level (issue #2192).
        if indent_delta == 0
            && indent_opens_on_cursor_line
            && (empty_indent_node_at_cursor || cursor_indent_count == 0)
        {
            indent_delta = 1;
            found_any_captures = true;
        }

        // If no captures were found, return None to trigger pattern-based fallback
        if !found_any_captures {
            tracing::debug!("No tree-sitter captures found, falling back to pattern matching");
            return None;
        }

        // Calculate final indent: reference line indent + delta
        let final_indent =
            (reference_line_indent as i32 + (indent_delta * tab_size as i32)).max(0) as usize;

        tracing::debug!(
            "Indent calculation: reference={}, delta={}, final={}",
            reference_line_indent,
            indent_delta,
            final_indent
        );

        Some(final_indent)
    }

    /// Get a single byte at a position
    fn byte_at(buffer: &Buffer, pos: usize) -> Option<u8> {
        if pos >= buffer.len() {
            return None;
        }
        buffer.slice_bytes(pos..pos + 1).first().copied()
    }

    /// Get the indent of the current line (the line cursor is on)
    fn get_current_line_indent(buffer: &Buffer, position: usize, tab_size: usize) -> usize {
        // Find start of current line
        let mut line_start = position;
        while line_start > 0 {
            if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
                break;
            }
            line_start = line_start.saturating_sub(1);
        }

        // Count leading whitespace on current line
        let mut indent = 0;
        let mut pos = line_start;
        while pos < position {
            match Self::byte_at(buffer, pos) {
                Some(b' ') => indent += 1,
                Some(b'\t') => indent += tab_size,
                Some(_) => break, // Hit non-whitespace
                None => break,
            }
            pos += 1;
        }

        indent
    }

    /// Get the indent of the line containing the given position
    /// This is a public API used for bracket expansion
    pub fn get_line_indent_at_position(buffer: &Buffer, position: usize, tab_size: usize) -> usize {
        // Find start of the line containing position
        let mut line_start = position;
        while line_start > 0 {
            if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
                break;
            }
            line_start = line_start.saturating_sub(1);
        }

        // Find end of line or buffer
        let mut line_end = position;
        while line_end < buffer.len() {
            if Self::byte_at(buffer, line_end) == Some(b'\n') {
                break;
            }
            line_end += 1;
        }

        // Count leading whitespace on the line
        let mut indent = 0;
        let mut pos = line_start;
        while pos < line_end {
            match Self::byte_at(buffer, pos) {
                Some(b' ') => indent += 1,
                Some(b'\t') => indent += tab_size,
                Some(_) => break, // Hit non-whitespace
                None => break,
            }
            pos += 1;
        }

        indent
    }

    /// Get the indent of the previous line (line before cursor's line)
    #[cfg(test)]
    fn get_previous_line_indent(buffer: &Buffer, position: usize, tab_size: usize) -> usize {
        // Find start of current line
        let mut line_start = position;
        while line_start > 0 {
            if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
                break;
            }
            line_start = line_start.saturating_sub(1);
        }

        // Find start of previous line
        if line_start == 0 {
            return 0;
        }

        let mut prev_line_start = line_start - 1;
        while prev_line_start > 0 {
            if Self::byte_at(buffer, prev_line_start.saturating_sub(1)) == Some(b'\n') {
                break;
            }
            prev_line_start = prev_line_start.saturating_sub(1);
        }

        // Count leading whitespace on previous line
        let mut indent = 0;
        let mut pos = prev_line_start;
        while pos < line_start - 1 {
            match Self::byte_at(buffer, pos) {
                Some(b' ') => indent += 1,
                Some(b'\t') => indent += tab_size,
                Some(_) => break, // Hit non-whitespace
                None => break,
            }
            pos += 1;
        }

        indent
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::buffer::Buffer;

    #[test]
    fn test_current_and_previous_line_indent() {
        let buffer = Buffer::from_str_test("fn main() {\n    let x = 1;");
        let tab_size = 4;

        // At end of buffer (end of line 2)
        let current_indent =
            IndentCalculator::get_current_line_indent(&buffer, buffer.len(), tab_size);
        assert_eq!(current_indent, 4, "Current line (line 2) has 4 spaces");

        let prev_indent =
            IndentCalculator::get_previous_line_indent(&buffer, buffer.len(), tab_size);
        assert_eq!(prev_indent, 0, "Previous line (line 1) has 0 spaces");
    }

    #[test]
    fn test_pattern_matching_basic() {
        let buffer = Buffer::from_str_test("fn main() {");
        let position = buffer.len();
        let result = IndentCalculator::calculate_indent_pattern(&buffer, position, 4);
        println!("Pattern result for 'fn main() {{': {:?}", result);
        assert_eq!(
            result,
            Some(4),
            "Should detect {{ and return 4 space indent"
        );
    }

    #[test]
    fn test_rust_indent_after_brace_debug() {
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("fn main() {");
        let position = buffer.len(); // After the {

        // Test pattern matching directly first
        let pattern_result = IndentCalculator::calculate_indent_pattern(&buffer, position, 4);
        println!("Pattern matching result: {:?}", pattern_result);

        // This should trigger tree-sitter parsing
        let indent = calc.calculate_indent(&buffer, position, &Language::Rust, 4);

        println!("Test buffer: {:?}", buffer.to_string().unwrap());
        println!("Position: {}", position);
        println!("Result indent: {:?}", indent);

        assert!(indent.is_some(), "Should return Some indent");
        let indent_val = indent.unwrap();
        println!("Indent value: {}", indent_val);

        // Should suggest indenting (4 spaces)
        assert_eq!(
            indent_val, 4,
            "Should indent by 4 spaces after opening brace"
        );
    }

    #[test]
    fn test_python_indent_after_colon() {
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():");
        let position = buffer.len(); // After the :

        let indent = calc.calculate_indent(&buffer, position, &Language::Python, 4);
        assert!(indent.is_some());
        // Should suggest indenting
        assert!(indent.unwrap() >= 4);
    }

    // ============================================================================
    // Issue #2192: pressing Enter at the end of a statement *inside* a Python
    // block must keep the current indent, not add a spurious second level.
    //
    // The cursor sits at the END of the statement line (where Enter is pressed),
    // i.e. there is no trailing newline, so `buffer.len()` is the end of the
    // statement. Before the fix these returned 8 (two levels) because Python's
    // `(block)` node starts at the block's first statement — on the cursor's
    // line — which tripped the "a block opens on this line" rescue.
    // ============================================================================

    #[test]
    fn test_python_enter_after_statement_keeps_indent_tabs() {
        // Case 1 from the issue (tab-indented body).
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():\n\tmy_var = 42");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Python, 4);
        assert_eq!(
            indent,
            Some(4),
            "Enter after a statement inside a function body must stay at one level (got {:?})",
            indent
        );
    }

    #[test]
    fn test_python_enter_after_statement_keeps_indent_spaces() {
        // Same as above but space-indented: the bug was not tab-specific.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():\n    my_var = 42");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Python, 4);
        assert_eq!(indent, Some(4), "got {:?}", indent);
    }

    #[test]
    fn test_python_enter_after_return_dedents() {
        // Case 2 from the issue. The doubling bug returned 8; the correct
        // behaviour (VS Code / PyCharm / neovim) is to dedent one level out of
        // the function body after `return`.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():\n\treturn 42");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Python, 4);
        assert_eq!(
            indent,
            Some(0),
            "Enter after `return` should dedent one level (got {:?})",
            indent
        );
    }

    #[test]
    fn test_python_flow_exit_statements_dedent() {
        // `pass`, `raise`, `break`, `continue` all end the block's flow and
        // dedent the next line one level, just like `return`.
        let mut calc = IndentCalculator::new();
        for stmt in ["pass", "raise ValueError", "break", "continue"] {
            let src = format!("while True:\n\tif x:\n\t\t{stmt}");
            let buffer = Buffer::from_str_test(&src);
            let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Python, 4);
            assert_eq!(
                indent,
                Some(4),
                "Enter after `{stmt}` (at col 8) should dedent to col 4 (got {:?})",
                indent
            );
        }
    }

    #[test]
    fn test_python_return_with_call_dedents_like_plain_return() {
        // `return foo()` ends in `)` (a @dedent token); it must dedent the same
        // way as `return 42` rather than merely maintaining indent.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():\n\treturn bar()");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Python, 4);
        assert_eq!(indent, Some(0), "got {:?}", indent);
    }

    #[test]
    fn test_python_non_flow_statement_does_not_dedent() {
        // A plain assignment must NOT dedent — only flow-exit statements do.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():\n\tx = returns_value()");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Python, 4);
        assert_eq!(
            indent,
            Some(4),
            "`x = returns_value()` is not a flow-exit statement (got {:?})",
            indent
        );
    }

    #[test]
    fn test_python_enter_after_nested_statement_keeps_indent() {
        // A statement two levels deep must stay at two levels (8), not jump to 12.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():\n    if x:\n        my_var = 42");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Python, 4);
        assert_eq!(indent, Some(8), "got {:?}", indent);
    }

    #[test]
    fn test_python_enter_after_block_header_still_indents() {
        // Regression guard: the fix must NOT break the genuine "open a block"
        // case. Enter after a nested `if x:` header should go one level deeper.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():\n    if x:");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Python, 4);
        assert_eq!(
            indent,
            Some(8),
            "Enter after a block header must still indent one level deeper (got {:?})",
            indent
        );
    }

    #[test]
    fn test_tree_sitter_used_for_complete_block() {
        // Test that tree-sitter is used when we have a complete block with
        // context. Uses TypeScript, one of the bundled grammars (most grammars
        // were dropped; their indentation is served by the rules tier instead).
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("function main() {\n    let x = 1;\n}");
        // Position after the closing }
        let position = buffer.len();

        // Tree-sitter should recognize this is a complete block
        // Pattern matching would see '}' and not indent, but tree-sitter context should work
        let ts_result =
            calc.calculate_indent_tree_sitter(&buffer, position, &Language::TypeScript, 4);

        // Tree-sitter should return Some (even if it's 0 indent)
        assert!(
            ts_result.is_some(),
            "Tree-sitter should handle complete blocks"
        );
    }

    #[test]
    fn test_nested_indent_maintained() {
        // Test that we maintain nested indentation correctly
        let mut calc = IndentCalculator::new();

        // Create nested structure - position at end of line with just whitespace
        let buffer = Buffer::from_str_test("fn main() {\n    if true {\n        ");
        let position = buffer.len();

        // This should be 8 spaces (maintaining nested indent from current line)
        let indent = calc.calculate_indent(&buffer, position, &Language::Rust, 4);
        assert_eq!(
            indent,
            Some(8),
            "Should maintain nested indent level (got {:?})",
            indent
        );
    }

    #[test]
    fn test_pattern_fallback_for_incomplete_syntax() {
        // Verify pattern matching kicks in when tree-sitter can't help
        let buffer = Buffer::from_str_test("fn main() {");
        let position = buffer.len();

        // Pattern matching should detect the '{'
        let pattern_result = IndentCalculator::calculate_indent_pattern(&buffer, position, 4);
        assert_eq!(
            pattern_result,
            Some(4),
            "Pattern matching should detect opening brace"
        );
    }

    #[test]
    fn test_fallback_to_previous_line() {
        let mut calc = IndentCalculator::new();
        // C# not supported, should fall back
        let buffer = Buffer::from_str_test("    var x = 1;");
        let position = buffer.len();

        let indent = calc.calculate_indent(&buffer, position, &Language::CSharp, 4);
        // Should fall back to previous line indent (4 spaces)
        assert_eq!(indent, Some(4));
    }

    #[test]
    fn test_typescript_interface_indent() {
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("interface User {");
        let position = buffer.len(); // Position after the {

        let indent = calc.calculate_indent(&buffer, position, &Language::TypeScript, 4);
        assert!(indent.is_some(), "TypeScript interface should get indent");
        assert_eq!(
            indent.unwrap(),
            4,
            "Should indent 4 spaces after opening brace"
        );
    }

    #[test]
    fn test_no_language_fallback_copies_indent() {
        // Test that files without language support (like .txt) copy current line indent
        let buffer = Buffer::from_str_test("    indented text");
        let position = buffer.len();

        let indent = IndentCalculator::calculate_indent_no_language(&buffer, position, 4);
        assert_eq!(indent, 4, "Should copy 4-space indent from current line");
    }

    #[test]
    fn test_no_language_fallback_with_brace() {
        // Test that pattern matching works for files without language support
        let buffer = Buffer::from_str_test("some text {");
        let position = buffer.len();

        let indent = IndentCalculator::calculate_indent_no_language(&buffer, position, 4);
        assert_eq!(
            indent, 4,
            "Should indent 4 spaces after brace even without language"
        );
    }

    #[test]
    fn test_tree_sitter_enter_after_close_brace_returns_zero() {
        // Verify tree-sitter correctly handles Enter after closing brace. Uses
        // TypeScript (a bundled grammar) so the direct tree-sitter assertion
        // below is meaningful.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("function main() {\n    let x = 1;\n}");
        let position = buffer.len(); // Position right after the }

        // Tree-sitter should recognize we're outside the block and return 0 indent
        let indent = calc.calculate_indent(&buffer, position, &Language::TypeScript, 4);
        assert_eq!(
            indent,
            Some(0),
            "Should return 0 indent after closing brace"
        );

        // Verify tree-sitter is being used (not just pattern fallback)
        let ts_result =
            calc.calculate_indent_tree_sitter(&buffer, position, &Language::TypeScript, 4);
        assert!(ts_result.is_some(), "Tree-sitter should handle this case");
    }

    #[test]
    fn test_tree_sitter_auto_dedent_on_close_brace() {
        // Verify tree-sitter correctly calculates dedent for closing delimiter
        let mut calc = IndentCalculator::new();

        // Simulate typing } on an indented line
        let buffer = Buffer::from_str_test("fn main() {\n    ");
        let position = buffer.len(); // Cursor after 4 spaces

        // Calculate where the } should be placed using tree-sitter
        let correct_indent =
            calc.calculate_dedent_for_delimiter(&buffer, position, '}', &Language::Rust, 4);

        // Should dedent to column 0 (same level as fn main)
        assert_eq!(
            correct_indent,
            Some(0),
            "Closing brace should dedent to column 0"
        );

        // Verify this uses tree-sitter by checking it works
        let nested_buffer = Buffer::from_str_test("fn main() {\n    if true {\n        ");
        let nested_pos = nested_buffer.len();

        let nested_indent = calc.calculate_dedent_for_delimiter(
            &nested_buffer,
            nested_pos,
            '}',
            &Language::Rust,
            4,
        );

        // Should return a valid indent level
        assert!(
            nested_indent.is_some(),
            "Nested closing brace should get valid indent"
        );
    }

    #[test]
    fn test_tree_sitter_handles_multiple_languages() {
        // Verify tree-sitter-based auto-dedent works across languages
        let mut calc = IndentCalculator::new();

        // Python
        let py_buffer = Buffer::from_str_test("def foo():\n    ");
        let py_indent = calc.calculate_indent(&py_buffer, py_buffer.len(), &Language::Python, 4);
        assert_eq!(py_indent, Some(4), "Python should indent after colon");

        // JavaScript
        let js_buffer = Buffer::from_str_test("function foo() {\n    ");
        let js_dedent = calc.calculate_dedent_for_delimiter(
            &js_buffer,
            js_buffer.len(),
            '}',
            &Language::JavaScript,
            4,
        );
        assert_eq!(js_dedent, Some(0), "JavaScript closing brace should dedent");

        // C++
        let cpp_buffer = Buffer::from_str_test("class Foo {\n    ");
        let cpp_dedent = calc.calculate_dedent_for_delimiter(
            &cpp_buffer,
            cpp_buffer.len(),
            '}',
            &Language::Cpp,
            4,
        );
        assert_eq!(cpp_dedent, Some(0), "C++ closing brace should dedent");
    }

    #[test]
    fn test_indent_on_empty_line_uses_reference() {
        // Hybrid heuristic: when the cursor is on a truly empty line between
        // code lines, indent calculation should use the previous non-empty
        // line as reference.
        let mut calc = IndentCalculator::new();

        // "fn main() {\n    let x = 1;\n\n}" — cursor on the empty line.
        let buffer = Buffer::from_str_test("fn main() {\n    let x = 1;\n\n}");
        let position = 27; // start of the empty line (after the second '\n')

        let indent = calc.calculate_indent(&buffer, position, &Language::Rust, 4);
        assert_eq!(
            indent,
            Some(4),
            "On an empty line inside a function body, should indent to match the reference line"
        );
    }

    #[test]
    fn test_indent_after_empty_line_incomplete_syntax() {
        // Test with incomplete syntax (no closing brace) - this is the real-world case
        let mut calc = IndentCalculator::new();

        let buffer = Buffer::from_str_test("fn main() {\n    let x = 1;\n");
        let position = buffer.len(); // After the second \n, start of empty line

        let indent = calc.calculate_indent(&buffer, position, &Language::Rust, 4);
        tracing::trace!("TEST: Without closing brace, indent = {:?}", indent);
        // With incomplete syntax, tree-sitter returns ERROR nodes
        // We should fall back to pattern matching or reference line heuristic
        assert_eq!(
            indent,
            Some(4),
            "After empty line in function body (incomplete syntax), should indent to 4 spaces using reference line"
        );
    }

    #[test]
    fn test_enter_at_start_of_unindented_line_after_blank_does_not_indent() {
        // Regression test for #1425: pressing Enter at the start of an
        // unindented line that follows blank lines after indented content
        // should not pull in the previous block's indent and displace the
        // existing line content.
        //
        //     ····line1
        //     ····line2
        //     ········line3
        //     ········line4
        //     <empty>
        //     unindented line   <- cursor at column 0, press Enter
        //
        // Expected: the existing "unindented line" stays at column 0.
        let buffer = Buffer::from_str_test(
            "    line1\n    line2\n        line3\n        line4\n\nunindented line",
        );
        let position = buffer
            .to_string()
            .unwrap()
            .find("unindented line")
            .expect("test fixture should contain marker");

        let indent = IndentCalculator::calculate_indent_no_language(&buffer, position, 4);
        assert_eq!(
            indent, 0,
            "Enter at column 0 of an existing non-empty line must not insert indentation"
        );
    }

    #[test]
    fn test_enter_at_start_of_indented_line_does_not_displace_content() {
        // Even when the existing line is itself indented, pressing Enter at
        // the very start of that line should not add extra indent (which
        // would push the existing leading whitespace further right).
        let buffer = Buffer::from_str_test("    line1\n    target");
        let position = buffer
            .to_string()
            .unwrap()
            .find("    target")
            .expect("test fixture should contain marker");

        let indent = IndentCalculator::calculate_indent_no_language(&buffer, position, 4);
        assert_eq!(
            indent, 0,
            "Enter at column 0 of an indented line must not add indent on top of the existing leading whitespace"
        );
    }

    #[test]
    fn test_enter_at_start_of_unindented_line_python() {
        // Same regression as #1425 but for a tree-sitter language: the
        // pattern fallback or tree-sitter logic must not inject indent that
        // displaces existing content on the line.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo():\n    pass\n\nunindented");
        let position = buffer
            .to_string()
            .unwrap()
            .find("unindented")
            .expect("test fixture should contain marker");

        let indent = calc.calculate_indent(&buffer, position, &Language::Python, 4);
        assert_eq!(
            indent,
            Some(0),
            "Enter at column 0 of an unindented Python line must not be auto-indented"
        );
    }

    #[test]
    fn test_enter_in_middle_of_leading_ws_preserves_content_column() {
        // Cursor at column 2 of "    indented_target" (in the middle of the
        // 4-space leading whitespace). Pressing Enter splits the indent: 2
        // spaces stay on line A, 2 spaces remain in front of `indented_target`
        // on line B. Auto-indent must equal the cursor's column (2), giving
        // line B a total of 2 + 2 = 4 leading spaces — preserving the
        // original column of `indented_target`.
        let buffer = Buffer::from_str_test("    line1\n    indented_target");
        let target = buffer
            .to_string()
            .unwrap()
            .find("    indented_target")
            .unwrap();
        let position = target + 2; // mid-indent

        let indent = IndentCalculator::calculate_indent_no_language(&buffer, position, 4);
        assert_eq!(
            indent, 2,
            "Splitting in the middle of leading whitespace must preserve the content column"
        );
    }

    #[test]
    fn test_enter_at_start_of_closing_brace_line_does_not_displace() {
        // Language-agnostic: at column 0 of a `}` line we must not insert
        // indent that pushes `}` rightward. Other editors (VS Code, Sublime)
        // create the empty line above and leave the `}` at column 0; the
        // user can press Tab to indent if they want to type code in front
        // of the close. The same rule applies uniformly to `end` (Lua,
        // Ruby), `</tag>` (HTML), `fi`/`done` (Bash) — there is no
        // language-specific list of closing tokens to maintain.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("fn main() {\n    let x = 1;\n}");
        let position = 27; // start of the `}` line

        let indent = calc.calculate_indent(&buffer, position, &Language::Rust, 4);
        assert_eq!(
            indent,
            Some(0),
            "Enter at column 0 of a `}}` line must not displace the closing delimiter"
        );
    }

    #[test]
    fn test_enter_inside_content_still_uses_smart_indent() {
        // Sanity check: when the cursor is past the leading whitespace, the
        // generalised fix must NOT short-circuit — smart auto-indent is still
        // expected to fire and indent inside an opened block.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("fn main() {");
        let position = buffer.len(); // end of the opening brace line

        let indent = calc.calculate_indent(&buffer, position, &Language::Rust, 4);
        assert_eq!(
            indent,
            Some(4),
            "Pressing Enter at the end of `fn main() {{` should still indent the new line"
        );
    }

    // ============================================================================
    // Cross-contamination regression tests for keyword-delimited languages.
    //
    // Before this change, the tree-sitter path fell through to
    // `calculate_indent_pattern` when it had no captures to apply. That fallback
    // hardcodes `{`, `[`, `(`, `:` as universal indent triggers, which is wrong
    // for Lua, Ruby, Bash, and Pascal: in those languages `(` opens a function
    // call (or subshell, or condition) — never a block.
    //
    // These tests pin down the corrected behaviour: for keyword-delimited
    // languages, when tree-sitter cannot decide, we copy the current line's
    // indent rather than asking the C-family pattern matcher. See issue #1425
    // and PR #1819.
    // ============================================================================

    #[test]
    fn test_lua_open_paren_on_function_call_does_not_trigger_indent() {
        // Lua: `foo(` is a function call, not a block opener. Tree-sitter does
        // not capture an @indent here. The C-family pattern fallback would
        // wrongly read `(` as an indent trigger and return +tab_size; after
        // this change it must not.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("foo(");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Lua, 4);
        assert_eq!(
            indent,
            Some(0),
            "Lua: open paren on a function call must not deepen indent"
        );
    }

    #[test]
    fn test_ruby_open_paren_on_function_call_does_not_trigger_indent() {
        // Ruby analogue: `foo(` is a method call, not a block opener.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("foo(");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Ruby, 4);
        assert_eq!(
            indent,
            Some(0),
            "Ruby: open paren on a method call must not deepen indent"
        );
    }

    #[test]
    fn test_bash_open_paren_on_subshell_does_not_trigger_indent() {
        // Bash: `result=$(` opens a subshell expansion, which Bash's
        // indents.scm does not capture as @indent. The pattern fallback would
        // wrongly read `(` as a trigger; after this change it must not.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("result=$(");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Bash, 4);
        assert_eq!(
            indent,
            Some(0),
            "Bash: open paren on a subshell must not deepen indent"
        );
    }

    #[test]
    fn test_lua_paren_in_block_does_not_cross_contaminate() {
        // Lua: cursor sits one indent level inside a function body and the
        // previous content line happens to end with `(` (a function call).
        // The C-family pattern fallback would have wrongly added an extra
        // tab_size on top of the body's indent. Tree-sitter is the source of
        // truth: it should keep the new line at the body's indent.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("function bar()\n    do_thing(\n");
        let position = buffer.len();
        let indent = calc.calculate_indent(&buffer, position, &Language::Lua, 4);
        // Tree-sitter has no useful capture here; the language is keyword-
        // delimited so the pattern fallback is suppressed. Expected: copy the
        // current line's indent (the line we just left was indented 4 spaces;
        // the new line is empty so its current indent is 0). Critically, it
        // must NOT be 8 (which is what the old `(`-trigger heuristic produced).
        assert!(
            matches!(indent, Some(0) | Some(4)),
            "Lua: `(` at end of a body line must not push past 4-space body indent (got {:?})",
            indent
        );
        assert_ne!(
            indent,
            Some(8),
            "Lua: previous-line `(` must not add C-family indent on top of body indent"
        );
    }

    #[test]
    fn test_ruby_def_opens_block() {
        // Ruby `def foo` opens a method block. The old tree-sitter+keyword
        // pipeline returned 0 on this incomplete input (no `end` yet); the
        // regex rules tier (RubyLike: `def` is a block opener, no `end` on the
        // line so `self_close` doesn't suppress it) correctly indents the body
        // one level — the improvement the old code aimed for but couldn't reach.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("def foo");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Ruby, 4);
        assert_eq!(
            indent,
            Some(4),
            "Ruby: `def foo` opens a method block — should indent +4"
        );
    }

    #[test]
    fn test_bash_then_opens_block_via_tree_sitter_structural_rescue() {
        // Bash: `if true; then` produces a tree-sitter `if_statement` node
        // (with a MISSING `fi`). The structural rescue inside the tree-sitter
        // path detects that an @indent block opens on the cursor's line and
        // requests one extra level of indent — without consulting the
        // C-family pattern matcher.
        let mut calc = IndentCalculator::new();
        let buffer = Buffer::from_str_test("if true; then");
        let indent = calc.calculate_indent(&buffer, buffer.len(), &Language::Bash, 4);
        assert_eq!(
            indent,
            Some(4),
            "Bash: `if true; then` opens a block (tree-sitter sees if_statement) — should indent +4"
        );
    }
}
