"""Tests for advanced EditFileTool enhancements inspired by claude-code:
- Delete-line newline cleanup
- Smart quote normalization (curly ↔ straight)
- Quote style preservation in replacements
- Indentation preservation when fallback match is trimmed
- Trailing whitespace stripping for new_text
- File size protection
- Stale detection with content-equality fallback
"""

import os

import pytest

from nanobot.agent.tools import file_state
from nanobot.agent.tools.filesystem import EditFileTool, ReadFileTool


@pytest.fixture(autouse=True)
def _clear_file_state():
    file_state.clear()
    yield
    file_state.clear()


# ---------------------------------------------------------------------------
# Delete-line newline cleanup
# ---------------------------------------------------------------------------


class TestDeleteLineCleanup:
    """When new_text='' and deleting a line, trailing newline should be consumed."""

    @pytest.fixture()
    def tool(self, tmp_path):
        return EditFileTool(workspace=tmp_path)

    @pytest.mark.asyncio
    async def test_delete_line_consumes_trailing_newline(self, tool, tmp_path):
        f = tmp_path / "a.py"
        f.write_text("line1\nline2\nline3\n", encoding="utf-8")
        result = await tool.execute(path=str(f), old_text="line2", new_text="")
        assert "Successfully" in result
        content = f.read_text()
        # Should not leave a blank line where line2 was
        assert content == "line1\nline3\n"

    @pytest.mark.asyncio
    async def test_delete_line_with_explicit_newline_in_old_text(self, tool, tmp_path):
        f = tmp_path / "a.py"
        f.write_text("line1\nline2\nline3\n", encoding="utf-8")
        result = await tool.execute(path=str(f), old_text="line2\n", new_text="")
        assert "Successfully" in result
        assert f.read_text() == "line1\nline3\n"

    @pytest.mark.asyncio
    async def test_delete_preserves_content_when_not_trailing_newline(self, tool, tmp_path):
        """Deleting a word mid-line should not consume extra characters."""
        f = tmp_path / "a.py"
        f.write_text("hello world here\n", encoding="utf-8")
        result = await tool.execute(path=str(f), old_text="world ", new_text="")
        assert "Successfully" in result
        assert f.read_text() == "hello here\n"


# ---------------------------------------------------------------------------
# Smart quote normalization
# ---------------------------------------------------------------------------


class TestQuoteStylePreservation:
    """When quote-normalized matching occurs, replacement should preserve actual quote style."""

    @pytest.fixture()
    def tool(self, tmp_path):
        return EditFileTool(workspace=tmp_path)

    @pytest.mark.asyncio
    async def test_replacement_preserves_curly_double_quotes(self, tool, tmp_path):
        f = tmp_path / "quotes.txt"
        f.write_text('message = “hello”\n', encoding="utf-8")
        result = await tool.execute(
            path=str(f),
            old_text='message = "hello"',
            new_text='message = "goodbye"',
        )
        assert "Successfully" in result
        assert f.read_text(encoding="utf-8") == 'message = “goodbye”\n'

    @pytest.mark.asyncio
    async def test_replacement_preserves_curly_apostrophe(self, tool, tmp_path):
        f = tmp_path / "apostrophe.txt"
        f.write_text("it’s fine\n", encoding="utf-8")
        result = await tool.execute(
            path=str(f),
            old_text="it's fine",
            new_text="it's better",
        )
        assert "Successfully" in result
        assert f.read_text(encoding="utf-8") == "it’s better\n"


# ---------------------------------------------------------------------------
# Indentation preservation
# ---------------------------------------------------------------------------


class TestIndentationPreservation:
    """Replacement should keep outer indentation when trim fallback matched."""

    @pytest.fixture()
    def tool(self, tmp_path):
        return EditFileTool(workspace=tmp_path)

    @pytest.mark.asyncio
    async def test_trim_fallback_preserves_outer_indentation(self, tool, tmp_path):
        f = tmp_path / "indent.py"
        f.write_text(
            "if True:\n"
            "    def foo():\n"
            "        pass\n",
            encoding="utf-8",
        )
        result = await tool.execute(
            path=str(f),
            old_text="def foo():\n    pass",
            new_text="def bar():\n    return 1",
        )
        assert "Successfully" in result
        assert f.read_text(encoding="utf-8") == (
            "if True:\n"
            "    def bar():\n"
            "        return 1\n"
        )


# ---------------------------------------------------------------------------
# Failure diagnostics
# ---------------------------------------------------------------------------


class TestEditDiagnostics:
    """Failure paths should offer actionable hints."""

    @pytest.fixture()
    def tool(self, tmp_path):
        return EditFileTool(workspace=tmp_path)

    @pytest.mark.asyncio
    async def test_ambiguous_match_reports_candidate_lines(self, tool, tmp_path):
        f = tmp_path / "dup.py"
        f.write_text("aaa\nbbb\naaa\nbbb\n", encoding="utf-8")
        result = await tool.execute(path=str(f), old_text="aaa\nbbb", new_text="xxx")
        assert "appears 2 times" in result.lower()
        assert "line 1" in result.lower()
        assert "line 3" in result.lower()
        assert "replace_all=true" in result

    @pytest.mark.asyncio
    async def test_not_found_reports_whitespace_hint(self, tool, tmp_path):
        f = tmp_path / "space.py"
        f.write_text("value =  1\n", encoding="utf-8")
        result = await tool.execute(path=str(f), old_text="value = 1", new_text="value = 2")
        assert "Error" in result
        assert "whitespace" in result.lower()

    @pytest.mark.asyncio
    async def test_not_found_reports_case_hint(self, tool, tmp_path):
        f = tmp_path / "case.py"
        f.write_text("HelloWorld\n", encoding="utf-8")
        result = await tool.execute(path=str(f), old_text="helloworld", new_text="goodbye")
        assert "Error" in result
        assert "letter case differs" in result.lower()


