"""Tests for Carve navigation tools."""

from __future__ import annotations

import pytest

from carve.parser import TreeSitterParser
from carve.tools import (
    ancestors,
    delete,
    error_locate,
    find_references,
    insert,
    inspect,
    move,
    project_import,
    project_serialize,
    search,
    subtree,
    update,
    validate_snippet,
)


@pytest.fixture
def parser() -> TreeSitterParser:
    """Create a parser instance."""
    return TreeSitterParser()


@pytest.fixture
def sample_python_code() -> str:
    """Sample Python code with classes and functions."""
    return '''
class User:
    """A user in the system."""

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

    def get_orders(self, status: str | None = None) -> list:
        """Get orders for this user."""
        orders = Order.find_by_user(self)
        if status:
            orders = [o for o in orders if o.status == status]
        return orders

    def cancel_order(self, order_id: int) -> bool:
        """Cancel a specific order."""
        order = self.get_orders()
        return True


class Order:
    """An order in the system."""

    def __init__(self, user: User, total: float):
        self.user = user
        self.total = total
        self.status = "pending"

    @classmethod
    def find_by_user(cls, user: User) -> list:
        """Find all orders for a user."""
        return []


def process_orders(users: list) -> int:
    """Process orders for multiple users."""
    count = 0
    for user in users:
        orders = user.get_orders()
        count += len(orders)
    return count
'''


@pytest.fixture
def python_store(parser: TreeSitterParser, sample_python_code: str, tmp_path):
    """Create a TreeStore from sample Python code."""
    return parser.to_tree_store(sample_python_code, "python", str(tmp_path / "test.py"))


class TestSubtree:
    """Tests for the subtree tool."""

    def test_subtree_root(self, python_store):
        """Test getting subtree from root."""
        result = subtree(python_store, "module", depth=1)

        assert result["node_id"] == "module"
        assert result["node_type"] == "module"
        assert "children" in result
        # Should have User, Order, and process_orders
        child_ids = [c["node_id"] for c in result["children"]]
        assert "module::User" in child_ids
        assert "module::Order" in child_ids
        assert "module::process_orders" in child_ids

    def test_subtree_class(self, python_store):
        """Test getting subtree of a class."""
        result = subtree(python_store, "module::User", depth=1)

        assert result["node_id"] == "module::User"
        assert "source" in result  # Root node gets full source
        assert "children" in result

        # Check children have signatures but not full source at depth 1
        for child in result["children"]:
            assert "signature" in child

    def test_subtree_depth_zero(self, python_store):
        """Test depth=0 returns only the node itself."""
        result = subtree(python_store, "module::User", depth=0)

        assert result["node_id"] == "module::User"
        assert "source" in result
        assert "children" not in result

    def test_subtree_depth_control(self, python_store):
        """Test depth limits child traversal."""
        # Depth 2 from module should show User's methods
        result = subtree(python_store, "module", depth=2)

        assert "children" in result
        user = next(c for c in result["children"] if c["node_id"] == "module::User")
        assert "children" in user

    def test_subtree_include_source(self, python_store):
        """Test include_source=True adds source to all nodes."""
        result = subtree(python_store, "module::User", depth=1, include_source=True)

        assert "source" in result
        for child in result["children"]:
            assert "source" in child

    def test_subtree_not_found(self, python_store):
        """Test subtree with non-existent node."""
        result = subtree(python_store, "module::NotExist", depth=1)
        assert "error" in result


class TestAncestors:
    """Tests for the ancestors tool."""

    def test_ancestors_method(self, python_store):
        """Test getting ancestors of a method."""
        result = ancestors(python_store, "module::User::get_orders")

        # Should be: get_orders -> User -> module
        assert len(result) >= 2
        assert result[0]["node_id"] == "module::User::get_orders"
        # Path should include User and module
        node_ids = [r["node_id"] for r in result]
        assert "module::User" in node_ids
        assert "module" in node_ids

    def test_ancestors_with_levels(self, python_store):
        """Test levels parameter limits results."""
        result = ancestors(python_store, "module::User::get_orders", levels=2)

        assert len(result) == 2
        assert result[0]["node_id"] == "module::User::get_orders"

    def test_ancestors_root(self, python_store):
        """Test ancestors of root returns just root."""
        result = ancestors(python_store, "module")

        assert len(result) == 1
        assert result[0]["node_id"] == "module"

    def test_ancestors_not_found(self, python_store):
        """Test ancestors with non-existent node."""
        result = ancestors(python_store, "module::NotExist")
        assert len(result) == 1
        assert "error" in result[0]

    def test_ancestors_includes_type(self, python_store):
        """Test each ancestor includes type info."""
        result = ancestors(python_store, "module::User")

        for ancestor in result:
            assert "node_id" in ancestor
            assert "type" in ancestor


class TestInspect:
    """Tests for the inspect tool."""

    def test_inspect_basic(self, python_store):
        """Test basic inspect returns required fields."""
        result = inspect(python_store, "module::User::get_orders")

        assert result["node_id"] == "module::User::get_orders"
        assert "type" in result
        assert "source" in result
        assert "def get_orders" in result["source"]
        assert "references" in result
        assert "referenced_by" in result

    def test_inspect_references(self, python_store):
        """Test that references are detected."""
        result = inspect(python_store, "module::User::get_orders")

        # get_orders references Order
        assert "module::Order" in result["references"]

    def test_inspect_referenced_by(self, python_store):
        """Test that referenced_by is detected."""
        result = inspect(python_store, "module::User::get_orders")

        # get_orders is referenced by cancel_order and process_orders
        assert "module::User::cancel_order" in result["referenced_by"]

    def test_inspect_with_metadata(self, python_store):
        """Test include_metadata returns metadata."""
        result = inspect(python_store, "module::User", include_metadata=True)

        assert "metadata" in result
        assert "version" in result["metadata"]
        assert "created_at" in result["metadata"]

    def test_inspect_not_found(self, python_store):
        """Test inspect with non-existent node."""
        result = inspect(python_store, "module::NotExist")
        assert "error" in result

    def test_inspect_includes_positions(self, python_store):
        """Test inspect includes start/end positions."""
        result = inspect(python_store, "module::User")

        assert "start_point" in result
        assert "end_point" in result


