import assert from "node:assert/strict";
import { createServer } from "node:http";
import test from "node:test";

import {
  isCloudWebSseRoute,
  proxyCloudApiRequest,
  upstreamRequestHeaders
} from "./cloud-web-proxy.mjs";

test("cloud web proxy preserves Code Agent short-connection headers", () => {
  const headers = upstreamRequestHeaders({
    headers: {
      accept: "application/json",
      "content-type": "application/json",
      prefer: "respond-async",
      "x-hwlab-short-connection": "1",
      "x-trace-id": "trc_proxy_short_connection",
      "x-request-id": "req_proxy_short_connection",
      cookie: "hwlab_session=must-forward",
      authorization: "Bearer hwl_live_must-forward"
    }
  }, "{\"message\":\"hello\"}");

  assert.equal(headers.accept, "application/json");
  assert.equal(headers["content-type"], "application/json");
  assert.equal(headers.prefer, "respond-async");
  assert.equal(headers["x-hwlab-short-connection"], "1");
  assert.equal(headers["x-trace-id"], "trc_proxy_short_connection");
  assert.equal(headers["x-request-id"], "req_proxy_short_connection");
  assert.equal(headers.cookie, "hwlab_session=must-forward");
  assert.equal(headers.authorization, "Bearer hwl_live_must-forward");
  assert.equal(headers["content-length"], Buffer.byteLength("{\"message\":\"hello\"}"));
});

test("cloud web proxy streams SSE chunks without fixed content-length", async () => {
  const upstream = createServer((request, response) => {
    assert.equal(request.headers["x-trace-id"], "trc_stream_proxy");
    response.writeHead(200, {
      "content-type": "text/event-stream; charset=utf-8",
      "cache-control": "no-store",
      "x-accel-buffering": "no"
    });
    setTimeout(() => response.write("event: snapshot\ndata: {\"events\":[{\"seq\":1}]}\n\n"), 40);
    setTimeout(() => response.write("event: runnerTrace\ndata: {\"snapshot\":{\"events\":[{\"seq\":1},{\"seq\":2}]}}\n\n"), 140);
    setTimeout(() => response.end(), 500);
  });
  await listen(upstream);

  const proxy = createServer((request, response) => {
    const url = new URL(request.url || "/", "http://cloud-web.local");
    proxyCloudApiRequest({
      target: new URL(url.pathname + url.search, serverUrl(upstream)),
      request,
      response,
      timeoutMs: 1000,
      forceStream: isCloudWebSseRoute(url.pathname)
    }).catch((error) => response.destroy(error));
  });
  await listen(proxy);

  try {
    const startedAt = Date.now();
    const response = await fetch(`${serverUrl(proxy)}/v1/agent/traces/trc_stream_proxy/stream`, {
      headers: {
        "x-trace-id": "trc_stream_proxy",
        accept: "text/event-stream"
      }
    });
    assert.equal(response.status, 200);
    assert.equal(response.headers.get("content-length"), null);
    assert.match(response.headers.get("content-type") || "", /^text\/event-stream/iu);
    assert.equal(response.headers.get("cache-control"), "no-store");
    assert.equal(response.headers.get("x-accel-buffering"), "no");

    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let text = "";
    let firstEventAt = null;
    let secondEventAt = null;
    while (!secondEventAt) {
      const { value, done } = await reader.read();
      if (done) break;
      text += decoder.decode(value, { stream: true });
      const eventCount = (text.match(/^event:/gmu) || []).length;
      if (eventCount >= 1 && firstEventAt === null) firstEventAt = Date.now() - startedAt;
      if (eventCount >= 2) secondEventAt = Date.now() - startedAt;
    }
    await reader.cancel().catch(() => {});

    assert.ok(firstEventAt !== null && firstEventAt < 250, `first SSE event was not flushed promptly: ${firstEventAt}ms`);
    assert.ok(secondEventAt !== null && secondEventAt < 450, `second SSE event was not flushed promptly: ${secondEventAt}ms`);
    assert.ok(secondEventAt > firstEventAt, "event count must grow at two distinct times");
  } finally {
    await close(proxy);
    await close(upstream);
  }
});

test("cloud web proxy does not reject after SSE headers are already sent", async () => {
  const upstream = createServer((request, response) => {
    request.resume();
    response.writeHead(200, { "content-type": "text/event-stream; charset=utf-8" });
    response.write("event: snapshot\ndata: {\"events\":[{\"seq\":1}]}\n\n");
    setTimeout(() => response.destroy(new Error("simulated upstream stream failure")), 40);
  });
  await listen(upstream);

  let rejected = false;
  const proxy = createServer((request, response) => {
    const url = new URL(request.url || "/", "http://cloud-web.local");
    proxyCloudApiRequest({
      target: new URL(url.pathname + url.search, serverUrl(upstream)),
      request,
      response,
      timeoutMs: 1000,
      forceStream: isCloudWebSseRoute(url.pathname)
    }).catch((error) => {
      rejected = true;
      if (!response.headersSent) response.destroy(error);
    });
  });
  await listen(proxy);

  try {
    const response = await fetch(`${serverUrl(proxy)}/v1/agent/traces/trc_stream_proxy_error/stream`, {
      headers: { accept: "text/event-stream" }
    });
    assert.equal(response.status, 200);
    const reader = response.body.getReader();
    await reader.read();
    await reader.read().catch(() => {});
    await new Promise((resolve) => setTimeout(resolve, 80));
    assert.equal(rejected, false, "stream errors after headers must not bubble into JSON error writers");
  } finally {
    await close(proxy);
    await close(upstream);
  }
});

test("cloud web proxy buffers normal JSON and sets content-length", async () => {
  const upstream = createServer((request, response) => {
    request.resume();
    setTimeout(() => {
      response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
      response.end(JSON.stringify({ ok: true }));
    }, 20);
  });
  await listen(upstream);

  const proxy = createServer((request, response) => {
    proxyCloudApiRequest({
      target: new URL(request.url || "/", serverUrl(upstream)),
      request,
      response,
      body: "",
      timeoutMs: 1000
    }).catch((error) => response.destroy(error));
  });
  await listen(proxy);

  try {
    const response = await fetch(`${serverUrl(proxy)}/v1`);
    assert.equal(response.status, 200);
    assert.equal(response.headers.get("content-length"), String(Buffer.byteLength("{\"ok\":true}")));
    assert.deepEqual(await response.json(), { ok: true });
  } finally {
    await close(proxy);
    await close(upstream);
  }
});

function listen(server) {
  return new Promise((resolve, reject) => {
    server.once("error", reject);
    server.listen(0, "127.0.0.1", resolve);
  });
}

function close(server) {
  return new Promise((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()));
  });
}

function serverUrl(server) {
  const address = server.address();
  return `http://127.0.0.1:${address.port}`;
}
