"""Tests for translating MCP-produced local files into virtual sandbox paths.

Regression coverage for GitHub issue #3597: Playwright MCP (and similar stdio
servers) write files to a path the sandbox/artifact API cannot resolve. The MCP
tool wrapper pins stdio cwd/temp under the thread's mounted user-data tree and
rewrites returned file references to ``/mnt/user-data/...`` virtual paths.
"""

from pathlib import Path
from unittest.mock import patch

import pytest
from mcp.types import CallToolResult, ResourceLink, TextContent

from deerflow.config.paths import VIRTUAL_PATH_PREFIX, Paths
from deerflow.mcp import tools as mcp_tools


@pytest.fixture
def paths(tmp_path: Path) -> Paths:
    return Paths(tmp_path)


def _patch_paths(paths: Paths):
    return patch("deerflow.mcp.tools.get_paths", return_value=paths)


def _workspace_file(paths: Paths, relative_path: str, *, content: bytes = b"data") -> Path:
    file_path = paths.sandbox_work_dir("t1", user_id="u1") / relative_path
    file_path.parent.mkdir(parents=True, exist_ok=True)
    file_path.write_bytes(content)
    return file_path


class TestLocalPathFromUri:
    def test_file_uri(self):
        assert mcp_tools._local_path_from_uri("file:///tmp/shot.png") == Path("/tmp/shot.png")

    def test_bare_absolute_path(self):
        assert mcp_tools._local_path_from_uri("/var/data/out.pdf") == Path("/var/data/out.pdf")

    def test_file_uri_with_url_encoded_spaces(self):
        assert mcp_tools._local_path_from_uri("file:///tmp/my%20shot.png") == Path("/tmp/my shot.png")

    def test_remote_uri_is_ignored(self):
        assert mcp_tools._local_path_from_uri("https://example.com/a.png") is None
        assert mcp_tools._local_path_from_uri("data:image/png;base64,AAAA") is None

    def test_relative_path_is_ignored_without_base_dir(self):
        assert mcp_tools._local_path_from_uri("relative/path.txt") is None

    def test_relative_path_uses_base_dir_when_provided(self, tmp_path: Path):
        assert mcp_tools._local_path_from_uri("./shot.png", base_dir=tmp_path) == tmp_path / "shot.png"
        assert mcp_tools._local_path_from_uri("temp/page.yml", base_dir=tmp_path) == tmp_path / "temp/page.yml"

    def test_file_uri_with_relative_path_is_ignored(self):
        assert mcp_tools._local_path_from_uri("file:relative.txt") is None

    def test_file_uri_with_empty_path_is_ignored(self):
        assert mcp_tools._local_path_from_uri("file://") is None

    def test_file_uri_with_localhost_host(self):
        # file://localhost/abs/path is the host form of file:///abs/path.
        assert mcp_tools._local_path_from_uri("file://localhost/tmp/shot.png") == Path("/tmp/shot.png")

    def test_empty_is_ignored(self):
        assert mcp_tools._local_path_from_uri("") is None


