"""QA tests for batch mutation edge cases.

Covers scenarios NOT already tested in test_batch.py:
- Batch with 3+ operations
- Batch insert at different positions
- Batch delete multiple siblings
- Batch undo then individual redo (should fail — batch is atomic)
- Batch on read-only / no-delete server
- Batch with one valid and one nonexistent node
- Batch disk state verification
- Batch with notes/metadata propagation
- Batch then single op then undo sequence
- Batch history entry format
"""

from __future__ import annotations

from pathlib import Path

import pytest

from carve.server import CarveServer
from carve.tools import batch, inspect, project_import, search

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

THREE_FUNCS = '''\
def alpha():
    """Alpha."""
    return 1


def beta():
    """Beta."""
    return 2


def gamma():
    """Gamma."""
    return 3
'''

CLASS_WITH_METHODS = '''\
class Widget:
    """A widget."""

    def render(self):
        """Render."""
        return "<div>"

    def update(self):
        """Update."""
        pass

    def delete(self):
        """Delete."""
        pass
'''


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


@pytest.fixture
def class_file(tmp_path: Path) -> Path:
    f = tmp_path / "widget.py"
    f.write_text(CLASS_WITH_METHODS)
    return f


@pytest.fixture
def three_func_server(three_func_file: Path) -> CarveServer:
    return CarveServer(file_path=three_func_file)


@pytest.fixture
def class_server(class_file: Path) -> CarveServer:
    return CarveServer(file_path=class_file)


# =============================================================================
# TestBatchThreeOps — more than 2 operations
# =============================================================================


class TestBatchThreeOps:
    """Batch with 3+ operations."""

    def test_batch_three_updates(self, three_func_file: Path) -> None:
        """Update all three functions in one batch."""
        store = project_import(str(three_func_file))

        ids = {}
        for name in ("alpha", "beta", "gamma"):
            results = search(store, pattern=name)
            ids[name] = [r for r in results if r["type"] == "function_definition"][0]["node_id"]

        result = batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["alpha"],
                        "snippet": "def alpha():\n    return 10",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["beta"],
                        "snippet": "def beta():\n    return 20",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["gamma"],
                        "snippet": "def gamma():\n    return 30",
                    },
                },
            ],
        )

        assert result["success"] is True
        assert len(result["results"]) == 3

        assert "return 10" in inspect(store, ids["alpha"])["source"]
        assert "return 20" in inspect(store, ids["beta"])["source"]
        assert "return 30" in inspect(store, ids["gamma"])["source"]

    def test_batch_three_updates_disk(self, three_func_file: Path) -> None:
        """All three updates reflected on disk."""
        store = project_import(str(three_func_file))

        ids = {}
        for name in ("alpha", "beta", "gamma"):
            results = search(store, pattern=name)
            ids[name] = [r for r in results if r["type"] == "function_definition"][0]["node_id"]

        batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["alpha"],
                        "snippet": "def alpha():\n    return 10",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["beta"],
                        "snippet": "def beta():\n    return 20",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["gamma"],
                        "snippet": "def gamma():\n    return 30",
                    },
                },
            ],
        )

        disk = three_func_file.read_text()
        assert "return 10" in disk
        assert "return 20" in disk
        assert "return 30" in disk


# =============================================================================
# TestBatchDeleteMultiple — delete multiple siblings
# =============================================================================


class TestBatchDeleteMultiple:
    """Delete multiple sibling nodes in one batch."""

    def test_delete_two_of_three(self, three_func_file: Path) -> None:
        """Delete beta and gamma, alpha survives."""
        store = project_import(str(three_func_file))

        beta_id = [r for r in search(store, pattern="beta") if r["type"] == "function_definition"][
            0
        ]["node_id"]
        gamma_id = [
            r for r in search(store, pattern="gamma") if r["type"] == "function_definition"
        ][0]["node_id"]

        result = batch(
            store,
            [
                {"tool": "delete", "kwargs": {"node_id": beta_id}},
                {"tool": "delete", "kwargs": {"node_id": gamma_id}},
            ],
        )

        assert result["success"] is True

        # Only alpha remains
        remaining = [r for r in search(store, pattern=".*") if r["type"] == "function_definition"]
        names = [r["node_id"].split("::")[-1] for r in remaining]
        assert "alpha" in names
        assert "beta" not in names
        assert "gamma" not in names


# =============================================================================
# TestBatchMixedInsertDelete — insert + delete in same batch
# =============================================================================


