from __future__ import annotations

from pathlib import Path

import pytest

from tests.mock.utils import collect_result
from vibe.acp.tools.builtins.write_file import AcpWriteFileState, WriteFile
from vibe.core.tools.base import ToolError
from vibe.core.tools.builtins.write_file import (
    WriteFileArgs,
    WriteFileConfig,
    WriteFileResult,
)
from vibe.core.types import ToolCallEvent, ToolResultEvent


class MockClient:
    def __init__(
        self, write_error: Exception | None = None, file_exists: bool = False
    ) -> None:
        self._write_error = write_error
        self._file_exists = file_exists
        self._write_text_file_called = False
        self._session_update_called = False
        self._last_write_params: dict[str, str] = {}

    async def write_text_file(
        self, content: str, path: str, session_id: str, **kwargs
    ) -> None:
        self._write_text_file_called = True
        self._last_write_params = {
            "content": content,
            "path": path,
            "session_id": session_id,
        }

        if self._write_error:
            raise self._write_error

    async def session_update(self, session_id: str, update, **kwargs) -> None:
        self._session_update_called = True


@pytest.fixture
def mock_client() -> MockClient:
    return MockClient()


@pytest.fixture
def acp_write_file_tool(
    mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> WriteFile:
    monkeypatch.chdir(tmp_path)
    config = WriteFileConfig()
    state = AcpWriteFileState.model_construct(
        client=mock_client, session_id="test_session_123"
    )
    return WriteFile(config_getter=lambda: config, state=state)


class TestAcpWriteFileBasic:
    def test_get_name(self) -> None:
        assert WriteFile.get_name() == "write_file"


class TestAcpWriteFileExecution:
    @pytest.mark.asyncio
    async def test_run_success_new_file(
        self, acp_write_file_tool: WriteFile, mock_client: MockClient, tmp_path: Path
    ) -> None:
        test_file = tmp_path / "test_file.txt"
        args = WriteFileArgs(path=str(test_file), content="Hello, world!")
        result = await collect_result(acp_write_file_tool.run(args))

        assert isinstance(result, WriteFileResult)
        assert result.path == str(test_file)
        assert result.content == "Hello, world!"
        assert result.bytes_written == len(b"Hello, world!")
        assert mock_client._write_text_file_called

        # Verify write_text_file was called correctly
        params = mock_client._last_write_params
        assert params["session_id"] == "test_session_123"
        assert params["path"] == str(test_file)
        assert params["content"] == "Hello, world!"

    @pytest.mark.asyncio
    async def test_run_existing_file_raises(
        self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.chdir(tmp_path)
        tool = WriteFile(
            config_getter=lambda: WriteFileConfig(),
            state=AcpWriteFileState.model_construct(
                client=mock_client, session_id="test_session"
            ),
        )

        test_file = tmp_path / "existing_file.txt"
        test_file.touch()
        args = WriteFileArgs(path=str(test_file), content="New content")
        with pytest.raises(ToolError, match="already exists"):
            await collect_result(tool.run(args))

        assert not mock_client._write_text_file_called

    @pytest.mark.asyncio
    @pytest.mark.parametrize("input_newline", ["\r\n", "\r", "\n"])
    @pytest.mark.parametrize("os_newline", ["\n", "\r\n"])
    async def test_run_writes_with_os_linesep(
        self,
        input_newline: str,
        os_newline: str,
        mock_client: MockClient,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        monkeypatch.chdir(tmp_path)
        monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", os_newline)
        tool = WriteFile(
            config_getter=lambda: WriteFileConfig(),
            state=AcpWriteFileState.model_construct(
                client=mock_client, session_id="test_session"
            ),
        )

        test_file = tmp_path / "test.txt"
        content = input_newline.join(["line 1", "line 2", "line 3"])
        args = WriteFileArgs(path=str(test_file), content=content)
        await collect_result(tool.run(args))

        assert mock_client._last_write_params["content"] == os_newline.join([
            "line 1",
            "line 2",
            "line 3",
        ])

    @pytest.mark.asyncio
    async def test_run_normalizes_mixed_newlines(
        self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.chdir(tmp_path)
        monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", "\n")
        tool = WriteFile(
            config_getter=lambda: WriteFileConfig(),
            state=AcpWriteFileState.model_construct(
                client=mock_client, session_id="test_session"
            ),
        )

        test_file = tmp_path / "test.txt"
        args = WriteFileArgs(
            path=str(test_file), content="line 1\r\nline 2\nline 3\rline 4"
        )
        await collect_result(tool.run(args))

        assert (
            mock_client._last_write_params["content"]
            == "line 1\nline 2\nline 3\nline 4"
        )

    @pytest.mark.asyncio
    async def test_run_write_error(
        self, mock_client: MockClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.chdir(tmp_path)
        mock_client._write_error = RuntimeError("Permission denied")

        tool = WriteFile(
            config_getter=lambda: WriteFileConfig(),
            state=AcpWriteFileState.model_construct(
                client=mock_client, session_id="test_session"
            ),
        )

        test_file = tmp_path / "test.txt"
        args = WriteFileArgs(path=str(test_file), content="test")
        with pytest.raises(ToolError) as exc_info:
            await collect_result(tool.run(args))

        assert str(exc_info.value) == f"Error writing {test_file}: Permission denied"

    @pytest.mark.asyncio
    async def test_run_without_connection(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.chdir(tmp_path)
        tool = WriteFile(
            config_getter=lambda: WriteFileConfig(),
            state=AcpWriteFileState.model_construct(
                client=None, session_id="test_session"
            ),
        )

        args = WriteFileArgs(path=str(tmp_path / "test.txt"), content="test")
        with pytest.raises(ToolError) as exc_info:
            await collect_result(tool.run(args))

        assert (
            str(exc_info.value)
            == "Client not available in tool state. This tool can only be used within an ACP session."
        )

    @pytest.mark.asyncio
    async def test_run_without_session_id(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.chdir(tmp_path)
        mock_client = MockClient()
        tool = WriteFile(
            config_getter=lambda: WriteFileConfig(),
            state=AcpWriteFileState.model_construct(
                client=mock_client, session_id=None
            ),
        )

        args = WriteFileArgs(path=str(tmp_path / "test.txt"), content="test")
        with pytest.raises(ToolError) as exc_info:
            await collect_result(tool.run(args))

        assert (
            str(exc_info.value)
            == "Session ID not available in tool state. This tool can only be used within an ACP session."
        )


class TestAcpWriteFileSessionUpdates:
    def test_tool_call_session_update(self) -> None:
        event = ToolCallEvent(
            tool_name="write_file",
            tool_call_id="test_call_123",
            args=WriteFileArgs(path="/tmp/test.txt", content="Hello"),
            tool_class=WriteFile,
        )

        update = WriteFile.tool_call_session_update(event)
        assert update is not None
        assert update.session_update == "tool_call"
        assert update.tool_call_id == "test_call_123"
        assert update.kind == "edit"
        assert update.title is not None
        assert update.content is not None
        assert isinstance(update.content, list)
        assert len(update.content) == 1
        assert update.content[0].type == "diff"
        assert update.content[0].path == "/tmp/test.txt"
        assert update.content[0].old_text is None
        assert update.content[0].new_text == "Hello"
        assert update.locations is not None
        assert len(update.locations) == 1
        assert update.locations[0].path == str(Path("/tmp/test.txt").resolve())

    def test_tool_call_session_update_passes_content_through(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setattr("vibe.acp.tools.builtins.write_file.os.linesep", "\r\n")
        content = "line 1\r\nline 2\nline 3\rline 4"
        event = ToolCallEvent(
            tool_name="write_file",
            tool_call_id="test_call_123",
            args=WriteFileArgs(path="/tmp/test.txt", content=content),
            tool_class=WriteFile,
        )

        update = WriteFile.tool_call_session_update(event)
        assert update is not None
        assert update.content is not None
        assert isinstance(update.content, list)
        assert update.content[0].new_text == content

    def test_tool_call_session_update_invalid_args(self) -> None:
        from vibe.core.types import FunctionCall, ToolCall

        class InvalidArgs:
            pass

        event = ToolCallEvent.model_construct(
            tool_name="write_file",
            tool_call_id="test_call_123",
            args=InvalidArgs(),  # type: ignore[arg-type]
            tool_class=WriteFile,
            llm_tool_call=ToolCall(
                function=FunctionCall(name="write_file", arguments="{}"),
                type="function",
                index=0,
            ),
        )

        update = WriteFile.tool_call_session_update(event)
        assert update is not None
        assert update.title == "write_file"

    def test_tool_result_session_update(self) -> None:
        result = WriteFileResult(path="/tmp/test.txt", content="Hello", bytes_written=5)

        event = ToolResultEvent(
            tool_name="write_file",
            tool_call_id="test_call_123",
            result=result,
            tool_class=WriteFile,
        )

        update = WriteFile.tool_result_session_update(event)
        assert update is not None
        assert update.session_update == "tool_call_update"
        assert update.tool_call_id == "test_call_123"
        assert update.status == "completed"
        assert update.content is not None
        assert isinstance(update.content, list)
        assert len(update.content) == 1
        assert update.content[0].type == "diff"
        assert update.content[0].path == "/tmp/test.txt"
        assert update.content[0].old_text is None
        assert update.content[0].new_text == "Hello"
        assert update.locations is not None
        assert len(update.locations) == 1
        assert update.locations[0].path == str(Path("/tmp/test.txt").resolve())

    def test_tool_result_session_update_invalid_result(self) -> None:
        class InvalidResult:
            pass

        event = ToolResultEvent.model_construct(
            tool_name="write_file",
            tool_call_id="test_call_123",
            result=InvalidResult(),  # type: ignore[arg-type]
            tool_class=WriteFile,
        )

        update = WriteFile.tool_result_session_update(event)
        assert update is not None
        assert update.status == "failed"
