"""Tests for ToolArtifactMiddleware diff stamping on edit_file/write_file."""

from __future__ import annotations

from types import SimpleNamespace
from typing import Any

import pytest
from langchain_core.messages import ToolMessage

from agent.middleware.tool_artifact import ToolArtifactMiddleware
from agent.utils import sandbox_state


class FakeReadResult:
    def __init__(
        self, *, content: str | None = None, encoding: str = "utf-8", error: str | None = None
    ) -> None:
        self.error = error
        self.file_data = None if content is None else {"content": content, "encoding": encoding}


class FakeBackend:
    def __init__(self, results: dict[str, Any]) -> None:
        self.results = results
        self.reads: list[str] = []

    def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> Any:
        self.reads.append(file_path)
        result = self.results.get(file_path)
        if isinstance(result, Exception):
            raise result
        if result is None:
            return FakeReadResult(error="File not found")
        return result

    async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> Any:
        return self.read(file_path, offset, limit)


def _request(name: str, args: dict[str, Any], thread_id: str = "t1") -> Any:
    return SimpleNamespace(
        tool_call={"name": name, "args": args, "id": "call-1"},
        runtime=SimpleNamespace(config={"configurable": {"thread_id": thread_id}}),
    )


def _ok(name: str) -> ToolMessage:
    return ToolMessage(content="ok", tool_call_id="call-1", status="success", name=name)


@pytest.fixture
def register_backend():
    registered: list[str] = []

    def _register(thread_id: str, backend: Any) -> Any:
        sandbox_state.SANDBOX_BACKENDS[thread_id] = backend
        registered.append(thread_id)
        return backend

    yield _register
    for thread_id in registered:
        sandbox_state.SANDBOX_BACKENDS.pop(thread_id, None)


async def test_edit_file_stamps_full_file_diff(register_backend) -> None:
    backend = FakeBackend({"/repo/a.py": FakeReadResult(content="line1\nOLD\nline3\n")})
    register_backend("t1", backend)
    request = _request(
        "edit_file", {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"}
    )

    async def handler(_req: Any) -> ToolMessage:
        return _ok("edit_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact == {
        "diff": {
            "filePath": "/repo/a.py",
            "originalContent": "line1\nOLD\nline3\n",
            "newContent": "line1\nNEW\nline3\n",
            "isNewFile": False,
        }
    }
    assert backend.reads == ["/repo/a.py"]


async def test_edit_file_replace_all(register_backend) -> None:
    backend = FakeBackend({"/repo/a.py": FakeReadResult(content="x x x")})
    register_backend("t1", backend)
    request = _request(
        "edit_file",
        {"file_path": "/repo/a.py", "old_string": "x", "new_string": "y", "replace_all": True},
    )

    async def handler(_req: Any) -> ToolMessage:
        return _ok("edit_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact["diff"]["newContent"] == "y y y"


async def test_edit_file_missing_old_string_skips(register_backend) -> None:
    backend = FakeBackend({"/repo/a.py": FakeReadResult(content="nothing here")})
    register_backend("t1", backend)
    request = _request(
        "edit_file", {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"}
    )

    async def handler(_req: Any) -> ToolMessage:
        return _ok("edit_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact is None


async def test_write_file_new_file(register_backend) -> None:
    backend = FakeBackend({"/repo/new.py": FakeReadResult(error="File not found")})
    register_backend("t1", backend)
    request = _request("write_file", {"file_path": "/repo/new.py", "content": "hello\n"})

    async def handler(_req: Any) -> ToolMessage:
        return _ok("write_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact == {
        "diff": {
            "filePath": "/repo/new.py",
            "originalContent": None,
            "newContent": "hello\n",
            "isNewFile": True,
        }
    }


async def test_write_file_overwrite(register_backend) -> None:
    backend = FakeBackend({"/repo/x.py": FakeReadResult(content="old content\n")})
    register_backend("t1", backend)
    request = _request("write_file", {"file_path": "/repo/x.py", "content": "new content\n"})

    async def handler(_req: Any) -> ToolMessage:
        return _ok("write_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact["diff"]["originalContent"] == "old content\n"
    assert result.artifact["diff"]["newContent"] == "new content\n"
    assert result.artifact["diff"]["isNewFile"] is False


async def test_non_edit_tool_is_untouched(register_backend) -> None:
    backend = FakeBackend({})
    register_backend("t1", backend)
    request = _request("read_file", {"file_path": "/repo/a.py"})

    async def handler(_req: Any) -> ToolMessage:
        return _ok("read_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact is None
    assert backend.reads == []


async def test_error_result_is_not_stamped(register_backend) -> None:
    backend = FakeBackend({"/repo/a.py": FakeReadResult(content="OLD")})
    register_backend("t1", backend)
    request = _request(
        "edit_file", {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"}
    )

    async def handler(_req: Any) -> ToolMessage:
        return ToolMessage(content="boom", tool_call_id="call-1", status="error", name="edit_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact is None


async def test_missing_backend_is_graceful() -> None:
    request = _request(
        "edit_file",
        {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"},
        thread_id="absent-thread",
    )

    async def handler(_req: Any) -> ToolMessage:
        return _ok("edit_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact is None


async def test_binary_read_skips(register_backend) -> None:
    backend = FakeBackend({"/repo/img.png": FakeReadResult(content="QkFTRTY0", encoding="base64")})
    register_backend("t1", backend)
    request = _request(
        "edit_file", {"file_path": "/repo/img.png", "old_string": "x", "new_string": "y"}
    )

    async def handler(_req: Any) -> ToolMessage:
        return _ok("edit_file")

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact is None


async def test_existing_artifact_is_merged(register_backend) -> None:
    backend = FakeBackend({"/repo/a.py": FakeReadResult(content="OLD\n")})
    register_backend("t1", backend)
    request = _request(
        "edit_file", {"file_path": "/repo/a.py", "old_string": "OLD", "new_string": "NEW"}
    )

    async def handler(_req: Any) -> ToolMessage:
        message = _ok("edit_file")
        message.artifact = {"existing": "kept"}
        return message

    result = await ToolArtifactMiddleware().awrap_tool_call(request, handler)

    assert result.artifact["existing"] == "kept"
    assert result.artifact["diff"]["newContent"] == "NEW\n"
