"""QA tests for undo/redo edge cases and potential bugs.

Covers scenarios NOT already tested in test_undo.py:
- Multi-step undo/redo roundtrips
- Redo after undo-to-checkpoint (one at a time)
- Checkpoint in redo stack + duplicate name detection
- Checkpoint eviction via max_undo
- Undo past multiple checkpoints
- Insert+update on same node then undo both
- Delete position preservation (node returns to correct position)
- Disk state correctness after undo
- Sidecar suppression during undo/redo
- _undo_to_checkpoint error handling consistency
"""

from __future__ import annotations

import json
import tempfile
from pathlib import Path

import pytest

from carve.server import CarveServer

# =============================================================================
# Fixtures
# =============================================================================

SAMPLE_PYTHON = '''
class Calculator:
    """A simple calculator."""

    def add(self, a: int, b: int) -> int:
        """Add two numbers."""
        return a + b

    def subtract(self, a: int, b: int) -> int:
        """Subtract two numbers."""
        return a - b
'''

THREE_FUNCS = """\
def alpha():
    return 1


def beta():
    return 2


def gamma():
    return 3
"""


@pytest.fixture
def temp_python_file() -> Path:
    with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
        f.write(SAMPLE_PYTHON)
        return Path(f.name)


@pytest.fixture
def server(temp_python_file: Path) -> CarveServer:
    return CarveServer(file_path=temp_python_file)


@pytest.fixture
def three_func_server(tmp_path: Path) -> CarveServer:
    py = tmp_path / "funcs.py"
    py.write_text(THREE_FUNCS)
    return CarveServer(file_path=py)


@pytest.fixture
def project_server(tmp_path: Path) -> CarveServer:
    proj = tmp_path / "proj"
    proj.mkdir()
    (proj / "main.py").write_text("def main():\n    pass\n")
    (proj / "utils.py").write_text("def helper():\n    pass\n")
    return CarveServer(file_path=proj)


# =============================================================================
# TestMultiStepUndoRedo — full forward/backward cycles
# =============================================================================


class TestMultiStepUndoRedo:
    """Multiple undo then multiple redo in sequence."""

    def test_three_updates_undo_all_redo_all(self, server: CarveServer) -> None:
        """Make 3 updates, undo all 3, redo all 3."""
        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]

        snippets = [
            "def add(self, a: int, b: int) -> int:\n    return a + b + 1",
            "def add(self, a: int, b: int) -> int:\n    return a + b + 2",
            "def add(self, a: int, b: int) -> int:\n    return a + b + 3",
        ]
        for s in snippets:
            server.update(add_id, s)

        # Verify final state
        assert "a + b + 3" in server.inspect(add_id)["source"]

        # Undo all 3
        for _ in range(3):
            r = server.undo()
            assert r["success"] is True

        # Should be back to original
        src = server.inspect(add_id)["source"]
        assert "a + b + 1" not in src
        assert "a + b + 2" not in src
        assert "a + b + 3" not in src
        assert "return a + b" in src

        # Redo all 3
        for _ in range(3):
            r = server.redo()
            assert r["success"] is True

        # Should be back to final state
        assert "a + b + 3" in server.inspect(add_id)["source"]

    def test_interleaved_insert_update_delete_undo_all(
        self,
        server: CarveServer,
    ) -> None:
        """Insert, update, delete — undo all three in reverse."""
        calc = server.search(pattern="Calculator")[0]
        calc_id = calc["node_id"]

        # 1. Insert
        ins = server.insert(calc_id, "end", "def mul(self, a, b):\n    return a * b")
        assert "error" not in ins
        mul_id = ins["node_id"]

        # 2. Update the new method
        server.update(mul_id, "def mul(self, a, b):\n    return a * b * 1")

        # 3. Delete subtract
        sub_nodes = [
            n
            for n in server.search(pattern="subtract")
            if "subtract" in n["node_id"].split("::")[-1]
        ]
        sub_id = sub_nodes[0]["node_id"]
        server.delete(sub_id)

        # Undo delete (subtract comes back)
        r = server.undo()
        assert r["success"] is True
        found = server.search(pattern="subtract")
        assert any("subtract" in n["node_id"].split("::")[-1] for n in found)

        # Undo update (mul reverts to original)
        r = server.undo()
        assert r["success"] is True
        src = server.inspect(mul_id)["source"]
        assert "a * b * 1" not in src

        # Undo insert (mul is gone)
        r = server.undo()
        assert r["success"] is True
        found = server.search(pattern="mul")
        assert not any("mul" in n["node_id"].split("::")[-1] for n in found)


