"""Tests for file.applyPatch functionality."""

import pytest

from skop_runner.file_manager import FileManager


@pytest.fixture
def file_manager(tmp_path):
    """Create a file manager with temporary directory."""
    return FileManager(str(tmp_path))


class TestApplyPatch:
    """Tests for applying patches directly through FileManager."""

    @pytest.mark.asyncio
    async def test_apply_simple_replacement(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("def hello():\n    print('hello')\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +1,2 @@
 def hello():
-    print('hello')
+    print('hello, world!')
"""

        result = await file_manager.apply_patch("test.py", patch)
        assert result["success"] is True
        assert test_file.read_text() == "def hello():\n    print('hello, world!')\n"

    @pytest.mark.asyncio
    async def test_apply_patch_add_lines(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("def foo():\n    pass\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +1,4 @@
 def foo():
-    pass
+    x = 1
+    y = 2
+    return x + y
"""

        await file_manager.apply_patch("test.py", patch)
        assert (
            test_file.read_text()
            == "def foo():\n    x = 1\n    y = 2\n    return x + y\n"
        )

    @pytest.mark.asyncio
    async def test_apply_patch_remove_lines(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("line1\nline2\nline3\nline4\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,4 +1,2 @@
 line1
-line2
-line3
 line4
"""

        await file_manager.apply_patch("test.py", patch)
        assert test_file.read_text() == "line1\nline4\n"

    @pytest.mark.asyncio
    async def test_apply_patch_multiple_hunks(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text(
            "# Header\n\ndef func1():\n    pass\n\ndef func2():\n    pass\n"
        )

        patch = (
            "--- a/test.py\n"
            "+++ b/test.py\n"
            "@@ -1,4 +1,4 @@\n"
            "-# Header\n"
            "+# Modified Header\n"
            " \n"
            " def func1():\n"
            "     pass\n"
            "@@ -5,3 +5,3 @@\n"
            " \n"
            " def func2():\n"
            "-    pass\n"
            "+    return 42\n"
        )

        await file_manager.apply_patch("test.py", patch)
        content = test_file.read_text()
        assert "# Modified Header" in content
        assert "return 42" in content

    @pytest.mark.asyncio
    async def test_apply_patch_file_not_found(self, file_manager):
        with pytest.raises(FileNotFoundError):
            await file_manager.apply_patch("missing.py", "diff")

    @pytest.mark.asyncio
    async def test_apply_patch_context_mismatch(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("completely different content\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +1,2 @@
 expected content
-old line
+new line
"""

        with pytest.raises(ValueError, match="context mismatch|Failed to apply"):
            await file_manager.apply_patch("test.py", patch)

    @pytest.mark.asyncio
    async def test_apply_patch_empty_patch(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("content\n")

        with pytest.raises(ValueError, match="Patch did not contain any changes"):
            await file_manager.apply_patch("test.py", "")

    @pytest.mark.asyncio
    async def test_apply_patch_fuzzy_match_fails(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text(
            "# Extra comment\n# Another comment\ndef hello():\n    print('hello')\n"
        )

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +1,2 @@
 def hello():
-    print('hello')
+    print('hello, world!')
"""

        with pytest.raises(ValueError, match="Failed to apply patch"):
            await file_manager.apply_patch("test.py", patch)

    @pytest.mark.asyncio
    async def test_apply_patch_large_shift_fuzzy_fails(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        lines = [f"# comment {i}\n" for i in range(200)]
        lines += ["def target():\n", "    return 1\n"]
        test_file.write_text("".join(lines))

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +1,2 @@
 def target():
-    return 1
+    return 42
"""

        with pytest.raises(ValueError, match="Failed to apply patch"):
            await file_manager.apply_patch("test.py", patch)

    @pytest.mark.asyncio
    async def test_apply_patch_add_new_function(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("def existing():\n    return 1\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +1,6 @@
 def existing():
     return 1
+
+
+def new_function():
+    return 2
"""

        await file_manager.apply_patch("test.py", patch)
        content = test_file.read_text()
        assert "def new_function()" in content
        assert "return 2" in content

    @pytest.mark.asyncio
    async def test_apply_patch_adds_trailing_newline(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("x = 1")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1 +1 @@
-x = 1
\\ No newline at end of file
+x = 2
"""

        await file_manager.apply_patch("test.py", patch)
        content = test_file.read_text()
        assert content == "x = 2\n"
        assert content.endswith("\n")

    @pytest.mark.asyncio
    async def test_apply_patch_removes_trailing_newline(self, file_manager, tmp_path):
        """patch-ng keeps trailing newline even when diff requests removal."""
        test_file = tmp_path / "test.py"
        test_file.write_text("x = 1\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1 +1 @@
-x = 1
+x = 2
\\ No newline at end of file
"""

        await file_manager.apply_patch("test.py", patch)
        content = test_file.read_text()
        assert content == "x = 2\n"
        assert content.endswith("\n")

    @pytest.mark.asyncio
    async def test_apply_patch_delete_entire_file(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("line1\nline2\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +0,0 @@
-line1
-line2
"""

        await file_manager.apply_patch("test.py", patch)
        assert test_file.read_text() == ""

    @pytest.mark.asyncio
    async def test_apply_patch_on_directory_fails(self, file_manager, tmp_path):
        dir_path = tmp_path / "folder"
        dir_path.mkdir()

        with pytest.raises(ValueError, match="Not a file"):
            await file_manager.apply_patch("folder", "@@ -1 +1 @@\n-old\n+new")


class TestApplyPatchViaMessageHandler:
    """Ensure MessageHandler wiring is correct."""

    @pytest.fixture
    def message_handler(self, tmp_path):
        from skop_runner.mock_backend import MockKernelBackend
        from skop_runner.file_manager import FileManager
        from skop_runner.message_handler import MessageHandler

        km = MockKernelBackend()
        fm = FileManager(str(tmp_path))
        return MessageHandler(km, fm)

    @pytest.mark.asyncio
    async def test_file_apply_patch_handler(self, message_handler, tmp_path):
        test_file = tmp_path / "handler_test.py"
        test_file.write_text("x = 1\n")

        patch = """\
--- a/handler_test.py
+++ b/handler_test.py
@@ -1 +1 @@
-x = 1
+x = 42
"""

        response = await message_handler.handle_message({
            "id": "1",
            "method": "file.applyPatch",
            "params": {"path": "handler_test.py", "patch": patch},
        })

        assert response["result"]["success"] is True
        assert test_file.read_text() == "x = 42\n"

    @pytest.mark.asyncio
    async def test_file_apply_patch_missing_params(self, message_handler):
        response = await message_handler.handle_message({
            "id": "1",
            "method": "file.applyPatch",
            "params": {"path": "handler_test.py"},
        })

        assert "error" in response
        assert response["error"]["code"] == -32602

    @pytest.mark.asyncio
    async def test_file_apply_patch_file_not_found(self, message_handler):
        response = await message_handler.handle_message({
            "id": "1",
            "method": "file.applyPatch",
            "params": {"path": "missing.py", "patch": "diff"},
        })

        assert "error" in response
        assert response["error"]["code"] == -32001


class TestNewlinePreservation:
    """Dedicated newline handling tests."""

    @pytest.mark.asyncio
    async def test_preserve_trailing_newline(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("x = 1\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1 +1 @@
-x = 1
+x = 2
"""

        await file_manager.apply_patch("test.py", patch)
        content = test_file.read_text()
        assert content == "x = 2\n"
        assert content.endswith("\n")

    @pytest.mark.asyncio
    async def test_preserve_no_trailing_newline(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("x = 1")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1 +1 @@
-x = 1
\\ No newline at end of file
+x = 2
\\ No newline at end of file
"""

        await file_manager.apply_patch("test.py", patch)
        content = test_file.read_text()
        assert content == "x = 2\n"
        assert content.endswith("\n")

    @pytest.mark.asyncio
    async def test_patch_with_tabs(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("def foo():\n\treturn 1\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +1,2 @@
 def foo():
-\treturn 1
+\treturn 42
"""

        await file_manager.apply_patch("test.py", patch)
        assert test_file.read_text() == "def foo():\n\treturn 42\n"

    @pytest.mark.asyncio
    async def test_patch_rejects_wrong_indentation(self, file_manager, tmp_path):
        test_file = tmp_path / "test.py"
        test_file.write_text("def foo():\n    return 1\n")

        patch = """\
--- a/test.py
+++ b/test.py
@@ -1,2 +1,2 @@
 def foo():
-\treturn 1
+\treturn 42
"""

        with pytest.raises(ValueError, match="context mismatch|Failed to apply"):
            await file_manager.apply_patch("test.py", patch)
