"""Tests for batch mutation functionality."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

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

# =============================================================================
# Sample code 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
'''

TWO_FUNCTIONS = '''
def greet(name: str) -> str:
    """Greet someone."""
    return f"Hello, {name}!"


def farewell(name: str) -> str:
    """Say goodbye."""
    return f"Goodbye, {name}!"
'''

ADD_SNIPPET = 'def add(self, a: int, b: int) -> int:\n    """Add numbers."""\n    return a + b + 0'

SUB_SNIPPET = (
    'def subtract(self, a: int, b: int) -> int:\n    """Sub numbers."""\n    return a - b - 0'
)

ADD_V2 = 'def add(self, a: int, b: int) -> int:\n    """Add v2."""\n    return a + b + 0'

SUB_V2 = 'def subtract(self, a: int, b: int) -> int:\n    """Sub v2."""\n    return a - b - 0'

ADD_V3 = 'def add(self, a: int, b: int) -> int:\n    """Add v3."""\n    return a + b + 0'

SUB_V3 = 'def subtract(self, a: int, b: int) -> int:\n    """Sub v3."""\n    return a - b - 0'

GREET_WARM = 'def greet(name: str) -> str:\n    """Greet warmly."""\n    return f"Hi, {name}!"'

GREET_HI = 'def greet(name: str) -> str:\n    """Hi."""\n    return f"Hi, {name}!"'


@pytest.fixture
def temp_python_file(tmp_path: Path) -> Path:
    f = tmp_path / "calc.py"
    f.write_text(SAMPLE_PYTHON)
    return f


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


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


# =============================================================================
# TestBatch — tool-level tests
# =============================================================================


class TestBatch:
    """Tests for the batch() tool function."""

    def test_batch_two_updates(self, temp_python_file: Path) -> None:
        """Update two functions in one batch, verify both changed."""
        store = project_import(str(temp_python_file))

        add_results = search(store, pattern="add")
        sub_results = search(store, pattern="subtract")
        add_id = [r for r in add_results if r["type"] == "function_definition"][0]["node_id"]
        sub_id = [r for r in sub_results if r["type"] == "function_definition"][0]["node_id"]

        result = batch(
            store,
            [
                {"tool": "update", "kwargs": {"node_id": add_id, "snippet": ADD_SNIPPET}},
                {"tool": "update", "kwargs": {"node_id": sub_id, "snippet": SUB_SNIPPET}},
            ],
        )

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

        assert "Add numbers" in inspect(store, add_id)["source"]
        assert "Sub numbers" in inspect(store, sub_id)["source"]

    def test_batch_insert_and_delete(self, two_func_file: Path) -> None:
        """Insert one node and delete another in one batch."""
        store = project_import(str(two_func_file))

        farewell_results = search(store, pattern="farewell")
        farewell_id = [r for r in farewell_results if r["type"] == "function_definition"][0][
            "node_id"
        ]

        root_results = search(store, pattern="module")
        parent_id = root_results[0]["node_id"]

        result = batch(
            store,
            [
                {
                    "tool": "insert",
                    "kwargs": {
                        "parent_id": parent_id,
                        "position": "end",
                        "snippet": ('def helper() -> None:\n    """A helper."""\n    pass'),
                    },
                },
                {"tool": "delete", "kwargs": {"node_id": farewell_id}},
            ],
        )

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

        # Helper was inserted
        helper_results = search(store, pattern="helper")
        assert any(r["type"] == "function_definition" for r in helper_results)

        # Farewell was deleted
        farewell_after = search(store, pattern="farewell")
        assert not any(r["type"] == "function_definition" for r in farewell_after)

        # Greet still exists
        greet_results = search(store, pattern="greet")
        assert any(r["type"] == "function_definition" for r in greet_results)

    def test_batch_insert_and_update(self, two_func_file: Path) -> None:
        """Insert and update in the same file."""
        store = project_import(str(two_func_file))

        greet_results = search(store, pattern="greet")
        greet_id = [r for r in greet_results if r["type"] == "function_definition"][0]["node_id"]
        root_results = search(store, pattern="module")
        parent_id = root_results[0]["node_id"]

        result = batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {"node_id": greet_id, "snippet": GREET_WARM},
                },
                {
                    "tool": "insert",
                    "kwargs": {
                        "parent_id": parent_id,
                        "position": "end",
                        "snippet": ('def extra() -> None:\n    """Extra function."""\n    pass'),
                    },
                },
            ],
        )

        assert result["success"] is True

        greet_info = inspect(store, greet_id)
        assert "Greet warmly" in greet_info["source"]

        extra_results = search(store, pattern="extra")
        assert any(r["type"] == "function_definition" for r in extra_results)

    def test_batch_validation_rejects_all(self, temp_python_file: Path) -> None:
        """One bad snippet rejects entire batch, tree unchanged."""
        store = project_import(str(temp_python_file))

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

        sub_results = search(store, pattern="subtract")
        sub_id = [r for r in sub_results if r["type"] == "function_definition"][0]["node_id"]

        result = batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": add_id,
                        "snippet": ("def add(self, a: int, b: int) -> int:\n    return a + b + 0"),
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": sub_id,
                        "snippet": "def subtract(self, a INVALID SYNTAX",
                    },
                },
            ],
        )

        assert result["success"] is False
        assert "errors" in result

        # Original tree is unchanged
        add_info = inspect(store, add_id)
        assert add_info["source"] == old_source

    def test_batch_overlapping_rejected(self, temp_python_file: Path) -> None:
        """Two ops on the same node are rejected."""
        store = project_import(str(temp_python_file))

        add_results = search(store, pattern="add")
        add_id = [r for r in add_results if r["type"] == "function_definition"][0]["node_id"]

        result = batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": add_id,
                        "snippet": ("def add(self, a: int, b: int) -> int:\n    return a + b + 0"),
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": add_id,
                        "snippet": ("def add(self, a: int, b: int) -> int:\n    return a + b + 1"),
                    },
                },
            ],
        )

        assert result["success"] is False
        assert "errors" in result

    def test_batch_empty_list(self, temp_python_file: Path) -> None:
        """Empty operations list succeeds with empty results."""
        store = project_import(str(temp_python_file))

        result = batch(store, [])

        assert result["success"] is True
        assert result["results"] == []

    def test_batch_unsupported_tool(self, temp_python_file: Path) -> None:
        """Move in batch returns error."""
        store = project_import(str(temp_python_file))

        result = batch(
            store,
            [
                {
                    "tool": "move",
                    "kwargs": {
                        "node_id": "whatever",
                        "dest_parent_id": "other",
                        "position": "end",
                    },
                },
            ],
        )

        assert result["success"] is False
        assert "errors" in result
        assert "Unsupported" in result["errors"][0]["error"]


# =============================================================================
# TestBatchServer — server-level tests (undo/redo)
# =============================================================================


class TestBatchServer:
    """Tests for CarveServer.batch() including undo/redo."""

    def test_batch_undo(self, server: CarveServer) -> None:
        """Batch then undo reverts all changes."""
        store = server._require_store()

        add_results = search(store, pattern="add")
        add_id = [r for r in add_results if r["type"] == "function_definition"][0]["node_id"]
        sub_results = search(store, pattern="subtract")
        sub_id = [r for r in sub_results if r["type"] == "function_definition"][0]["node_id"]

        old_add_source = inspect(store, add_id)["source"]
        old_sub_source = inspect(store, sub_id)["source"]

        result = server.batch(
            [
                {"tool": "update", "kwargs": {"node_id": add_id, "snippet": ADD_V2}},
                {"tool": "update", "kwargs": {"node_id": sub_id, "snippet": SUB_V2}},
            ]
        )
        assert result["success"] is True

        assert "Add v2" in inspect(store, add_id)["source"]
        assert "Sub v2" in inspect(store, sub_id)["source"]

        undo_result = server.undo()
        assert undo_result.get("success") is True

        assert inspect(store, add_id)["source"] == old_add_source
        assert inspect(store, sub_id)["source"] == old_sub_source

    def test_batch_redo(self, server: CarveServer) -> None:
        """Undo then redo reapplies all changes."""
        store = server._require_store()

        add_results = search(store, pattern="add")
        add_id = [r for r in add_results if r["type"] == "function_definition"][0]["node_id"]
        sub_results = search(store, pattern="subtract")
        sub_id = [r for r in sub_results if r["type"] == "function_definition"][0]["node_id"]

        result = server.batch(
            [
                {"tool": "update", "kwargs": {"node_id": add_id, "snippet": ADD_V3}},
                {"tool": "update", "kwargs": {"node_id": sub_id, "snippet": SUB_V3}},
            ]
        )
        assert result["success"] is True

        server.undo()
        redo_result = server.redo()
        assert redo_result.get("success") is True

        assert "Add v3" in inspect(store, add_id)["source"]
        assert "Sub v3" in inspect(store, sub_id)["source"]


# =============================================================================
# TestBatchCrossFile — multi-file tests
# =============================================================================


class TestBatchCrossFile:
    """Tests for batch mutations across multiple files."""

    def test_batch_cross_file(self, tmp_path: Path) -> None:
        """Ops on two files in a ProjectStore."""
        (tmp_path / "a.py").write_text('def alpha() -> str:\n    """Alpha."""\n    return "a"\n')
        (tmp_path / "b.py").write_text('def beta() -> str:\n    """Beta."""\n    return "b"\n')

        store = project_import(str(tmp_path))

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

        result = batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": alpha_id,
                        "snippet": ('def alpha() -> str:\n    """Alpha v2."""\n    return "a2"'),
                    },
                },
                {
                    "tool": "update",
                    "kwargs": {
                        "node_id": beta_id,
                        "snippet": ('def beta() -> str:\n    """Beta v2."""\n    return "b2"'),
                    },
                },
            ],
        )

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

        assert "Alpha v2" in inspect(store, alpha_id)["source"]
        assert "Beta v2" in inspect(store, beta_id)["source"]

        assert "Alpha v2" in (tmp_path / "a.py").read_text()
        assert "Beta v2" in (tmp_path / "b.py").read_text()


# =============================================================================
# TestBatchSidecar — sidecar persistence
# =============================================================================


class TestBatchSidecar:
    """Tests for sidecar writes during batch mutations."""

    def test_batch_sidecar(self, tmp_path: Path) -> None:
        """Verify sidecar entries written for each op in a batch."""
        settings = tmp_path / "carvesettings.toml"
        settings.write_text("[carve]\nsidecars = true\n")

        py_file = tmp_path / "app.py"
        py_file.write_text(TWO_FUNCTIONS)

        store = project_import(str(tmp_path))

        greet_results = search(store, pattern="greet")
        greet_id = [r for r in greet_results if r["type"] == "function_definition"][0]["node_id"]
        farewell_results = search(store, pattern="farewell")
        farewell_id = [r for r in farewell_results if r["type"] == "function_definition"][0][
            "node_id"
        ]

        result = batch(
            store,
            [
                {
                    "tool": "update",
                    "kwargs": {"node_id": greet_id, "snippet": GREET_HI},
                },
                {"tool": "delete", "kwargs": {"node_id": farewell_id}},
            ],
        )

        assert result["success"] is True

        crv_path = tmp_path / "app.py.crv"
        assert crv_path.exists()
        entries = json.loads(crv_path.read_text())
        operations = [e["operation"] for e in entries]
        assert "update" in operations
        assert "delete" in operations