# =============================================================================
# TestRedoAfterCheckpointUndo — redo individual operations after undo-to
# =============================================================================


class TestRedoAfterCheckpointUndo:
    """After undo(to='cp'), redo each operation one at a time."""

    def test_redo_one_at_a_time_after_checkpoint_undo(
        self,
        server: CarveServer,
    ) -> None:
        """undo(to=cp) then redo 3 times restores all changes."""
        server.checkpoint("start")

        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]

        # 3 updates
        for i in range(1, 4):
            server.update(
                add_id,
                f"def add(self, a: int, b: int) -> int:\n    return a + b + {i}",
            )

        # Undo to checkpoint (undoes 3 updates)
        r = server.undo(to="start")
        assert r["success"] is True
        assert r["undone_count"] == 3

        # Verify original state (trailing whitespace may vary)
        src = server.inspect(add_id)["source"]
        assert "return a + b" in src
        assert "a + b + " not in src  # no modified version

        # Redo checkpoint first (no-op)
        r = server.redo()
        assert r["success"] is True
        assert "checkpoint" in r["description"]

        # Redo operations one at a time
        for i in range(1, 4):
            r = server.redo()
            assert r["success"] is True
            src = server.inspect(add_id)["source"]
            assert f"a + b + {i}" in src

        # No more to redo
        r = server.redo()
        assert "error" in r


# =============================================================================
# TestCheckpointEdgeCases
# =============================================================================


class TestCheckpointEdgeCases:
    """Edge cases for checkpoint behavior."""

    def test_duplicate_checkpoint_in_redo_stack(self, server: CarveServer) -> None:
        """Cannot create same-name checkpoint when original is in redo stack.

        Duplicate detection checks both _undo_entries and _redo_entries to
        prevent ambiguous checkpoint names.
        """
        server.checkpoint("v1")

        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]
        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return a + b + 1")

        # Undo to v1 — checkpoint moves to redo
        server.undo(to="v1")

        # Now v1 is in redo stack. Creating another v1 should be rejected.
        result = server.checkpoint("v1")
        assert "error" in result
        assert "already exists" in result["error"]

    def test_checkpoint_evicted_by_max_undo(self, server: CarveServer) -> None:
        """Checkpoint gets evicted when max_undo is exceeded."""
        server._max_undo = 3
        server.checkpoint("will_evict")

        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]

        # 3 more operations push the checkpoint out (stack was at 1, now 4 > 3)
        for i in range(3):
            server.update(add_id, f"def add(self, a, b):\n    return a + b + {i}")

        # Checkpoint should be evicted
        result = server.undo(to="will_evict")
        assert "error" in result
        assert "not found" in result["error"].lower()

    def test_undo_past_multiple_checkpoints(self, server: CarveServer) -> None:
        """Undo to an earlier checkpoint skips intermediate checkpoints."""
        server.checkpoint("cp1")

        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]

        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return a + b + 1")

        server.checkpoint("cp2")

        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return a + b + 2")

        server.checkpoint("cp3")

        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return a + b + 3")

        # Undo to cp1 — pops: update +3, cp3, update +2, cp2, update +1 (5 entries)
        r = server.undo(to="cp1")
        assert r["success"] is True
        # undone_count only counts actual operations (not checkpoint entries)
        # 3 actual operations above cp1: update+1, update+2, update+3
        assert r["undone_count"] == 3

        # Verify original state
        src = server.inspect(add_id)["source"]
        assert "return a + b" in src
        assert "a + b + " not in src

    def test_undo_to_checkpoint_at_bottom_of_stack(self, server: CarveServer) -> None:
        """Undo to checkpoint when it's the first entry with no operations after."""
        server.checkpoint("origin")

        # Undo to origin — 0 operations to undo, just move checkpoint to redo
        r = server.undo(to="origin")
        assert r["success"] is True
        assert r["undone_count"] == 0

    def test_undo_single_step_past_checkpoint(self, server: CarveServer) -> None:
        """Single undo when top of stack is a checkpoint."""
        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]
        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return a + b + 1")

        server.checkpoint("mid")

        # Undo — should pop the checkpoint (no actual operation)
        r = server.undo()
        assert r["success"] is True
        assert r["undone_count"] == 0
        assert "checkpoint" in r["description"]

        # The update should still be in effect
        src = server.inspect(add_id)["source"]
        assert "a + b + 1" in src

        # Undo again — now the update gets undone
        r = server.undo()
        assert r["success"] is True
        assert r["undone_count"] == 1