class TestBatchMixedInsertDelete:
    """Insert and delete in the same batch."""

    def test_insert_and_delete_different_nodes(
        self,
        three_func_file: Path,
    ) -> None:
        """Delete gamma, insert a replacement at the end."""
        store = project_import(str(three_func_file))

        gamma_id = [
            r for r in search(store, pattern="gamma") if r["type"] == "function_definition"
        ][0]["node_id"]
        root = search(store, node_type="module")[0]

        result = batch(
            store,
            [
                {"tool": "delete", "kwargs": {"node_id": gamma_id}},
                {
                    "tool": "insert",
                    "kwargs": {
                        "parent_id": root["node_id"],
                        "position": "end",
                        "snippet": "def delta():\n    return 4",
                    },
                },
            ],
        )

        assert result["success"] is True

        funcs = [r for r in search(store, pattern=".*") if r["type"] == "function_definition"]
        names = [r["node_id"].split("::")[-1] for r in funcs]
        assert "gamma" not in names
        assert "delta" in names
        assert "alpha" in names
        assert "beta" in names


# =============================================================================
# TestBatchValidationAllOrNothing — atomicity
# =============================================================================


class TestBatchValidationAllOrNothing:
    """One bad op rejects the whole batch, nothing changes."""

    def test_one_bad_node_id(self, three_func_file: Path) -> None:
        """One op targets nonexistent node — entire batch rejected."""
        store = project_import(str(three_func_file))

        alpha_id = [
            r for r in search(store, pattern="alpha") if r["type"] == "function_definition"
        ][0]["node_id"]
        old_source = inspect(store, alpha_id)["source"]

        result = batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": alpha_id,
                        "snippet": "def alpha():\n    return 999",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": "module::nonexistent",
                        "snippet": "def nope():\n    pass",
                    },
                },
            ],
        )

        assert result["success"] is False

        # Alpha should be unchanged
        assert inspect(store, alpha_id)["source"] == old_source

    def test_one_bad_syntax(self, three_func_file: Path) -> None:
        """One op has invalid syntax — entire batch rejected."""
        store = project_import(str(three_func_file))

        alpha_id = [
            r for r in search(store, pattern="alpha") if r["type"] == "function_definition"
        ][0]["node_id"]
        beta_id = [r for r in search(store, pattern="beta") if r["type"] == "function_definition"][
            0
        ]["node_id"]
        old_alpha = inspect(store, alpha_id)["source"]
        old_beta = inspect(store, beta_id)["source"]

        result = batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": alpha_id,
                        "snippet": "def alpha():\n    return 999",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": beta_id,
                        "snippet": "def beta( BROKEN SYNTAX",
                    },
                },
            ],
        )

        assert result["success"] is False

        # Both unchanged
        assert inspect(store, alpha_id)["source"] == old_alpha
        assert inspect(store, beta_id)["source"] == old_beta


# =============================================================================
# TestBatchUndoAtomic — batch undo is a single operation
# =============================================================================


class TestBatchUndoAtomic:
    """Batch undo/redo is atomic — one undo reverts the whole batch."""

    def test_batch_undo_is_single_step(
        self,
        three_func_server: CarveServer,
    ) -> None:
        """Batch of 3 ops takes exactly 1 undo to revert."""
        store = three_func_server._require_store()

        ids = {}
        for name in ("alpha", "beta", "gamma"):
            results = search(store, pattern=name)
            ids[name] = [r for r in results if r["type"] == "function_definition"][0]["node_id"]

        three_func_server.batch(
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["alpha"],
                        "snippet": "def alpha():\n    return 10",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["beta"],
                        "snippet": "def beta():\n    return 20",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": ids["gamma"],
                        "snippet": "def gamma():\n    return 30",
                    },
                },
            ]
        )

        # History should have exactly 1 entry
        h = three_func_server.history()
        assert len(h["undo"]) == 1
        assert "batch (3 ops)" in h["undo"][0]["description"]

        # One undo reverts all 3
        r = three_func_server.undo()
        assert r["success"] is True

        assert "return 1" in inspect(store, ids["alpha"])["source"]
        assert "return 2" in inspect(store, ids["beta"])["source"]
        assert "return 3" in inspect(store, ids["gamma"])["source"]

    def test_batch_then_single_op_then_undo_sequence(
        self,
        three_func_server: CarveServer,
    ) -> None:
        """Batch, then single update, then undo each — correct order."""
        store = three_func_server._require_store()

        alpha_id = [
            r for r in search(store, pattern="alpha") if r["type"] == "function_definition"
        ][0]["node_id"]
        beta_id = [r for r in search(store, pattern="beta") if r["type"] == "function_definition"][
            0
        ]["node_id"]

        # Batch update alpha + beta
        three_func_server.batch(
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": alpha_id,
                        "snippet": "def alpha():\n    return 10",
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": beta_id,
                        "snippet": "def beta():\n    return 20",
                    },
                },
            ]
        )

        # Single update on alpha
        three_func_server.update(alpha_id, "def alpha():\n    return 100")

        # Undo single update
        three_func_server.undo()
        assert "return 10" in inspect(store, alpha_id)["source"]

        # Undo batch
        three_func_server.undo()
        assert "return 1" in inspect(store, alpha_id)["source"]
        assert "return 2" in inspect(store, beta_id)["source"]


