// SPDX-License-Identifier: MIT

use rlsp_yaml_parser::node::{Document, Node};
use rlsp_yaml_parser::{LineIndex, Span};
use tower_lsp::lsp_types::{Position, SelectionRange};

use crate::lsp_util::span_to_lsp;

/// Compute selection ranges for the given YAML documents and cursor positions.
///
/// For each position, returns a `SelectionRange` whose parent chain expands
/// from innermost node to outermost document root.
/// Returns an empty `Vec` if `docs` or `positions` is empty.
#[must_use]
pub fn selection_ranges(docs: &[Document<Span>], positions: &[Position]) -> Vec<SelectionRange> {
    if docs.is_empty() || positions.is_empty() {
        return Vec::new();
    }

    positions
        .iter()
        .filter_map(|pos| selection_range_for_position(docs, *pos))
        .collect()
}

/// Build a `SelectionRange` chain for a single cursor position.
fn selection_range_for_position(
    docs: &[Document<Span>],
    position: Position,
) -> Option<SelectionRange> {
    let line = position.line as usize;
    let col = position.character as usize;

    // Find the document whose root span contains this cursor position.
    // Pos convention: line is 1-based, column is 0-based.
    let doc = docs.iter().find(|d| {
        let idx = d.line_index();
        let loc = node_span(&d.root);
        let start_line_0 = idx.line_column(loc.start).0.saturating_sub(1) as usize;
        let end_line_0 = idx.line_column(loc.end).0.saturating_sub(1) as usize;
        line >= start_line_0 && line <= end_line_0
    })?;

    let idx = doc.line_index();

    // Collect ancestor spans innermost-first from the AST walk
    let mut ancestor_spans: Vec<Span> = Vec::new();
    collect_ancestor_spans(&doc.root, line, col, &mut ancestor_spans, idx);

    if ancestor_spans.is_empty() {
        return None;
    }

    // Derive doc root range from AST span directly
    let doc_root_span = node_span(&doc.root);
    let doc_root = span_to_lsp(doc_root_span, idx);

    // Build the SelectionRange chain: innermost first in ancestor_spans,
    // outermost (doc root) is the final parent.
    let mut current: Option<Box<SelectionRange>> = Some(Box::new(SelectionRange {
        range: doc_root,
        parent: None,
    }));

    // ancestor_spans[0] is innermost, last is closest-to-root.
    // We want to wrap them outermost-first, so iterate in reverse.
    for span in ancestor_spans.iter().rev() {
        let range = span_to_lsp(*span, idx);
        // Avoid emitting the doc root twice
        if range == doc_root {
            continue;
        }
        let sr = SelectionRange {
            range,
            parent: current,
        };
        current = Some(Box::new(sr));
    }

    current.map(|b| *b)
}

/// Extract the `Span` (loc) from a `Node<Span>`.
const fn node_span(node: &Node<Span>) -> Span {
    match node {
        Node::Scalar { loc, .. }
        | Node::Mapping { loc, .. }
        | Node::Sequence { loc, .. }
        | Node::Alias { loc, .. } => *loc,
    }
}

