#!/usr/bin/env python3
"""End-to-end test of the codex-review MCP bridge.

Drives the bridge over MCP stdio exactly as dsh will: initialize, tools/list,
then a `codex` call followed by a `codex-reply` on the returned threadId. A
passing run proves both the MCP surface and the app-server translation.
"""

from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

SERVER = str(Path(__file__).resolve().parent / "server.py")


class McpClient:
    def __init__(self, argv: list[str]) -> None:
        self.proc = subprocess.Popen(
            argv,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            bufsize=0,
        )
        self._id = 0

    def _send(self, obj: dict) -> None:
        payload = json.dumps(obj).encode("utf-8")
        assert self.proc.stdin
        self.proc.stdin.write(f"Content-Length: {len(payload)}\r\n\r\n".encode() + payload)
        self.proc.stdin.flush()

    def _read(self) -> dict:
        assert self.proc.stdout
        length = None
        while True:
            line = self.proc.stdout.readline()
            if not line:
                raise EOFError("server closed the stream")
            text = line.decode("utf-8").strip()
            if not text:
                continue
            if text.lower().startswith("content-length:"):
                length = int(text.split(":", 1)[1].strip())
                break
            return json.loads(text)
        while True:
            sep = self.proc.stdout.readline()
            if sep in (b"\r\n", b"\n"):
                break
        body = self.proc.stdout.read(length or 0)
        return json.loads(body.decode("utf-8"))

    def request(self, method: str, params: dict | None = None) -> dict:
        self._id += 1
        rid = self._id
        msg: dict = {"jsonrpc": "2.0", "id": rid, "method": method}
        if params is not None:
            msg["params"] = params
        self._send(msg)
        while True:
            resp = self._read()
            if resp.get("id") == rid:
                return resp

    def notify(self, method: str, params: dict | None = None) -> None:
        msg: dict = {"jsonrpc": "2.0", "method": method}
        if params is not None:
            msg["params"] = params
        self._send(msg)


def main() -> int:
    client = McpClient([sys.executable, SERVER])

    init = client.request("initialize", {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "t", "version": "1"}})
    print("initialize ->", json.dumps(init.get("result", {}).get("serverInfo")))
    client.notify("notifications/initialized")

    tools = client.request("tools/list")
    names = [t["name"] for t in tools["result"]["tools"]]
    print("tools ->", names)
    assert set(names) == {"codex", "codex-reply"}, "unexpected tool surface"

    print("\n--- codex call (starts a thread) ---")
    first = client.request("tools/call", {
        "name": "codex",
        "arguments": {"prompt": "Reply with exactly: ALPHA", "cwd": str(Path.home())},
    })
    result = first["result"]
    text = "".join(b.get("text", "") for b in result.get("content", []))
    thread_id = (result.get("structuredContent") or {}).get("threadId")
    print("  isError:", result.get("isError"))
    print("  text:", repr(text[:200]))
    print("  threadId:", thread_id)
    assert not result.get("isError"), "first call failed"
    assert thread_id, "no threadId returned"

    print("\n--- codex-reply (continues the thread) ---")
    second = client.request("tools/call", {
        "name": "codex-reply",
        "arguments": {"prompt": "Now reply with exactly: BETA", "threadId": thread_id},
    })
    result2 = second["result"]
    text2 = "".join(b.get("text", "") for b in result2.get("content", []))
    print("  isError:", result2.get("isError"))
    print("  text:", repr(text2[:200]))
    print("  threadId:", (result2.get("structuredContent") or {}).get("threadId"))
    assert not result2.get("isError"), "reply call failed"

    print("\nRESULT: OK")
    client.proc.kill()
    return 0


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