class TestLocalUriToVirtualPath:
    def test_workspace_file_translates_to_virtual_workspace_path(self, paths: Paths):
        src = _workspace_file(paths, "temp/page.yml")

        with _patch_paths(paths):
            result = mcp_tools._local_uri_to_virtual_path(str(src), thread_id="t1", user_id="u1")

        assert result == f"{VIRTUAL_PATH_PREFIX}/workspace/temp/page.yml"

    def test_outputs_file_translates_without_copy(self, paths: Paths):
        outputs = paths.sandbox_outputs_dir("t1", user_id="u1")
        outputs.mkdir(parents=True)
        src = outputs / "report.pdf"
        src.write_bytes(b"pdf")

        with _patch_paths(paths):
            result = mcp_tools._local_uri_to_virtual_path(str(src), thread_id="t1", user_id="u1")

        assert result == f"{VIRTUAL_PATH_PREFIX}/outputs/report.pdf"
        assert list(outputs.iterdir()) == [src]

    def test_relative_review_case_translates_against_cwd(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, "temp/page-2026-06-16T10-21-46-864Z.yml")

        with _patch_paths(paths):
            result = mcp_tools._local_uri_to_virtual_path(
                "temp/page-2026-06-16T10-21-46-864Z.yml",
                thread_id="t1",
                user_id="u1",
                source_base_dir=workspace,
            )

        assert result == f"{VIRTUAL_PATH_PREFIX}/workspace/temp/page-2026-06-16T10-21-46-864Z.yml"

    def test_file_uri_inside_user_data_translates(self, paths: Paths):
        src = _workspace_file(paths, "shot.png")

        with _patch_paths(paths):
            result = mcp_tools._local_uri_to_virtual_path(f"file://{src}", thread_id="t1", user_id="u1")

        assert result == f"{VIRTUAL_PATH_PREFIX}/workspace/shot.png"

    def test_file_outside_user_data_is_not_exposed(self, tmp_path: Path, paths: Paths):
        src = tmp_path / "outside.txt"
        src.write_text("secret")

        with _patch_paths(paths):
            result = mcp_tools._local_uri_to_virtual_path(str(src), thread_id="t1", user_id="u1")

        assert result is None
        assert not paths.sandbox_outputs_dir("t1", user_id="u1").exists()

    def test_missing_file_directory_and_remote_uri_are_ignored(self, tmp_path: Path, paths: Paths):
        with _patch_paths(paths):
            assert mcp_tools._local_uri_to_virtual_path(str(tmp_path / "missing.png"), thread_id="t1", user_id="u1") is None
            assert mcp_tools._local_uri_to_virtual_path(str(tmp_path), thread_id="t1", user_id="u1") is None
            assert mcp_tools._local_uri_to_virtual_path("https://example.com/a.png", thread_id="t1", user_id="u1") is None

    def test_symlink_escape_is_not_exposed(self, tmp_path: Path, paths: Paths):
        outside = tmp_path / "outside.txt"
        outside.write_text("secret")
        link = paths.sandbox_work_dir("t1", user_id="u1") / "link.txt"
        link.parent.mkdir(parents=True)
        try:
            link.symlink_to(outside)
        except (OSError, NotImplementedError):
            pytest.skip("symlinks not supported on this platform")

        with _patch_paths(paths):
            result = mcp_tools._local_uri_to_virtual_path(str(link), thread_id="t1", user_id="u1")

        assert result is None