# =============================================================================
# TestDeletePositionPreservation
# =============================================================================


class TestDeletePositionPreservation:
    """Verify that undo-of-delete puts the node back at the right position."""

    def test_delete_middle_function_undo_position(
        self,
        three_func_server: CarveServer,
    ) -> None:
        """Delete the middle function, undo, verify it's back in the middle."""
        server = three_func_server

        # Find beta (middle function)
        beta_nodes = [
            n for n in server.search(pattern="beta") if n.get("type") == "function_definition"
        ]
        beta_id = beta_nodes[0]["node_id"]

        # Delete beta
        server.delete(beta_id)

        # Verify beta is gone
        found = server.search(pattern="beta")
        assert not any(n.get("type") == "function_definition" for n in found)

        # Undo
        r = server.undo()
        assert r["success"] is True

        # Verify beta is back — and in the right position
        # Get module children to verify order
        root = server.search(node_type="module")
        root_id = root[0]["node_id"]
        tree = server.subtree(root_id, depth=1)

        # Children should be: alpha, beta, gamma (in order)
        child_names = [c["node_id"].split("::")[-1] for c in tree.get("children", [])]
        func_names = [n for n in child_names if n in ("alpha", "beta", "gamma")]
        assert func_names == ["alpha", "beta", "gamma"], f"Got order: {func_names}"

    def test_delete_first_function_undo_position(
        self,
        three_func_server: CarveServer,
    ) -> None:
        """Delete the first function, undo, verify it's back at the start."""
        server = three_func_server

        alpha_nodes = [
            n for n in server.search(pattern="alpha") if n.get("type") == "function_definition"
        ]
        alpha_id = alpha_nodes[0]["node_id"]

        server.delete(alpha_id)
        server.undo()

        root = server.search(node_type="module")
        root_id = root[0]["node_id"]
        tree = server.subtree(root_id, depth=1)
        child_names = [c["node_id"].split("::")[-1] for c in tree.get("children", [])]
        func_names = [n for n in child_names if n in ("alpha", "beta", "gamma")]
        assert func_names[0] == "alpha"


# =============================================================================
# TestDiskStateAfterUndo — verify file on disk matches
# =============================================================================


class TestDiskStateAfterUndo:
    """Verify that undo/redo properly updates the file on disk."""

    def test_disk_matches_after_undo_update(self, temp_python_file: Path) -> None:
        """After undo of update, file on disk should have original content."""
        server = CarveServer(file_path=temp_python_file)

        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]

        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return a + b + 999")

        # Disk should have the update
        updated_disk = temp_python_file.read_text()
        assert "999" in updated_disk

        # Undo
        server.undo()

        # Disk should revert
        reverted_disk = temp_python_file.read_text()
        assert "999" not in reverted_disk
        # Should match original (modulo formatting)
        assert "return a + b" in reverted_disk

    def test_disk_matches_after_undo_insert(self, temp_python_file: Path) -> None:
        """After undo of insert, file on disk should not have the new function."""
        server = CarveServer(file_path=temp_python_file)
        calc = server.search(pattern="Calculator")[0]

        server.insert(calc["node_id"], "end", "def newmethod(self):\n    pass")
        assert "newmethod" in temp_python_file.read_text()

        server.undo()
        assert "newmethod" not in temp_python_file.read_text()

    def test_disk_matches_after_redo(self, temp_python_file: Path) -> None:
        """After redo, file on disk should have the re-applied change."""
        server = CarveServer(file_path=temp_python_file)

        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]

        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return 42")
        server.undo()
        assert "return 42" not in temp_python_file.read_text()

        server.redo()
        assert "return 42" in temp_python_file.read_text()