class TestSearch:
    """Tests for the search tool."""

    def test_search_by_pattern(self, python_store):
        """Test searching by name pattern."""
        result = search(python_store, pattern="order")

        # Should find get_orders, cancel_order, Order, process_orders
        node_ids = [r["node_id"] for r in result]
        assert any("get_orders" in nid for nid in node_ids)
        assert any("Order" in nid for nid in node_ids)

    def test_search_by_type(self, python_store):
        """Test searching by node type."""
        result = search(python_store, node_type="class_definition")

        # Should find User and Order classes
        assert len(result) == 2
        node_ids = [r["node_id"] for r in result]
        assert "module::User" in node_ids
        assert "module::Order" in node_ids

    def test_search_by_pattern_and_type(self, python_store):
        """Test combining pattern and type filters."""
        result = search(python_store, pattern="User", node_type="class_definition")

        assert len(result) == 1
        assert result[0]["node_id"] == "module::User"

    def test_search_with_scope(self, python_store):
        """Test limiting search to a scope."""
        result = search(python_store, pattern="order", scope="module::User")

        # Should only find methods within User class
        for r in result:
            assert r["node_id"].startswith("module::User")

    def test_search_returns_signatures(self, python_store):
        """Test search results include signatures."""
        result = search(python_store, pattern="get_orders")

        for r in result:
            assert "signature" in r
            assert "type" in r

    def test_search_no_matches(self, python_store):
        """Test search with no matches returns empty list."""
        result = search(python_store, pattern="zzz_nonexistent")
        assert result == []

    def test_search_invalid_regex(self, python_store):
        """Test search with invalid regex returns error."""
        result = search(python_store, pattern="[invalid")
        assert len(result) == 1
        assert "error" in result[0]

    def test_search_scope_not_found(self, python_store):
        """Test search with non-existent scope."""
        result = search(python_store, pattern="test", scope="module::NotExist")
        assert len(result) == 1
        assert "error" in result[0]

    def test_search_case_insensitive(self, python_store):
        """Test search is case-insensitive."""
        result = search(python_store, pattern="USER")

        node_ids = [r["node_id"] for r in result]
        assert "module::User" in node_ids

    def test_search_matches_name_not_ancestors(self, python_store):
        """Test that search matches the node's own name, not ancestor path components."""
        result = search(python_store, pattern="User")

        node_ids = [r["node_id"] for r in result]
        # Should find the User class itself
        assert "module::User" in node_ids
        # Should NOT find children just because their path contains "User"
        assert "module::User::__init__" not in node_ids
        assert not any(nid.startswith("module::User::") for nid in node_ids)


class TestFindReferences:
    """Tests for the find_references tool."""

    def test_find_references_outgoing(self, python_store):
        """Test that outgoing references (what this node uses) are found."""
        result = find_references(python_store, "module::User::get_orders")

        assert "error" not in result
        assert result["node_id"] == "module::User::get_orders"
        # get_orders references Order.find_by_user
        ref_ids = [r["node_id"] for r in result["references"]]
        assert "module::Order" in ref_ids

    def test_find_references_incoming(self, python_store):
        """Test that incoming references (what uses this node) are found."""
        result = find_references(python_store, "module::User::get_orders")

        refby_ids = [r["node_id"] for r in result["referenced_by"]]
        # get_orders is referenced by cancel_order and process_orders
        assert "module::User::cancel_order" in refby_ids
        assert "module::process_orders" in refby_ids

    def test_find_references_includes_type_and_signature(self, python_store):
        """Test that each reference entry includes type and signature."""
        result = find_references(python_store, "module::User::get_orders")

        for ref in result["references"]:
            assert "node_id" in ref
            assert "type" in ref
            assert "signature" in ref

        for ref in result["referenced_by"]:
            assert "node_id" in ref
            assert "type" in ref
            assert "signature" in ref

    def test_find_references_class(self, python_store):
        """Test finding references for a class node."""
        result = find_references(python_store, "module::Order")

        assert "error" not in result
        # Order is referenced by User.get_orders (Order.find_by_user call)
        refby_ids = [r["node_id"] for r in result["referenced_by"]]
        assert any("get_orders" in rid for rid in refby_ids)

    def test_find_references_no_references(self, parser):
        """Test a node with no outgoing references to named siblings."""
        source = "def aaa():\n    return 42\n\ndef bbb():\n    return 99\n"
        store = parser.to_tree_store(source, "python", "test.py")
        result = find_references(store, "module::aaa")

        assert "error" not in result
        # aaa doesn't mention bbb, so outgoing references should be empty
        ref_ids = [r["node_id"] for r in result["references"]]
        assert "module::bbb" not in ref_ids

    def test_find_references_not_found(self, python_store):
        """Test error when node doesn't exist."""
        result = find_references(python_store, "module::NonExistent")

        assert "error" in result
        assert "not found" in result["error"].lower()

    def test_find_references_richer_than_inspect(self, python_store):
        """Test that find_references returns dicts with detail, not bare IDs."""
        result = find_references(python_store, "module::User::get_orders")

        # inspect returns bare string IDs; find_references returns dicts
        if result["references"]:
            assert isinstance(result["references"][0], dict)
        if result["referenced_by"]:
            assert isinstance(result["referenced_by"][0], dict)

    def test_find_references_bidirectional(self, python_store):
        """Test that A referencing B means B is referenced_by A."""
        result_a = find_references(python_store, "module::User::get_orders")
        # get_orders references Order
        ref_ids = [r["node_id"] for r in result_a["references"]]
        assert "module::Order" in ref_ids

        result_b = find_references(python_store, "module::Order")
        # Order should be referenced_by get_orders
        refby_ids = [r["node_id"] for r in result_b["referenced_by"]]
        assert "module::User::get_orders" in refby_ids

    def test_find_references_no_ancestor_bloat(self, python_store):
        """Test that ancestor chain nodes are pruned from results.

        When get_orders references Order, we should get 'module::Order'
        (the class) — not also 'module' (which contains Order in its source).
        When get_orders is referenced_by cancel_order, we should get
        cancel_order — not also User, block, etc.
        """
        result = find_references(python_store, "module::User::get_orders")

        # referenced_by should NOT include module::User (ancestor of cancel_order)
        refby_ids = [r["node_id"] for r in result["referenced_by"]]
        assert "module::User" not in refby_ids
        assert "module" not in refby_ids
        # But should include the actual callers
        assert "module::User::cancel_order" in refby_ids
        assert "module::process_orders" in refby_ids

    def test_find_references_no_indexed_leaf_noise(self, parser):
        """Test that indexed leaf nodes (identifier, parameters) are excluded."""
        source = (
            "class Room:\n"
            "    def __init__(self, name: str):\n"
            "        self.name = name\n"
            "\n"
            "def create_room():\n"
            "    return Room(name='Hall')\n"
        )
        store = parser.to_tree_store(source, "python")
        result = find_references(store, "module::Room")

        # referenced_by should include create_room (it calls Room(...))
        refby_ids = [r["node_id"] for r in result["referenced_by"]]
        assert "module::create_room" in refby_ids
        # Should NOT include indexed nodes like keyword_argument or identifier
        for rid in refby_ids:
            assert "[" not in rid, f"Indexed node in results: {rid}"