# =============================================================================
# TestBatchPermissions — read-only / no-delete
# =============================================================================


class TestBatchPermissions:
    """Batch respects server permission modes."""

    def test_batch_blocked_in_read_only(self, three_func_file: Path) -> None:
        """Batch rejected in read-only mode."""
        server = CarveServer(file_path=three_func_file, read_only=True)
        store = server._require_store()

        alpha_id = [
            r for r in search(store, pattern="alpha") if r["type"] == "function_definition"
        ][0]["node_id"]

        with pytest.raises(PermissionError):
            server.batch(
                [
                    {
                        "tool": "update",
                        "kwargs": {
                            "node_id": alpha_id,
                            "snippet": "def alpha():\n    return 0",
                        },
                    },
                ]
            )

    def test_batch_delete_blocked_by_no_delete(self, tmp_path: Path) -> None:
        """Batch delete of file/dir node blocked by --no-delete."""
        proj = tmp_path / "proj"
        proj.mkdir()
        (proj / "a.py").write_text("x = 1\n")

        server = CarveServer(file_path=proj, no_delete=True)
        store = server._require_store()

        files = search(store, node_type="file")
        file_id = files[0]["node_id"]

        with pytest.raises(PermissionError):
            server.batch(
                [
                    {"tool": "delete", "kwargs": {"node_id": file_id}},
                ]
            )


# =============================================================================
# TestBatchHistoryFormat
# =============================================================================


class TestBatchHistoryFormat:
    """Verify batch entries in history look correct."""

    def test_batch_history_entry(self, three_func_server: CarveServer) -> None:
        """History shows batch with op count and batch_restore operation."""
        store = three_func_server._require_store()

        alpha_id = [
            r for r in search(store, pattern="alpha") if r["type"] == "function_definition"
        ][0]["node_id"]

        three_func_server.batch(
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": alpha_id,
                        "snippet": "def alpha():\n    return 10",
                    },
                },
            ]
        )

        h = three_func_server.history()
        assert len(h["undo"]) == 1
        entry = h["undo"][0]
        assert "batch" in entry["description"]
        assert entry["operation"] == "batch_restore"


# =============================================================================
# TestBatchFailedDoesNotPushUndo
# =============================================================================


class TestBatchFailedDoesNotPushUndo:
    """Failed batch should not create an undo entry."""

    def test_failed_batch_no_undo(self, three_func_server: CarveServer) -> None:
        """Batch with bad syntax doesn't push undo."""
        store = three_func_server._require_store()

        alpha_id = [
            r for r in search(store, pattern="alpha") if r["type"] == "function_definition"
        ][0]["node_id"]

        three_func_server.batch(
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": alpha_id,
                        "snippet": "def alpha( BROKEN",
                    },
                },
            ]
        )

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


# =============================================================================
# TestBatchFileDeleteRejected
# =============================================================================


class TestBatchFileDeleteRejected:
    """Batch rejects file/directory node deletion."""

    def test_batch_cannot_delete_file_node(self, tmp_path: Path) -> None:
        """Deleting a file node via batch returns error."""
        proj = tmp_path / "proj"
        proj.mkdir()
        (proj / "a.py").write_text("x = 1\n")

        store = project_import(str(proj))
        files = search(store, node_type="file")
        file_id = files[0]["node_id"]

        result = batch(
            store,
            [
                {"tool": "delete", "kwargs": {"node_id": file_id}},
            ],
        )

        assert result["success"] is False
        assert "file/directory" in result["errors"][0]["error"].lower()
