"""Tool JSON schemas + executors. HTTP is monkeypatched — no network."""
import json

import pytest

import tools

# the original core tools (Phase-1) — must always remain present
_CORE = {
    "memory_search", "memory_remember", "task_add", "tasks_list", "task_done",
    "ultron_leads", "ultron_team_activity", "ultron_stats",
    "ultron_contact_stats",
}
# Phase-2 phone-power server-side tools
_PHONE = {
    "save_place", "get_place", "list_places",
    "save_contact", "get_contact", "list_contacts",
    "sms_watch_add", "sms_watch_remove", "sms_watch_list",
    "routine_create", "routine_list", "routine_cancel",
}
_EXPECTED = _CORE | _PHONE


def test_tool_specs_are_valid_openai_function_schemas():
    names = []
    for spec in tools.TOOL_SPECS:
        assert spec["type"] == "function"
        fn = spec["function"]
        assert isinstance(fn["name"], str) and fn["name"]
        assert isinstance(fn["description"], str) and fn["description"]
        params = fn["parameters"]
        assert params["type"] == "object"
        assert isinstance(params["properties"], dict)
        req = params.get("required", [])
        assert isinstance(req, list)
        # every required field must be declared in properties
        for r in req:
            assert r in params["properties"], f"{fn['name']}: {r} not declared"
        # schema must be JSON-serialisable
        json.dumps(spec)
        names.append(fn["name"])
    assert set(names) == _EXPECTED
    # no duplicates, and every spec has a matching executor
    assert len(names) == len(set(names)) == len(_EXPECTED)
    assert set(names) == set(tools._EXECUTORS)


async def test_memory_search_formats_facts(monkeypatch):
    async def fake_req(method, path, params=None, body=None, timeout=20.0):
        assert method == "GET" and path == "/search"
        assert params["q"] == "coffee"
        return {"facts": [{"fact": "Ahmed likes flat whites", "id": "m1",
                           "linked": False}]}
    monkeypatch.setattr(tools, "_mem_request", fake_req)
    out = await tools.execute("memory_search", {"query": "coffee"})
    assert "Ahmed likes flat whites" in out
    assert "m1" in out


async def test_memory_remember_posts(monkeypatch):
    seen = {}
    async def fake_req(method, path, params=None, body=None, timeout=20.0):
        seen["path"] = path
        seen["body"] = body
        return {"id": "x"}
    monkeypatch.setattr(tools, "_mem_request", fake_req)
    out = await tools.execute("memory_remember", {"text": "Ali is the new PM"})
    assert out == "Remembered."
    assert seen["path"] == "/remember"
    assert seen["body"]["text"] == "Ali is the new PM"


async def test_tasks_list_and_add(monkeypatch):
    async def fake_req(method, path, params=None, body=None, timeout=20.0):
        if path == "/task":
            return {"id": "t9"}
        if path == "/tasks":
            return {"tasks": [{"id": "t1", "text": "Call supplier",
                               "due": "2026-07-11T09:00:00"}]}
        return {}
    monkeypatch.setattr(tools, "_mem_request", fake_req)
    add = await tools.execute("task_add", {"text": "Email Sara",
                                           "due": "2026-07-11T09:00:00"})
    assert "Email Sara" in add and "reminder set" in add
    lst = await tools.execute("tasks_list", {})
    assert "Call supplier" in lst and "t1" in lst


async def test_ultron_leads_uses_search_leads_rpc(monkeypatch):
    captured = {}
    async def fake_rpc(fn, args=None, timeout=30.0):
        captured["fn"] = fn
        captured["args"] = args
        return [{"company": "Acme Coffee", "category": "cafe",
                 "city": "Al Khobar", "phone": "0501234567"}]
    monkeypatch.setattr(tools, "_rpc", fake_rpc)
    out = await tools.execute("ultron_leads", {"query": "coffee",
                                               "city": "Al Khobar",
                                               "phone_filter": "mobile"})
    assert captured["fn"] == "search_leads"
    assert captured["args"]["p_city"] == "Al Khobar"
    assert captured["args"]["p_phone"] == "mobile"
    assert "Acme Coffee" in out


async def test_ultron_stats_and_team_and_contacts(monkeypatch):
    async def fake_rpc(fn, args=None, timeout=30.0):
        if fn == "get_dashboard_stats":
            return {"total": 1234, "verified": 200}
        if fn == "get_team_activity":
            return [{"user": "Sara", "contacts": 10}]
        if fn == "get_contact_stats":
            assert args["p_range"] == "7d"
            return {"days": []}
        return {}
    monkeypatch.setattr(tools, "_rpc", fake_rpc)
    assert "1234" in await tools.execute("ultron_stats", {})
    assert "Sara" in await tools.execute("ultron_team_activity", {})
    await tools.execute("ultron_contact_stats", {"days": 7})


async def test_executor_never_raises_on_tool_error(monkeypatch):
    async def boom(*a, **k):
        raise RuntimeError("network down")
    monkeypatch.setattr(tools, "_mem_request", boom)
    out = await tools.execute("memory_search", {"query": "x"})
    assert "failed" in out.lower()


async def test_unknown_tool():
    assert "unknown tool" in await tools.execute("nope", {})


def test_salience_gate():
    assert tools._is_salient("The new supplier is Journey Joy, based in Riyadh.")
    assert not tools._is_salient("hi")
    assert not tools._is_salient("what time is it?")
    assert not tools._is_salient("thanks")
