import assert from "node:assert/strict";
import { test } from "node:test";
import { GatewayRegistry } from "../src/modules/trapos-cloud-gateway/registry.js";
import type { ComputerResponse } from "../src/modules/trapos-cloud-gateway/protocol.js";
import { handleMcpRequest, type McpDeps } from "../src/modules/mcp/tools.js";
import { FakeSocket, silentLog } from "./helpers.js";

const NETWORK = "global";

function makeDeps(registry: GatewayRegistry): McpDeps {
  return { registry, networkId: NETWORK, defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, probeTimeoutMs: 5_000 };
}

function rpc(method: string, params?: unknown, id: unknown = 1): Record<string, unknown> {
  return { jsonrpc: "2.0", id, method, params };
}

function resultText(reply: unknown): string {
  const result = (reply as { result?: { content?: { text?: string }[] } }).result;
  return result?.content?.[0]?.text ?? "";
}

function response(traposId: string, messageId: string, type: string, ok: boolean, payload: Record<string, unknown> = {}): ComputerResponse {
  return { event: "computer_response", traposSenderId: traposId, messageId, type, ok, payload };
}

// Run a tool call and resolve the single server_request it emits with `payload`.
async function callAndAnswer(
  deps: McpDeps,
  socket: FakeSocket,
  traposId: string,
  toolName: string,
  args: Record<string, unknown>,
  ok: boolean,
  payload: Record<string, unknown>,
): Promise<string> {
  const pending = handleMcpRequest(rpc("tools/call", { name: toolName, arguments: args }), deps);
  // registry.request sends synchronously; a tick lets the async chain reach the await.
  await Promise.resolve();
  const frame = socket.lastFrame();
  deps.registry.resolveResponse(socket, NETWORK, traposId, response(traposId, frame.messageId as string, frame.type as string, ok, payload));
  return resultText(await pending);
}

test("initialize advertises the trapos-mcp server", async () => {
  const reply = await handleMcpRequest(rpc("initialize"), makeDeps(new GatewayRegistry(silentLog)));
  const info = (reply as { result: { serverInfo: { name: string } } }).result.serverInfo;
  assert.equal(info.name, "trapos-mcp");
});

test("tools/list exposes the tools addressed by traposId", async () => {
  const reply = await handleMcpRequest(rpc("tools/list"), makeDeps(new GatewayRegistry(silentLog)));
  const tools = (reply as { result: { tools: { name: string; inputSchema: { required?: string[] } }[] } }).result.tools;
  assert.deepEqual(tools.map((t) => t.name).sort(), ["exec-lua", "probe-computers", "run-file", "write-file"]);
  const exec = tools.find((t) => t.name === "exec-lua");
  assert.deepEqual(exec?.inputSchema.required, ["traposId", "code"]);
  const run = tools.find((t) => t.name === "run-file");
  assert.deepEqual(run?.inputSchema.required, ["traposId", "path"]);
});

test("notifications/initialized is a no-op (null)", async () => {
  const reply = await handleMcpRequest(rpc("notifications/initialized", undefined, null), makeDeps(new GatewayRegistry(silentLog)));
  assert.equal(reply, null);
});

test("probe-computers reports no computers when registry is empty", async () => {
  const reply = await handleMcpRequest(rpc("tools/call", { name: "probe-computers" }), makeDeps(new GatewayRegistry(silentLog)));
  assert.equal(resultText(reply), "No computers connected.");
});

test("probe-computers pings each connected computer by traposId", async () => {
  const reg = new GatewayRegistry(silentLog);
  const deps = makeDeps(reg);
  const a = new FakeSocket();
  reg.register(NETWORK, "7:base", a);

  const pending = handleMcpRequest(rpc("tools/call", { name: "probe-computers" }), deps);
  await Promise.resolve();
  const frame = a.lastFrame();
  assert.equal(frame.type, "ping");
  reg.resolveResponse(a, NETWORK, "7:base", response("7:base", frame.messageId as string, "ping", true, { traposId: "7:base" }));
  assert.equal(resultText(await pending), "ok from 7:base");
});

test("exec-lua formats returns and trimmed output on success", async () => {
  const reg = new GatewayRegistry(silentLog);
  const deps = makeDeps(reg);
  const a = new FakeSocket();
  reg.register(NETWORK, "7:base", a);

  const text = await callAndAnswer(deps, a, "7:base", "exec-lua", { traposId: "7:base", code: "print('hi')" }, true, {
    ok: true,
    returns: [{ type: "number", value: 42 }],
    output: "hi\n",
  });

  assert.equal(a.lastFrame().type, "exec-lua");
  assert.deepEqual((a.lastFrame().payload as { code: string }).code, "print('hi')");
  assert.equal(text, ["computer: 7:base", "ok: true", `returns: ${JSON.stringify([{ type: "number", value: 42 }])}`, "output:", "hi"].join("\n"));
});

