"""Tests for bug fixes.

Bug 2: create_file tool — create new files in a ProjectStore via tree mutation.
Bug 1 (round 6): Formatter not running on project_serialize without output_path.
Bug 2 (round 6): .crv.crv cascading sidecar files from undo/redo.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from carve.models import ProjectStore
from carve.parser import TreeSitterParser
from carve.server import CarveServer
from carve.tools import (
    create_file,
    insert,
    project_import,
    project_serialize,
    search,
    subtree,
    update,
)


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


@pytest.fixture
def sample_project(tmp_path: Path) -> Path:
    """Minimal multi-file project on disk."""
    project = tmp_path / "proj"
    project.mkdir()

    sub = project / "pkg"
    sub.mkdir()
    (sub / "core.py").write_text("def run():\n    pass\n")

    (project / "main.py").write_text('"""Entry point."""\n\ndef main():\n    pass\n')
    return project


@pytest.fixture
def project_store(parser: TreeSitterParser, sample_project: Path) -> ProjectStore:
    return parser.to_project_store(sample_project)


# =============================================================================
# Bug 2: create_file
# =============================================================================


class TestCreateFileBasic:
    """Basic create_file functionality."""

    def test_create_file_in_root(self, project_store: ProjectStore) -> None:
        result = create_file(
            project_store,
            "proj",
            "utils.py",
            source="def helper():\n    return 42\n",
        )
        assert "error" not in result
        assert result["node_id"] == "proj/utils.py"
        assert result["language"] == "python"

    def test_create_file_in_subdirectory(self, project_store: ProjectStore) -> None:
        result = create_file(
            project_store,
            "proj/pkg",
            "helpers.py",
            source="x = 1\n",
        )
        assert "error" not in result
        assert result["node_id"] == "proj/pkg/helpers.py"

    def test_created_file_in_tree(self, project_store: ProjectStore) -> None:
        create_file(project_store, "proj", "new.py", source="y = 2\n")
        node = project_store.get_node("proj/new.py")
        assert node is not None
        assert node.node_type == "file"

    def test_created_file_source_stored(self, project_store: ProjectStore) -> None:
        source = "def greet():\n    print('hi')\n"
        create_file(project_store, "proj", "greet.py", source=source)
        assert project_store.file_sources["proj/greet.py"] == source

    def test_created_file_language_stored(self, project_store: ProjectStore) -> None:
        create_file(project_store, "proj", "app.js", source="const x = 1;\n")
        assert project_store.file_languages["proj/app.js"] == "javascript"

    def test_created_file_empty_source(self, project_store: ProjectStore) -> None:
        result = create_file(project_store, "proj", "empty.py")
        assert "error" not in result
        assert project_store.file_sources["proj/empty.py"] == ""

    def test_language_override(self, project_store: ProjectStore) -> None:
        result = create_file(project_store, "proj", "script.mjs", source="", language="javascript")
        assert result["language"] == "javascript"


class TestCreateFileNavigation:
    """Verify created files appear in navigation tools."""

    def test_subtree_shows_new_file(self, project_store: ProjectStore) -> None:
        create_file(project_store, "proj", "extra.py", source="z = 3\n")
        result = subtree(project_store, "proj", depth=1)
        child_ids = [c["node_id"] for c in result["children"]]
        assert "proj/extra.py" in child_ids

    def test_search_finds_nodes_in_new_file(self, project_store: ProjectStore) -> None:
        create_file(project_store, "proj", "search_target.py", source="def findme():\n    pass\n")
        results = search(project_store, pattern="findme")
        ids = [r["node_id"] for r in results]
        assert any("findme" in nid for nid in ids)

    def test_subtree_from_new_file(self, project_store: ProjectStore) -> None:
        create_file(
            project_store,
            "proj",
            "deep.py",
            source="class Foo:\n    def bar(self):\n        pass\n",
        )
        result = subtree(project_store, "proj/deep.py", depth=2)
        assert "error" not in result
        assert result["node_type"] == "file"
        child_ids = [c["node_id"] for c in result.get("children", [])]
        assert "proj/deep.py::Foo" in child_ids


class TestCreateFileSerialize:
    """Verify created files are serializable."""

    def test_serialize_includes_new_file(self, project_store: ProjectStore, tmp_path: Path) -> None:
        source = "NEW_VAR = True\n"
        create_file(project_store, "proj", "new_config.py", source=source)

        output_dir = tmp_path / "output"
        result = project_serialize(project_store, str(output_dir))
        assert isinstance(result, dict)
        assert "new_config.py" in result
        assert result["new_config.py"] == source
        assert (output_dir / "new_config.py").exists()


class TestCreateFileMutations:
    """Verify insert/update work on nodes inside a newly created file."""

    def test_insert_into_new_file(self, project_store: ProjectStore) -> None:
        create_file(project_store, "proj", "mutable.py", source='"""Module."""\n')
        result = insert(
            project_store,
            "proj/mutable.py",
            "end",
            "def added():\n    return 1\n",
        )
        assert "error" not in result
        assert "added" in result["node_id"]

    def test_insert_then_update_in_new_file(self, project_store: ProjectStore) -> None:
        create_file(project_store, "proj", "evolve.py", source='"""Evolve."""\n')
        ins = insert(
            project_store,
            "proj/evolve.py",
            "end",
            "def stub():\n    ...",
        )
        assert "node_id" in ins

        upd = update(
            project_store,
            ins["node_id"],
            "def stub():\n    return 'real'",
        )
        assert upd.get("success") is True
        assert "real" in project_store.file_sources["proj/evolve.py"]


class TestCreateFileErrors:
    """Error cases for create_file."""

    def test_not_a_project_store(self) -> None:
        parser = TreeSitterParser()
        tree_store = parser.to_tree_store("x = 1\n", "python")
        result = create_file(tree_store, "module", "new.py")
        assert "error" in result
        assert "ProjectStore" in result["error"]

    def test_parent_not_found(self, project_store: ProjectStore) -> None:
        result = create_file(project_store, "nonexistent", "new.py")
        assert "error" in result
        assert "not found" in result["error"]

    def test_parent_not_directory(self, project_store: ProjectStore) -> None:
        result = create_file(project_store, "proj/main.py", "nested.py")
        assert "error" in result
        assert "directory" in result["error"]

    def test_duplicate_filename(self, project_store: ProjectStore) -> None:
        result = create_file(project_store, "proj", "main.py")
        assert "error" in result
        assert "already exists" in result["error"]

    def test_unsupported_extension(self, project_store: ProjectStore) -> None:
        """Unrecognized extensions are created as 'text' language files."""
        result = create_file(project_store, "proj", "readme.rst")
        assert "error" not in result
        assert result["language"] == "text"
        assert result["node_id"] == "proj/readme.rst"


# =============================================================================
# Bug 1 (round 6): Formatter not running on project_serialize without output_path
# =============================================================================


BADLY_SPACED_PYTHON = """\
def foo():
    pass