class TestErrorLocate:
    """Tests for the error_locate tool."""

    def test_python_traceback_single_frame(self, parser):
        """Test locating a single Python traceback frame."""
        source = "def greet(name):\n    return 'Hello, ' + name\n\ndef main():\n    greet(42)\n"
        store = parser.to_tree_store(source, "python", "app.py")

        tb = (
            "Traceback (most recent call last):\n"
            '  File "app.py", line 5, in main\n'
            "    greet(42)\n"
            "TypeError: can only concatenate str to str\n"
        )
        result = error_locate(store, tb)

        assert "error" not in result
        assert len(result["frames"]) == 1
        frame = result["frames"][0]
        assert frame["file"] == "app.py"
        assert frame["line"] == 5
        assert frame["node_id"] is not None
        assert "main" in frame["node_id"]

    def test_python_traceback_multi_frame(self, parser):
        """Test locating multiple Python traceback frames."""
        source = (
            "def helper():\n"
            "    raise ValueError('oops')\n"
            "\n"
            "def caller():\n"
            "    helper()\n"
            "\n"
            "def main():\n"
            "    caller()\n"
        )
        store = parser.to_tree_store(source, "python", "app.py")

        tb = (
            "Traceback (most recent call last):\n"
            '  File "app.py", line 8, in main\n'
            "    caller()\n"
            '  File "app.py", line 5, in caller\n'
            "    helper()\n"
            '  File "app.py", line 2, in helper\n'
            "    raise ValueError('oops')\n"
            "ValueError: oops\n"
        )
        result = error_locate(store, tb)

        assert "error" not in result
        assert len(result["frames"]) == 3

        node_ids = [f["node_id"] for f in result["frames"]]
        assert any("main" in nid for nid in node_ids if nid)
        assert any("caller" in nid for nid in node_ids if nid)
        assert any("helper" in nid for nid in node_ids if nid)

    def test_python_traceback_class_method(self, python_store):
        """Test locating a frame inside a class method."""
        # python_store has User.get_orders starting around line 9
        node = python_store.get_node("module::User::get_orders")
        assert node is not None
        method_line = node.start_point[0] + 1  # 1-indexed

        tb = (
            "Traceback (most recent call last):\n"
            f'  File "test.py", line {method_line}, in get_orders\n'
            "    orders = Order.find_by_user(self)\n"
            'NameError: name "Order" is not defined\n'
        )
        result = error_locate(python_store, tb)

        assert "error" not in result
        assert len(result["frames"]) == 1
        assert result["frames"][0]["node_id"] is not None
        # Should resolve to get_orders or a node within it
        assert "get_orders" in result["frames"][0]["node_id"]

    def test_javascript_stack_trace(self, parser):
        """Test locating JavaScript stack trace frames."""
        source = (
            "function divide(a, b) {\n"
            "    return a / b;\n"
            "}\n"
            "\n"
            "function main() {\n"
            "    divide(1, 0);\n"
            "}\n"
        )
        store = parser.to_tree_store(source, "javascript", "app.js")

        tb = "Error: Division by zero\n    at divide (app.js:2:5)\n    at main (app.js:6:5)\n"
        result = error_locate(store, tb)

        assert "error" not in result
        assert len(result["frames"]) == 2
        node_ids = [f["node_id"] for f in result["frames"]]
        assert any("divide" in nid for nid in node_ids if nid)
        assert any("main" in nid for nid in node_ids if nid)

    def test_frame_includes_type_and_signature(self, parser):
        """Test that resolved frames include node_type and signature."""
        source = "def foo():\n    return 1\n"
        store = parser.to_tree_store(source, "python", "test.py")

        tb = '  File "test.py", line 1, in foo\n'
        result = error_locate(store, tb)

        assert len(result["frames"]) == 1
        frame = result["frames"][0]
        assert "node_type" in frame
        assert "signature" in frame
        assert "def foo" in frame["signature"]

    def test_unmatched_file_returns_null_node_id(self, parser):
        """Test that frames from unknown files get node_id=None."""
        source = "def foo():\n    pass\n"
        store = parser.to_tree_store(source, "python", "app.py")

        tb = '  File "other.py", line 1, in bar\n'
        result = error_locate(store, tb)

        assert len(result["frames"]) == 1
        assert result["frames"][0]["node_id"] is None

    def test_no_frames_returns_error(self, python_store):
        """Test that a string with no parseable frames returns an error."""
        result = error_locate(python_store, "just some random text\nno stack trace here")

        assert "error" in result
        assert "no stack frames" in result["error"].lower()

    def test_in_memory_store_no_file_path(self, parser):
        """Test that an in-memory store (no file_path) still resolves."""
        source = "def lonely():\n    return 42\n"
        store = parser.to_tree_store(source, "python")  # no file_path

        tb = '  File "anything.py", line 1, in lonely\n'
        result = error_locate(store, tb)

        assert len(result["frames"]) == 1
        assert result["frames"][0]["node_id"] is not None

    def test_project_store_locates_correct_file(self, tmp_path):
        """Test error_locate with a ProjectStore matches the right file."""
        proj = tmp_path / "proj"
        proj.mkdir()
        (proj / "main.py").write_text("def main():\n    helper()\n")
        (proj / "utils.py").write_text("def helper():\n    raise RuntimeError('boom')\n")

        store = project_import(proj)

        tb = (
            "Traceback (most recent call last):\n"
            '  File "main.py", line 2, in main\n'
            "    helper()\n"
            '  File "utils.py", line 2, in helper\n'
            "    raise RuntimeError('boom')\n"
            "RuntimeError: boom\n"
        )
        result = error_locate(store, tb)

        assert "error" not in result
        assert len(result["frames"]) == 2

        # First frame should be in main.py
        assert "main" in result["frames"][0]["node_id"]
        # Second frame should be in utils.py
        assert "helper" in result["frames"][1]["node_id"]

    def test_line_outside_any_node(self, parser):
        """Test a line number beyond all nodes returns node_id=None."""
        source = "x = 1\n"
        store = parser.to_tree_store(source, "python", "test.py")

        tb = '  File "test.py", line 999, in foo\n'
        result = error_locate(store, tb)

        assert len(result["frames"]) == 1
        assert result["frames"][0]["node_id"] is None


