import pytest


from api.services.tool_executor import ToolExecutor, ToolExecutionError


def test_apply_patch_blocks_default_api_cursor_artifact(tmp_path):
    # Arrange: create a file to update.
    target = tmp_path / "FrontEnd" / "src" / "components" / "Example.tsx"
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text("export const x = 1\n", encoding="utf-8")

    ex = ToolExecutor(workspace_root=str(tmp_path))

    patch = "\n".join(
        [
            "*** Begin Patch",
            f"*** Update File: {target.relative_to(tmp_path)}",
            "@@",
            " export const x = 1",
            "+default_api.cursor_ide_browser_browser_navigate({ url: '/x' })",
            "*** End Patch",
        ]
    )

    # Act / Assert
    with pytest.raises(ToolExecutionError) as ei:
        ex.execute_tool("ApplyPatch", {"patch": patch})
    assert "Cursor tool-call artifacts" in str(ei.value)


def test_write_blocks_cursor_ide_browser_artifact(tmp_path):
    ex = ToolExecutor(workspace_root=str(tmp_path))
    with pytest.raises(ToolExecutionError):
        ex.execute_tool(
            "Write",
            {
                "path": "notes.txt",
                "content": "try calling cursor_ide_browser_browser_navigate in code",
            },
        )


def test_apply_patch_allows_normal_code(tmp_path):
    target = tmp_path / "a.txt"
    target.write_text("hello\n", encoding="utf-8")

    ex = ToolExecutor(workspace_root=str(tmp_path))
    patch = "\n".join(
        [
            "*** Begin Patch",
            "*** Update File: a.txt",
            "@@",
            " hello",
            "+world",
            "*** End Patch",
        ]
    )

    out = ex.execute_tool("ApplyPatch", {"patch": patch})
    assert out["status"] == "success"
    assert target.read_text(encoding="utf-8") == "hello\nworld\n"


def test_str_replace_replace_all(tmp_path):
    p = tmp_path / "x.txt"
    p.write_text("a\na\na\n", encoding="utf-8")
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool("StrReplace", {"path": "x.txt", "old_string": "a", "new_string": "b", "replace_all": True})
    assert out["status"] == "success"
    assert p.read_text(encoding="utf-8") == "b\nb\nb\n"


def test_str_replace_whitespace_fallback(tmp_path):
    p = tmp_path / "x.txt"
    p.write_text("function  hello(\n  a,\n  b\n) {\n  return a + b\n}\n", encoding="utf-8")
    te = ToolExecutor(workspace_root=str(tmp_path))
    old = "function hello(a, b) { return a + b }"
    out = te.execute_tool("StrReplace", {"path": "x.txt", "old_string": old, "new_string": "/* replaced */", "replace_all": False})
    assert out["status"] == "success"
    assert "replaced" in p.read_text(encoding="utf-8")


def test_shell_dry_run_blocks_destructive(tmp_path):
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool("Shell", {"command": "rm -rf .", "dry_run": True})
    assert out.get("blocked") is True


def test_write_dedup_scoped_by_request_id(tmp_path):
    te = ToolExecutor(workspace_root=str(tmp_path))
    r1 = te.execute_tool(
        "Write",
        {"path": "x.txt", "content": "one", "__tool_call_id": "call_same", "__request_id": "reqA"},
    )
    r2 = te.execute_tool(
        "Write",
        {"path": "x.txt", "content": "two", "__tool_call_id": "call_same", "__request_id": "reqA"},
    )
    # Same request/tool_call_id => second suppressed
    assert r1["status"] == "success"
    assert r2["status"] == "duplicate_suppressed"
    assert (tmp_path / "x.txt").read_text(encoding="utf-8") == "one"

    r3 = te.execute_tool(
        "Write",
        {"path": "x.txt", "content": "three", "__tool_call_id": "call_same", "__request_id": "reqB"},
    )
    # Different request id => not suppressed
    assert r3["status"] == "success"
    assert (tmp_path / "x.txt").read_text(encoding="utf-8") == "three"


def test_shell_default_dry_run_from_env(tmp_path, monkeypatch):
    monkeypatch.setenv("TOOL_EXECUTOR_SHELL_DRY_RUN_DEFAULT", "1")
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool("Shell", {"command": "echo hi"})
    assert out.get("dry_run") is True
    assert out.get("returncode") == 0


