"""Tests for draft/promote snippet validation.

Verifies the transactional validation model: mutations are applied to a
draft copy of the source, tree-sitter validates the full file in context,
and only clean drafts are promoted to the live tree.
"""

from __future__ import annotations

from unittest.mock import patch

import pytest

from carve.parser import TreeSitterParser
from carve.tools import (
    _reindent_snippet,
    _strip_carve_comments,
    _validate_in_context,
    insert,
    update,
)


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


@pytest.fixture
def python_store(parser: TreeSitterParser):
    """Store with a class containing two methods."""
    source = '''\
class User:
    """A user model."""

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

    def greet(self) -> str:
        return f"Hello, {self.name}"
'''
    return parser.to_tree_store(source, "python")


@pytest.fixture
def js_store(parser: TreeSitterParser):
    """Store with a simple JavaScript module."""
    source = """\
function greet(name) {
    return "Hello, " + name;
}

function add(a, b) {
    return a + b;
}
"""
    return parser.to_tree_store(source, "javascript")


# =============================================================================
# _strip_carve_comments
# =============================================================================


class TestStripCarveComments:
    """Tests for stripping # carve: metadata comments."""

    def test_strip_python_carve_comment(self):
        snippet = "# carve: indent=4\ndef foo():\n    pass"
        result = _strip_carve_comments(snippet)
        assert result == "def foo():\n    pass"

    def test_strip_javascript_carve_comment(self):
        snippet = "// carve: indent=2\nfunction foo() { return 1; }"
        result = _strip_carve_comments(snippet)
        assert result == "function foo() { return 1; }"

    def test_strip_multiple_comments(self):
        snippet = "# carve: indent=4\n# carve: parent=Foo\ndef foo():\n    pass"
        result = _strip_carve_comments(snippet)
        assert result == "def foo():\n    pass"

    def test_preserve_regular_comments(self):
        snippet = "# Regular comment\ndef foo():\n    # Another comment\n    pass"
        result = _strip_carve_comments(snippet)
        assert result == snippet

    def test_no_comments(self):
        snippet = "def foo():\n    pass"
        result = _strip_carve_comments(snippet)
        assert result == snippet

    def test_indented_carve_comment(self):
        snippet = "    # carve: indent=8\n    def foo():\n        pass"
        result = _strip_carve_comments(snippet)
        assert result == "    def foo():\n        pass"

    def test_empty_string(self):
        assert _strip_carve_comments("") == ""


# =============================================================================
# _reindent_snippet
# =============================================================================


class TestReindentSnippet:
    """Tests for re-indenting snippets to target column."""

    def test_indent_first_line(self):
        snippet = "def foo():\n    pass"
        result = _reindent_snippet(snippet, 4, indent_first_line=True)
        assert result == "    def foo():\n        pass"

    def test_no_indent_first_line(self):
        snippet = "def foo():\n    pass"
        result = _reindent_snippet(snippet, 4, indent_first_line=False)
        assert result == "def foo():\n        pass"

    def test_zero_indent(self):
        snippet = "def foo():\n    pass"
        result = _reindent_snippet(snippet, 0, indent_first_line=True)
        assert result == "def foo():\n    pass"

    def test_single_line(self):
        snippet = "x = 1"
        result = _reindent_snippet(snippet, 4, indent_first_line=True)
        assert result == "    x = 1"

    def test_preserves_blank_lines(self):
        snippet = "def foo():\n\n    pass"
        result = _reindent_snippet(snippet, 4, indent_first_line=True)
        lines = result.split("\n")
        assert lines[0] == "    def foo():"
        assert lines[1] == ""
        assert lines[2] == "        pass"

    def test_strips_outer_whitespace(self):
        snippet = "\n  def foo():\n      pass\n  "
        result = _reindent_snippet(snippet, 0, indent_first_line=True)
        assert result == "def foo():\n      pass"


# =============================================================================
# _validate_in_context
# =============================================================================


