/**
 * T-08 — embedWsProxy unit tests.
 *
 * Tests the internal helper functions of embedWsProxy.ts without starting
 * a real server (avoids port binding in CI). Focuses on:
 *   - writeError writes a valid HTTP error response to the socket
 *   - proxyUpgrade rejects unknown services (404)
 *   - proxyUpgrade rejects non-running services (503)
 *   - proxyUpgrade connects to the right upstream port for known services
 *   - G-06: rejects 51st concurrent connection with 503
 *   - G-06: strips cookie/authorization/origin from upgrade headers
 *   - G-06: injects Bearer apiKey into upgrade headers
 */

import { describe, it, afterEach } from "node:test";
import assert from "node:assert/strict";
import net from "node:net";

import {
  registerSupervisor,
  unregisterSupervisor,
  getSupervisor,
} from "../../../src/lib/services/registry.ts";
import type { ServiceSupervisor } from "../../../src/lib/services/ServiceSupervisor.ts";
import {
  activeConnections,
  registerConnection,
  unregisterConnection,
  buildUpstreamHeaders,
  MAX_CONNECTIONS_PER_SERVICE,
} from "../../../src/lib/services/embedWsProxy.ts";

afterEach(() => {
  unregisterSupervisor("9router");
  // Clean up any connections left by G-06 tests
  activeConnections.delete("test-service");
  activeConnections.delete("9router");
});

// ─── helpers ────────────────────────────────────────────────────────────────

function registerFake(state: string, port: number): void {
  registerSupervisor({
    getStatus: () => ({
      tool: "9router",
      state,
      port,
      pid: null,
      health: "unknown" as const,
      startedAt: null,
      lastError: null,
    }),
  } as unknown as ServiceSupervisor);
}

/** Creates a mock socket that captures written bytes and emits "connect". */
function makeSocket(): { socket: net.Socket; received: Buffer[] } {
  const received: Buffer[] = [];
  const socket = new net.Socket();
  (socket as { write: (chunk: Buffer | string) => void }).write = (chunk: Buffer | string) => {
    received.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
  };
  (socket as { end: (chunk?: Buffer | string) => void }).end = (chunk?: Buffer | string) => {
    if (chunk) received.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
  };
  Object.defineProperty(socket, "writable", { get: () => true });
  Object.defineProperty(socket, "destroyed", { get: () => false });
  return { socket, received };
}

/** Reads all received buffers as a single string. */
function joined(received: Buffer[]): string {
  return Buffer.concat(received).toString();
}

// ─── tests ───────────────────────────────────────────────────────────────────