class TestRewriteLocalPathsInText:
    def test_review_case_temp_relative_path_is_rewritten(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, "temp/page-2026-06-16T10-21-46-864Z.yml")
        text = "Saved as temp/page-2026-06-16T10-21-46-864Z.yml."

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1", source_base_dir=workspace)

        assert result == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/temp/page-2026-06-16T10-21-46-864Z.yml."

    def test_relative_output_dir_path_is_rewritten(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, "artifacts/page.png")
        text = "Screenshot saved to artifacts/page.png"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1", source_base_dir=workspace)

        assert result == f"Screenshot saved to {VIRTUAL_PATH_PREFIX}/workspace/artifacts/page.png"

    def test_absolute_output_dir_path_inside_user_data_is_rewritten(self, paths: Paths):
        src = _workspace_file(paths, "absolute-output/page.png")
        text = f"Screenshot saved to {src}"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1")

        assert result == f"Screenshot saved to {VIRTUAL_PATH_PREFIX}/workspace/absolute-output/page.png"

    def test_tmpdir_output_under_workspace_is_rewritten(self, paths: Paths):
        src = _workspace_file(paths, ".mcp/tmp/page.png")
        text = f"Saved to {src}"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1")

        assert result == f"Saved to {VIRTUAL_PATH_PREFIX}/workspace/.mcp/tmp/page.png"

    def test_old_tmp_path_outside_user_data_is_left_untouched(self, tmp_path: Path, paths: Paths):
        src = tmp_path / "playwright-mcp-output" / "page.png"
        src.parent.mkdir()
        src.write_bytes(b"png")
        text = f"Saved to {src}"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1")

        assert result == text

    def test_playwright_markdown_path_is_rewritten_twice_without_copy(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, ".playwright-mcp/page.png", content=b"png")
        text = "### Result\n- [Screenshot](.playwright-mcp/page.png)\npath: '.playwright-mcp/page.png'"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1", source_base_dir=workspace)

        assert result.count(f"{VIRTUAL_PATH_PREFIX}/workspace/.playwright-mcp/page.png") == 2
        assert not paths.sandbox_outputs_dir("t1", user_id="u1").exists()

    def test_bare_filename_is_rewritten_only_when_changed_file_matches_uniquely(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        src = _workspace_file(paths, "page-2026-06-16T10-21-46-864Z.yml")
        text = "Saved as page-2026-06-16T10-21-46-864Z.yml."

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(
                text,
                thread_id="t1",
                user_id="u1",
                source_base_dir=workspace,
                changed_files=[src],
            )

        assert result == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/page-2026-06-16T10-21-46-864Z.yml."

    def test_bare_filename_without_changed_file_is_left_untouched(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, "page.yml")
        text = "Saved as page.yml"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1", source_base_dir=workspace)

        assert result == text

    def test_bare_filename_with_multiple_changed_matches_is_left_untouched(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        a = _workspace_file(paths, "a/page.yml")
        b = _workspace_file(paths, "b/page.yml")
        text = "Saved as page.yml"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(
                text,
                thread_id="t1",
                user_id="u1",
                source_base_dir=workspace,
                changed_files=[a, b],
            )

        assert result == text

    def test_bare_filename_does_not_rewrite_longer_filename(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        src = _workspace_file(paths, "page.yml")
        text = "Backup is page.yml.bak"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(
                text,
                thread_id="t1",
                user_id="u1",
                source_base_dir=workspace,
                changed_files=[src],
            )

        assert result == text

    def test_multiple_distinct_paths_in_one_message_all_rewritten(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, "temp/a.png")
        _workspace_file(paths, "temp/b.png")
        text = "Saved temp/a.png and temp/b.png together."

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1", source_base_dir=workspace)

        assert result == (f"Saved {VIRTUAL_PATH_PREFIX}/workspace/temp/a.png and {VIRTUAL_PATH_PREFIX}/workspace/temp/b.png together.")

    def test_markdown_link_in_parentheses_is_rewritten_without_eating_paren(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, "temp/shot.png")
        text = "See ![shot](temp/shot.png) now"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1", source_base_dir=workspace)

        assert result == f"See ![shot]({VIRTUAL_PATH_PREFIX}/workspace/temp/shot.png) now"

    def test_path_for_nonexistent_relative_file_is_left_untouched(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        text = "Saved as temp/never-created.png"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(text, thread_id="t1", user_id="u1", source_base_dir=workspace)

        assert result == text

    def test_bare_filename_is_case_sensitive(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        src = _workspace_file(paths, "Page.yml")
        text = "saved as page.yml"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(
                text,
                thread_id="t1",
                user_id="u1",
                source_base_dir=workspace,
                changed_files=[src],
            )

        assert result == text

    def test_bare_filename_not_rewritten_when_used_as_directory_segment(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        src = _workspace_file(paths, "page.yml")
        text = "nested page.yml/inner.txt path"

        with _patch_paths(paths):
            result = mcp_tools._rewrite_local_paths_in_text(
                text,
                thread_id="t1",
                user_id="u1",
                source_base_dir=workspace,
                changed_files=[src],
            )

        assert result == text


class TestWorkspaceSnapshots:
    def test_changed_workspace_files_detects_created_and_modified_files(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        existing = _workspace_file(paths, "existing.txt", content=b"old")
        before = mcp_tools._snapshot_workspace_files(workspace)

        existing.write_bytes(b"new")
        created = _workspace_file(paths, "created.txt", content=b"created")

        changed = set(mcp_tools._changed_workspace_files(workspace, before))

        assert changed == {existing, created}

    def test_snapshot_of_missing_directory_is_empty(self, tmp_path: Path):
        assert mcp_tools._snapshot_workspace_files(tmp_path / "does-not-exist") == {}

    def test_no_change_yields_no_changed_files(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, "stable.txt")
        before = mcp_tools._snapshot_workspace_files(workspace)

        assert mcp_tools._changed_workspace_files(workspace, before) == []

    def test_deleted_file_is_not_reported_as_changed(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        victim = _workspace_file(paths, "victim.txt")
        before = mcp_tools._snapshot_workspace_files(workspace)

        victim.unlink()

        assert mcp_tools._changed_workspace_files(workspace, before) == []


class TestPrepareStdioWorkspace:
    def test_creates_dirs_and_returns_snapshot(self, paths: Paths):
        existing = _workspace_file(paths, "existing.txt", content=b"old")

        source_base_dir, tmp_dir, before = mcp_tools._prepare_stdio_workspace(paths, thread_id="t1", user_id="u1")

        assert source_base_dir == paths.sandbox_work_dir("t1", user_id="u1")
        assert tmp_dir == source_base_dir / mcp_tools._MCP_TMP_SUBDIR
        assert tmp_dir.is_dir()
        assert before == {existing: (existing.stat().st_mtime_ns, existing.stat().st_size)}


class TestResultHasTextContent:
    def test_text_content_is_detected(self):
        result = CallToolResult(content=[TextContent(type="text", text="hi")], isError=False)
        assert mcp_tools._result_has_text_content(result) is True

    def test_embedded_text_resource_is_detected(self):
        from mcp.types import EmbeddedResource, TextResourceContents

        res = TextResourceContents(uri="mem://n.txt", text="n", mimeType="text/plain")
        result = CallToolResult(content=[EmbeddedResource(type="resource", resource=res)], isError=False)
        assert mcp_tools._result_has_text_content(result) is True

    def test_image_only_result_has_no_text(self):
        from mcp.types import ImageContent

        result = CallToolResult(content=[ImageContent(type="image", data="QUJD", mimeType="image/png")], isError=False)
        assert mcp_tools._result_has_text_content(result) is False

    def test_empty_content_has_no_text(self):
        result = CallToolResult(content=[], isError=False)
        assert mcp_tools._result_has_text_content(result) is False


class TestConvertCallToolResultRewrites:
    def test_resource_link_image_inside_workspace_rewritten(self, paths: Paths):
        src = _workspace_file(paths, "page.png", content=b"png")
        result = CallToolResult(
            content=[ResourceLink(type="resource_link", name="page", uri=f"file://{src}", mimeType="image/png")],
            isError=False,
        )

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["type"] == "image"
        assert content[0]["url"] == f"{VIRTUAL_PATH_PREFIX}/workspace/page.png"

    def test_resource_link_file_inside_outputs_rewritten(self, paths: Paths):
        outputs = paths.sandbox_outputs_dir("t1", user_id="u1")
        outputs.mkdir(parents=True)
        src = outputs / "doc.pdf"
        src.write_bytes(b"pdf")
        result = CallToolResult(
            content=[ResourceLink(type="resource_link", name="doc", uri=f"file://{src}", mimeType="application/pdf")],
            isError=False,
        )

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["type"] == "file"
        assert content[0]["url"] == f"{VIRTUAL_PATH_PREFIX}/outputs/doc.pdf"

    def test_resource_link_outside_user_data_untouched(self, tmp_path: Path, paths: Paths):
        src = tmp_path / "page.png"
        src.write_bytes(b"png")
        uri = f"file://{src}"
        result = CallToolResult(
            content=[ResourceLink(type="resource_link", name="page", uri=uri, mimeType="image/png")],
            isError=False,
        )

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["url"] == uri

    def test_remote_resource_link_untouched(self, paths: Paths):
        url = "https://example.com/remote.png"
        result = CallToolResult(
            content=[ResourceLink(type="resource_link", name="r", uri=url, mimeType="image/png")],
            isError=False,
        )

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["url"] == url

    def test_text_review_case_rewritten(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        _workspace_file(paths, "temp/page-2026-06-16T10-21-46-864Z.yml")
        result = CallToolResult(
            content=[TextContent(type="text", text="Saved as temp/page-2026-06-16T10-21-46-864Z.yml")],
            isError=False,
        )

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1", source_base_dir=workspace)

        assert content[0]["text"] == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/temp/page-2026-06-16T10-21-46-864Z.yml"

    def test_text_bare_filename_rewritten_from_changed_files(self, paths: Paths):
        workspace = paths.sandbox_work_dir("t1", user_id="u1")
        src = _workspace_file(paths, "page-2026.yml")
        result = CallToolResult(content=[TextContent(type="text", text="Saved as page-2026.yml")], isError=False)

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(
                result,
                thread_id="t1",
                user_id="u1",
                source_base_dir=workspace,
                changed_files=[src],
            )

        assert content[0]["text"] == f"Saved as {VIRTUAL_PATH_PREFIX}/workspace/page-2026.yml"

    def test_no_context_does_not_rewrite(self, paths: Paths):
        src = _workspace_file(paths, "x.png", content=b"png")
        uri = f"file://{src}"
        result = CallToolResult(
            content=[ResourceLink(type="resource_link", name="x", uri=uri, mimeType="image/png")],
            isError=False,
        )

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result)

        assert content[0]["url"] == uri

    def test_text_content_passthrough(self, paths: Paths):
        result = CallToolResult(content=[TextContent(type="text", text="hello")], isError=False)

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["type"] == "text"
        assert content[0]["text"] == "hello"

    def test_image_content_passthrough(self, paths: Paths):
        from mcp.types import ImageContent

        result = CallToolResult(content=[ImageContent(type="image", data="QUJD", mimeType="image/png")], isError=False)

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["type"] == "image"

    def test_embedded_text_resource(self, paths: Paths):
        from mcp.types import EmbeddedResource, TextResourceContents

        res = TextResourceContents(uri="mem://note.txt", text="note", mimeType="text/plain")
        result = CallToolResult(content=[EmbeddedResource(type="resource", resource=res)], isError=False)

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["type"] == "text"
        assert content[0]["text"] == "note"

    def test_embedded_blob_image_resource(self, paths: Paths):
        from mcp.types import BlobResourceContents, EmbeddedResource

        res = BlobResourceContents(uri="mem://img.png", blob="QUJD", mimeType="image/png")
        result = CallToolResult(content=[EmbeddedResource(type="resource", resource=res)], isError=False)

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["type"] == "image"

    def test_embedded_blob_file_resource(self, paths: Paths):
        from mcp.types import BlobResourceContents, EmbeddedResource

        res = BlobResourceContents(uri="mem://doc.pdf", blob="QUJD", mimeType="application/pdf")
        result = CallToolResult(content=[EmbeddedResource(type="resource", resource=res)], isError=False)

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["type"] == "file"

    def test_unknown_content_item_stringified(self, paths: Paths):
        class _Weird:
            def __str__(self) -> str:
                return "weird-item"

        result = CallToolResult(content=[TextContent(type="text", text="x")], isError=False)
        result.content = [_Weird()]  # bypass pydantic validation on the union

        with _patch_paths(paths):
            content, _ = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert content[0]["type"] == "text"
        assert content[0]["text"] == "weird-item"

    def test_error_result_raises_tool_exception(self, paths: Paths):
        from langchain_core.tools import ToolException

        result = CallToolResult(content=[TextContent(type="text", text="boom")], isError=True)

        with _patch_paths(paths), pytest.raises(ToolException, match="boom"):
            mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

    def test_structured_content_becomes_artifact(self, paths: Paths):
        result = CallToolResult(content=[TextContent(type="text", text="ok")], structuredContent={"k": "v"}, isError=False)

        with _patch_paths(paths):
            _, artifact = mcp_tools._convert_call_tool_result(result, thread_id="t1", user_id="u1")

        assert artifact == {"structured_content": {"k": "v"}}