test("exec-lua surfaces an ok:false result with its captured output", async () => {
  const reg = new GatewayRegistry(silentLog);
  const deps = makeDeps(reg);
  const a = new FakeSocket();
  reg.register(NETWORK, "7:base", a);

  const text = await callAndAnswer(deps, a, "7:base", "exec-lua", { traposId: "7:base", code: "error('boom')" }, true, {
    ok: false,
    error: "boom",
    output: "partial",
  });

  assert.equal(text, ["computer: 7:base", "ok: false", "error: boom", "output:", "partial"].join("\n"));
});

test("exec-lua maps a transport failure to a coded error", async () => {
  const reg = new GatewayRegistry(silentLog);
  const deps = makeDeps(reg);
  // No computer registered for this traposId -> not_connected.
  const reply = await handleMcpRequest(
    rpc("tools/call", { name: "exec-lua", arguments: { traposId: "9:ghost", code: "x" } }),
    deps,
  );
  assert.match(resultText(reply), /^computer: 9:ghost\nok: false\nerror: not_connected:/);
});

test("run-file sends the path and formats execution output", async () => {
  const reg = new GatewayRegistry(silentLog);
  const deps = makeDeps(reg);
  const a = new FakeSocket();
  reg.register(NETWORK, "7:base", a);

  const text = await callAndAnswer(
    deps,
    a,
    "7:base",
    "run-file",
    { traposId: "7:base", path: "/hello.lua", args: ["install", "pkg"] },
    true,
    {
      ok: true,
      returns: [],
      output: "secret\n",
    },
  );

  assert.equal(a.lastFrame().type, "run-file");
  assert.deepEqual(a.lastFrame().payload, { path: "/hello.lua", args: ["install", "pkg"] });
  assert.equal(text, ["computer: 7:base", "ok: true", "returns: []", "output:", "secret"].join("\n"));
});

test("run-file defaults args to an empty array", async () => {
  const reg = new GatewayRegistry(silentLog);
  const deps = makeDeps(reg);
  const a = new FakeSocket();
  reg.register(NETWORK, "7:base", a);

  await callAndAnswer(deps, a, "7:base", "run-file", { traposId: "7:base", path: "/hello.lua" }, true, {
    ok: true,
    returns: [],
    output: "",
  });

  assert.deepEqual(a.lastFrame().payload, { path: "/hello.lua", args: [] });
});

test("run-file rejects non-string args", async () => {
  const reply = await handleMcpRequest(
    rpc("tools/call", { name: "run-file", arguments: { traposId: "7:base", path: "/hello.lua", args: ["ok", 2] } }),
    makeDeps(new GatewayRegistry(silentLog)),
  );
  const error = (reply as { error?: { code: number; message: string } }).error;
  assert.equal(error?.code, -32602);
  assert.equal(error?.message, "args must be an array of strings");
});

test("write-file reports the written path and byte count", async () => {
  const reg = new GatewayRegistry(silentLog);
  const deps = makeDeps(reg);
  const a = new FakeSocket();
  reg.register(NETWORK, "7:base", a);

  const text = await callAndAnswer(
    deps,
    a,
    "7:base",
    "write-file",
    { traposId: "7:base", path: "/foo.txt", content: "hello" },
    true,
    { ok: true, path: "/foo.txt", bytes: 5 },
  );

  assert.equal(a.lastFrame().type, "write-file");
  assert.equal(text, ["computer: 7:base", "ok: true", "path: /foo.txt", "bytes: 5"].join("\n"));
});

test("exec-lua rejects a missing traposId with an invalid-params error", async () => {
  const reply = await handleMcpRequest(
    rpc("tools/call", { name: "exec-lua", arguments: { code: "x" } }),
    makeDeps(new GatewayRegistry(silentLog)),
  );
  const error = (reply as { error?: { code: number; message: string } }).error;
  assert.equal(error?.code, -32602);
  assert.match(error?.message ?? "", /traposId/);
});

test("registry.list returns the traposIds registered for a network", () => {
  const reg = new GatewayRegistry(silentLog);
  reg.register(NETWORK, "7:base", new FakeSocket());
  reg.register(NETWORK, "8:turtle", new FakeSocket());
  reg.register("other", "9:base", new FakeSocket());
  assert.deepEqual(reg.list(NETWORK).sort(), ["7:base", "8:turtle"]);
  assert.deepEqual(reg.list("other"), ["9:base"]);
});