# ---------------------------------------------------------------------------
# Advanced fallback replacement behavior
# ---------------------------------------------------------------------------


class TestAdvancedReplaceAll:
    """replace_all should work correctly for fallback-based matches too."""

    @pytest.fixture()
    def tool(self, tmp_path):
        return EditFileTool(workspace=tmp_path)

    @pytest.mark.asyncio
    async def test_replace_all_preserves_each_match_indentation(self, tool, tmp_path):
        f = tmp_path / "indent_multi.py"
        f.write_text(
            "if a:\n"
            "    def foo():\n"
            "        pass\n"
            "if b:\n"
            "        def foo():\n"
            "            pass\n",
            encoding="utf-8",
        )
        result = await tool.execute(
            path=str(f),
            old_text="def foo():\n    pass",
            new_text="def bar():\n    return 1",
            replace_all=True,
        )
        assert "Successfully" in result
        assert f.read_text(encoding="utf-8") == (
            "if a:\n"
            "    def bar():\n"
            "        return 1\n"
            "if b:\n"
            "        def bar():\n"
            "            return 1\n"
        )

    @pytest.mark.asyncio
    async def test_trim_and_quote_fallback_match_succeeds(self, tool, tmp_path):
        f = tmp_path / "quote_indent.py"
        f.write_text("    message = “hello”\n", encoding="utf-8")
        result = await tool.execute(
            path=str(f),
            old_text='message = "hello"',
            new_text='message = "goodbye"',
        )
        assert "Successfully" in result
        assert f.read_text(encoding="utf-8") == "    message = “goodbye”\n"


# ---------------------------------------------------------------------------
# Trailing whitespace stripping on new_text
# ---------------------------------------------------------------------------


class TestTrailingWhitespaceStrip:
    """new_text trailing whitespace should be stripped (except .md files)."""

    @pytest.fixture()
    def tool(self, tmp_path):
        return EditFileTool(workspace=tmp_path)

    @pytest.mark.asyncio
    async def test_strips_trailing_whitespace_from_new_text(self, tool, tmp_path):
        f = tmp_path / "a.py"
        f.write_text("x = 1\n", encoding="utf-8")
        result = await tool.execute(
            path=str(f), old_text="x = 1", new_text="x = 2   \ny = 3  ",
        )
        assert "Successfully" in result
        content = f.read_text()
        assert "x = 2\ny = 3\n" == content

    @pytest.mark.asyncio
    async def test_preserves_trailing_whitespace_in_markdown(self, tool, tmp_path):
        f = tmp_path / "doc.md"
        f.write_text("# Title\n", encoding="utf-8")
        # Markdown uses trailing double-space for line breaks
        result = await tool.execute(
            path=str(f), old_text="# Title", new_text="# Title  \nSubtitle  ",
        )
        assert "Successfully" in result
        content = f.read_text()
        # Trailing spaces should be preserved for markdown
        assert "Title  " in content
        assert "Subtitle  " in content


# ---------------------------------------------------------------------------
# File size protection
# ---------------------------------------------------------------------------


class TestFileSizeProtection:
    """Editing extremely large files should be rejected."""

    @pytest.fixture()
    def tool(self, tmp_path):
        return EditFileTool(workspace=tmp_path)

    @pytest.mark.asyncio
    async def test_rejects_file_over_size_limit(self, tool, tmp_path):
        f = tmp_path / "huge.txt"
        f.write_text("x", encoding="utf-8")

        class FakeStat:
            def __init__(self, real_stat):
                self._real = real_stat

            def __getattr__(self, name):
                return getattr(self._real, name)

            @property
            def st_size(self):
                return 2 * 1024 * 1024 * 1024  # 2 GiB

        import unittest.mock
        with unittest.mock.patch.object(type(f), 'stat', return_value=FakeStat(f.stat())):
            result = await tool.execute(path=str(f), old_text="x", new_text="y")
        assert "Error" in result
        assert "too large" in result.lower() or "size" in result.lower()


# ---------------------------------------------------------------------------
# Stale detection with content-equality fallback
# ---------------------------------------------------------------------------


class TestStaleDetectionContentFallback:
    """When mtime changed but file content is unchanged, edit should proceed without warning."""

    @pytest.fixture()
    def read_tool(self, tmp_path):
        return ReadFileTool(workspace=tmp_path)

    @pytest.fixture()
    def edit_tool(self, tmp_path):
        return EditFileTool(workspace=tmp_path)

    @pytest.mark.asyncio
    async def test_mtime_bump_same_content_no_warning(self, read_tool, edit_tool, tmp_path):
        f = tmp_path / "a.py"
        f.write_text("hello world", encoding="utf-8")
        await read_tool.execute(path=str(f))

        # Bump mtime without changing content.
        stat = f.stat()
        os.utime(f, (stat.st_atime, stat.st_mtime + 10))

        result = await edit_tool.execute(path=str(f), old_text="world", new_text="earth")
        assert "Successfully" in result
        # Should NOT warn about modification since content is the same
        assert "modified" not in result.lower()