class TestIntegration:
    """Integration tests combining multiple tools."""

    def test_navigate_and_inspect(self, python_store):
        """Test navigating with subtree then inspecting."""
        # First, get overview
        tree = subtree(python_store, "module", depth=1)
        assert "children" in tree

        # Find a class
        user = next(c for c in tree["children"] if "User" in c["node_id"])

        # Inspect it
        details = inspect(python_store, user["node_id"])
        assert "source" in details
        assert "class User" in details["source"]

    def test_search_and_ancestors(self, python_store):
        """Test searching then getting ancestors."""
        # Search for a method
        results = search(python_store, pattern="get_orders")
        assert len(results) >= 1

        # Get its ancestors
        method_id = results[0]["node_id"]
        path = ancestors(python_store, method_id)

        assert len(path) >= 2  # At least the method and its parent

    def test_workflow_explore_codebase(self, python_store):
        """Test typical exploration workflow."""
        # 1. Get overview
        overview = subtree(python_store, "module", depth=2)
        assert "children" in overview

        # 2. Search for specific functionality
        order_stuff = search(python_store, pattern="order")
        assert len(order_stuff) > 0

        # 3. Inspect a specific function
        for item in order_stuff:
            if "function" in item["type"] or "definition" in item["type"]:
                details = inspect(python_store, item["node_id"])
                assert "source" in details
                break

        # 4. Check what references it
        user_details = inspect(python_store, "module::User::get_orders")
        assert "references" in user_details


# =============================================================================
# Mutation Tool Tests
# =============================================================================


class TestValidateSnippet:
    """Tests for the validate_snippet tool."""

    def test_valid_python_function(self):
        """Test validating a valid Python function."""
        snippet = "def hello():\n    return 'world'"
        result = validate_snippet(snippet, "python")

        assert result["valid"] is True
        assert result["errors"] == []
        assert "function" in result["node_type"]

    def test_valid_python_class(self):
        """Test validating a valid Python class."""
        snippet = "class Foo:\n    pass"
        result = validate_snippet(snippet, "python")

        assert result["valid"] is True
        assert result["errors"] == []

    def test_invalid_python_syntax(self):
        """Test validating Python code with syntax errors."""
        snippet = "def broken(\n    return"  # Missing closing paren and colon
        result = validate_snippet(snippet, "python")

        assert result["valid"] is False
        assert len(result["errors"]) > 0

    def test_valid_javascript_function(self):
        """Test validating a valid JavaScript function."""
        snippet = "function greet() { return 'hello'; }"
        result = validate_snippet(snippet, "javascript")

        assert result["valid"] is True
        assert result["errors"] == []

    def test_invalid_javascript_syntax(self):
        """Test validating JavaScript with syntax errors."""
        snippet = "function broken { return"  # Missing parens and closing brace
        result = validate_snippet(snippet, "javascript")

        assert result["valid"] is False
        assert len(result["errors"]) > 0

    def test_unsupported_language(self):
        """Test validating with unsupported language."""
        result = validate_snippet("code", "unknown_lang")

        assert result["valid"] is False
        assert any("unsupported" in str(e).lower() for e in result["errors"])

    def test_empty_snippet(self):
        """Test validating empty snippet."""
        result = validate_snippet("", "python")

        # Empty is syntactically valid (empty module)
        assert result["valid"] is True


class TestInsert:
    """Tests for the insert tool."""

    def test_insert_at_end(self, python_store):
        """Test inserting a new method at the end of a class."""
        snippet = "def new_method(self):\n    return 42"
        result = insert(python_store, "module::User", "end", snippet)

        assert "error" not in result
        assert "node_id" in result
        assert "new_method" in result["node_id"]

        # Verify the node was added
        new_node = python_store.get_node(result["node_id"])
        assert new_node is not None

    def test_insert_at_start(self, python_store):
        """Test inserting a new method at the start of a class."""
        snippet = "def first_method(self):\n    pass"
        result = insert(python_store, "module::User", "start", snippet)

        assert "error" not in result
        assert "node_id" in result

    def test_insert_before_sibling(self, python_store):
        """Test inserting before a specific sibling."""
        snippet = "def before_orders(self):\n    pass"
        result = insert(python_store, "module::User", "before:get_orders", snippet)

        assert "error" not in result
        assert "node_id" in result

    def test_insert_after_sibling(self, python_store):
        """Test inserting after a specific sibling."""
        snippet = "def after_orders(self):\n    pass"
        result = insert(python_store, "module::User", "after:get_orders", snippet)

        assert "error" not in result
        assert "node_id" in result

    def test_insert_invalid_snippet(self, python_store):
        """Test inserting invalid code returns error."""
        snippet = "def broken(\n    return"
        result = insert(python_store, "module::User", "end", snippet)

        assert "error" in result
        assert "errors" in result

    def test_insert_parent_not_found(self, python_store):
        """Test inserting into non-existent parent."""
        snippet = "def method(): pass"
        result = insert(python_store, "module::NotExist", "end", snippet)

        assert "error" in result
        assert "not found" in result["error"].lower()

    def test_insert_invalid_position(self, python_store):
        """Test inserting with invalid position."""
        snippet = "def method(): pass"
        result = insert(python_store, "module::User", "middle", snippet)

        assert "error" in result
        assert "invalid position" in result["error"].lower()

    def test_insert_sibling_not_found(self, python_store):
        """Test inserting before/after non-existent sibling."""
        snippet = "def method(): pass"
        result = insert(python_store, "module::User", "before:nonexistent", snippet)

        assert "error" in result
        assert "not found" in result["error"].lower()

    def test_insert_updates_source(self, python_store):
        """Test that insert updates the store's source."""
        original_source = python_store.source
        snippet = "def added_method(self):\n    return 'added'"
        result = insert(python_store, "module::User", "end", snippet)

        assert "error" not in result
        assert "added_method" in python_store.source
        assert len(python_store.source) > len(original_source)


class TestUpdate:
    """Tests for the update tool."""

    def test_update_method(self, python_store):
        """Test updating a method's implementation."""
        new_snippet = "def get_orders(self, status=None, limit=10):\n    return []"
        result = update(python_store, "module::User::get_orders", new_snippet)

        assert result.get("success") is True
        assert result["version"] == 2  # Should increment from 1 to 2

    def test_update_increments_version(self, python_store):
        """Test that update increments version correctly."""
        node = python_store.get_node("module::User::get_orders")
        assert node is not None
        original_version = node.metadata.version

        new_snippet = "def get_orders(self):\n    return []"
        result = update(python_store, "module::User::get_orders", new_snippet)

        assert result["version"] == original_version + 1

    def test_update_records_history(self, python_store):
        """Test that update records version history."""
        # First update
        update(
            python_store,
            "module::User::get_orders",
            "def get_orders(self):\n    return []",
            prompt="simplify",
        )

        # Second update
        update(
            python_store,
            "module::User::get_orders",
            "def get_orders(self):\n    return ['order']",
            prompt="add data",
        )

        node = python_store.get_node("module::User::get_orders")
        assert node is not None
        assert node.metadata.version == 3
        assert len(node.metadata.history) == 2

    def test_update_with_prompt(self, python_store):
        """Test that update records prompt metadata."""
        update(
            python_store,
            "module::User::get_orders",
            "def get_orders(self):\n    return []",
            prompt="Simplify method",
            model="test-model",
        )

        node = python_store.get_node("module::User::get_orders")
        assert node is not None
        assert node.metadata.prompt == "Simplify method"
        assert node.metadata.model == "test-model"

    def test_update_invalid_snippet(self, python_store):
        """Test updating with invalid code returns error."""
        result = update(python_store, "module::User::get_orders", "def broken(")

        assert "error" in result

    def test_update_node_not_found(self, python_store):
        """Test updating non-existent node."""
        result = update(python_store, "module::NotExist", "def foo(): pass")

        assert "error" in result
        assert "not found" in result["error"].lower()

    def test_update_changes_source(self, python_store):
        """Test that update modifies the store's source."""
        new_code = "def get_orders(self):\n    # Updated implementation\n    return []"
        update(python_store, "module::User::get_orders", new_code)

        assert "# Updated implementation" in python_store.source


