"""Tests for the ts_query tree-sitter query tool."""

from __future__ import annotations

from pathlib import Path

import pytest

from carve.models import ProjectStore, TreeStore
from carve.parser import TreeSitterParser
from carve.tools import ts_query


@pytest.fixture
def parser() -> TreeSitterParser:
    return TreeSitterParser()


@pytest.fixture
def sample_python() -> str:
    return '''class User:
    """A user."""

    def __init__(self, name: str):
        self.name = name

    def greet(self) -> str:
        return f"Hello, {self.name}"


def standalone(x: int) -> int:
    return x + 1
'''


@pytest.fixture
def python_store(parser: TreeSitterParser, sample_python: str) -> TreeStore:
    return parser.to_tree_store(sample_python, "python")


@pytest.fixture
def project_store(parser: TreeSitterParser, tmp_path: Path) -> ProjectStore:
    """Create a multi-file project store."""
    project = tmp_path / "myproject"
    project.mkdir()

    app = project / "app"
    app.mkdir()

    (app / "models.py").write_text(
        "class User:\n    def __init__(self, name: str):\n        self.name = name\n\n"
        "class Order:\n    def __init__(self, user: User, total: float):\n"
        "        self.user = user\n        self.total = total\n"
    )

    (app / "views.py").write_text(
        'def index():\n    return "Hello"\n\n'
        'def user_detail(user_id: int):\n    return f"User {user_id}"\n'
    )

    (project / "config.js").write_text("function getConfig() {\n  return { debug: true };\n}\n")

    return parser.to_project_store(project)


# =============================================================================
# TestTsQueryBasic — core functionality
# =============================================================================


class TestTsQueryBasic:
    def test_find_all_functions(self, python_store: TreeStore) -> None:
        """Query for all function definitions — should find methods and standalone."""
        result = ts_query(
            python_store,
            "(function_definition name: (identifier) @fn)",
        )
        assert "error" not in result
        names = []
        for match in result["matches"]:
            for node in match["captures"]["fn"]:
                names.append(node["text"])
        assert "__init__" in names
        assert "greet" in names
        assert "standalone" in names
        assert len(names) == 3

    def test_captures_include_text_and_positions(self, python_store: TreeStore) -> None:
        """Captures should include text, node_type, start_point, end_point."""
        result = ts_query(
            python_store,
            "(function_definition name: (identifier) @fn)",
        )
        assert "error" not in result
        # Find the 'greet' capture
        greet_capture = None
        for match in result["matches"]:
            for node in match["captures"]["fn"]:
                if node["text"] == "greet":
                    greet_capture = node
                    break
        assert greet_capture is not None
        assert greet_capture["node_type"] == "identifier"
        assert isinstance(greet_capture["start_point"], list)
        assert len(greet_capture["start_point"]) == 2
        assert isinstance(greet_capture["end_point"], list)
        assert len(greet_capture["end_point"]) == 2

    def test_captures_map_to_carve_node_ids(self, python_store: TreeStore) -> None:
        """Each capture should have a node_id pointing to the enclosing named node."""
        result = ts_query(
            python_store,
            "(function_definition name: (identifier) @fn)",
        )
        assert "error" not in result
        for match in result["matches"]:
            for node in match["captures"]["fn"]:
                assert "node_id" in node
                if node["text"] == "greet":
                    assert node["node_id"] == "module::User::greet"
                elif node["text"] == "__init__":
                    assert node["node_id"] == "module::User::__init__"
                elif node["text"] == "standalone":
                    assert node["node_id"] == "module::standalone"

    def test_multi_pattern_query(self, python_store: TreeStore) -> None:
        """Query with multiple patterns should return matches from both."""
        result = ts_query(
            python_store,
            "(function_definition name: (identifier) @fn)\n"
            "(class_definition name: (identifier) @cls)",
        )
        assert "error" not in result
        fn_names = []
        cls_names = []
        for match in result["matches"]:
            if "fn" in match["captures"]:
                for node in match["captures"]["fn"]:
                    fn_names.append(node["text"])
            if "cls" in match["captures"]:
                for node in match["captures"]["cls"]:
                    cls_names.append(node["text"])
        assert "greet" in fn_names
        assert "User" in cls_names

    def test_no_matches_returns_empty(self, python_store: TreeStore) -> None:
        """Query that matches nothing should return empty matches list."""
        result = ts_query(
            python_store,
            "(while_statement) @loop",
        )
        assert "error" not in result
        assert result["matches"] == []


# =============================================================================
# TestTsQueryScoping — scope parameter
# =============================================================================


class TestTsQueryScoping:
    def test_no_scope_queries_whole_file(self, python_store: TreeStore) -> None:
        """Without scope, all functions in the file should be found."""
        result = ts_query(
            python_store,
            "(function_definition name: (identifier) @fn)",
        )
        names = [n["text"] for m in result["matches"] for n in m["captures"]["fn"]]
        assert len(names) == 3  # __init__, greet, standalone

    def test_scope_to_class(self, python_store: TreeStore) -> None:
        """Scoping to a class should only find methods within it."""
        result = ts_query(
            python_store,
            "(function_definition name: (identifier) @fn)",
            scope="module::User",
        )
        assert "error" not in result
        names = [n["text"] for m in result["matches"] for n in m["captures"]["fn"]]
        assert "__init__" in names
        assert "greet" in names
        assert "standalone" not in names

    def test_scope_not_found(self, python_store: TreeStore) -> None:
        """Scoping to a nonexistent node should return error."""
        result = ts_query(
            python_store,
            "(function_definition name: (identifier) @fn)",
            scope="module::NonExistent",
        )
        assert "error" in result
        assert "not found" in result["error"]


