"""Device-tool WS bridge: action frame out, action_result in, timeout, and the
resolve/dispatch routing. No network, no real phone — a FakeWS captures frames
and we feed results back by hand."""
import asyncio
import json

import device_tools
import main
import phone_store


class FakeWS:
    def __init__(self):
        self.sent = []
        self.binary = []

    async def send_json(self, frame):
        self.sent.append(frame)

    async def send_bytes(self, data):
        self.binary.append(data)


def _session():
    return main.Session(ws=FakeWS())


def test_device_tool_specs_valid_and_named():
    for spec in device_tools.DEVICE_TOOL_SPECS:
        assert spec["type"] == "function"
        fn = spec["function"]
        assert fn["name"] and fn["description"]
        json.dumps(spec)                       # serialisable
    assert device_tools.DEVICE_TOOL_NAMES == {
        "navigate", "play_music", "call_contact", "take_screenshot",
        "open_camera", "open_app", "open_url", "set_timer", "get_location"}


async def test_device_call_resolves_on_matching_result():
    sess = _session()
    task = asyncio.create_task(sess._device_call("take_screenshot", {}))
    await asyncio.sleep(0)                      # let the action frame go out
    frame = sess.ws.sent[-1]
    assert frame["type"] == "action" and frame["action"] == "take_screenshot"
    aid = frame["id"]
    sess._resolve_action({"id": aid, "ok": True, "data": {"message": "snap"}})
    res = await task
    assert res["ok"] and res["data"]["message"] == "snap"
    assert sess._pending == {}                  # cleaned up


async def test_device_call_times_out_without_client():
    sess = _session()
    res = await sess._device_call("get_location", {}, timeout=0.02)
    assert res == {"ok": False, "error": "timeout"}
    assert sess._pending == {}


async def test_navigate_resolves_saved_place(monkeypatch):
    sess = _session()

    async def fake_get(kind, key):
        assert kind == phone_store.KIND_PLACE
        return {"label": "office", "address": "Tower 1", "lat": 26.3, "lng": 50.2}
    monkeypatch.setattr(main.phone_store, "get_item", fake_get)

    task = asyncio.create_task(sess._dispatch_device("navigate",
                                                     {"destination": "office"}))
    await asyncio.sleep(0)
    frame = sess.ws.sent[-1]
    assert frame["action"] == "navigate"
    assert frame["params"]["address"] == "Tower 1"
    assert frame["params"]["lat"] == 26.3
    sess._resolve_action({"id": frame["id"], "ok": True, "data": {}})
    out = await task
    assert "office" in out.lower()


async def test_call_contact_dials_number_but_never_returns_it(monkeypatch):
    sess = _session()
    number = "0501234567"

    async def fake_get(kind, key):
        assert kind == phone_store.KIND_CONTACT
        return {"name": "father", "number": number}
    monkeypatch.setattr(main.phone_store, "get_item", fake_get)

    task = asyncio.create_task(sess._dispatch_device("call_contact",
                                                     {"name": "father"}))
    await asyncio.sleep(0)
    frame = sess.ws.sent[-1]
    assert frame["action"] == "call"
    assert frame["params"]["number"] == number     # phone gets the real number
    sess._resolve_action({"id": frame["id"], "ok": True, "data": {}})
    out = await task
    assert "father" in out.lower()
    assert number not in out                        # brain never sees it


async def test_call_contact_unknown_returns_error_no_frame(monkeypatch):
    sess = _session()

    async def fake_get(kind, key):
        return None
    monkeypatch.setattr(main.phone_store, "get_item", fake_get)
    out = await sess._dispatch_device("call_contact", {"name": "ghost"})
    assert "no number" in out.lower()
    assert not sess.ws.sent                         # nothing dialled


async def test_get_location_labels_known_place(monkeypatch):
    sess = _session()

    async def fake_match(lat, lng):
        return "home"
    monkeypatch.setattr(sess, "_match_place", fake_match)

    task = asyncio.create_task(sess._dispatch_device("get_location", {}))
    await asyncio.sleep(0)
    frame = sess.ws.sent[-1]
    sess._resolve_action({"id": frame["id"], "ok": True,
                          "data": {"lat": 26.3, "lng": 50.2}})
    out = await task
    assert "home" in out


async def test_dispatch_tool_routes_device_vs_server(monkeypatch):
    sess = _session()
    seen = {}

    async def fake_exec(name, args):
        seen["server"] = name
        return "server-ok"
    monkeypatch.setattr(main.tools, "execute", fake_exec)

    async def fake_device(name, args):
        seen["device"] = name
        return "device-ok"
    monkeypatch.setattr(sess, "_dispatch_device", fake_device)

    assert await sess._dispatch_tool("memory_search", {}) == "server-ok"
    assert await sess._dispatch_tool("navigate", {}) == "device-ok"
    assert seen == {"server": "memory_search", "device": "navigate"}


async def test_device_reply_timeout_and_error_wording():
    assert "didn't respond" in main.Session._device_reply(
        {"ok": False, "error": "timeout"}, "x")
    assert "Done" in main.Session._device_reply({"ok": True}, "Done.")
    assert main.Session._device_reply(
        {"ok": True, "data": {"message": "custom"}}, "x") == "custom"


async def test_fail_pending_resolves_on_close():
    sess = _session()
    loop = asyncio.get_running_loop()
    fut = loop.create_future()
    sess._pending["abc"] = fut
    sess._fail_pending("connection closed")
    assert fut.result() == {"ok": False, "error": "connection closed"}
    assert sess._pending == {}