class TestDelete:
    """Tests for the delete tool."""

    def test_delete_method(self, python_store):
        """Test deleting a method."""
        result = delete(python_store, "module::User::cancel_order")

        assert result.get("success") is True

        # Verify node is gone
        node = python_store.get_node("module::User::cancel_order")
        assert node is None

    def test_delete_with_references_warns(self, python_store):
        """Test deleting a node that is referenced warns."""
        # get_orders is referenced by cancel_order and process_orders
        result = delete(python_store, "module::User::get_orders")

        assert result.get("success") is True
        # Should have warnings about references
        assert "warnings" in result
        assert len(result["warnings"]) > 0

    def test_delete_node_not_found(self, python_store):
        """Test deleting non-existent node."""
        result = delete(python_store, "module::NotExist")

        assert "error" in result
        assert "not found" in result["error"].lower()

    def test_delete_cannot_delete_root(self, python_store):
        """Test that root cannot be deleted."""
        result = delete(python_store, "module")

        assert "error" in result

    def test_delete_updates_source(self, python_store):
        """Test that delete removes code from source."""
        original_source = python_store.source
        assert "cancel_order" in original_source

        delete(python_store, "module::User::cancel_order")

        assert "def cancel_order" not in python_store.source

    def test_delete_class(self, python_store):
        """Test deleting an entire class."""
        result = delete(python_store, "module::Order")

        assert result.get("success") is True

        node = python_store.get_node("module::Order")
        assert node is None


class TestMove:
    """Tests for the move tool."""

    def test_move_method_within_class(self, parser, tmp_path):
        """Test moving a method to a different position in the same class."""
        source = (
            "class Foo:\n"
            "    def alpha(self):\n"
            "        pass\n"
            "    def beta(self):\n"
            "        pass\n"
            "    def gamma(self):\n"
            "        pass\n"
        )
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        # Move gamma before alpha
        result = move(store, "module::Foo::gamma", "module::Foo", "before:alpha")

        assert result.get("success") is True
        assert result["node_id"] == "module::Foo::gamma"
        # gamma should come before alpha in source
        assert store.source.index("def gamma") < store.source.index("def alpha")

    def test_move_function_into_class(self, parser, tmp_path):
        """Test moving a top-level function into a class."""
        source = (
            "class Foo:\n"
            "    def existing(self):\n"
            "        pass\n"
            "\n"
            "def standalone():\n"
            "    return 42\n"
        )
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = move(store, "module::standalone", "module::Foo", "end")

        assert result.get("success") is True
        assert "Foo::standalone" in result["node_id"]
        # Verify the node is now inside Foo
        node = store.get_node(result["node_id"])
        assert node is not None

    def test_move_method_to_module(self, parser, tmp_path):
        """Test moving a method out of a class to module level."""
        source = (
            "class Foo:\n    def bar(self):\n        pass\n    def baz(self):\n        return 1\n"
        )
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = move(store, "module::Foo::baz", "module", "end")

        assert result.get("success") is True
        assert result["node_id"] == "module::baz"
        # Verify it's at module level with no indentation
        for line in store.source.split("\n"):
            if line.strip().startswith("def baz"):
                assert not line.startswith(" "), f"Expected no indent: {line!r}"

    def test_move_reorder_siblings(self, parser, tmp_path):
        """Test reordering two sibling functions using after:."""
        source = "def first():\n    pass\n\ndef second():\n    pass\n\ndef third():\n    pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        # Move first after second
        result = move(store, "module::first", "module", "after:second")

        assert result.get("success") is True
        assert store.source.index("def second") < store.source.index("def first")

    def test_move_updates_source(self, parser, tmp_path):
        """Test that move correctly updates the store source."""
        source = (
            "class A:\n"
            "    def method_a(self):\n"
            "        return 'a'\n"
            "\n"
            "class B:\n"
            "    def method_b(self):\n"
            "        return 'b'\n"
        )
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = move(store, "module::A::method_a", "module::B", "end")

        assert result.get("success") is True
        # method_a should appear inside class B in the source
        b_pos = store.source.index("class B")
        method_a_pos = store.source.index("def method_a")
        assert method_a_pos > b_pos

    def test_move_updates_node_id(self, parser, tmp_path):
        """Test that the node ID changes to reflect new parent."""
        source = "class A:\n    def method(self):\n        pass\n\nclass B:\n    pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = move(store, "module::A::method", "module::B", "end")

        assert result.get("success") is True
        assert result["node_id"] == "module::B::method"
        # Old ID should not exist
        assert store.get_node("module::A::method") is None
        # New ID should exist
        assert store.get_node("module::B::method") is not None

    def test_move_preserves_metadata(self, parser, tmp_path):
        """Test that metadata (prompt, version) is preserved after move."""
        source = "class A:\n    def method(self):\n        pass\n\nclass B:\n    pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))

        # Set some metadata first via update
        update(store, "module::A::method", "def method(self):\n    return 1", prompt="test prompt")
        node = store.get_node("module::A::method")
        assert node is not None
        assert node.metadata.version == 2
        assert node.metadata.prompt == "test prompt"

        result = move(store, "module::A::method", "module::B", "end")
        assert result.get("success") is True

        moved_node = store.get_node(result["node_id"])
        assert moved_node is not None
        assert moved_node.metadata.version == 2
        assert moved_node.metadata.prompt == "test prompt"

    def test_move_not_found(self, python_store):
        """Test error when node doesn't exist."""
        result = move(python_store, "module::NonExistent", "module::User", "end")
        assert "error" in result
        assert "not found" in result["error"].lower()

    def test_move_dest_not_found(self, python_store):
        """Test error when destination parent doesn't exist."""
        result = move(python_store, "module::User::get_orders", "module::NonExistent", "end")
        assert "error" in result
        assert "not found" in result["error"].lower()

    def test_move_into_descendant(self, python_store):
        """Test error when trying to move a node into its own subtree."""
        result = move(python_store, "module::User", "module::User::get_orders", "end")
        assert "error" in result
        assert "subtree" in result["error"].lower()

    def test_move_root(self, python_store):
        """Test error when trying to move root."""
        result = move(python_store, "module", "module::User", "end")
        assert "error" in result
        assert "root" in result["error"].lower()

    def test_move_blank_line_between_nodes(self, parser, tmp_path):
        """Test that moved nodes get proper blank-line separators."""
        source = "def alpha():\n    pass\n\ndef beta():\n    pass\n\ndef gamma():\n    pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = move(store, "module::gamma", "module", "before:alpha")
        assert result.get("success") is True
        # There should be a blank line between gamma and alpha
        lines = store.source.split("\n")
        gamma_end = next(i for i, line in enumerate(lines) if "pass" in line)
        # The line after gamma's pass should be blank
        assert lines[gamma_end + 1].strip() == "", (
            f"Expected blank line after moved node, got: {lines[gamma_end + 1]!r}"
        )

    def test_move_no_extra_blank_lines_at_removal_site(self, parser):
        """Test that removal site doesn't leave extra blank lines."""
        source = "def first():\n    pass\n\ndef second():\n    pass\n\ndef third():\n    pass\n"
        store = parser.to_tree_store(source, "python")
        move(store, "module::first", "module", "after:second")
        # Should not have more than one blank line in a row
        assert "\n\n\n" not in store.source

    def test_move_no_leading_blank_line(self, parser, tmp_path):
        """Test that moving from BOF doesn't leave leading blank lines."""
        source = "def first():\n    pass\n\ndef second():\n    pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        move(store, "module::first", "module", "after:second")
        assert not store.source.startswith("\n"), (
            f"Source starts with blank line: {store.source[:30]!r}"
        )

    def test_move_method_out_of_class_preserves_source(self, parser, tmp_path):
        """Test that moving a method out of a class doesn't corrupt source."""
        source = (
            "class Foo:\n    def bar(self):\n        pass\n    def baz(self):\n        return 1\n"
        )
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = move(store, "module::Foo::baz", "module", "end")
        assert result.get("success") is True
        # bar should be intact
        assert "def bar(self):\n        pass" in store.source
        # baz should be at module level
        assert "def baz(self):\n    return 1" in store.source