# =============================================================================
# TestSidecarSuppression — no .crv writes during undo/redo
# =============================================================================


class TestSidecarSuppression:
    """Verify that undo/redo does not create cascading sidecar writes."""

    def test_no_sidecar_during_undo(self, tmp_path: Path) -> None:
        """Undo should not write to the sidecar file."""
        py_file = tmp_path / "test_sidecar.py"
        py_file.write_text("def foo():\n    pass\n")
        crv_file = tmp_path / "test_sidecar.py.crv"

        server = CarveServer(file_path=py_file)

        foo_nodes = [
            n for n in server.search(pattern="foo") if n.get("type") == "function_definition"
        ]
        foo_id = foo_nodes[0]["node_id"]

        # Update creates a sidecar entry
        server.update(foo_id, "def foo():\n    return 1\n")

        # Count sidecar entries before undo
        entries_before = json.loads(crv_file.read_text()) if crv_file.exists() else []
        count_before = len(entries_before)

        # Undo — should NOT add sidecar entries
        server.undo()

        entries_after = json.loads(crv_file.read_text()) if crv_file.exists() else []
        count_after = len(entries_after)

        assert count_after == count_before, (
            f"Sidecar entries grew from {count_before} to {count_after} during undo"
        )

    def test_no_sidecar_during_redo(self, tmp_path: Path) -> None:
        """Redo should not write to the sidecar file."""
        py_file = tmp_path / "test_sidecar.py"
        py_file.write_text("def foo():\n    pass\n")
        crv_file = tmp_path / "test_sidecar.py.crv"

        server = CarveServer(file_path=py_file)

        foo_nodes = [
            n for n in server.search(pattern="foo") if n.get("type") == "function_definition"
        ]
        foo_id = foo_nodes[0]["node_id"]

        server.update(foo_id, "def foo():\n    return 1\n")
        server.undo()

        entries_before = json.loads(crv_file.read_text()) if crv_file.exists() else []
        count_before = len(entries_before)

        # Redo — should NOT add sidecar entries
        server.redo()

        entries_after = json.loads(crv_file.read_text()) if crv_file.exists() else []
        count_after = len(entries_after)

        assert count_after == count_before, (
            f"Sidecar entries grew from {count_before} to {count_after} during redo"
        )


# =============================================================================
# TestUndoToCheckpointErrorHandling
# =============================================================================


class TestUndoToCheckpointErrorHandling:
    """Edge cases in _undo_to_checkpoint error handling."""

    def test_undo_to_checkpoint_with_empty_undo_stack(
        self,
        server: CarveServer,
    ) -> None:
        """undo(to='name') when stack is empty returns 'Nothing to undo'."""
        result = server.undo(to="nonexistent")
        assert "error" in result

    def test_checkpoint_not_found_after_mutations(
        self,
        server: CarveServer,
    ) -> None:
        """undo(to='name') for non-existent checkpoint with mutations on stack."""
        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]
        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return 1")

        result = server.undo(to="no_such_checkpoint")
        assert "error" in result
        assert "not found" in result["error"].lower()

        # The mutation should still be undoable (stack not corrupted)
        r = server.undo()
        assert r["success"] is True


# =============================================================================
# TestHistoryDetails
# =============================================================================


class TestHistoryDetails:
    """Detailed history output verification."""

    def test_history_operation_field(self, server: CarveServer) -> None:
        """History entries have correct 'operation' field."""
        calc = server.search(pattern="Calculator")[0]

        # Insert
        server.insert(calc["node_id"], "end", "def mul(self, a, b):\n    return a * b")

        # Update
        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]
        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return 1")

        history = server.history()
        assert len(history["undo"]) == 2

        # Insert's undo_op is "delete", update's undo_op is "update"
        ops = [e["operation"] for e in history["undo"]]
        assert "delete" in ops  # undo of insert = delete
        assert "update" in ops  # undo of update = update

    def test_history_after_undo_redo_cycle(self, server: CarveServer) -> None:
        """History correctly reflects undo/redo state."""
        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]

        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return 1")
        server.update(add_id, "def add(self, a: int, b: int) -> int:\n    return 2")

        # 2 items on undo stack
        h = server.history()
        assert len(h["undo"]) == 2
        assert len(h["redo"]) == 0

        # Undo one
        server.undo()
        h = server.history()
        assert len(h["undo"]) == 1
        assert len(h["redo"]) == 1

        # Redo it back
        server.redo()
        h = server.history()
        assert len(h["undo"]) == 2
        assert len(h["redo"]) == 0


