import assert from "node:assert/strict";
import { test } from "node:test";
import { createConnection } from "../src/modules/trapos-cloud-gateway/connection.js";
import { GatewayRegistry } from "../src/modules/trapos-cloud-gateway/registry.js";
import type { OpencodeProbe, ProbeResult } from "../src/opencode.js";
import { FakeSocket, silentLog } from "./helpers.js";

function fakeProbe(result: ProbeResult): OpencodeProbe {
  return { probe: () => Promise.resolve(result) };
}

function setup(opts?: { secret?: string; probe?: ProbeResult }) {
  const socket = new FakeSocket();
  const registry = new GatewayRegistry(silentLog);
  const expectedSecret = opts?.secret;
  const connection = createConnection({
    socket,
    registry,
    probe: fakeProbe(opts?.probe ?? { ok: true }),
    resolveNetwork: (secret) => (expectedSecret === undefined || secret === expectedSecret ? "global" : null),
    log: silentLog,
  });
  return { socket, registry, connection };
}

function helloFrame(traposId: string | undefined, secret?: string): string {
  const payload: Record<string, unknown> = {};
  if (traposId !== undefined) payload.traposId = traposId;
  if (secret !== undefined) payload.secret = secret;
  return JSON.stringify({ event: "computer_request", traposSenderId: traposId ?? "?", messageId: "h1", type: "hello", payload });
}

function requestFrame(type: string, messageId = "r1"): string {
  return JSON.stringify({ event: "computer_request", traposSenderId: "7:", messageId, type, payload: {} });
}

test("pre-hello non-hello request -> not_authenticated, no close, not registered", async () => {
  const { socket, registry, connection } = setup();
  await connection.onMessage(requestFrame("probe_server"));
  const frame = socket.lastFrame();
  assert.equal(frame.event, "server_response");
  assert.equal(frame.ok, false);
  assert.deepEqual(frame.error, { code: "not_authenticated", message: "hello required before any other request" });
  assert.equal(socket.closed, false);
  assert.equal(registry.count(), 0);
});

test("pre-hello wrong-direction frame is dropped silently", async () => {
  const { socket, connection } = setup();
  await connection.onMessage(
    JSON.stringify({ event: "computer_response", traposSenderId: "7:", messageId: "x", type: "ping", ok: true, payload: {} }),
  );
  assert.equal(socket.sent.length, 0);
});

test("hello missing traposId -> not_authenticated, no close, not registered", async () => {
  const { socket, registry, connection } = setup();
  await connection.onMessage(helloFrame(undefined));
  const frame = socket.lastFrame();
  assert.equal(frame.ok, false);
  assert.deepEqual(frame.error, { code: "not_authenticated", message: "missing traposId" });
  assert.equal(socket.closed, false);
  assert.equal(registry.count(), 0);
});

test("hello bad secret -> unauthorized and close", async () => {
  const { socket, registry, connection } = setup({ secret: "right" });
  await connection.onMessage(helloFrame("7:", "wrong"));
  const frame = socket.lastFrame();
  assert.equal(frame.ok, false);
  assert.deepEqual(frame.error, { code: "unauthorized", message: "invalid secret" });
  assert.equal(socket.closed, true);
  assert.equal(registry.count(), 0);
});

test("hello ok -> registers and replies with networkId", async () => {
  const { socket, registry, connection } = setup();
  await connection.onMessage(helloFrame("7:", "anything"));
  const frame = socket.lastFrame();
  assert.equal(frame.event, "server_response");
  assert.equal(frame.type, "hello");
  assert.equal(frame.ok, true);
  assert.deepEqual(frame.payload, { networkId: "global" });
  assert.equal(registry.has("global", "7:"), true);
  assert.equal(registry.count(), 1);
});

test("post-hello probe_server (ok) -> serverOk + opencodeOk, no detail", async () => {
  const { socket, connection } = setup({ probe: { ok: true } });
  await connection.onMessage(helloFrame("7:"));
  await connection.onMessage(requestFrame("probe_server"));
  const frame = socket.lastFrame();
  assert.equal(frame.ok, true);
  assert.deepEqual(frame.payload, { serverOk: true, opencodeOk: true });
});

test("post-hello probe_server (fail) -> opencodeOk:false + opencodeDetail", async () => {
  const { socket, connection } = setup({ probe: { ok: false, detail: "http://x -> 401" } });
  await connection.onMessage(helloFrame("7:"));
  await connection.onMessage(requestFrame("probe_server"));
  const frame = socket.lastFrame();
  assert.deepEqual(frame.payload, { serverOk: true, opencodeOk: false, opencodeDetail: "http://x -> 401" });
});

test("post-hello unknown type -> unknown_type error", async () => {
  const { socket, connection } = setup();
  await connection.onMessage(helloFrame("7:"));
  await connection.onMessage(requestFrame("does_not_exist"));
  const frame = socket.lastFrame();
  assert.equal(frame.ok, false);
  assert.equal((frame.error as { code: string }).code, "unknown_type");
});

test("post-hello duplicate hello is ignored", async () => {
  const { socket, registry, connection } = setup();
  await connection.onMessage(helloFrame("7:"));
  const afterFirst = socket.sent.length;
  await connection.onMessage(helloFrame("9:"));
  assert.equal(socket.sent.length, afterFirst); // no reply
  assert.equal(registry.has("global", "7:"), true);
  assert.equal(registry.has("global", "9:"), false);
});

test("post-hello wrong-direction frame is dropped", async () => {
  const { socket, connection } = setup();
  await connection.onMessage(helloFrame("7:"));
  const afterHello = socket.sent.length;
  await connection.onMessage(
    JSON.stringify({ event: "server_request", traposReceiverId: "7:", messageId: "g1", type: "ping", payload: {} }),
  );
  assert.equal(socket.sent.length, afterHello);
});

test("reverse direction: gateway request resolved by computer_response", async () => {
  const { socket, registry, connection } = setup();
  await connection.onMessage(helloFrame("7:"));

  const promise = registry.request("global", "7:", "ping", {});
  const sent = socket.lastFrame();
  assert.equal(sent.event, "server_request");
  assert.equal(sent.type, "ping");

  await connection.onMessage(
    JSON.stringify({ event: "computer_response", traposSenderId: "7:", messageId: sent.messageId, type: "ping", ok: true, payload: { traposId: "7:" } }),
  );
  assert.deepEqual(await promise, { traposId: "7:" });
});

test("onClose unregisters the connection", async () => {
  const { registry, connection } = setup();
  await connection.onMessage(helloFrame("7:"));
  assert.equal(registry.count(), 1);
  connection.onClose();
  assert.equal(registry.count(), 0);
});

test("malformed frame is dropped silently", async () => {
  const { socket, connection } = setup();
  await connection.onMessage("not json at all");
  assert.equal(socket.sent.length, 0);
});