class TestValidateInContext:
    """Tests for full-file contextual validation."""

    def test_valid_python_file(self):
        source = "class Foo:\n    def bar(self):\n        pass\n"
        result = _validate_in_context(source, "python")
        assert result["valid"] is True
        assert result["errors"] == []

    def test_valid_javascript_file(self):
        source = "function foo() { return 1; }\n"
        result = _validate_in_context(source, "javascript")
        assert result["valid"] is True

    def test_invalid_python_missing_colon(self):
        # Missing colon on def → tree-sitter parse error
        source = "class Foo:\n    def bar(self)\n        pass\n"
        result = _validate_in_context(source, "python")
        assert result["valid"] is False
        assert len(result["errors"]) > 0

    def test_invalid_python_syntax(self):
        source = "def foo(:\n    pass\n"
        result = _validate_in_context(source, "python")
        assert result["valid"] is False

    def test_error_contains_context(self):
        source = "def foo(:\n    pass\n"
        result = _validate_in_context(source, "python")
        assert result["errors"][0]["type"] in ("context_error", "missing_node")
        assert "start_point" in result["errors"][0]
        assert "end_point" in result["errors"][0]

    def test_empty_source(self):
        result = _validate_in_context("", "python")
        assert result["valid"] is True


# =============================================================================
# Draft/Promote Insert
# =============================================================================


class TestDraftPromoteInsert:
    """Tests that insert() uses draft/promote validation."""

    def test_insert_valid_snippet_succeeds(self, python_store):
        result = insert(
            python_store,
            "module::User",
            "end",
            "def logout(self):\n    self.name = None",
        )
        assert "error" not in result
        assert "node_id" in result
        assert "logout" in result["node_id"]

    def test_insert_properly_indented_in_source(self, python_store):
        """Verify the inserted code has correct absolute indentation in the file."""
        insert(
            python_store,
            "module::User",
            "end",
            "def logout(self):\n    self.name = None",
        )
        # The method body should be at 8-space indent (4 class + 4 method body)
        assert "        self.name = None" in python_store.source

    def test_insert_strips_carve_comments(self, python_store):
        snippet = "# carve: indent=4\ndef logout(self):\n    pass"
        result = insert(python_store, "module::User", "end", snippet)
        assert "error" not in result
        # The carve comment should NOT appear in the file
        assert "# carve:" not in python_store.source

    def test_insert_with_isolation_error_fails(self, python_store):
        """Snippet with syntax error caught by isolation check."""
        result = insert(
            python_store,
            "module::User",
            "end",
            "def broken(\n    return",
        )
        assert "error" in result
        assert "errors" in result

    def test_insert_at_various_positions(self, python_store):
        """Insert at start, end, before, and after all work."""
        for position in ["start", "end", "before:greet", "after:__init__"]:
            # Reload a fresh store for each test
            store = TreeSitterParser().to_tree_store(
                "class User:\n    def __init__(self):\n        pass\n"
                "    def greet(self):\n        pass\n",
                "python",
            )
            result = insert(store, "module::User", position, "def new_method(self):\n    pass")
            assert "error" not in result, f"Failed for position={position}"

    def test_insert_js_function(self, js_store):
        """Insert works for JavaScript too."""
        result = insert(
            js_store,
            "program",
            "end",
            "function multiply(a, b) { return a * b; }",
        )
        assert "error" not in result
        assert "multiply" in result["node_id"]


# =============================================================================
# Draft/Promote Update
# =============================================================================


class TestDraftPromoteUpdate:
    """Tests that update() uses draft/promote validation."""

    def test_update_valid_snippet_succeeds(self, python_store):
        result = update(
            python_store,
            "module::User::greet",
            'def greet(self) -> str:\n    return f"Hi, {self.name}!"',
        )
        assert result.get("success") is True

    def test_update_properly_indented_in_source(self, python_store):
        """Verify the updated code has correct absolute indentation."""
        update(
            python_store,
            "module::User::greet",
            'def greet(self) -> str:\n    return f"Hi, {self.name}!"',
        )
        # Body should be at 8-space indent
        assert '        return f"Hi, {self.name}!"' in python_store.source

    def test_update_strips_carve_comments(self, python_store):
        snippet = '# carve: indent=4\ndef greet(self) -> str:\n    return "yo"'
        result = update(python_store, "module::User::greet", snippet)
        assert result.get("success") is True
        assert "# carve:" not in python_store.source

    def test_update_with_isolation_error_fails(self, python_store):
        result = update(
            python_store,
            "module::User::greet",
            "def broken(",
        )
        assert "error" in result

    def test_update_records_metadata(self, python_store):
        result = update(
            python_store,
            "module::User::greet",
            "def greet(self):\n    return 'hi'",
            prompt="simplify greeting",
            model="test-model",
        )
        assert result.get("success") is True
        node = python_store.get_node("module::User::greet")
        assert node is not None
        assert node.metadata.prompt == "simplify greeting"
        assert node.metadata.model == "test-model"
        assert node.metadata.version == 2

    def test_update_js_function(self, js_store):
        result = update(
            js_store,
            "program::greet",
            'function greet(name) { return "Hi, " + name + "!"; }',
        )
        assert result.get("success") is True


