from __future__ import annotations

import asyncio
from pathlib import Path

import pytest

from tests.mock.utils import collect_result
from vibe.core.tools.base import BaseToolState, ToolError
from vibe.core.tools.builtins.edit import Edit, EditArgs, EditConfig, EditResult
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay
from vibe.core.types import ToolResultEvent


def _make_edit() -> Edit:
    return Edit(config_getter=lambda: EditConfig(), state=BaseToolState())


def _write(tmp_path: Path, name: str, content: str) -> Path:
    p = tmp_path / name
    p.write_text(content)
    return p


@pytest.mark.asyncio
async def test_exact_match_replaces(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "hello world\n")
    edit = _make_edit()

    result = await collect_result(
        edit.run(EditArgs(file_path="f.txt", old_string="hello", new_string="goodbye"))
    )

    assert (tmp_path / "f.txt").read_text() == "goodbye world\n"
    assert result.file == str(tmp_path / "f.txt")
    assert result.message == "The file has been updated successfully."


@pytest.mark.asyncio
async def test_replace_all(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "aaa bbb aaa\n")
    edit = _make_edit()

    result = await collect_result(
        edit.run(
            EditArgs(
                file_path="f.txt", old_string="aaa", new_string="ccc", replace_all=True
            )
        )
    )

    assert (tmp_path / "f.txt").read_text() == "ccc bbb ccc\n"
    assert result.message == (
        "The file has been updated. All occurrences were successfully replaced"
    )


@pytest.mark.asyncio
async def test_not_found_raises(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "hello world\n")
    edit = _make_edit()

    with pytest.raises(ToolError, match="String to replace not found in file"):
        await collect_result(
            edit.run(EditArgs(file_path="f.txt", old_string="missing", new_string="x"))
        )


@pytest.mark.asyncio
async def test_multiple_matches_without_replace_all_raises(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "aaa bbb aaa\n")
    edit = _make_edit()

    with pytest.raises(ToolError, match="Found 2 matches"):
        await collect_result(
            edit.run(EditArgs(file_path="f.txt", old_string="aaa", new_string="ccc"))
        )


@pytest.mark.asyncio
async def test_old_equals_new_raises(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "hello\n")
    edit = _make_edit()

    with pytest.raises(ToolError, match="No changes to make"):
        await collect_result(
            edit.run(
                EditArgs(file_path="f.txt", old_string="hello", new_string="hello")
            )
        )


@pytest.mark.asyncio
async def test_empty_old_string_raises(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "hello\n")
    edit = _make_edit()

    with pytest.raises(ToolError, match="old_string cannot be empty"):
        await collect_result(
            edit.run(EditArgs(file_path="f.txt", old_string="", new_string="x"))
        )