# =============================================================================
# TestUndoRedoFileCreation — multi-file undo/redo of file creation
# =============================================================================


class TestUndoRedoFileCreation:
    """Undo/redo cycle for file creation in project stores."""

    def test_redo_file_creation(self, project_server: CarveServer) -> None:
        """Undo file creation, then redo restores the file."""
        dirs = project_server.search(node_type="directory")
        dir_id = dirs[0]["node_id"]

        result = project_server.insert(
            dir_id,
            "end",
            "x = 1\n",
            file_name="extra.py",
        )
        assert "error" not in result

        # Undo — file gone
        project_server.undo()
        files = project_server.search(node_type="file")
        assert not any("extra" in f["node_id"] for f in files)

        # Redo — file back
        redo_result = project_server.redo()
        assert redo_result["success"] is True
        files = project_server.search(node_type="file")
        assert any("extra" in f["node_id"] for f in files)


# =============================================================================
# TestInsertUpdateSameNodeUndoBoth
# =============================================================================


class TestInsertUpdateSameNodeUndoBoth:
    """Insert a node, update it, then undo both."""

    def test_insert_then_update_then_undo_both(self, server: CarveServer) -> None:
        """Insert + update same node, undo update then undo insert."""
        calc = server.search(pattern="Calculator")[0]

        ins = server.insert(
            calc["node_id"],
            "end",
            "def divide(self, a, b):\n    return a / b",
        )
        assert "error" not in ins
        div_id = ins["node_id"]

        # Update the new method
        server.update(
            div_id,
            "def divide(self, a, b):\n    if b == 0: raise ZeroDivisionError\n    return a / b",
        )
        assert "ZeroDivisionError" in server.inspect(div_id)["source"]

        # Undo update — method reverts to original insert
        r = server.undo()
        assert r["success"] is True
        src = server.inspect(div_id)["source"]
        assert "ZeroDivisionError" not in src
        assert "return a / b" in src

        # Undo insert — method is gone
        r = server.undo()
        assert r["success"] is True
        found = server.search(pattern="divide")
        assert not any("divide" in n["node_id"].split("::")[-1] for n in found)

    def test_insert_update_undo_all_redo_all(self, server: CarveServer) -> None:
        """Full roundtrip: insert, update, undo both, redo both."""
        calc = server.search(pattern="Calculator")[0]

        ins = server.insert(
            calc["node_id"],
            "end",
            "def power(self, a, b):\n    return a ** b",
        )
        assert "error" not in ins
        pow_id = ins["node_id"]

        server.update(pow_id, "def power(self, base, exp):\n    return base ** exp")

        # Undo both
        server.undo()  # undo update
        server.undo()  # undo insert

        found = server.search(pattern="power")
        assert not any("power" in n["node_id"].split("::")[-1] for n in found)

        # Redo insert
        r = server.redo()
        assert r["success"] is True
        found = server.search(pattern="power")
        assert any("power" in n["node_id"].split("::")[-1] for n in found)
        # Should have original insert source (not updated)
        pow_node = [n for n in found if "power" in n["node_id"].split("::")[-1]][0]
        src = server.inspect(pow_node["node_id"])["source"]
        assert "a ** b" in src

        # Redo update
        r = server.redo()
        assert r["success"] is True
        src = server.inspect(pow_node["node_id"])["source"]
        assert "base ** exp" in src


# =============================================================================
# TestReadOnlyBlocksCheckpoint
# =============================================================================