/// Recursively collect ancestor spans for the cursor, innermost-first.
///
/// `rlsp-yaml-parser` Pos convention: line is 1-based, column is 0-based.
/// LSP Position: both 0-based.
///
/// Container nodes (Mapping, Sequence) carry full spans from the loader
/// (start from the opening token, end from the closing token), so `node.loc`
/// is used directly without any recursive child-span fallback.
fn collect_ancestor_spans(
    node: &Node<Span>,
    line: usize,
    col: usize,
    ancestor_spans: &mut Vec<Span>,
    idx: &LineIndex,
) {
    let depth_before = ancestor_spans.len();

    match node {
        Node::Mapping { entries, .. } => {
            for (key, value) in entries {
                let key_span = node_span(key);
                let key_line_0 = idx.line_column(key_span.start).0.saturating_sub(1) as usize;
                let val_end = node_span(value).end;
                let entry_end_line_0 = idx.line_column(val_end).0.saturating_sub(1) as usize;

                if line < key_line_0 || line > entry_end_line_0 {
                    continue;
                }

                // Recurse into value first (innermost wins)
                collect_ancestor_spans(value, line, col, ancestor_spans, idx);
                if ancestor_spans.len() > depth_before {
                    ancestor_spans.push(Span {
                        start: key_span.start,
                        end: val_end,
                    });
                    break;
                }

                // Check if cursor is on the key itself
                let key_start_col = idx.line_column(key_span.start).1 as usize;
                let key_end_col = idx.line_column(key_span.end).1 as usize;
                if key_line_0 == line && col >= key_start_col && col <= key_end_col {
                    ancestor_spans.push(key_span);
                    ancestor_spans.push(Span {
                        start: key_span.start,
                        end: val_end,
                    });
                    break;
                }

                // Cursor is within the entry's line range but not in key or value
                if key_line_0 == line {
                    ancestor_spans.push(Span {
                        start: key_span.start,
                        end: val_end,
                    });
                    break;
                }
            }
        }
        Node::Sequence { items, .. } => {
            for item in items {
                let item_span = node_span(item);
                let start_line_0 = idx.line_column(item_span.start).0.saturating_sub(1) as usize;
                let end_line_0 = idx.line_column(item_span.end).0.saturating_sub(1) as usize;

                if line < start_line_0 || line > end_line_0 {
                    continue;
                }

                collect_ancestor_spans(item, line, col, ancestor_spans, idx);
                if ancestor_spans.len() > depth_before {
                    ancestor_spans.push(item_span);
                    break;
                }

                // Leaf item — cursor is within its line range
                if col >= idx.line_column(item_span.start).1 as usize {
                    ancestor_spans.push(item_span);
                    break;
                }
            }
        }
        Node::Scalar { loc, .. } | Node::Alias { loc, .. } => {
            let start_line_0 = idx.line_column(loc.start).0.saturating_sub(1) as usize;
            let end_line_0 = idx.line_column(loc.end).0.saturating_sub(1) as usize;
            let start_col = idx.line_column(loc.start).1 as usize;
            if line >= start_line_0 && line <= end_line_0 && col >= start_col {
                ancestor_spans.push(*loc);
            }
        }
    }
}

#[cfg(test)]
#[expect(
    clippy::indexing_slicing,
    clippy::expect_used,
    clippy::unwrap_used,
    clippy::cast_possible_truncation,
    reason = "test code"
)]
mod tests {
    use std::fmt::Write as _;

    use rstest::rstest;

    use super::*;
    use crate::test_utils::parse_docs as parse_docs_inner;

    #[expect(clippy::unnecessary_wraps, reason = "callers use Option API")]
    fn parse_docs(text: &str) -> Option<Vec<Document<Span>>> {
        Some(parse_docs_inner(text))
    }

    fn pos(line: u32, character: u32) -> Position {
        Position::new(line, character)
    }

    // ---- Basic expansion tests ----

    #[test]
    fn should_return_value_range_expanding_to_key_value_then_document() {
        let text = "key: value\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(0, 6)]);

        assert_eq!(
            result.len(),
            1,
            "should return one SelectionRange per position"
        );
        let sr = &result[0];
        assert_eq!(sr.range.start.line, 0);
        assert!(
            sr.parent.is_some(),
            "should have a parent range (key-value pair)"
        );
        let parent = sr.parent.as_ref().expect("parent");
        assert_eq!(parent.range.start.line, 0);
        assert!(
            parent.parent.is_some(),
            "should have a grandparent range (document root)"
        );
    }

    #[test]
    fn should_return_key_range_expanding_to_key_value_then_document() {
        let text = "key: value\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(0, 1)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        assert_eq!(sr.range.start.line, 0);
        assert!(sr.parent.is_some(), "should have parent (key-value pair)");
        let parent = sr.parent.as_ref().expect("parent");
        assert_eq!(parent.range.start.line, 0);
        assert!(
            parent.parent.is_some(),
            "should have grandparent (document root)"
        );
    }

