#!/usr/bin/python3
"""
End-to-end test for the RecapMuse MCP server: spawns server.py as a real stdio
subprocess and drives the actual JSON-RPC protocol, exactly as Claude Desktop
would. Asserts INTERNAL CONSISTENCY against whatever corpus is present, never a
frozen snapshot of it (the corpus legitimately drifts; see Galley Text's
test_mcp.mjs, which false-failed for weeks on a hardcoded snippet count).

Run:  /usr/bin/python3 test_mcp.py            (against the live corpus)
      RECAPMUSE_REC_DIR=... test_mcp.py       (against a fixture)
"""
from __future__ import annotations

import json
import os
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
SERVER = os.path.join(HERE, "server.py")

passed = failed = 0


def check(name: str, ok: bool, detail: str = ""):
    global passed, failed
    passed += ok
    failed += not ok
    sys.stdout.write(f"  {'PASS' if ok else 'FAIL'}  {name}{('  ' + detail) if detail and not ok else ''}\n")


def main() -> int:
    proc = subprocess.Popen(["/usr/bin/python3", SERVER], stdin=subprocess.PIPE,
                            stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
    rid = 0

    def rpc(method: str, params: dict | None = None):
        nonlocal rid
        rid += 1
        proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": rid, "method": method,
                                     "params": params or {}}) + "\n")
        proc.stdin.flush()
        line = proc.stdout.readline()
        return json.loads(line)

    def call(tool: str, args: dict | None = None):
        r = rpc("tools/call", {"name": tool, "arguments": args or {}})
        return r.get("result", {}).get("structuredContent"), r

    # --- protocol layer
    r = rpc("initialize", {"protocolVersion": "2025-03-26", "capabilities": {},
                           "clientInfo": {"name": "test", "version": "0"}})
    check("initialize returns serverInfo", r.get("result", {}).get("serverInfo", {}).get("name") == "recapmuse")
    check("initialize echoes protocolVersion", r.get("result", {}).get("protocolVersion") == "2025-03-26")
    proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n")
    proc.stdin.flush()
    r = rpc("ping")
    check("ping replies", r.get("result") == {})
    r = rpc("tools/list")
    tools = {t["name"] for t in r.get("result", {}).get("tools", [])}
    check("tools/list names", tools == {"status", "list_conversations", "get_conversation",
                                        "search_conversations", "ask"}, str(tools))
    r = rpc("nonsense/method")
    check("unknown method -> -32601", r.get("error", {}).get("code") == -32601)
    r = rpc("tools/call", {"name": "no_such_tool", "arguments": {}})
    check("unknown tool -> -32602", r.get("error", {}).get("code") == -32602)

    # --- status: the corpus facts everything else is checked against
    st, _ = call("status")
    check("status ok", isinstance(st, dict) and st.get("server") == "recapmuse", str(st)[:200])
    check("status reachable", st.get("reachable") is True, st.get("recordings_dir", ""))
    has_diary = bool(st.get("diary_present"))
    n_convs = int(st.get("conversations") or 0)
    check("status counts self-consistent",
          (st.get("days") or 0) >= 0 and n_convs >= 0 and (st.get("transcribed_clips") or 0) >= 0)
    check("status states the privacy contract", "local" in (st.get("privacy") or "").lower())

    # --- list / get / search / ask, all against whatever is really there
    lst, _ = call("list_conversations", {"scope": "all", "limit": 200})
    if has_diary:
        check("list ok", lst.get("ok") is True, str(lst)[:200])
        check("list total matches status", lst.get("total") == n_convs,
              f"list {lst.get('total')} vs status {n_convs}")
        convs = lst.get("conversations") or []
        if convs:
            first = next((c for c in convs if c.get("title")), convs[0])
            got, _ = call("get_conversation", {"title": first["title"], "date": first["date"]})
            check("get_conversation finds a listed title", got.get("ok") is True, str(got)[:200])
            check("get_conversation carries a transcript field",
                  isinstance(got.get("conversation", {}).get("transcript"), str))
            # search for a word from a real summary; it must find at least that conversation
            word = next((w for w in (first.get("summary") or "").split() if len(w) > 5), None)
            if word:
                sr, _ = call("search_conversations", {"query": word.strip(".,")})
                check("search finds a known summary word", sr.get("ok") is True and sr.get("hits", 0) >= 1,
                      f"word={word!r} -> {str(sr)[:200]}")
            miss, _ = call("get_conversation", {"title": "zz-no-such-conversation-zz"})
            check("get_conversation honest miss", miss.get("ok") is False)
    else:
        check("list reports missing diary honestly", lst.get("ok") is False)

    bad, _ = call("search_conversations", {})
    check("search without query -> honest error", bad.get("ok") is False)
    ans, _ = call("ask", {"question": "what did I talk about?", "scope": "all"})
    check("ask returns an answer object", isinstance(ans, dict) and "answer" in ans, str(ans)[:200])
    check("ask stays on device", "on-device" in (ans.get("tier") or ""), ans.get("tier", ""))

    proc.stdin.close()
    proc.wait(timeout=10)
    sys.stdout.write(f"\n{passed} passed, {failed} failed\n")
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main())