class TestMutationIntegration:
    """Integration tests for mutation tools."""

    def test_insert_then_update(self, python_store):
        """Test inserting a node then updating it."""
        # Insert
        insert_result = insert(
            python_store, "module::User", "end", "def temp_method(self):\n    pass"
        )
        assert "node_id" in insert_result
        node_id = insert_result["node_id"]

        # Update
        update_result = update(
            python_store, node_id, "def temp_method(self):\n    return 'updated'"
        )
        assert update_result.get("success") is True

        # Verify
        node = python_store.get_node(node_id)
        assert node is not None
        assert "updated" in node.source

    def test_insert_then_delete(self, python_store):
        """Test inserting a node then deleting it."""
        # Insert
        insert_result = insert(
            python_store, "module::User", "end", "def ephemeral(self):\n    pass"
        )
        assert "node_id" in insert_result
        node_id = insert_result["node_id"]

        # Delete
        delete_result = delete(python_store, node_id)
        assert delete_result.get("success") is True

        # Verify gone
        node = python_store.get_node(node_id)
        assert node is None

    def test_multiple_updates_version_history(self, python_store):
        """Test that multiple updates build correct version history."""
        node_id = "module::User::get_orders"

        # Multiple updates
        for i in range(3):
            update(python_store, node_id, f"def get_orders(self):\n    return [{i}]")

        node = python_store.get_node(node_id)
        assert node is not None
        assert node.metadata.version == 4  # Original 1 + 3 updates
        assert len(node.metadata.history) == 3

    def test_workflow_add_feature(self, python_store):
        """Test typical workflow: add a new feature to a class."""
        # 1. Validate snippet first
        snippet = '''def get_active_orders(self) -> list:
    """Get orders that are not cancelled."""
    return [o for o in self.get_orders() if o.status != 'cancelled']'''

        validation = validate_snippet(snippet, "python")
        assert validation["valid"] is True

        # 2. Insert the new method
        result = insert(python_store, "module::User", "after:get_orders", snippet)
        assert "node_id" in result

        # 3. Verify it's accessible
        new_node = python_store.get_node(result["node_id"])
        assert new_node is not None
        assert "get_active_orders" in new_node.source


# =============================================================================
# Project Tool Tests
# =============================================================================


class TestProjectImport:
    """Tests for the project_import tool."""

    def test_import_python_file(self, tmp_path):
        """Test importing a Python file."""
        # Create a test file
        test_file = tmp_path / "example.py"
        test_file.write_text("def hello():\n    return 'world'\n\nclass Greeter:\n    pass\n")

        store = project_import(test_file)

        assert store is not None
        assert store.language == "python"
        assert store.root is not None
        assert len(store.all_nodes()) > 1

    def test_import_javascript_file(self, tmp_path):
        """Test importing a JavaScript file."""
        test_file = tmp_path / "example.js"
        test_file.write_text("function greet() { return 'hello'; }\n")

        store = project_import(test_file)

        assert store is not None
        assert store.language == "javascript"
        assert store.root is not None

    def test_import_preserves_source(self, tmp_path):
        """Test that import preserves the original source."""
        source = "def foo():\n    pass\n\ndef bar():\n    return 42\n"
        test_file = tmp_path / "test.py"
        test_file.write_text(source)

        store = project_import(test_file)

        assert store.source == source

    def test_import_creates_nodes(self, tmp_path):
        """Test that import creates correct tree nodes."""
        test_file = tmp_path / "test.py"
        test_file.write_text("class MyClass:\n    def method(self):\n        pass\n")

        store = project_import(test_file)

        # Should have module, class, and method nodes
        nodes = store.all_nodes()
        node_types = [n.node_type for n in nodes]
        assert "class_definition" in node_types
        assert "function_definition" in node_types

    def test_import_file_not_found(self):
        """Test importing a non-existent file raises error."""
        with pytest.raises(FileNotFoundError):
            project_import("/nonexistent/path/to/file.py")

    def test_import_sets_file_path(self, tmp_path):
        """Test that import sets the file_path on the store."""
        test_file = tmp_path / "test.py"
        test_file.write_text("x = 1\n")

        store = project_import(test_file)

        assert store.file_path == str(test_file)


