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

function response(messageId: string, ok: boolean, payload: Record<string, unknown> = {}, error?: { code: string; message?: string }): ComputerResponse {
  return { event: "computer_response", traposSenderId: "7:", messageId, type: "ping", ok, payload, error };
}

function sentMessageId(socket: FakeSocket): string {
  return socket.lastFrame().messageId as string;
}

test("register / has / count / unregister", () => {
  const reg = new GatewayRegistry(silentLog);
  const a = new FakeSocket();
  reg.register("global", "7:", a);
  assert.equal(reg.has("global", "7:"), true);
  assert.equal(reg.count(), 1);
  reg.unregister("global", "7:", a);
  assert.equal(reg.has("global", "7:"), false);
  assert.equal(reg.count(), 0);
});

test("newest-wins evicts previous socket for same key", () => {
  const reg = new GatewayRegistry(silentLog);
  const a = new FakeSocket();
  const b = new FakeSocket();
  reg.register("global", "7:", a);
  reg.register("global", "7:", b);
  assert.equal(a.closed, true);
  // Distinct close code lets the displaced Lua client yield instead of reconnect-looping.
  assert.equal(a.closeCode, DISPLACED_CLOSE_CODE);
  assert.equal(b.closed, false);
  assert.equal(reg.count(), 1);
});

test("same traposId under different networks coexist", () => {
  const reg = new GatewayRegistry(silentLog);
  reg.register("global", "7:", new FakeSocket());
  reg.register("other", "7:", new FakeSocket());
  assert.equal(reg.count(), 2);
});

test("request sends a server_request frame and resolves on matching ok response", async () => {
  const reg = new GatewayRegistry(silentLog);
  const a = new FakeSocket();
  reg.register("global", "7:", a);

  const promise = reg.request("global", "7:", "ping", {});
  const frame = a.lastFrame();
  assert.equal(frame.event, "server_request");
  assert.equal(frame.type, "ping");
  assert.equal(frame.traposReceiverId, "7:");

  reg.resolveResponse(a, "global", "7:", response(frame.messageId as string, true, { traposId: "7:" }));
  assert.deepEqual(await promise, { traposId: "7:" });
});

test("request rejects with the response error code on ok:false", async () => {
  const reg = new GatewayRegistry(silentLog);
  const a = new FakeSocket();
  reg.register("global", "7:", a);

  const promise = reg.request("global", "7:", "ping", {});
  reg.resolveResponse(a, "global", "7:", response(sentMessageId(a), false, {}, { code: "unknown_type", message: "nope" }));

  await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "unknown_type");
});

test("request rejects not_connected when no socket registered", async () => {
  const reg = new GatewayRegistry(silentLog);
  await assert.rejects(
    reg.request("global", "nobody", "ping", {}),
    (err: unknown) => err instanceof GatewayError && err.code === "not_connected",
  );
});

test("request rejects timeout after the deadline", async () => {
  const reg = new GatewayRegistry(silentLog);
  reg.register("global", "7:", new FakeSocket());
  await assert.rejects(
    reg.request("global", "7:", "ping", {}, 10),
    (err: unknown) => err instanceof GatewayError && err.code === "timeout",
  );
});

test("messageId correlation: concurrent requests resolve independently", async () => {
  const reg = new GatewayRegistry(silentLog);
  const a = new FakeSocket();
  reg.register("global", "7:", a);

  const p1 = reg.request("global", "7:", "ping", { n: 1 });
  const id1 = a.lastFrame().messageId as string;
  const p2 = reg.request("global", "7:", "ping", { n: 2 });
  const id2 = a.lastFrame().messageId as string;
  assert.notEqual(id1, id2);

  // resolve out of order
  reg.resolveResponse(a, "global", "7:", response(id2, true, { n: 2 }));
  reg.resolveResponse(a, "global", "7:", response(id1, true, { n: 1 }));
  assert.deepEqual(await p1, { n: 1 });
  assert.deepEqual(await p2, { n: 2 });
});

test("pending rejected on unregister (close)", async () => {
  const reg = new GatewayRegistry(silentLog);
  const a = new FakeSocket();
  reg.register("global", "7:", a);
  const promise = reg.request("global", "7:", "ping", {});
  reg.unregister("global", "7:", a);
  await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "not_connected");
});

test("eviction: old socket's pending rejected and late unregister(A) does not evict B", async () => {
  const reg = new GatewayRegistry(silentLog);
  const a = new FakeSocket();
  const b = new FakeSocket();
  reg.register("global", "7:", a);
  const promise = reg.request("global", "7:", "ping", {});

  reg.register("global", "7:", b); // evict A
  await assert.rejects(promise, (err: unknown) => err instanceof GatewayError && err.code === "not_connected");

  reg.unregister("global", "7:", a); // late close from evicted A — must be a no-op
  assert.equal(reg.has("global", "7:"), true);
  assert.equal(reg.count(), 1);
});

test("resolveResponse with no matching pending is a silent no-op", () => {
  const reg = new GatewayRegistry(silentLog);
  const a = new FakeSocket();
  reg.register("global", "7:", a);
  // should not throw
  reg.resolveResponse(a, "global", "7:", response("unknown-id", true, {}));
});
