"""Tests for the Carve parser."""

import pytest

from carve.models import TreeNode
from carve.node_id import extract_node_name
from carve.parser import (
    LanguageNotSupportedError,
    TreeSitterParser,
    detect_language,
    get_language,
)


class TestLanguageDetection:
    """Tests for language detection from file extensions."""

    def test_detect_python(self) -> None:
        """Test detection of Python files."""
        assert detect_language("test.py") == "python"
        assert detect_language("test.pyw") == "python"
        assert detect_language("test.pyi") == "python"

    def test_detect_javascript(self) -> None:
        """Test detection of JavaScript files."""
        assert detect_language("app.js") == "javascript"
        assert detect_language("app.mjs") == "javascript"
        assert detect_language("app.cjs") == "javascript"
        assert detect_language("app.jsx") == "javascript"

    def test_detect_html(self) -> None:
        """Test detection of HTML files."""
        assert detect_language("index.html") == "html"
        assert detect_language("index.htm") == "html"

    def test_detect_css(self) -> None:
        """Test detection of CSS files."""
        assert detect_language("styles.css") == "css"

    def test_detect_json(self) -> None:
        """Test detection of JSON files."""
        assert detect_language("config.json") == "json"

    def test_unsupported_extension(self) -> None:
        """Test that unsupported extensions fall back to 'text'."""
        assert detect_language("file.xyz") == "text"

    def test_case_insensitive(self) -> None:
        """Test that detection is case-insensitive."""
        assert detect_language("TEST.PY") == "python"
        assert detect_language("App.JS") == "javascript"


class TestLanguageLoading:
    """Tests for loading tree-sitter language grammars."""

    def test_get_python_language(self) -> None:
        """Test loading Python grammar."""
        lang = get_language("python")
        assert lang is not None

    def test_get_javascript_language(self) -> None:
        """Test loading JavaScript grammar."""
        lang = get_language("javascript")
        assert lang is not None

    def test_get_html_language(self) -> None:
        """Test loading HTML grammar."""
        lang = get_language("html")
        assert lang is not None

    def test_unsupported_language(self) -> None:
        """Test that unsupported languages raise an error."""
        with pytest.raises(LanguageNotSupportedError):
            get_language("cobol")


class TestPythonParsing:
    """Tests for parsing Python code."""

    def test_parse_simple_function(self) -> None:
        """Test parsing a simple Python function."""
        parser = TreeSitterParser()
        source = """
def hello(name: str) -> str:
    return f"Hello, {name}!"
"""
        root = parser.parse(source, "python")
        assert root is not None
        assert root.type == "module"

    def test_parse_class(self) -> None:
        """Test parsing a Python class."""
        parser = TreeSitterParser()
        source = """
class User:
    def __init__(self, name: str):
        self.name = name

    def greet(self) -> str:
        return f"Hello, {self.name}!"
"""
        root = parser.parse(source, "python")
        assert root is not None

        # Find the class
        class_node = None
        for child in root.children:
            if child.type == "class_definition":
                class_node = child
                break

        assert class_node is not None

    def test_to_tree_store_python(self) -> None:
        """Test building a TreeStore from Python source."""
        parser = TreeSitterParser()
        source = """
class Calculator:
    def add(self, a: int, b: int) -> int:
        return a + b

    def subtract(self, a: int, b: int) -> int:
        return a - b
"""
        store = parser.to_tree_store(source, "python")
        assert store.language == "python"
        assert store.root is not None
        assert store.root.node_type == "module"

    def test_node_id_generation_python(self) -> None:
        """Test that Python nodes get proper hierarchical IDs."""
        parser = TreeSitterParser()
        source = """
class User:
    def get_name(self) -> str:
        return self.name
"""
        store = parser.to_tree_store(source, "python")

        # Named nodes get clean hierarchical IDs
        assert store.get_node("module") is not None
        assert store.get_node("module::User") is not None
        assert store.get_node("module::User::get_name") is not None