@pytest.mark.asyncio
async def test_file_not_found_raises(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    edit = _make_edit()

    with pytest.raises(ToolError, match="File does not exist"):
        await collect_result(
            edit.run(
                EditArgs(
                    file_path="/nonexistent/file.py", old_string="x", new_string="y"
                )
            )
        )


@pytest.mark.asyncio
async def test_empty_file_path_raises(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    edit = _make_edit()

    with pytest.raises(ToolError, match="File path cannot be empty"):
        await collect_result(
            edit.run(EditArgs(file_path="", old_string="x", new_string="y"))
        )


@pytest.mark.asyncio
async def test_deletion_removes_exact_string(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "line1\nline2\nline3\n")
    edit = _make_edit()

    await collect_result(
        edit.run(EditArgs(file_path="f.txt", old_string="line2\n", new_string=""))
    )

    assert (tmp_path / "f.txt").read_text() == "line1\nline3\n"


@pytest.mark.asyncio
async def test_parallel_edits_same_file_all_land(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "A\nB\nC\nD\n")
    edit = _make_edit()

    await asyncio.gather(
        collect_result(
            edit.run(EditArgs(file_path="f.txt", old_string="A", new_string="A1"))
        ),
        collect_result(
            edit.run(EditArgs(file_path="f.txt", old_string="B", new_string="B1"))
        ),
        collect_result(
            edit.run(EditArgs(file_path="f.txt", old_string="C", new_string="C1"))
        ),
        collect_result(
            edit.run(EditArgs(file_path="f.txt", old_string="D", new_string="D1"))
        ),
    )

    assert (tmp_path / "f.txt").read_text() == "A1\nB1\nC1\nD1\n"


@pytest.mark.asyncio
async def test_relative_path_resolved_from_cwd(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    (tmp_path / "sub").mkdir(exist_ok=True)
    _write(tmp_path / "sub", "f.txt", "old")
    edit = _make_edit()

    result = await collect_result(
        edit.run(EditArgs(file_path="sub/f.txt", old_string="old", new_string="new"))
    )

    assert (tmp_path / "sub" / "f.txt").read_text() == "new"
    assert result.file == str(tmp_path / "sub" / "f.txt")


def test_format_call_display() -> None:
    args = EditArgs(file_path="/abs/foo.py", old_string="old", new_string="new")
    display = Edit.format_call_display(args)

    assert isinstance(display, ToolCallDisplay)
    assert display.summary == "Editing foo.py"


def test_get_result_display() -> None:
    result = EditResult(
        file="/path/to/foo.py",
        message="The file has been updated successfully.",
        old_string="old",
        new_string="new",
    )
    event = ToolResultEvent(
        tool_call_id="test", tool_name="edit", tool_class=None, result=result
    )
    display = Edit.get_result_display(event)

    assert isinstance(display, ToolResultDisplay)
    assert display.success is True
    assert "foo.py" in display.message


def test_ui_start_lines_not_part_of_model_contract() -> None:
    result = EditResult(file="/x", message="m", old_string="a", new_string="b")
    result._ui_start_lines = [42]

    assert result.ui_start_lines == [42]
    assert "ui_start_lines" not in result.model_dump()
    assert "_ui_start_lines" not in result.model_dump()
    assert "ui_start_lines" not in result.model_dump_json()
    assert "ui_start_lines" not in EditResult.model_fields
    assert "ui_start_lines" not in EditResult.model_json_schema().get("properties", {})
    assert "ui_start_lines" not in dict(result)


@pytest.mark.asyncio
async def test_ui_start_lines_computed_at_edit_site(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "alpha\nbeta\ngamma\ndelta\n")
    edit = _make_edit()

    result = await collect_result(
        edit.run(EditArgs(file_path="f.txt", old_string="gamma", new_string="GAMMA"))
    )

    assert result.ui_start_lines == [3]


@pytest.mark.asyncio
async def test_ui_start_lines_set_for_pure_deletion(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "keep1\nkeep2\nremove\nkeep3\n")
    edit = _make_edit()

    result = await collect_result(
        edit.run(EditArgs(file_path="f.txt", old_string="remove\n", new_string=""))
    )

    assert result.ui_start_lines == [3]


@pytest.mark.asyncio
async def test_ui_start_lines_lists_all_occurrences_for_replace_all(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "x\ntgt\ny\ntgt\nz\ntgt\n")
    edit = _make_edit()

    result = await collect_result(
        edit.run(
            EditArgs(
                file_path="f.txt", old_string="tgt", new_string="TGT", replace_all=True
            )
        )
    )

    assert result.ui_start_lines == [2, 4, 6]


@pytest.mark.asyncio
async def test_ui_start_lines_single_entry_without_replace_all(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.chdir(tmp_path)
    _write(tmp_path, "f.txt", "a\nuniq\nb\n")
    edit = _make_edit()

    result = await collect_result(
        edit.run(EditArgs(file_path="f.txt", old_string="uniq", new_string="UNIQ"))
    )

    assert result.ui_start_lines == [2]