# =============================================================================
# TestTsQueryProjectStore — multi-file
# =============================================================================


class TestTsQueryProjectStore:
    def test_queries_all_files(self, project_store: ProjectStore) -> None:
        """Without scope, should find matches from all files."""
        result = ts_query(
            project_store,
            "(function_definition name: (identifier) @fn)",
        )
        assert "error" not in result
        names = [n["text"] for m in result["matches"] for n in m["captures"]["fn"]]
        # models.py has __init__ (x2), views.py has index + user_detail
        assert "__init__" in names
        assert "index" in names
        assert "user_detail" in names

    def test_scope_to_file(self, project_store: ProjectStore) -> None:
        """Scoping to a file should only search that file."""
        # Find the views.py file ID
        views_file_id = None
        for file_id in project_store.file_sources:
            if file_id.endswith("views.py"):
                views_file_id = file_id
                break
        assert views_file_id is not None

        result = ts_query(
            project_store,
            "(function_definition name: (identifier) @fn)",
            scope=views_file_id,
        )
        assert "error" not in result
        names = [n["text"] for m in result["matches"] for n in m["captures"]["fn"]]
        assert "index" in names
        assert "user_detail" in names
        assert "__init__" not in names

    def test_scope_to_directory(self, project_store: ProjectStore) -> None:
        """Scoping to a directory should search all files under it."""
        # Find the app directory node ID
        app_dir_id = None
        for node in project_store.all_nodes():
            if node.node_type == "directory" and node.node_id.endswith("/app"):
                app_dir_id = node.node_id
                break
        assert app_dir_id is not None

        result = ts_query(
            project_store,
            "(function_definition name: (identifier) @fn)",
            scope=app_dir_id,
        )
        assert "error" not in result
        names = [n["text"] for m in result["matches"] for n in m["captures"]["fn"]]
        # app/models.py and app/views.py, but not config.js
        assert "__init__" in names
        assert "index" in names

    def test_results_include_file_key(self, project_store: ProjectStore) -> None:
        """Each match from a ProjectStore should include a 'file' key."""
        result = ts_query(
            project_store,
            "(function_definition name: (identifier) @fn)",
        )
        assert "error" not in result
        assert len(result["matches"]) > 0
        for match in result["matches"]:
            assert "file" in match


# =============================================================================
# TestTsQueryInjections — JS/CSS inside HTML
# =============================================================================


@pytest.fixture
def html_store(parser: TreeSitterParser) -> TreeStore:
    """Create an HTML store with script and style elements."""
    html_source = (
        "<html>\n<head>\n<script>\n"
        "function greet(name) {\n  return 'Hello ' + name;\n}\n"
        "</script>\n<style>\n"
        "body { color: red; }\n.main { display: flex; }\n"
        "</style>\n</head>\n<body></body>\n</html>\n"
    )
    return parser.to_tree_store(html_source, "html")


class TestTsQueryInjections:
    def test_js_query_in_html(self, html_store: TreeStore) -> None:
        """JS function query should find functions inside <script>."""
        result = ts_query(
            html_store,
            "(function_declaration name: (identifier) @fn)",
        )
        assert "error" not in result
        names = [n["text"] for m in result["matches"] for n in m["captures"]["fn"]]
        assert "greet" in names

    def test_css_query_in_html(self, html_store: TreeStore) -> None:
        """CSS rule_set query should find rules inside <style>."""
        result = ts_query(
            html_store,
            "(rule_set (selectors) @sel)",
        )
        assert "error" not in result
        texts = [n["text"] for m in result["matches"] for n in m["captures"]["sel"]]
        assert "body" in texts

    def test_injection_maps_to_carve_node_id(self, html_store: TreeStore) -> None:
        """JS captures should map to Carve node IDs under the script element."""
        result = ts_query(
            html_store,
            "(function_declaration name: (identifier) @fn)",
        )
        assert "error" not in result
        for match in result["matches"]:
            for node in match["captures"]["fn"]:
                if node["text"] == "greet":
                    assert "node_id" in node
                    assert "greet" in node["node_id"]

    def test_html_only_pattern_still_works(self, html_store: TreeStore) -> None:
        """An HTML-only pattern should still work alongside injections."""
        result = ts_query(
            html_store,
            "(script_element) @script",
        )
        assert "error" not in result
        assert len(result["matches"]) == 1

    def test_invalid_pattern_all_languages(self, html_store: TreeStore) -> None:
        """A pattern invalid for all languages (HTML, JS, CSS) should error."""
        result = ts_query(
            html_store,
            "(totally_bogus_node_type @x)",
        )
        assert "error" in result


# =============================================================================
# TestTsQueryErrors
# =============================================================================


class TestTsQueryErrors:
    def test_invalid_pattern(self, python_store: TreeStore) -> None:
        """An invalid query pattern should return an error."""
        result = ts_query(
            python_store,
            "(invalid_nonsense @bad",
        )
        assert "error" in result

    def test_wrong_language_pattern_skipped(self, project_store: ProjectStore) -> None:
        """A Python-specific pattern should silently skip JS files."""
        # This pattern is valid Python syntax but references Python-only node types
        # JS files should be silently skipped (not cause an error)
        result = ts_query(
            project_store,
            "(class_definition name: (identifier) @cls)",
        )
        assert "error" not in result
        names = [n["text"] for m in result["matches"] for n in m["captures"]["cls"]]
        # Should find Python classes but not crash on JS
        assert "User" in names
        assert "Order" in names