def test_read_utf8_decode_error_does_not_fallback_to_latin1_mojibake(tmp_path):
    """
    Regression test: prior behavior would fall back to latin-1 when *any* byte
    in the file was invalid UTF-8, which turns valid UTF-8 sequences (e.g. "→")
    into mojibake ("â" with C1 controls).
    """
    p = tmp_path / "x.txt"
    # Include a valid UTF-8 arrow + one invalid byte to force a UnicodeDecodeError in strict mode.
    p.write_bytes(b"Models \xe2\x86\x92 OpenAI-compatible \xff\n")
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool("Read", {"path": "x.txt"})
    assert "→" in out
    assert "â" not in out  # quick sanity check against common mojibake prefix
    # Replacement character indicates we did best-effort UTF-8 decode.
    assert "\ufffd" in out


def test_str_replace_repairs_common_latin1_decoded_utf8_mojibake(tmp_path):
    """
    StrReplace should tolerate a common agent failure mode where a UTF-8 snippet
    was copied from a latin-1 decoded view (producing C1 controls).
    """
    p = tmp_path / "x.txt"
    p.write_text("In Cursor settings (Models → Custom), set Base URL.\n", encoding="utf-8")

    # Produce the mojibake variant of "→" as if decoded from ISO-8859-1.
    mojibake_arrow = b"\xe2\x86\x92".decode("latin-1")
    old = f"In Cursor settings (Models {mojibake_arrow} Custom), set Base URL.\n"

    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool(
        "StrReplace",
        {
            "path": "x.txt",
            "old_string": old,
            "new_string": "In Cursor settings (Models -> Custom), set Base URL.\n",
            "replace_all": False,
        },
    )
    assert out["status"] == "success"
    assert p.read_text(encoding="utf-8") == "In Cursor settings (Models -> Custom), set Base URL.\n"


def test_str_replace_multiple_matches_requires_occurrence_or_anchors(tmp_path):
    p = tmp_path / "x.txt"
    p.write_text("alpha\nalpha\nalpha\n", encoding="utf-8")
    te = ToolExecutor(workspace_root=str(tmp_path))
    with pytest.raises(ToolExecutionError) as ei:
        te.execute_tool(
            "StrReplace",
            {"path": "x.txt", "old_string": "alpha", "new_string": "beta", "replace_all": False},
        )
    # Should guide toward occurrence/anchors instead of replace_all thrash
    assert "occurrence" in str(ei.value).lower()


def test_str_replace_occurrence_targets_specific_match(tmp_path):
    p = tmp_path / "x.txt"
    p.write_text("alpha\nalpha\nalpha\n", encoding="utf-8")
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool(
        "StrReplace",
        {"path": "x.txt", "old_string": "alpha", "new_string": "beta", "occurrence": 2},
    )
    assert out["status"] == "success"
    assert p.read_text(encoding="utf-8") == "alpha\nbeta\nalpha\n"


def test_str_replace_before_after_anchors_disambiguate(tmp_path):
    p = tmp_path / "x.txt"
    p.write_text("A: value\nB: value\n", encoding="utf-8")
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool(
        "StrReplace",
        {
            "path": "x.txt",
            "old_string": "value",
            "new_string": "VALUE",
            "before": "B:",
            "window": 10,
        },
    )
    assert out["status"] == "success"
    assert p.read_text(encoding="utf-8") == "A: value\nB: VALUE\n"


def test_str_replace_dry_run_returns_previews_and_does_not_modify(tmp_path):
    p = tmp_path / "x.txt"
    p.write_text("one two one two\n", encoding="utf-8")
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool(
        "StrReplace",
        {"path": "x.txt", "old_string": "one", "new_string": "X", "dry_run": True, "max_preview_matches": 2},
    )
    assert out["status"] == "dry_run"
    assert out["match_count"] == 2
    assert isinstance(out.get("matches_preview"), list)
    assert p.read_text(encoding="utf-8") == "one two one two\n"


def test_find_in_file_basic_and_occurrence(tmp_path):
    p = tmp_path / "x.txt"
    p.write_text("one two one two\n", encoding="utf-8")
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool("FindInFile", {"path": "x.txt", "query": "one"})
    assert out["status"] == "success"
    assert out["match_count"] == 2
    assert isinstance(out.get("matches_preview"), list)

    out2 = te.execute_tool("FindInFile", {"path": "x.txt", "query": "one", "occurrence": 2})
    assert out2["match_count"] == 2  # total matches still reported
    assert len(out2.get("matches_preview") or []) == 1


def test_find_in_file_whitespace_agnostic(tmp_path):
    p = tmp_path / "x.txt"
    p.write_text("function  hello(\n  a,\n  b\n) {\n  return a + b\n}\n", encoding="utf-8")
    te = ToolExecutor(workspace_root=str(tmp_path))
    out = te.execute_tool(
        "FindInFile",
        {
            "path": "x.txt",
            "query": "function hello(a, b) { return a + b }",
            "whitespace_agnostic": True,
        },
    )
    assert out["status"] == "success"
    assert out["match_count"] == 1