class TestJavaScriptParsing:
    """Tests for parsing JavaScript code."""

    def test_parse_function(self) -> None:
        """Test parsing a JavaScript function."""
        parser = TreeSitterParser()
        source = """
function greet(name) {
    return `Hello, ${name}!`;
}
"""
        root = parser.parse(source, "javascript")
        assert root is not None
        assert root.type == "program"

    def test_parse_class(self) -> None:
        """Test parsing a JavaScript class."""
        parser = TreeSitterParser()
        source = """
class Counter {
    constructor() {
        this.count = 0;
    }

    increment() {
        this.count++;
    }
}
"""
        root = parser.parse(source, "javascript")
        assert root is not None

    def test_to_tree_store_javascript(self) -> None:
        """Test building a TreeStore from JavaScript source."""
        parser = TreeSitterParser()
        source = """
function add(a, b) {
    return a + b;
}
"""
        store = parser.to_tree_store(source, "javascript")
        assert store.language == "javascript"
        assert store.root is not None

    def test_node_id_generation_javascript(self) -> None:
        """Test that JavaScript nodes get proper hierarchical IDs."""
        parser = TreeSitterParser()
        source = """
class Calculator {
    add(a, b) {
        return a + b;
    }
}
"""
        store = parser.to_tree_store(source, "javascript")

        assert store.get_node("program") is not None
        assert store.get_node("program::Calculator") is not None
        assert store.get_node("program::Calculator::add") is not None


class TestHTMLParsing:
    """Tests for parsing HTML code."""

    def test_parse_simple_html(self) -> None:
        """Test parsing simple HTML."""
        parser = TreeSitterParser()
        source = """
<!DOCTYPE html>
<html>
<head>
    <title>Test</title>
</head>
<body>
    <h1>Hello</h1>
</body>
</html>
"""
        root = parser.parse(source, "html")
        assert root is not None
        assert root.type == "document"

    def test_to_tree_store_html(self) -> None:
        """Test building a TreeStore from HTML source."""
        parser = TreeSitterParser()
        source = """
<div id="container">
    <p>Hello World</p>
</div>
"""
        store = parser.to_tree_store(source, "html")
        assert store.language == "html"
        assert store.root is not None