class TestProjectSerialize:
    """Tests for the project_serialize tool."""

    def test_serialize_returns_source(self, python_store):
        """Test that serialize returns the store's source."""
        source = project_serialize(python_store)

        assert source == python_store.source
        assert "class User" in source
        assert "class Order" in source

    def test_serialize_to_file(self, python_store, tmp_path):
        """Test serializing to a file."""
        output_file = tmp_path / "output.py"

        source = project_serialize(python_store, output_file)

        assert output_file.exists()
        assert output_file.read_text() == source

    def test_serialize_creates_parent_dirs(self, python_store, tmp_path):
        """Test that serialize creates parent directories."""
        output_file = tmp_path / "subdir" / "nested" / "output.py"

        project_serialize(python_store, output_file)

        assert output_file.exists()

    def test_serialize_after_mutation(self, python_store, tmp_path):
        """Test serializing after modifying the tree."""
        # Make a modification
        update(
            python_store,
            "module::User::get_orders",
            "def get_orders(self):\n    # Modified\n    return []",
        )

        source = project_serialize(python_store)

        assert "# Modified" in source

    def test_serialize_roundtrip(self, tmp_path):
        """Test import -> serialize -> import roundtrip."""
        original_source = "def test():\n    return 42\n"
        original_file = tmp_path / "original.py"
        original_file.write_text(original_source)

        # Import
        store = project_import(original_file)

        # Serialize to new file
        output_file = tmp_path / "output.py"
        project_serialize(store, output_file)

        # Import the output
        store2 = project_import(output_file)

        # Should be equivalent
        assert store.source == store2.source
        assert len(store.all_nodes()) == len(store2.all_nodes())


class TestProjectIntegration:
    """Integration tests for project tools."""

    def test_import_modify_serialize(self, tmp_path):
        """Test full workflow: import, modify, serialize."""
        # Create original file
        original = tmp_path / "original.py"
        original.write_text("def greet(name):\n    return f'Hello, {name}'\n")

        # Import
        store = project_import(original)

        # Modify
        update(
            store,
            "module::greet",
            "def greet(name, greeting='Hello'):\n    return f'{greeting}, {name}'",
        )

        # Serialize
        output = tmp_path / "modified.py"
        source = project_serialize(store, output)

        # Verify
        assert "greeting='Hello'" in source
        assert output.read_text() == source

    def test_import_add_function_serialize(self, tmp_path):
        """Test adding a function then serializing."""
        # Create original file
        original = tmp_path / "utils.py"
        original.write_text("def existing():\n    pass\n")

        # Import
        store = project_import(original)

        # Add new function
        insert(store, "module", "end", "def new_function():\n    return 'added'")

        # Serialize
        source = project_serialize(store)

        assert "def existing" in source
        assert "def new_function" in source
        assert "added" in source


class TestAutoSync:
    """Tests that mutations auto-sync files to disk."""

    def test_insert_syncs_single_file(self, tmp_path):
        """Insert on a TreeStore with file_path writes the file."""
        original = tmp_path / "app.py"
        original.write_text("def existing():\n    pass\n")

        store = project_import(original)
        insert(store, "module", "end", "def added():\n    return 1")

        # File should already be updated — no project_serialize needed
        on_disk = original.read_text()
        assert "def added" in on_disk

    def test_update_syncs_single_file(self, tmp_path):
        """Update on a TreeStore with file_path writes the file."""
        original = tmp_path / "app.py"
        original.write_text("def greet():\n    pass\n")

        store = project_import(original)
        update(store, "module::greet", "def greet():\n    return 'hello'")

        on_disk = original.read_text()
        assert "return 'hello'" in on_disk

    def test_delete_syncs_single_file(self, tmp_path):
        """Delete on a TreeStore with file_path writes the file."""
        original = tmp_path / "app.py"
        original.write_text("def keep():\n    pass\n\ndef remove():\n    pass\n")

        store = project_import(original)
        delete(store, "module::remove")

        on_disk = original.read_text()
        assert "def keep" in on_disk
        assert "def remove" not in on_disk

    def test_insert_syncs_project_file(self, tmp_path):
        """Insert on a ProjectStore writes the affected file."""
        proj = tmp_path / "proj"
        proj.mkdir()
        (proj / "main.py").write_text("def main():\n    pass\n")

        store = project_import(proj)
        file_id = f"{proj.name}/main.py"
        insert(store, f"{file_id}::main", "end", "x = 1")

        on_disk = (proj / "main.py").read_text()
        assert "x = 1" in on_disk

    def test_update_syncs_project_file(self, tmp_path):
        """Update on a ProjectStore writes the affected file."""
        proj = tmp_path / "proj"
        proj.mkdir()
        (proj / "main.py").write_text("def main():\n    pass\n")

        store = project_import(proj)
        file_id = f"{proj.name}/main.py"
        update(store, f"{file_id}::main", "def main():\n    return 42")

        on_disk = (proj / "main.py").read_text()
        assert "return 42" in on_disk

    def test_no_sync_without_file_path(self, parser):
        """In-memory stores (no file_path) don't try to write."""
        store = parser.to_tree_store("def foo():\n    pass\n", "python")
        # file_path is None — should not raise
        insert(store, "module", "end", "def bar():\n    return 1")
        update(store, "module::foo", "def foo():\n    return 2")


class TestInsertIndentation:
    """Tests for insert indentation preservation (Bug 1 fix)."""

    def test_insert_start_preserves_sibling_indent(self, parser, tmp_path):
        """Insert at start of class body, verify siblings keep indentation."""
        source = "class Foo:\n    def bar(self):\n        pass\n    def baz(self):\n        pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = insert(store, "module::Foo", "start", "def first(self):\n    pass")
        assert "error" not in result
        # All method defs should be at 4-space indent
        for line in store.source.split("\n"):
            if line.strip().startswith("def "):
                assert line.startswith("    def "), f"Method lost indentation: {line!r}"

    def test_insert_before_preserves_sibling_indent(self, parser, tmp_path):
        """Insert before a specific sibling, verify other siblings keep indentation."""
        source = "class Foo:\n    def bar(self):\n        pass\n    def baz(self):\n        pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = insert(store, "module::Foo", "before:baz", "def middle(self):\n    pass")
        assert "error" not in result
        for line in store.source.split("\n"):
            if line.strip().startswith("def "):
                assert line.startswith("    def "), f"Method lost indentation: {line!r}"

    def test_sequential_inserts_preserve_indent(self, parser, tmp_path):
        """Two inserts in a row don't corrupt indentation."""
        source = "class Foo:\n    def bar(self):\n        pass\n    def baz(self):\n        pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        # First insert at start
        result1 = insert(store, "module::Foo", "start", "def first(self):\n    pass")
        assert "error" not in result1
        # Second insert at end
        result2 = insert(store, "module::Foo", "end", "def last(self):\n    pass")
        assert "error" not in result2
        for line in store.source.split("\n"):
            if line.strip().startswith("def "):
                assert line.startswith("    def "), f"Method lost indentation: {line!r}"
        assert "def first" in store.source
        assert "def last" in store.source

    def test_insert_end_preserves_indent(self, parser, tmp_path):
        """Insert at end, verify everything is correct."""
        source = "class Foo:\n    def bar(self):\n        pass\n    def baz(self):\n        pass\n"
        store = parser.to_tree_store(source, "python", str(tmp_path / "test.py"))
        result = insert(store, "module::Foo", "end", "def last(self):\n    pass")
        assert "error" not in result
        for line in store.source.split("\n"):
            if line.strip().startswith("def "):
                assert line.startswith("    def "), f"Method lost indentation: {line!r}"

    def test_insert_into_project_file_preserves_indent(self, tmp_path):
        """Same with ProjectStore + auto-sync disk check."""
        source = "class Foo:\n    def bar(self):\n        pass\n    def baz(self):\n        pass\n"
        original = tmp_path / "app.py"
        original.write_text(source)

        store = project_import(original)
        result = insert(store, "module::Foo", "start", "def first(self):\n    pass")
        assert "error" not in result
        on_disk = original.read_text()
        for line in on_disk.split("\n"):
            if line.strip().startswith("def "):
                assert line.startswith("    def "), f"Line lost indentation on disk: {line!r}"


