"""Tests for MemoryTool and MemoryBackend — all 6 picoagents commands.

Commands tested: view, create, str_replace, insert, delete, rename.
Also covers: directory listing via view, parent-dir auto-creation, and
path-traversal protection.

Run with: uv run pytest tests/tools/test_memory_tool.py -v
"""

from __future__ import annotations

import pytest

from agentbyte.tools.memory_tool import MemoryBackend, MemoryTool


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def backend(tmp_path):
    """MemoryBackend rooted at a temporary directory."""
    return MemoryBackend(base_path=tmp_path)


@pytest.fixture()
def tool(tmp_path):
    """MemoryTool rooted at a temporary directory."""
    return MemoryTool(base_path=tmp_path)


# ---------------------------------------------------------------------------
# MemoryBackend — unit tests per command
# ---------------------------------------------------------------------------


class TestMemoryBackendCreate:
    def test_create_writes_file_content(self, backend):
        msg = backend.create("notes.md", "Hello world")
        assert "notes.md" in msg
        assert (backend.base_path / "notes.md").read_text() == "Hello world"

    def test_create_overwrites_existing_file(self, backend):
        backend.create("file.txt", "original")
        backend.create("file.txt", "updated")
        assert (backend.base_path / "file.txt").read_text() == "updated"

    def test_create_makes_parent_directories(self, backend):
        backend.create("bugs/race/notes.md", "race condition pattern")
        assert (backend.base_path / "bugs" / "race" / "notes.md").exists()


class TestMemoryBackendView:
    def test_view_returns_file_content(self, backend):
        backend.create("doc.md", "# Title\ncontent")
        assert backend.view("doc.md") == "# Title\ncontent"

    def test_view_directory_returns_listing(self, backend):
        backend.create("a/one.md", "1")
        backend.create("a/two.md", "2")
        listing = backend.view("a")
        assert any("one.md" in entry for entry in listing)
        assert any("two.md" in entry for entry in listing)

    def test_view_root_lists_all_files(self, backend):
        backend.create("x.md", "x")
        backend.create("sub/y.md", "y")
        listing = backend.view("/")  # leading slash stripped
        assert len(listing) == 2

    def test_view_missing_path_raises(self, backend):
        with pytest.raises(FileNotFoundError):
            backend.view("missing.md")


class TestMemoryBackendStrReplace:
    def test_str_replace_first_occurrence_only(self, backend):
        backend.create("file.md", "foo bar foo")
        backend.str_replace("file.md", "foo", "baz")
        assert (backend.base_path / "file.md").read_text() == "baz bar foo"

    def test_str_replace_missing_string_raises(self, backend):
        backend.create("file.md", "hello")
        with pytest.raises(ValueError, match="not found"):
            backend.str_replace("file.md", "missing", "x")

    def test_str_replace_missing_file_raises(self, backend):
        with pytest.raises(FileNotFoundError):
            backend.str_replace("ghost.md", "a", "b")


class TestMemoryBackendInsert:
    def test_insert_after_line_zero_prepends(self, backend):
        backend.create("file.md", "line1\nline2\n")
        backend.insert("file.md", 0, "line0")
        lines = (backend.base_path / "file.md").read_text().splitlines()
        assert lines[0] == "line0"
        assert lines[1] == "line1"

    def test_insert_after_middle_line(self, backend):
        backend.create("file.md", "line1\nline2\nline3\n")
        backend.insert("file.md", 2, "inserted")
        lines = (backend.base_path / "file.md").read_text().splitlines()
        assert lines[2] == "inserted"
        assert lines[3] == "line3"

    def test_insert_out_of_range_raises(self, backend):
        backend.create("file.md", "only one line\n")
        with pytest.raises(ValueError, match="out of range"):
            backend.insert("file.md", 999, "x")

    def test_insert_missing_file_raises(self, backend):
        with pytest.raises(FileNotFoundError):
            backend.insert("ghost.md", 0, "x")


class TestMemoryBackendDelete:
    def test_delete_removes_file(self, backend):
        backend.create("to_delete.md", "bye")
        backend.delete("to_delete.md")
        assert not (backend.base_path / "to_delete.md").exists()

    def test_delete_missing_file_raises(self, backend):
        with pytest.raises(FileNotFoundError):
            backend.delete("ghost.md")

    def test_delete_directory_raises(self, backend):
        (backend.base_path / "mydir").mkdir()
        with pytest.raises(ValueError, match="only supports files"):
            backend.delete("mydir")