class TestNodeIdGeneration:
    """Tests for node ID generation (language-agnostic via tree-sitter fields)."""

    def test_python_module_id(self) -> None:
        """Test module-level ID generation."""
        parser = TreeSitterParser()
        source = "x = 1"
        store = parser.to_tree_store(source, "python")
        assert store.root is not None
        assert store.root.node_id == "module"

    def test_python_class_id(self) -> None:
        """Test class ID generation."""
        parser = TreeSitterParser()
        source = """
class MyClass:
    pass
"""
        store = parser.to_tree_store(source, "python")
        node = store.get_node("module::MyClass")
        assert node is not None
        assert node.node_type == "class_definition"

    def test_python_method_id(self) -> None:
        """Test method ID generation — body wrappers are transparent."""
        parser = TreeSitterParser()
        source = """
class MyClass:
    def my_method(self):
        pass
"""
        store = parser.to_tree_store(source, "python")
        # Method ID skips the block wrapper
        node = store.get_node("module::MyClass::my_method")
        assert node is not None
        assert node.node_type == "function_definition"

    def test_extract_node_name(self) -> None:
        """Test extracting names from nodes (language-agnostic)."""
        parser = TreeSitterParser()
        source = """
def my_function():
    pass
"""
        root = parser.parse(source, "python")

        func_node = None
        for child in root.children:
            if child.type == "function_definition":
                func_node = child
                break

        assert func_node is not None
        name = extract_node_name(func_node)
        assert name == "my_function"

    def test_extract_node_name_class(self) -> None:
        """Test extracting class names."""
        parser = TreeSitterParser()
        source = "class Foo:\n    pass"
        root = parser.parse(source, "python")

        class_node = None
        for child in root.children:
            if child.type == "class_definition":
                class_node = child
                break

        assert class_node is not None
        assert extract_node_name(class_node) == "Foo"

    def test_extract_node_name_none_for_unnamed(self) -> None:
        """Test that unnamed nodes return None."""
        parser = TreeSitterParser()
        source = "x = 1"
        root = parser.parse(source, "python")
        # expression_statement has no 'name' field
        for child in root.children:
            if child.type == "expression_statement":
                assert extract_node_name(child) is None
                break

    def test_body_wrapper_transparency(self) -> None:
        """Test that body wrapper nodes are transparent in ID hierarchy."""
        parser = TreeSitterParser()
        source = """
class User:
    def __init__(self):
        self.name = "test"

    def greet(self):
        return "hello"
"""
        store = parser.to_tree_store(source, "python")

        # Methods skip the block wrapper in their IDs
        init = store.get_node("module::User::__init__")
        assert init is not None
        assert init.node_type == "function_definition"

        greet = store.get_node("module::User::greet")
        assert greet is not None
        assert greet.node_type == "function_definition"

        # The block node itself still exists in the tree
        user = store.get_node("module::User")
        assert user is not None
        block_children = [c for c in user.children if c.node_type == "block"]
        assert len(block_children) == 1

    def test_unnamed_nodes_get_type_index_ids(self) -> None:
        """Test that unnamed nodes get type[sibling_index] IDs."""
        parser = TreeSitterParser()
        source = """
def foo():
    x = 1
    y = 2
"""
        store = parser.to_tree_store(source, "python")
        foo = store.get_node("module::foo")
        assert foo is not None

        # The block's children (statements) should have type[index] IDs
        # that skip the block wrapper
        all_nodes = store.all_nodes()
        expression_nodes = [n for n in all_nodes if "expression_statement" in n.node_id]
        assert len(expression_nodes) >= 2

    def test_full_cst_depth(self) -> None:
        """Test that the full CST is captured — statements inside functions are nodes."""
        parser = TreeSitterParser()
        source = """
def handle(verb):
    if verb == "look":
        return "You see a room."
    if verb == "go":
        return "You go north."
    return "Unknown."
"""
        store = parser.to_tree_store(source, "python")
        all_nodes = store.all_nodes()

        # Should have if_statement nodes inside the function
        if_nodes = [n for n in all_nodes if n.node_type == "if_statement"]
        assert len(if_nodes) == 2

        # Should have return_statement nodes
        return_nodes = [n for n in all_nodes if n.node_type == "return_statement"]
        assert len(return_nodes) >= 1

    def test_javascript_body_wrapper_transparency(self) -> None:
        """Test body wrapper transparency for JavaScript (statement_block)."""
        parser = TreeSitterParser()
        source = """
class Animal {
    constructor(name) {
        this.name = name;
    }

    speak() {
        return this.name;
    }
}
"""
        store = parser.to_tree_store(source, "javascript")

        # Methods skip the class_body wrapper
        assert store.get_node("program::Animal") is not None
        # constructor has a 'name' field in JS grammar? Let's check by type
        animal = store.get_node("program::Animal")
        assert animal is not None

        # class_body should be transparent — methods accessible without it in ID
        method_names = [c.node_id for c in (animal.walk()) if c.node_type == "method_definition"]
        assert any("speak" in mid for mid in method_names)


class TestTreeStore:
    """Tests for TreeStore functionality."""

    def test_get_node(self) -> None:
        """Test retrieving a node by ID."""
        parser = TreeSitterParser()
        source = """
class User:
    def get_name(self):
        return self.name
"""
        store = parser.to_tree_store(source, "python")

        # Should be able to find the root
        root = store.get_node("module")
        assert root is not None
        assert root.node_type == "module"

    def test_all_nodes(self) -> None:
        """Test getting all nodes."""
        parser = TreeSitterParser()
        source = """
def func1():
    pass

def func2():
    pass
"""
        store = parser.to_tree_store(source, "python")
        all_nodes = store.all_nodes()

        # Should have at least the module and two functions
        assert len(all_nodes) >= 3

    def test_find_by_type(self) -> None:
        """Test finding nodes by type."""
        parser = TreeSitterParser()
        source = """
class A:
    pass

class B:
    pass
"""
        store = parser.to_tree_store(source, "python")
        classes = store.find_by_type("class_definition")

        assert len(classes) == 2