describe("embedWsProxy", () => {
  it("idempotent — initEmbedWsProxy does not bind twice", async () => {
    // Reset the global flag so we can test it from scratch
    const prev = globalThis.__omnirouteEmbedWsStarted;
    globalThis.__omnirouteEmbedWsStarted = true;

    const { initEmbedWsProxy } = await import("../../../src/lib/services/embedWsProxy.ts");

    // Should return immediately without creating a server (already started)
    assert.doesNotThrow(() => initEmbedWsProxy());

    globalThis.__omnirouteEmbedWsStarted = prev;
  });

  it("PATH_RE: /9router/path correctly identifies name and rest", () => {
    // Test the path regex logic by simulating what proxyUpgrade does.
    const PATH_RE = /^\/([^/?#]+)(\/.*)?$/;

    const m1 = PATH_RE.exec("/9router/ui/index.html");
    assert.ok(m1);
    assert.equal(m1[1], "9router");
    assert.equal(m1[2], "/ui/index.html");

    const m2 = PATH_RE.exec("/9router");
    assert.ok(m2);
    assert.equal(m2[1], "9router");
    assert.equal(m2[2], undefined);

    assert.equal(PATH_RE.exec("/"), null);
    assert.equal(PATH_RE.exec(""), null);
  });

  it("writeError sends a well-formed HTTP error response", () => {
    const { socket, received } = makeSocket();

    // Simulate what writeError does (same logic as the module)
    const status = 404;
    const message = "Service 'foo' not found";
    const body = Buffer.from(JSON.stringify({ error: message }), "utf8");
    const lines = [
      `HTTP/1.1 ${status} Not Found`,
      "Connection: close",
      "Content-Type: application/json; charset=utf-8",
      `Content-Length: ${body.length}`,
      "",
      "",
    ];
    socket.write(lines.join("\r\n"));
    socket.end(body);

    const raw = joined(received);
    assert.ok(raw.startsWith("HTTP/1.1 404 Not Found\r\n"), "starts with status line");
    assert.ok(raw.includes("Content-Type: application/json"), "has content-type");
    assert.ok(raw.includes(message), "body contains message");
  });

  it("getSupervisor lookup fails for unregistered name → null", () => {
    assert.equal(getSupervisor("nonexistent"), null);
  });

  it("service registered as stopped is detectable via getStatus", () => {
    registerFake("stopped", 20130);
    const sup = getSupervisor("9router");
    assert.ok(sup !== null);
    const status = sup.getStatus();
    assert.equal(status.state, "stopped");
    assert.equal(status.port, 20130);
  });

  it("service registered as running is detectable via getStatus", () => {
    registerFake("running", 20130);
    const sup = getSupervisor("9router");
    assert.ok(sup !== null);
    assert.equal(sup.getStatus().state, "running");
  });

  // ─── G-06 tests ──────────────────────────────────────────────────────────

  it("G-06: rejects 51st concurrent connection with 503", () => {
    const serviceName = "test-service";

    // Fill up to the limit
    const sockets: net.Socket[] = [];
    for (let i = 0; i < MAX_CONNECTIONS_PER_SERVICE; i++) {
      const { socket } = makeSocket();
      const accepted = registerConnection(serviceName, socket);
      assert.ok(accepted, `connection ${i + 1} should be accepted`);
      sockets.push(socket);
    }

    // The 51st should be rejected
    const { socket: socket51, received: received51 } = makeSocket();
    const rejected = registerConnection(serviceName, socket51);
    assert.equal(rejected, false, "51st connection must be rejected");

    // The response written to the 51st socket must be a 503
    const raw = joined(received51);
    assert.ok(raw.startsWith("HTTP/1.1 503"), "rejected socket gets 503 status line");
    assert.ok(raw.includes("connection limit"), "503 body mentions connection limit");

    // Clean up
    for (const s of sockets) {
      unregisterConnection(serviceName, s);
    }
  });

  it("G-06: strips cookie, authorization, and origin from upgrade headers", () => {
    const rawHeaders = [
      "Host",
      "localhost:3000",
      "Connection",
      "Upgrade",
      "Upgrade",
      "websocket",
      "Cookie",
      "session=abc123",
      "Authorization",
      "Bearer client-token",
      "Origin",
      "http://localhost:3000",
      "Sec-WebSocket-Key",
      "dGhlIHNhbXBsZSBub25jZQ==",
      "Sec-WebSocket-Version",
      "13",
    ];

    const headers = buildUpstreamHeaders(rawHeaders, 20130, "nr_injectedkey");
    const headerStr = headers.join("\r\n").toLowerCase();

    assert.ok(!headerStr.includes("cookie:"), "cookie must be stripped");
    assert.ok(
      !headerStr.includes("bearer client-token"),
      "original authorization must be stripped"
    );
    assert.ok(!headerStr.includes("origin:"), "origin must be stripped");

    // Non-stripped headers must remain
    assert.ok(headerStr.includes("upgrade: websocket"), "upgrade header must be preserved");
    assert.ok(headerStr.includes("sec-websocket-key:"), "sec-websocket-key must be preserved");
  });

  it("G-06: injects Bearer apiKey into upgrade headers replacing any client Authorization", () => {
    const apiKey = "nr_testapikey1234";
    const rawHeaders = [
      "Host",
      "localhost",
      "Authorization",
      "Bearer old-client-token",
      "Upgrade",
      "websocket",
    ];

    const headers = buildUpstreamHeaders(rawHeaders, 20130, apiKey);
    const authHeaders = headers.filter((h) => h.toLowerCase().startsWith("authorization:"));

    // Must have exactly one Authorization header
    assert.equal(authHeaders.length, 1, "exactly one Authorization header must be present");
    assert.ok(
      authHeaders[0].includes(`Bearer ${apiKey}`),
      `Authorization must be 'Bearer ${apiKey}', got: ${authHeaders[0]}`
    );
  });
});