class TestFormatter:
    """Tests for the post-serialization formatter hook."""

    def _write_formatter_script(self, directory, name, body):
        """Write a small Python formatter script and return its path."""
        script = directory / name
        script.write_text(body)
        return str(script)

    def _write_settings(self, directory, formatter_cmd, language="python"):
        """Write a carvesettings.toml with a formatter entry."""
        settings = directory / "carvesettings.toml"
        # Use TOML single-quoted literal string to avoid escape issues
        settings.write_text(f"[formatters]\n{language} = '{formatter_cmd}'\n")

    def _make_strip_ws_cmd(self, directory):
        path = self._write_formatter_script(
            directory,
            "_strip_ws.py",
            (
                "import sys\n"
                "p = sys.argv[1]\n"
                "lines = open(p).read().splitlines()\n"
                "open(p, 'w').write('\\n'.join(l.rstrip() for l in lines) + '\\n')\n"
            ),
        )
        return f"python3 {path} {{file}}"

    def _make_add_header_cmd(self, directory):
        path = self._write_formatter_script(
            directory,
            "_add_header.py",
            (
                "import sys\n"
                "p = sys.argv[1]\n"
                "t = open(p).read()\n"
                "open(p, 'w').write('# formatted\\n' + t)\n"
            ),
        )
        return f"python3 {path} {{file}}"

    def test_formatter_runs_after_insert(self, tmp_path):
        """Formatter modifies disk file and store source is resynced."""
        src = tmp_path / "app.py"
        src.write_text("def foo():   \n    pass   \n")
        cmd = self._make_strip_ws_cmd(tmp_path)
        self._write_settings(tmp_path, cmd)

        store = project_import(src)
        result = insert(store, "module", "end", "def bar():\n    return 1")
        assert "error" not in result

        # Formatter should have stripped trailing whitespace
        on_disk = src.read_text()
        for line in on_disk.splitlines():
            assert line == line.rstrip(), f"Trailing whitespace not stripped: {line!r}"

        # Store source should match disk
        assert store.source == on_disk

    def test_formatter_runs_after_update(self, tmp_path):
        """Formatter runs after update and store is resynced."""
        src = tmp_path / "app.py"
        src.write_text("def foo():   \n    pass   \n")
        cmd = self._make_strip_ws_cmd(tmp_path)
        self._write_settings(tmp_path, cmd)

        store = project_import(src)
        result = update(store, "module::foo", "def foo():\n    return 42   ")
        assert "error" not in result

        on_disk = src.read_text()
        for line in on_disk.splitlines():
            assert line == line.rstrip(), f"Trailing whitespace not stripped: {line!r}"
        assert store.source == on_disk

    def test_formatter_reparses_tree(self, tmp_path):
        """After formatting, tree byte offsets match the formatted source."""
        src = tmp_path / "app.py"
        src.write_text("def foo():\n    pass\n")
        cmd = self._make_add_header_cmd(tmp_path)
        self._write_settings(tmp_path, cmd)

        store = project_import(src)
        result = insert(store, "module", "end", "def bar():\n    return 1")
        assert "error" not in result

        # The formatter prepends '# formatted\n', so source should start with it
        assert store.source.startswith("# formatted\n")

        # Verify tree nodes have correct byte offsets against formatted source
        for node in store.all_nodes():
            if node.source and node.start_byte < len(store.source):
                expected = store.source[node.start_byte : node.end_byte]
                assert node.source == expected, (
                    f"Node {node.node_id} source mismatch: {node.source!r} != {expected!r}"
                )

    def test_no_formatter_configured(self, tmp_path):
        """Without carvesettings.toml, mutations work unchanged."""
        src = tmp_path / "app.py"
        src.write_text("def foo():   \n    pass   \n")

        store = project_import(src)
        result = insert(store, "module", "end", "def bar():\n    return 1")
        assert "error" not in result
        # Trailing whitespace should still be present (no formatter)
        assert "foo():   " in store.source

    def test_formatter_command_failure(self, tmp_path):
        """If formatter exits non-zero, source is unchanged (graceful)."""
        src = tmp_path / "app.py"
        src.write_text("def foo():\n    pass\n")
        self._write_settings(tmp_path, "false {file}")

        store = project_import(src)
        result = update(store, "module::foo", "def foo():\n    return 1")
        assert "error" not in result
        # Source should be updated by the mutation but NOT by the formatter
        assert "return 1" in store.source

    def test_formatter_with_project_store(self, tmp_path):
        """Formatter works with multi-file ProjectStore."""
        proj = tmp_path / "myproj"
        proj.mkdir()
        (proj / "a.py").write_text("def hello():   \n    pass   \n")
        (proj / "b.py").write_text("x = 1   \n")
        cmd = self._make_strip_ws_cmd(proj)
        self._write_settings(proj, cmd)

        store = project_import(proj)
        result = insert(store, "myproj/a.py", "end", "def world():\n    return 2")
        assert "error" not in result

        on_disk = (proj / "a.py").read_text()
        for line in on_disk.splitlines():
            assert line == line.rstrip(), f"Trailing ws not stripped: {line!r}"

    def test_formatter_in_project_serialize(self, tmp_path):
        """project_serialize runs formatter on written files."""
        src = tmp_path / "app.py"
        src.write_text("def foo():   \n    pass   \n")
        cmd = self._make_strip_ws_cmd(tmp_path)
        self._write_settings(tmp_path, cmd)

        store = project_import(src)
        out = tmp_path / "out.py"
        result = project_serialize(store, output_path=out)
        on_disk = out.read_text()
        for line in on_disk.splitlines():
            assert line == line.rstrip(), f"Trailing ws not stripped: {line!r}"
        # Return value should also reflect formatted source
        assert isinstance(result, str)
        assert result == on_disk