class TestTreeNode:
    """Tests for TreeNode functionality."""

    def test_signature(self) -> None:
        """Test getting node signature."""
        node = TreeNode(
            node_id="test::func",
            node_type="function_definition",
            source="def my_func(a, b):\n    return a + b",
        )
        assert node.signature() == "def my_func(a, b):"

    def test_walk(self) -> None:
        """Test walking a node and its descendants."""
        child1 = TreeNode(
            node_id="parent::child1",
            node_type="function_definition",
            source="def child1(): pass",
        )
        child2 = TreeNode(
            node_id="parent::child2",
            node_type="function_definition",
            source="def child2(): pass",
        )
        parent = TreeNode(
            node_id="parent",
            node_type="class_definition",
            source="class Parent:\n    def child1(): pass\n    def child2(): pass",
            children=[child1, child2],
        )

        walked = parent.walk()
        assert len(walked) == 3
        assert parent in walked
        assert child1 in walked
        assert child2 in walked

    def test_find_descendant(self) -> None:
        """Test finding a descendant node."""
        grandchild = TreeNode(
            node_id="root::child::grandchild",
            node_type="function_definition",
            source="def grandchild(): pass",
        )
        child = TreeNode(
            node_id="root::child",
            node_type="class_definition",
            source="class Child: ...",
            children=[grandchild],
        )
        root = TreeNode(
            node_id="root",
            node_type="module",
            source="...",
            children=[child],
        )

        found = root.find_descendant("root::child::grandchild")
        assert found is not None
        assert found.node_id == "root::child::grandchild"


class TestPositionLookup:
    """Tests for finding nodes at positions."""

    def test_get_node_at_position(self) -> None:
        """Test finding a node at a specific position."""
        parser = TreeSitterParser()
        source = """def hello():
    pass

def world():
    pass
"""
        root = parser.parse(source, "python")

        # Position should be inside 'hello' function (row 0, col 4 is in 'def hello')
        node = parser.get_node_at_position(root, 0, 4)
        assert node is not None
        # Should find something related to the function definition

    def test_get_node_at_position_out_of_bounds(self) -> None:
        """Test that out-of-bounds positions return None appropriately."""
        parser = TreeSitterParser()
        source = "x = 1"
        root = parser.parse(source, "python")

        # Position way after the content
        node = parser.get_node_at_position(root, 100, 0)
        assert node is None