class TestReadOnlyMode:
    """Read-only mode blocks mutation-related undo operations."""

    def test_read_only_blocks_checkpoint(self, temp_python_file: Path) -> None:
        """Checkpoint creation should work in read-only mode (it's not a mutation).

        OR it should be blocked — document the actual behavior.
        """
        ro_server = CarveServer(file_path=temp_python_file, read_only=True)
        # Checkpoint doesn't call _check_writable, so it should succeed
        # unless the implementation blocks it
        result = ro_server.checkpoint("test")
        # Either works or raises — document the behavior
        if isinstance(result, dict) and "error" not in result:
            assert result["success"] is True
        # If it raised PermissionError, the test framework will catch it


# =============================================================================
# TestUndoWithNoStore
# =============================================================================


class TestUndoWithNoStore:
    """Undo/redo when no store is loaded."""

    def test_undo_without_store(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Undo when no file is loaded."""
        monkeypatch.chdir(tmp_path)
        monkeypatch.delenv("CARVE_WORKSPACE", raising=False)
        server = CarveServer()
        result = server.undo()
        assert "error" in result

    def test_redo_without_store(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        """Redo when no file is loaded."""
        monkeypatch.chdir(tmp_path)
        monkeypatch.delenv("CARVE_WORKSPACE", raising=False)
        server = CarveServer()
        result = server.redo()
        assert "error" in result


# =============================================================================
# TestUndoRedoMoveSameFile
# =============================================================================


class TestUndoRedoMoveSameFile:
    """Undo/redo for within-file moves."""

    def test_undo_redo_same_file_move(self, three_func_server: CarveServer) -> None:
        """Move gamma before alpha, undo, redo."""
        server = three_func_server

        gamma_nodes = [
            n for n in server.search(pattern="gamma") if n.get("type") == "function_definition"
        ]
        gamma_id = gamma_nodes[0]["node_id"]

        root = server.search(node_type="module")
        root_id = root[0]["node_id"]

        # Move gamma to start
        move_result = server.move(gamma_id, root_id, "start")
        assert "error" not in move_result

        # Verify gamma is first
        tree = server.subtree(root_id, depth=1)
        child_names = [c["node_id"].split("::")[-1] for c in tree.get("children", [])]
        func_names = [n for n in child_names if n in ("alpha", "beta", "gamma")]
        assert func_names[0] == "gamma"

        # Undo — gamma should be back at end
        r = server.undo()
        assert r["success"] is True

        tree = server.subtree(root_id, depth=1)
        child_names = [c["node_id"].split("::")[-1] for c in tree.get("children", [])]
        func_names = [n for n in child_names if n in ("alpha", "beta", "gamma")]
        assert func_names == ["alpha", "beta", "gamma"]

        # Redo — gamma should be at start again
        r = server.redo()
        assert r["success"] is True

        tree = server.subtree(root_id, depth=1)
        child_names = [c["node_id"].split("::")[-1] for c in tree.get("children", [])]
        func_names = [n for n in child_names if n in ("alpha", "beta", "gamma")]
        assert func_names[0] == "gamma"


# =============================================================================
# TestUndoStackIntegrity — verify stack isn't corrupted by normal operations
# =============================================================================


class TestUndoStackIntegrity:
    """Verify undo/redo stacks stay consistent through various operations."""

    def test_navigation_does_not_affect_undo_stack(
        self,
        server: CarveServer,
    ) -> None:
        """Read-only operations (search, inspect, subtree, ancestors) don't push undo."""
        add_nodes = [
            n for n in server.search(pattern="add") if "add" in n["node_id"].split("::")[-1]
        ]
        add_id = add_nodes[0]["node_id"]

        server.inspect(add_id)
        server.subtree(add_id, depth=1)
        server.ancestors(add_id)
        server.search(pattern="Calculator")

        h = server.history()
        assert len(h["undo"]) == 0
        assert len(h["redo"]) == 0

    def test_failed_mutation_does_not_push_undo(self, server: CarveServer) -> None:
        """A failed mutation should not add to the undo stack."""
        # Try to update a non-existent node
        result = server.update("nonexistent::node", "def foo():\n    pass")
        assert "error" in result

        h = server.history()
        assert len(h["undo"]) == 0

    def test_failed_insert_does_not_push_undo(self, server: CarveServer) -> None:
        """A failed insert should not add to the undo stack."""
        result = server.insert("nonexistent::parent", "end", "def foo():\n    pass")
        assert "error" in result

        h = server.history()
        assert len(h["undo"]) == 0