class TestMemoryBackendRename:
    def test_rename_moves_file(self, backend):
        backend.create("old.md", "content")
        backend.rename("old.md", "new.md")
        assert not (backend.base_path / "old.md").exists()
        assert (backend.base_path / "new.md").read_text() == "content"

    def test_rename_creates_parent_dirs(self, backend):
        backend.create("src.md", "data")
        backend.rename("src.md", "archive/src.md")
        assert (backend.base_path / "archive" / "src.md").exists()

    def test_rename_missing_source_raises(self, backend):
        with pytest.raises(FileNotFoundError):
            backend.rename("ghost.md", "new.md")

    def test_rename_existing_destination_raises(self, backend):
        backend.create("a.md", "a")
        backend.create("b.md", "b")
        with pytest.raises(FileExistsError):
            backend.rename("a.md", "b.md")


class TestMemoryBackendSecurity:
    def test_path_traversal_raises(self, backend):
        with pytest.raises(ValueError, match="outside memory directory"):
            backend.view("../../etc/passwd")

    def test_absolute_path_is_sandboxed(self, backend):
        """Leading slashes are stripped, so /etc/evil.md maps inside the sandbox."""
        # This should NOT raise — the path is treated as relative to base_path.
        backend.create("/etc/evil.md", "sandboxed content")
        content = backend.view("/etc/evil.md")
        assert content == "sandboxed content"

    def test_empty_path_raises(self, backend):
        with pytest.raises(ValueError, match="cannot be empty"):
            backend.view("")


# ---------------------------------------------------------------------------
# MemoryTool — async integration tests via execute()
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_tool_create_and_view(tool):
    result = await tool.execute({"command": "create", "path": "memo.md", "file_text": "hello"})
    assert result.success is True

    result = await tool.execute({"command": "view", "path": "memo.md"})
    assert result.success is True
    assert result.result == "hello"


@pytest.mark.asyncio
async def test_tool_str_replace(tool):
    await tool.execute({"command": "create", "path": "f.md", "file_text": "old text here"})
    result = await tool.execute(
        {"command": "str_replace", "path": "f.md", "old_str": "old", "new_str": "new"}
    )
    assert result.success is True
    view = await tool.execute({"command": "view", "path": "f.md"})
    assert "new text" in view.result


@pytest.mark.asyncio
async def test_tool_insert(tool):
    await tool.execute({"command": "create", "path": "lines.md", "file_text": "A\nB\n"})
    result = await tool.execute(
        {"command": "insert", "path": "lines.md", "insert_line": 1, "insert_text": "INSERTED"}
    )
    assert result.success is True


@pytest.mark.asyncio
async def test_tool_delete(tool):
    await tool.execute({"command": "create", "path": "gone.md", "file_text": "bye"})
    result = await tool.execute({"command": "delete", "path": "gone.md"})
    assert result.success is True


@pytest.mark.asyncio
async def test_tool_rename(tool):
    await tool.execute({"command": "create", "path": "src.md", "file_text": "data"})
    result = await tool.execute({"command": "rename", "path": "src.md", "new_path": "dst.md"})
    assert result.success is True


@pytest.mark.asyncio
async def test_tool_view_directory(tool):
    await tool.execute({"command": "create", "path": "dir/a.md", "file_text": "a"})
    await tool.execute({"command": "create", "path": "dir/b.md", "file_text": "b"})
    result = await tool.execute({"command": "view", "path": "dir"})
    assert result.success is True
    assert isinstance(result.result, list)
    assert len(result.result) == 2


@pytest.mark.asyncio
async def test_tool_unknown_command_returns_failure(tool):
    result = await tool.execute({"command": "rm_rf"})
    assert result.success is False
    assert "Unknown command" in result.error


@pytest.mark.asyncio
async def test_tool_path_traversal_returns_failure(tool):
    result = await tool.execute({"command": "view", "path": "../../etc/passwd"})
    assert result.success is False


@pytest.mark.asyncio
async def test_tool_parameters_schema_lists_all_commands(tool):
    schema = tool.parameters
    commands = schema["properties"]["command"]["enum"]
    assert set(commands) == {"view", "create", "str_replace", "insert", "delete", "rename"}