class TestLanguageInjection:
    """Tests for language injection (JS/CSS inside HTML)."""

    HTML_WITH_SCRIPT = """\
<html>
<body>
<script>
function hello() {
    return "hi";
}
function world() {
    return "world";
}
</script>
</body>
</html>"""

    HTML_WITH_STYLE = """\
<html>
<head>
<style>
.player { color: red; }
#board { display: grid; }
h1 { font-size: 2em; }
</style>
</head>
</html>"""

    HTML_WITH_BOTH = """\
<html>
<head>
<style>
.box { border: 1px solid black; }
</style>
</head>
<body>
<script>
function greet(name) {
    return "Hello, " + name;
}
</script>
</body>
</html>"""

    def test_html_script_injection(self) -> None:
        """JS functions inside <script> are parsed into the tree."""
        parser = TreeSitterParser()
        store = parser.to_tree_store(self.HTML_WITH_SCRIPT, "html")

        all_nodes = store.all_nodes()
        func_nodes = [n for n in all_nodes if n.node_type == "function_declaration"]
        assert len(func_nodes) == 2

        func_names = {n.node_id.rsplit("::", 1)[-1] for n in func_nodes}
        assert func_names == {"hello", "world"}

    def test_html_script_language_field(self) -> None:
        """Script element and its children have language='javascript'."""
        parser = TreeSitterParser()
        store = parser.to_tree_store(self.HTML_WITH_SCRIPT, "html")

        all_nodes = store.all_nodes()
        script_nodes = [n for n in all_nodes if n.node_type == "script_element"]
        assert len(script_nodes) == 1
        assert script_nodes[0].language == "javascript"

        func_nodes = [n for n in all_nodes if n.node_type == "function_declaration"]
        for fn in func_nodes:
            assert fn.language == "javascript"

    def test_html_style_injection(self) -> None:
        """CSS rules inside <style> are parsed into the tree."""
        parser = TreeSitterParser()
        store = parser.to_tree_store(self.HTML_WITH_STYLE, "html")

        all_nodes = store.all_nodes()
        rule_nodes = [n for n in all_nodes if n.node_type == "rule_set"]
        assert len(rule_nodes) == 3

    def test_html_style_css_selectors_as_names(self) -> None:
        """CSS rule_set nodes use their selector text as the ID name."""
        parser = TreeSitterParser()
        store = parser.to_tree_store(self.HTML_WITH_STYLE, "html")

        all_nodes = store.all_nodes()
        rule_ids = {n.node_id.rsplit("::", 1)[-1] for n in all_nodes if n.node_type == "rule_set"}
        assert ".player" in rule_ids
        assert "#board" in rule_ids
        assert "h1" in rule_ids

    def test_html_style_language_field(self) -> None:
        """Style element and its children have language='css'."""
        parser = TreeSitterParser()
        store = parser.to_tree_store(self.HTML_WITH_STYLE, "html")

        all_nodes = store.all_nodes()
        style_nodes = [n for n in all_nodes if n.node_type == "style_element"]
        assert len(style_nodes) == 1
        assert style_nodes[0].language == "css"

        rule_nodes = [n for n in all_nodes if n.node_type == "rule_set"]
        for rn in rule_nodes:
            assert rn.language == "css"

    def test_injection_byte_offsets(self) -> None:
        """Injected nodes have correct byte offsets into the original HTML source."""
        parser = TreeSitterParser()
        source = self.HTML_WITH_SCRIPT
        source_bytes = source.encode("utf-8")
        store = parser.to_tree_store(source, "html")

        all_nodes = store.all_nodes()
        func_nodes = [n for n in all_nodes if n.node_type == "function_declaration"]

        for fn in func_nodes:
            # The source extracted via byte offsets should match the node's source
            extracted = source_bytes[fn.start_byte : fn.end_byte].decode("utf-8")
            assert extracted == fn.source
            assert "function" in fn.source

    def test_injection_node_ids(self) -> None:
        """Injected nodes have hierarchical IDs through the script/style element."""
        parser = TreeSitterParser()
        store = parser.to_tree_store(self.HTML_WITH_SCRIPT, "html")

        all_nodes = store.all_nodes()
        hello = [n for n in all_nodes if n.node_id.endswith("::hello")]
        assert len(hello) == 1
        # The ID should include "script" from the script_element shorthand
        assert "::script::" in hello[0].node_id

    def test_injection_with_both_script_and_style(self) -> None:
        """Both script and style injections work in the same HTML file."""
        parser = TreeSitterParser()
        store = parser.to_tree_store(self.HTML_WITH_BOTH, "html")

        all_nodes = store.all_nodes()
        func_nodes = [n for n in all_nodes if n.node_type == "function_declaration"]
        assert len(func_nodes) == 1
        assert func_nodes[0].node_id.endswith("::greet")

        rule_nodes = [n for n in all_nodes if n.node_type == "rule_set"]
        assert len(rule_nodes) == 1

    def test_inspect_injected_node(self) -> None:
        """End-to-end: get_node() on an injected JS function returns the right source."""
        parser = TreeSitterParser()
        store = parser.to_tree_store(self.HTML_WITH_SCRIPT, "html")

        # Find the hello function's full ID
        all_nodes = store.all_nodes()
        hello = [n for n in all_nodes if n.node_id.endswith("::hello")]
        assert len(hello) == 1

        # Retrieve it via get_node()
        found = store.get_node(hello[0].node_id)
        assert found is not None
        assert found.node_type == "function_declaration"
        assert 'return "hi"' in found.source

    def test_empty_script_no_crash(self) -> None:
        """An empty <script></script> doesn't crash."""
        parser = TreeSitterParser()
        html = "<html><body><script></script></body></html>"
        store = parser.to_tree_store(html, "html")
        assert store.root is not None

    def test_empty_style_no_crash(self) -> None:
        """An empty <style></style> doesn't crash."""
        parser = TreeSitterParser()
        html = "<html><head><style></style></head></html>"
        store = parser.to_tree_store(html, "html")
        assert store.root is not None

    def test_injection_with_full_html(self) -> None:
        """Parse HTML with both JS functions and CSS rules via injection."""
        parser = TreeSitterParser()
        html = (
            "<!DOCTYPE html>\n<html>\n<head>\n"
            "    <style>\n"
            "        .board { display: grid; }\n"
            "        .cell { background: #16213e; }\n"
            "        h1 { color: red; }\n"
            "        button { padding: 12px; }\n"
            "        @keyframes pulse { 50% { transform: scale(1.1); } }\n"
            "    </style>\n"
            "</head>\n<body>\n"
            "    <script>\n"
            "        function render() { return 1; }\n"
            "        function makeMove(i) { return i; }\n"
            "        function checkWin() { return null; }\n"
            "        function highlightWin(line) { return line; }\n"
            "        function updateScore() { return 0; }\n"
            "        function resetGame() { return; }\n"
            "    </script>\n"
            "</body>\n</html>"
        )
        store = parser.to_tree_store(html, "html")
        all_nodes = store.all_nodes()

        # JS functions should be present
        js_func_names = {
            n.node_id.rsplit("::", 1)[-1]
            for n in all_nodes
            if n.node_type == "function_declaration"
        }
        assert "render" in js_func_names
        assert "makeMove" in js_func_names
        assert "checkWin" in js_func_names
        assert "highlightWin" in js_func_names
        assert "updateScore" in js_func_names
        assert "resetGame" in js_func_names

        # CSS rules should be present
        css_rule_names = {
            n.node_id.rsplit("::", 1)[-1] for n in all_nodes if n.node_type == "rule_set"
        }
        assert ".cell" in css_rule_names
        assert ".board" in css_rule_names
        assert "h1" in css_rule_names
        assert "button" in css_rule_names

        # Keyframes should be named
        kf_nodes = [n for n in all_nodes if n.node_type == "keyframes_statement"]
        assert len(kf_nodes) == 1
        assert kf_nodes[0].node_id.endswith("::pulse")