# =============================================================================
# Transactional Safety
# =============================================================================


class TestTransactionalSafety:
    """Verify the tree is NOT modified when contextual validation fails."""

    def test_insert_context_failure_leaves_tree_unchanged(self, python_store):
        """If contextual validation fails, tree and source are untouched."""
        original_source = python_store.source
        original_node_count = len(python_store.all_nodes())

        # Patch _validate_in_context to simulate a contextual failure
        with patch(
            "carve.tools._validate_in_context",
            return_value={
                "valid": False,
                "errors": [{"type": "context_error", "text": "simulated"}],
            },
        ):
            result = insert(
                python_store,
                "module::User",
                "end",
                "def safe_method(self):\n    pass",
            )

        assert "error" in result
        assert "context" in result["error"].lower()
        # Source unchanged
        assert python_store.source == original_source
        # No new nodes
        assert len(python_store.all_nodes()) == original_node_count
        # Node not in tree
        assert python_store.get_node("module::User::safe_method") is None

    def test_update_context_failure_leaves_tree_unchanged(self, python_store):
        """If contextual validation fails, node and source are untouched."""
        original_source = python_store.source
        node = python_store.get_node("module::User::greet")
        assert node is not None
        original_node_source = node.source
        original_version = node.metadata.version

        with patch(
            "carve.tools._validate_in_context",
            return_value={
                "valid": False,
                "errors": [{"type": "context_error", "text": "simulated"}],
            },
        ):
            result = update(
                python_store,
                "module::User::greet",
                "def greet(self):\n    return 'CHANGED'",
            )

        assert "error" in result
        # Source unchanged
        assert python_store.source == original_source
        # Node unchanged
        node_after = python_store.get_node("module::User::greet")
        assert node_after is not None
        assert node_after.source == original_node_source
        assert node_after.metadata.version == original_version

    def test_update_context_failure_no_metadata_recorded(self, python_store):
        """Metadata (version, history) must not change on failed update."""
        node = python_store.get_node("module::User::greet")
        assert node is not None
        assert node.metadata.version == 1
        assert len(node.metadata.history) == 0

        with patch(
            "carve.tools._validate_in_context",
            return_value={
                "valid": False,
                "errors": [{"type": "context_error", "text": "simulated"}],
            },
        ):
            update(
                python_store,
                "module::User::greet",
                "def greet(self):\n    return 'x'",
                prompt="should not be recorded",
            )

        assert node.metadata.version == 1
        assert len(node.metadata.history) == 0
        assert node.metadata.prompt is None


# =============================================================================
# Insert-then-Update roundtrip
# =============================================================================


class TestInsertThenUpdate:
    """Verify that a node inserted via insert() can be updated via update()."""

    def test_insert_then_update_roundtrip(self, python_store):
        # Insert a new method
        ins = insert(
            python_store,
            "module::User",
            "end",
            "def temp(self):\n    pass",
        )
        assert "node_id" in ins
        node_id = ins["node_id"]

        # Update it
        upd = update(
            python_store,
            node_id,
            "def temp(self):\n    return 42",
        )
        assert upd.get("success") is True

        # Source has the updated code
        assert "return 42" in python_store.source
        assert "pass" not in python_store.source.split("def temp")[1]

    def test_insert_then_update_preserves_other_nodes(self, python_store):
        ins = insert(
            python_store,
            "module::User",
            "end",
            "def extra(self):\n    pass",
        )
        node_id = ins["node_id"]

        # Capture greet source before update
        greet = python_store.get_node("module::User::greet")
        assert greet is not None
        greet_source = greet.source

        update(python_store, node_id, "def extra(self):\n    return 99")

        # greet unchanged
        greet_after = python_store.get_node("module::User::greet")
        assert greet_after is not None
        assert greet_after.source == greet_source