def bar():
    pass
"""


class TestSerializeFormatterDefaultPath:
    """project_serialize with no output_path should still format via the store's path."""

    @pytest.fixture
    def formatted_project(self, tmp_path: Path) -> ProjectStore:
        """Create a project with a carvesettings.toml that configures ruff format."""
        proj = tmp_path / "proj"
        proj.mkdir()
        (proj / "carvesettings.toml").write_text(
            '[formatters]\npython = "ruff format --quiet {file}"\n'
        )
        (proj / "main.py").write_text(BADLY_SPACED_PYTHON)
        store = project_import(str(proj))
        assert isinstance(store, ProjectStore)
        return store

    def test_project_store_serialize_formats_without_output_path(
        self,
        formatted_project: ProjectStore,
        tmp_path: Path,
    ) -> None:
        """Calling project_serialize(store) with no output_path should format on disk."""
        result = project_serialize(formatted_project)
        assert isinstance(result, dict)
        # The formatter (ruff) normalises to exactly 2 blank lines between top-level defs
        main_source = result["main.py"]
        assert "\n\n\n\n" not in main_source, "Triple+ blank lines should have been formatted away"

    @pytest.fixture
    def formatted_single_file(self, tmp_path: Path) -> Path:
        """Create a single .py file with a carvesettings.toml in its directory."""
        (tmp_path / "carvesettings.toml").write_text(
            '[formatters]\npython = "ruff format --quiet {file}"\n'
        )
        py_file = tmp_path / "single.py"
        py_file.write_text(BADLY_SPACED_PYTHON)
        return py_file

    def test_tree_store_serialize_formats_without_output_path(
        self,
        formatted_single_file: Path,
    ) -> None:
        """Calling project_serialize(store) on a TreeStore with no output_path formats."""
        store = project_import(str(formatted_single_file))
        result = project_serialize(store)
        assert isinstance(result, str)
        assert "\n\n\n\n" not in result, "Triple+ blank lines should have been formatted away"