class TestNewLanguageDetection:
    """Tests for language detection of newly added languages."""

    def test_detect_markdown(self) -> None:
        assert detect_language("README.md") == "markdown"

    def test_detect_toml(self) -> None:
        assert detect_language("pyproject.toml") == "toml"

    def test_detect_yaml(self) -> None:
        assert detect_language("config.yaml") == "yaml"
        assert detect_language("config.yml") == "yaml"

    def test_detect_bash(self) -> None:
        assert detect_language("script.sh") == "bash"

    def test_detect_jinja(self) -> None:
        assert detect_language("template.jinja") == "jinja"
        assert detect_language("template.jinja2") == "jinja"
        assert detect_language("template.j2") == "jinja"


class TestNewLanguageLoading:
    """Tests for loading tree-sitter grammars for newly added languages."""

    def test_get_markdown_language(self) -> None:
        lang = get_language("markdown")
        assert lang is not None

    def test_get_toml_language(self) -> None:
        lang = get_language("toml")
        assert lang is not None

    def test_get_yaml_language(self) -> None:
        lang = get_language("yaml")
        assert lang is not None

    def test_get_bash_language(self) -> None:
        lang = get_language("bash")
        assert lang is not None

    def test_get_jinja_language(self) -> None:
        lang = get_language("jinja")
        assert lang is not None