# =============================================================================
# Bug 1 Regression: Serialize after insert+update sequences
# =============================================================================


class TestSerializeAfterMutations:
    """Regression test for Bug 1: project_serialize garbled after insert+update.

    Reproduces the exact scenario from BUGS.md: insert 7 stub nodes, update
    all 7 with real implementations, then serialize. The output must be valid
    Python containing all updated content with no interleaved garbage.
    """

    def test_serialize_valid_after_inserts_and_updates(self):
        """7 inserts + 7 updates + serialize → valid Python."""
        parser = TreeSitterParser()
        store = parser.to_tree_store('"""My game module."""\n', "python")

        stubs = [
            ("imports", "from dataclasses import dataclass, field"),
            ("Player", 'class Player:\n    """A player."""\n    ...'),
            ("Room", 'class Room:\n    """A room."""\n    ...'),
            ("World", 'class World:\n    """World type alias."""\n    ...'),
            ("handle_command", 'def handle_command():\n    """Handle."""\n    ...'),
            ("parse_input", 'def parse_input():\n    """Parse."""\n    ...'),
            ("main", 'def main():\n    """Run."""\n    ...'),
        ]

        full_impls = [
            ("imports", "from dataclasses import dataclass, field\nfrom typing import Dict"),
            (
                "Player",
                "class Player:\n"
                '    """A player."""\n'
                "    name: str\n"
                '    current_room: str = "start"\n'
                "    inventory: list[str] = field(default_factory=list)\n"
                "    moves: int = 0",
            ),
            (
                "Room",
                "class Room:\n"
                '    """A room."""\n'
                "    description: str\n"
                "    exits: dict[str, str] = field(default_factory=dict)\n"
                "    items: list[str] = field(default_factory=list)",
            ),
            ("World", "World = Dict[str, Room]"),
            (
                "handle_command",
                "def handle_command(verb: str, noun: str) -> str:\n"
                '    """Handle a command and return the response text."""\n'
                '    if verb == "look":\n'
                '        return "You look around."\n'
                "    return f\"I don't understand '{verb}'.\"",
            ),
            (
                "parse_input",
                "def parse_input(raw: str) -> tuple[str, str]:\n"
                '    """Parse raw input into verb and noun."""\n'
                "    words = raw.strip().lower().split()\n"
                "    if not words:\n"
                '        return ("", "")\n'
                "    verb = words[0]\n"
                '    noun = words[1] if len(words) > 1 else ""\n'
                "    return (verb, noun)",
            ),
            (
                "main",
                "def main() -> None:\n"
                '    """Run the game loop."""\n'
                '    raw = input("> ")\n'
                "    verb, noun = parse_input(raw)\n"
                "    result = handle_command(verb, noun)\n"
                "    print(result)",
            ),
        ]

        # Phase 1: Insert all stubs
        node_ids: list[str] = []
        for _name, snippet in stubs:
            result = insert(store, "module", "end", snippet)
            assert "node_id" in result, f"Insert failed: {result}"
            node_ids.append(result["node_id"])

        # Phase 2: Update all stubs with real implementations
        for node_id, (_name, snippet) in zip(node_ids, full_impls, strict=True):
            result = update(store, node_id, snippet)
            assert result.get("success") is True, f"Update failed for {node_id}: {result}"

        # Phase 3: Serialize and verify
        source = store.source

        # Must be parseable Python (no syntax errors)
        validation = _validate_in_context(source, "python")
        assert validation["valid"], f"Garbled output:\n{source}\nErrors: {validation['errors']}"

        # All updated content must be present
        assert "from dataclasses import dataclass, field" in source
        assert "from typing import Dict" in source
        assert "class Player:" in source
        assert "current_room" in source
        assert "class Room:" in source
        assert "World = Dict[str, Room]" in source
        assert "def handle_command" in source
        assert "I don't understand" in source
        assert "def parse_input" in source
        assert "def main" in source
        assert "parse_input(raw)" in source