    #[test]
    fn should_return_sequence_item_expanding_to_sequence_then_document() {
        let text = "items:\n  - one\n  - two\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(1, 5)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        assert_eq!(sr.range.start.line, 1);
        assert!(sr.parent.is_some(), "should have parent (sequence)");
        assert!(
            sr.parent.as_ref().expect("parent").parent.is_some(),
            "should have grandparent (document root)"
        );
    }

    #[test]
    fn should_handle_nested_mapping() {
        let text = "server:\n  host: localhost\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(1, 8)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        assert_eq!(sr.range.start.line, 1);
        assert!(sr.parent.is_some(), "should have parent (host: localhost)");
        let parent = sr.parent.as_ref().expect("parent");
        // Grandparent is the server mapping, which is also the doc root — no further parent.
        assert!(
            parent.parent.is_some(),
            "should have grandparent (server mapping = doc root)"
        );
    }

    #[test]
    fn should_handle_multiple_positions() {
        let text = "name: Alice\nage: 30\n";
        let docs = parse_docs(text);
        let positions = [pos(0, 6), pos(1, 5)];
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &positions);

        assert_eq!(
            result.len(),
            2,
            "should return one SelectionRange per position"
        );
        assert_eq!(result[0].range.start.line, 0);
        assert_eq!(result[1].range.start.line, 1);
    }

    #[test]
    fn should_handle_sequence_of_mappings() {
        let text = "users:\n  - name: Alice\n    age: 30\n  - name: Bob\n    age: 25\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(1, 10)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        assert_eq!(sr.range.start.line, 1);
        assert!(sr.parent.is_some(), "should have parent (name: Alice)");
    }

    #[test]
    fn should_scope_selection_to_current_document_in_multi_doc_yaml() {
        let text = "doc1key: value1\n---\ndoc2key: value2\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(2, 0)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        let mut outermost = sr;
        while let Some(ref p) = outermost.parent {
            outermost = p;
        }
        assert!(
            outermost.range.start.line >= 2,
            "outermost range should be scoped to the second document (start >= line 2), \
             got start line {}",
            outermost.range.start.line
        );
    }

    #[test]
    fn should_handle_first_document_in_multi_doc_yaml() {
        let text = "doc1key: value1\n---\ndoc2key: value2\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(0, 0)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        let mut outermost = sr;
        while let Some(ref p) = outermost.parent {
            outermost = p;
        }
        assert!(
            outermost.range.end.line <= 1,
            "outermost range should not cross the --- separator (end line must be <= 1), \
             got end line {}",
            outermost.range.end.line
        );
    }

    // ---- Safety / edge case tests ----

    #[test]
    fn should_return_empty_for_empty_document() {
        let result = selection_ranges(&[], &[pos(0, 0)]);
        assert!(
            result.is_empty(),
            "should return empty Vec for empty document"
        );
    }

    #[test]
    fn should_handle_empty_positions_slice() {
        let text = "key: value\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[]);
        assert!(
            result.is_empty(),
            "should return empty Vec for empty positions slice"
        );
    }

    #[test]
    fn should_return_empty_for_cursor_on_dot_dot_dot_line() {
        let text = "key: value\n...\nother: val\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(1, 0)]);
        assert!(
            result.is_empty(),
            "cursor on '...' line should produce no selection range"
        );
    }

    // Group: selection_ranges_does_not_panic — no assertion, must not panic
    #[rstest]
    #[case::position_beyond_document("key: value\n", pos(99, 0))]
    #[case::position_beyond_line_length("key: value\n", pos(0, 999))]
    #[case::cursor_on_document_separator("a: 1\n---\nb: 2\n", pos(1, 0))]
    #[case::cursor_on_comment_only_document("# just a comment\n", pos(0, 2))]
    #[case::cursor_on_comment_line_in_middle(
        "key: value\n# this is a comment\nother: data\n",
        pos(1, 5)
    )]
    #[case::sequence_value_in_mapping("items:\n  - alpha\n  - beta\n  - gamma\n", pos(1, 4))]
    #[case::deeply_nested_sequence_value("data:\n  - nested:\n      - deep_value\n", pos(2, 10))]
    #[case::key_at_column_zero("empty:\nother: val\n", pos(0, 0))]
    #[case::alias_in_sequence("base: &anchor value\ncopy:\n  - *anchor\n", pos(2, 4))]
    fn selection_ranges_does_not_panic(#[case] text: &str, #[case] position: Position) {
        let docs = parse_docs(text);
        let _ = selection_ranges(docs.as_deref().unwrap_or(&[]), &[position]);
    }

    #[test]
    fn should_not_panic_on_deeply_nested_yaml_ast_walk() {
        // Build 64 levels of nesting (kept modest for stack safety in debug builds).
        let mut text = String::new();
        for i in 0..64usize {
            let indent = "  ".repeat(i);
            writeln!(text, "{indent}l{i}:").unwrap();
        }
        let leaf_indent = "  ".repeat(64);
        writeln!(text, "{leaf_indent}leaf: deep").unwrap();

        let docs = parse_docs(&text);
        let result = selection_ranges(
            docs.as_deref().unwrap_or(&[]),
            &[pos(64, leaf_indent.len() as u32)],
        );

        let mut depth = 0usize;
        if let Some(sr) = result.first() {
            let mut current = sr;
            while let Some(ref p) = current.parent {
                depth += 1;
                current = p;
                assert!(
                    depth <= 200,
                    "parent chain should be bounded (not infinite)"
                );
            }
        }
    }

    // ---- Additional coverage tests ----

    #[test]
    fn should_scope_document_end_at_dot_dot_dot_terminator() {
        let text = "key: value\n...\nafter: end\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(0, 5)]);

        if let Some(sr) = result.first() {
            let mut outermost = sr;
            while let Some(ref p) = outermost.parent {
                outermost = p;
            }
            assert!(
                outermost.range.end.line <= 1,
                "document root should end at or before '...', got end line {}",
                outermost.range.end.line
            );
        }
    }

    #[test]
    fn should_handle_single_line_document() {
        let text = "key: value";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(0, 5)]);
        if let Some(sr) = result.first() {
            let mut outermost = sr;
            while let Some(ref p) = outermost.parent {
                outermost = p;
            }
            assert_eq!(outermost.range.start.line, outermost.range.end.line);
        }
    }

    #[test]
    fn should_correctly_find_document_for_line_after_separator() {
        let text = "a: 1\n---\nb: 2\n---\nc: 3\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(4, 3)]);
        if let Some(sr) = result.first() {
            let mut outermost = sr;
            while let Some(ref p) = outermost.parent {
                outermost = p;
            }
            assert!(
                outermost.range.start.line >= 4,
                "outermost range should be scoped to third document, got start line {}",
                outermost.range.start.line
            );
        }
    }

    // ---- Tests for real-span traversal (containers have correct spans after loader fix) ----

    /// Test 14 — nested mapping value selection produces correct line bounds.
    ///
    /// With real container spans, `node.loc` is used directly; no recursive
    /// `effective_start`/`effective_end` fallback is needed.
    #[test]
    fn nested_mapping_value_selection_has_correct_line_bounds() {
        let text = "server:\n  host: localhost\n  port: 8080\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(2, 8)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        assert_eq!(
            sr.range.start.line, 2,
            "innermost should be on line 2 (port value)"
        );

        let parent = sr
            .parent
            .as_ref()
            .expect("should have parent (port: 8080 entry)");
        assert_eq!(
            parent.range.start.line, 2,
            "entry range should start on line 2"
        );

        let grandparent = parent
            .parent
            .as_ref()
            .expect("should have grandparent (server mapping)");
        assert!(
            grandparent.range.start.line <= 1,
            "server mapping should start at line 0 or 1, got {}",
            grandparent.range.start.line
        );
    }

    /// Test 15 — sequence item selection produces correct line bounds.
    ///
    /// Sequence items were the primary consumer of `effective_start`/`effective_end`
    /// for zero-span containers. With real spans, direct `node.loc` access suffices.
    #[test]
    fn sequence_item_selection_has_correct_line_bounds() {
        let text = "items:\n  - alpha\n  - beta\n  - gamma\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(3, 5)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        assert_eq!(
            sr.range.start.line, 3,
            "innermost should be on line 3 (gamma)"
        );

        assert!(
            sr.parent.is_some(),
            "should have parent covering sequence items"
        );
        assert!(
            sr.parent.as_ref().expect("parent").parent.is_some(),
            "should have at least three levels of parent chain"
        );
    }

    /// Test 17 — cursor on key of nested mapping expands correctly.
    ///
    /// `collect_ancestor_spans` key-detection logic must work after replacing
    /// `effective_end(value).unwrap_or(key_span.end)` with direct `node.loc.end`.
    #[test]
    fn cursor_on_key_of_nested_mapping_expands_correctly() {
        let text = "outer:\n  inner: leaf\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(1, 2)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        // Innermost: the `inner` key span
        assert_eq!(sr.range.start.line, 1, "key span should be on line 1");

        let parent = sr
            .parent
            .as_ref()
            .expect("should have parent (inner: leaf entry)");
        assert_eq!(
            parent.range.start.line, 1,
            "entry range should start at line 1"
        );

        let grandparent = parent.parent.as_ref().expect("should have grandparent");
        assert!(
            grandparent.range.start.line <= 1,
            "outer mapping parent should start at line 0 or 1, got {}",
            grandparent.range.start.line
        );
    }

    /// Test 18 — three-level nesting produces at least 4 levels in the selection chain.
    #[test]
    fn deeply_nested_sequence_selection_chain_depth() {
        let text = "list:\n  - nested:\n      - leaf\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(2, 8)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];

        let mut depth = 1usize;
        let mut current = sr;
        while let Some(ref p) = current.parent {
            depth += 1;
            current = p;
        }
        assert!(
            depth >= 4,
            "expected at least 4 levels in selection chain, got {depth}"
        );
    }

    /// Test 19 — regression: top-level key:value selection has exact line 0 bounds.
    #[test]
    fn regression_value_range_start_line_is_zero_for_top_level_key() {
        let text = "key: value\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(0, 6)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        assert_eq!(sr.range.start.line, 0, "value range start should be line 0");
        assert_eq!(sr.range.end.line, 0, "value range end should be line 0");

        let parent = sr
            .parent
            .as_ref()
            .expect("should have parent (key-value entry)");
        assert_eq!(
            parent.range.start.line, 0,
            "entry range start should be line 0"
        );
        assert_eq!(parent.range.end.line, 0, "entry range end should be line 0");
    }

    /// Test 20 — regression: nested mapping selection has correct grandparent line.
    ///
    /// With real container spans, the outer `server` mapping now has a proper span
    /// that covers both lines. The grandparent range should start at line 0.
    #[test]
    fn regression_nested_mapping_host_line_is_one() {
        let text = "server:\n  host: localhost\n";
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[pos(1, 8)]);

        assert_eq!(result.len(), 1);
        let sr = &result[0];
        assert_eq!(
            sr.range.start.line, 1,
            "value (localhost) should be on line 1"
        );
        assert_eq!(sr.range.end.line, 1, "value end should also be line 1");

        let parent = sr
            .parent
            .as_ref()
            .expect("should have parent (host: localhost)");
        assert_eq!(parent.range.start.line, 1, "entry should start on line 1");

        let grandparent = parent
            .parent
            .as_ref()
            .expect("should have grandparent (server mapping = doc root)");
        assert_eq!(
            grandparent.range.start.line, 0,
            "server mapping should start at line 0 (has real span now)"
        );
        // grandparent IS the doc root (the server mapping is the top-level node);
        // no further parent is expected.
    }

    // ---- Regression cases (AST-based scoping) ----

    #[rstest]
    #[case::key_expands_to_entry_to_mapping_to_root(
        "server:\n  host: localhost\n  port: 8080\n",
        pos(1, 2)
    )]
    #[case::nested_leaf_scalar_produces_full_chain(
        "outer:\n  inner:\n    leaf: deep\n",
        pos(2, 10)
    )]
    #[case::multi_doc_cursor_in_doc2_excludes_doc1_ranges(
        "doc1key: value1\n---\ndoc2key: value2\n",
        pos(2, 0)
    )]
    #[case::comment_line_returns_no_selection_range(
        "key: value\n# a comment\nother: data\n",
        pos(1, 5)
    )]
    fn selection_ranges_regression(#[case] text: &str, #[case] position: Position) {
        let docs = parse_docs(text);
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &[position]);

        match position {
            // case (a): key expands to entry → mapping → doc root; at least 4 levels
            p if p == pos(1, 2) && text.starts_with("server:") => {
                assert_eq!(result.len(), 1, "case (a): should return one range");
                let sr = &result[0];
                assert_eq!(sr.range.start.line, 1, "case (a): innermost on line 1");
                // Walk to outermost and verify it starts at line 0
                let mut outermost = sr;
                let mut depth = 1usize;
                while let Some(ref p) = outermost.parent {
                    depth += 1;
                    outermost = p;
                }
                assert!(depth >= 3, "case (a): expected >= 3 levels, got {depth}");
                assert_eq!(
                    outermost.range.start.line, 0,
                    "case (a): outermost (doc root) starts at line 0"
                );
            }
            // case (b): nested leaf scalar produces full chain; depth >= 5
            p if p == pos(2, 10) => {
                assert_eq!(result.len(), 1, "case (b): should return one range");
                let sr = &result[0];
                assert_eq!(sr.range.start.line, 2, "case (b): innermost on line 2");
                let mut depth = 1usize;
                let mut current = sr;
                while let Some(ref p) = current.parent {
                    depth += 1;
                    current = p;
                }
                assert!(depth >= 4, "case (b): expected >= 4 levels, got {depth}");
            }
            // case (c): cursor in doc 2 — outermost must be scoped to doc 2
            p if p == pos(2, 0) && text.starts_with("doc1key") => {
                assert_eq!(result.len(), 1, "case (c): should return one range");
                let sr = &result[0];
                let mut outermost = sr;
                while let Some(ref p) = outermost.parent {
                    outermost = p;
                }
                assert!(
                    outermost.range.start.line >= 2,
                    "case (c): outermost start must be >= 2 (doc 2), got {}",
                    outermost.range.start.line
                );
                assert!(
                    outermost.range.end.line >= 2,
                    "case (c): outermost end must be >= 2 (doc 2), got {}",
                    outermost.range.end.line
                );
            }
            // case (d): comment line returns no selection range
            p if p == pos(1, 5) => {
                assert!(
                    result.is_empty(),
                    "case (d): comment line should return empty"
                );
            }
            _ => {}
        }
    }

    // LSP-3: multibyte key — selection range character offsets are codepoint-based
    #[test]
    fn selection_ranges_multibyte_key_character_correct() {
        let text = "日本語: val\n";
        let docs = parse_docs(text);
        let positions = vec![Position {
            line: 0,
            character: 0,
        }];
        let result = selection_ranges(docs.as_deref().unwrap_or(&[]), &positions);
        assert!(!result.is_empty(), "expected at least one selection range");
        // Walk the range chain and look for a range that covers [0:0..0:3]
        // (3 codepoints for "日本語", not 9 bytes).
        let sr = &result[0];
        let mut current = sr;
        let mut found_key_range = false;
        loop {
            if current.range.start.line == 0
                && current.range.start.character == 0
                && current.range.end.line == 0
                && current.range.end.character == 3
            {
                found_key_range = true;
                break;
            }
            match &current.parent {
                Some(p) => current = p,
                None => break,
            }
        }
        assert!(
            found_key_range,
            "expected a selection range [0:0..0:3] for '日本語' (3 codepoints, not 9 bytes)"
        );
    }
}