class TestBashParsing:
    """Tests for parsing Bash code."""

    def test_parse_function(self) -> None:
        parser = TreeSitterParser()
        source = 'greet() {\n    echo "Hello, $1"\n}\n'
        root = parser.parse(source, "bash")
        assert root is not None
        assert root.type == "program"

    def test_function_node_id(self) -> None:
        parser = TreeSitterParser()
        source = 'greet() {\n    echo "Hello"\n}\n'
        store = parser.to_tree_store(source, "bash")
        assert store.get_node("program::greet") is not None

    def test_compound_statement_transparency(self) -> None:
        """Compound_statement (function body) is transparent in IDs."""
        parser = TreeSitterParser()
        source = 'greet() {\n    echo "Hello"\n    echo "World"\n}\n'
        store = parser.to_tree_store(source, "bash")
        greet = store.get_node("program::greet")
        assert greet is not None

        # compound_statement exists in tree but children skip it in IDs
        all_nodes = store.all_nodes()
        cmd_nodes = [n for n in all_nodes if n.node_type == "command" and "greet" in n.node_id]
        assert len(cmd_nodes) == 2

        # Commands should NOT have "compound_statement" in their IDs
        for cmd in cmd_nodes:
            assert "compound_statement" not in cmd.node_id

    def test_do_group_transparency(self) -> None:
        """do_group (loop body) is transparent in IDs."""
        parser = TreeSitterParser()
        source = "for i in 1 2 3; do\n    echo $i\ndone\n"
        store = parser.to_tree_store(source, "bash")
        all_nodes = store.all_nodes()

        # The command inside do_group should not have "do_group" in its ID
        cmd_nodes = [n for n in all_nodes if n.node_type == "command"]
        assert len(cmd_nodes) >= 1
        for cmd in cmd_nodes:
            assert "do_group" not in cmd.node_id

    def test_command_named_by_command_name(self) -> None:
        """Commands get named by their command_name field."""
        parser = TreeSitterParser()
        source = "greet() {\n    echo hello\n}\n"
        store = parser.to_tree_store(source, "bash")
        all_nodes = store.all_nodes()
        echo_cmds = [n for n in all_nodes if n.node_id.endswith("::echo")]
        assert len(echo_cmds) == 1
        assert echo_cmds[0].node_type == "command"


class TestTOMLParsing:
    """Tests for parsing TOML code."""

    def test_parse_toml(self) -> None:
        parser = TreeSitterParser()
        source = '[server]\nhost = "localhost"\nport = 8080\n'
        root = parser.parse(source, "toml")
        assert root is not None
        assert root.type == "document"

    def test_table_node_id(self) -> None:
        parser = TreeSitterParser()
        source = '[server]\nhost = "localhost"\n\n[database]\nname = "mydb"\n'
        store = parser.to_tree_store(source, "toml")
        assert store.get_node("document::server") is not None
        assert store.get_node("document::database") is not None

    def test_table_array_node_id(self) -> None:
        parser = TreeSitterParser()
        source = '[[servers]]\nname = "alpha"\n'
        store = parser.to_tree_store(source, "toml")
        assert store.get_node("document::servers") is not None

    def test_table_contains_pairs(self) -> None:
        parser = TreeSitterParser()
        source = '[server]\nhost = "localhost"\nport = 8080\n'
        store = parser.to_tree_store(source, "toml")
        server = store.get_node("document::server")
        assert server is not None
        pairs = [c for c in server.children if c.node_type == "pair"]
        assert len(pairs) == 2


class TestYAMLParsing:
    """Tests for parsing YAML code."""

    def test_parse_yaml(self) -> None:
        parser = TreeSitterParser()
        source = "server:\n  host: localhost\n  port: 8080\n"
        root = parser.parse(source, "yaml")
        assert root is not None
        assert root.type == "stream"

    def test_mapping_pair_node_ids(self) -> None:
        parser = TreeSitterParser()
        source = "name: myapp\nversion: 1.0\n"
        store = parser.to_tree_store(source, "yaml")
        all_nodes = store.all_nodes()
        pair_ids = [n.node_id for n in all_nodes if n.node_type == "block_mapping_pair"]
        # Should have "name" and "version" as named pairs
        assert any("::name" in pid for pid in pair_ids)
        assert any("::version" in pid for pid in pair_ids)

    def test_nested_mapping_pairs(self) -> None:
        parser = TreeSitterParser()
        source = "server:\n  host: localhost\n  port: 8080\n"
        store = parser.to_tree_store(source, "yaml")
        all_nodes = store.all_nodes()
        pair_names = {
            n.node_id.rsplit("::", 1)[-1] for n in all_nodes if n.node_type == "block_mapping_pair"
        }
        assert "server" in pair_names
        assert "host" in pair_names
        assert "port" in pair_names