# =============================================================================
# Bug 2 (round 6): .crv.crv cascading sidecar files from undo/redo
# =============================================================================


class TestNoCrvCrvOnUndoRedo:
    """Undo/redo must not create .crv.crv files."""

    @pytest.fixture
    def sidecar_server(self, tmp_path: Path) -> CarveServer:
        """Project with sidecars enabled."""
        proj = tmp_path / "proj"
        proj.mkdir()
        (proj / "carvesettings.toml").write_text("sidecars = true\n")
        (proj / "app.py").write_text("def hello():\n    pass\n")
        return CarveServer(file_path=proj)

    def test_undo_does_not_create_crv_crv(
        self,
        sidecar_server: CarveServer,
        tmp_path: Path,
    ) -> None:
        server = sidecar_server
        proj = tmp_path / "proj"

        # Mutate so a .crv sidecar is created
        hello = [
            n for n in server.search(pattern="hello") if n.get("type") == "function_definition"
        ]
        assert len(hello) > 0
        hello_id = hello[0]["node_id"]
        server.update(hello_id, "def hello():\n    print('hi')\n")

        # .crv should exist
        crv = proj / "app.py.crv"
        assert crv.exists()

        # Undo the update
        undo_result = server.undo()
        assert undo_result["success"] is True

        # .crv.crv must NOT exist
        crv_crv = proj / "app.py.crv.crv"
        assert not crv_crv.exists(), ".crv.crv should not be created by undo"

    def test_redo_does_not_create_crv_crv(
        self,
        sidecar_server: CarveServer,
        tmp_path: Path,
    ) -> None:
        server = sidecar_server
        proj = tmp_path / "proj"

        hello = [
            n for n in server.search(pattern="hello") if n.get("type") == "function_definition"
        ]
        hello_id = hello[0]["node_id"]
        server.update(hello_id, "def hello():\n    print('hi')\n")

        server.undo()
        server.redo()

        crv_crv = proj / "app.py.crv.crv"
        assert not crv_crv.exists(), ".crv.crv should not be created by redo"

    def test_original_sidecar_unchanged_after_undo(
        self,
        sidecar_server: CarveServer,
        tmp_path: Path,
    ) -> None:
        """The .crv file should have exactly one entry (the original update),
        not extra entries from undo replays."""
        server = sidecar_server
        proj = tmp_path / "proj"

        hello = [
            n for n in server.search(pattern="hello") if n.get("type") == "function_definition"
        ]
        hello_id = hello[0]["node_id"]
        server.update(hello_id, "def hello():\n    print('hi')\n")

        crv = proj / "app.py.crv"
        entries_before = json.loads(crv.read_text())
        count_before = len(entries_before)

        server.undo()

        entries_after = json.loads(crv.read_text())
        assert len(entries_after) == count_before, "Undo should not add sidecar entries"
