"""BrowserDeleteItem tool logic (the resolve JS is proven live; this pins the pure pieces and
the resolve->trusted-click->verify orchestration with a mocked tool runner)."""
import json

import pytest

from backend.apps.agents.browser import browser_delete_script as d


def test_flag_default_off(monkeypatch):
    monkeypatch.delenv("OSW_DELETE_SCRIPT", raising=False)
    assert d.delete_tool_enabled() is False
    monkeypatch.setenv("OSW_DELETE_SCRIPT", "1")
    assert d.delete_tool_enabled() is True


def test_expression_embeds_target_json_safe():
    # a target with quotes must not break out of the JS string literal
    tricky = 'he said "hi"; alert(1)'
    expr = d.resolve_expression("more", tricky)
    assert json.dumps(tricky) in expr           # embedded as a JSON literal
    assert 'const TARGET = ' + json.dumps(tricky) in expr
    assert "const STEP = \"more\"" in expr


def make_exec(step_results, click_ok=True, verify_after_refresh=None):
    """Tool-runner mock: BrowserEvaluate returns the scripted resolve result for the step it was
    called with (read out of the expression); BrowserClickPoint records real-input clicks; the
    location probe and BrowserNavigate model the refresh-reverify pass."""
    calls = {"clicks": [], "steps": [], "navs": []}

    async def execute(tool, params, bid, tid):
        if tool == "BrowserEvaluate":
            expr = params["expression"]
            if "location.href" in expr:
                return {"value": {"href": "https://site.test/profile"}}
            step = expr.split('const STEP = "', 1)[1].split('"', 1)[0]
            calls["steps"].append(step)
            if step == "verify" and calls["navs"] and verify_after_refresh is not None:
                return {"value": verify_after_refresh}
            if step not in step_results:  # menu-flow fixtures: no direct control on the item
                return {"value": {"ok": False, "stage": step, "optional": True, "msg": "none"}}
            return {"value": step_results[step]}
        if tool == "BrowserClickPoint":
            calls["clicks"].append((params["xPercent"], params["yPercent"]))
            return {"ok": True} if click_ok else {"error": "no webview"}
        if tool == "BrowserNavigate":
            calls["navs"].append(params["url"])
            return {"ok": True}
        return {"ok": True}
    return execute, calls


POS = {"xPct": 50.0, "yPct": 40.0}


@pytest.mark.asyncio
async def test_full_flow_clicks_each_stage_with_real_input():
    ex, calls = make_exec({
        "more": {"ok": True, "stage": "more", **POS},
        "menuitem": {"ok": True, "stage": "menuitem", **POS},
        "confirm": {"ok": True, "stage": "confirm", **POS},
        "verify": {"ok": True, "stage": "verify"},
    })
    r = await d.run_delete("coffee notes abc123", "b1", "", ex)
    assert r == {"removed": True, "stage": "done", "msg": "item removed"}
    assert len(calls["clicks"]) == 3            # more, menuitem, confirm: all real-input clicks
    assert calls["steps"][-1] == "verify"


@pytest.mark.asyncio
async def test_verify_still_present_is_honest():
    # still-present survives the refresh re-verify too: honest not-removed, one refresh attempted
    ex, calls = make_exec({
        "more": {"ok": True, "stage": "more", **POS},
        "menuitem": {"ok": True, "stage": "menuitem", **POS},
        "confirm": {"ok": True, "stage": "confirm", **POS},
        "verify": {"ok": False, "stage": "verify"},
    })
    r = await d.run_delete("coffee notes abc123", "b1", "", ex)
    assert r["removed"] is False and r["stage"] == "done"
    assert calls["navs"] == ["https://site.test/profile"]


@pytest.mark.asyncio
async def test_stale_tile_flips_removed_after_refresh():
    """Shreddit keeps the dead tile mounted until a reload; the refresh re-verify turns a real
    server-side delete into removed=True instead of an honest-but-wrong still-present."""
    ex, calls = make_exec({
        "more": {"ok": True, "stage": "more", **POS},
        "menuitem": {"ok": True, "stage": "menuitem", **POS},
        "confirm": {"ok": True, "stage": "confirm", **POS},
        "verify": {"ok": False, "stage": "verify"},
    }, verify_after_refresh={"ok": True, "stage": "verify"})
    r = await d.run_delete("coffee notes abc123", "b1", "", ex)
    assert r["removed"] is True
    assert len(calls["navs"]) == 1


@pytest.mark.asyncio
async def test_missing_confirm_is_optional_verify_decides():
    ex, calls = make_exec({
        "more": {"ok": True, "stage": "more", **POS},
        "menuitem": {"ok": True, "stage": "menuitem", **POS},
        "confirm": {"ok": False, "stage": "confirm", "optional": True, "msg": "no confirm dialog appeared"},
        "verify": {"ok": True, "stage": "verify"},
    })
    r = await d.run_delete("coffee notes abc123", "b1", "", ex)
    assert r["removed"] is True
    assert len(calls["clicks"]) == 2            # confirm never clicked


@pytest.mark.asyncio
async def test_direct_delete_skips_menu_walk():
    """Row-action sites (Gmail): a literal Delete control on the item is clicked straight; the
    menu ladder never runs and a page-wide non-dialog 'Delete' earns NO bonus confirm click."""
    ex, calls = make_exec({
        "direct": {"ok": True, "stage": "direct", "label": "delete", **POS},
        "confirm": {"ok": True, "stage": "confirm", "fromDialog": False, **POS},
        "verify": {"ok": True, "stage": "verify"},
    })
    r = await d.run_delete("note to self abc123", "b1", "", ex)
    assert r["removed"] is True
    assert len(calls["clicks"]) == 1
    assert "more" not in calls["steps"] and "menuitem" not in calls["steps"]


@pytest.mark.asyncio
async def test_direct_delete_confirms_only_in_dialog():
    ex, calls = make_exec({
        "direct": {"ok": True, "stage": "direct", "label": "move to trash", **POS},
        "confirm": {"ok": True, "stage": "confirm", "fromDialog": True, **POS},
        "verify": {"ok": True, "stage": "verify"},
    })
    r = await d.run_delete("note to self abc123", "b1", "", ex)
    assert r["removed"] is True
    assert len(calls["clicks"]) == 2   # direct + real-dialog confirm


@pytest.mark.asyncio
async def test_stage_failure_reports_stage_and_never_removed():
    ex, calls = make_exec({
        "more": {"ok": False, "stage": "find", "msg": "target text not on this page"},
    })
    r = await d.run_delete("coffee notes abc123", "b1", "", ex)
    assert r == {"removed": False, "stage": "find", "msg": "target text not on this page"}
    assert not calls["clicks"]


@pytest.mark.asyncio
async def test_unreadable_resolve_is_honest_failure():
    async def execute(tool, params, bid, tid):
        return {"error": "eval blew up"}
    r = await d.run_delete("coffee notes abc123", "b1", "", execute)
    assert r["removed"] is False and r["stage"] == "eval"