class TestMarkdownParsing:
    """Tests for parsing Markdown code."""

    def test_parse_markdown(self) -> None:
        parser = TreeSitterParser()
        source = "# Hello World\n\nSome text\n"
        root = parser.parse(source, "markdown")
        assert root is not None
        assert root.type == "document"

    def test_heading_node_ids(self) -> None:
        parser = TreeSitterParser()
        source = "# Introduction\n\n## Getting Started\n\n### Details\n"
        store = parser.to_tree_store(source, "markdown")
        all_nodes = store.all_nodes()
        heading_names = {
            n.node_id.rsplit("::", 1)[-1] for n in all_nodes if n.node_type == "atx_heading"
        }
        assert "Introduction" in heading_names
        assert "Getting Started" in heading_names
        assert "Details" in heading_names

    def test_fenced_code_block_named_by_language(self) -> None:
        parser = TreeSitterParser()
        source = "# Code\n\n```python\nprint(1)\n```\n"
        store = parser.to_tree_store(source, "markdown")
        all_nodes = store.all_nodes()
        code_blocks = [n for n in all_nodes if n.node_type == "fenced_code_block"]
        assert len(code_blocks) == 1
        assert code_blocks[0].node_id.endswith("::python")

    def test_setext_heading(self) -> None:
        parser = TreeSitterParser()
        source = "Hello\n=====\n\nSome text\n"
        store = parser.to_tree_store(source, "markdown")
        all_nodes = store.all_nodes()
        headings = [n for n in all_nodes if n.node_type == "setext_heading"]
        assert len(headings) == 1
        assert headings[0].node_id.endswith("::Hello")


class TestJinjaParsing:
    """Tests for parsing Jinja template code."""

    def test_parse_jinja(self) -> None:
        parser = TreeSitterParser()
        source = "{% macro greet(name) %}Hello {{ name }}{% endmacro %}\n"
        root = parser.parse(source, "jinja")
        assert root is not None
        assert root.type == "source_file"

    def test_macro_node_id(self) -> None:
        """Macro nodes get named via their name field."""
        parser = TreeSitterParser()
        source = "{% macro greet(name) %}Hello {{ name }}{% endmacro %}\n"
        store = parser.to_tree_store(source, "jinja")
        assert store.get_node("source_file::greet") is not None

    def test_block_node_id(self) -> None:
        """Block tags get named via their identifier."""
        parser = TreeSitterParser()
        source = "{% block header %}Content{% endblock %}\n"
        store = parser.to_tree_store(source, "jinja")
        all_nodes = store.all_nodes()
        block_tags = [n for n in all_nodes if n.node_type == "jinja_tag"]
        # The {% block header %} tag should be named "header"
        block_names = {n.node_id.rsplit("::", 1)[-1] for n in block_tags}
        assert "header" in block_names

    def test_for_loop(self) -> None:
        """For loops are parsed as jinja_for nodes."""
        parser = TreeSitterParser()
        source = "{% for item in items %}{{ item }}{% endfor %}\n"
        store = parser.to_tree_store(source, "jinja")
        all_nodes = store.all_nodes()
        for_nodes = [n for n in all_nodes if n.node_type == "jinja_for"]
        assert len(for_nodes) == 1

    def test_if_statement(self) -> None:
        """If statements are parsed as jinja_if nodes."""
        parser = TreeSitterParser()
        source = "{% if show %}Hello{% endif %}\n"
        store = parser.to_tree_store(source, "jinja")
        all_nodes = store.all_nodes()
        if_nodes = [n for n in all_nodes if n.node_type == "jinja_if"]
        assert len(if_nodes) == 1
