import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { once } from "node:events";
import { mkdtemp, rm } from "node:fs/promises";
import { createServer as createHttpServer, request as httpRequest } from "node:http";
import { createServer as createNetServer } from "node:net";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";

const foundryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");

test("Map Gateway BFF caches only confirmed hits, honors validators and aborts abandoned streams", async () => {
  const root = await mkdtemp(join(tmpdir(), "nodedc-foundry-map-proxy-"));
  const gatewayPort = await freePort();
  const foundryPort = await freePort();
  let conditionalHeader = "";
  let resolveSlowClosed;
  const slowClosed = new Promise((resolveClosed) => { resolveSlowClosed = resolveClosed; });
  let resolveIdleClosed;
  const idleClosed = new Promise((resolveClosed) => { resolveIdleClosed = resolveClosed; });
  const gateway = createHttpServer((request, response) => {
    const url = new URL(request.url || "/", "http://gateway.local");
    if (url.pathname === "/healthz") {
      response.writeHead(200, { "content-type": "application/json" });
      response.end(JSON.stringify({ cache: { persistent: true, entries: 1, bytes: 4 } }));
      return;
    }
    if (url.pathname.startsWith("/api/map/ion/assets/")) {
      response.writeHead(200, { "content-type": "application/json" });
      response.end(JSON.stringify({ ok: true, credentialMode: "gateway" }));
      return;
    }
    if (url.pathname !== "/api/map/cache") {
      response.writeHead(404);
      response.end();
      return;
    }
    const target = String(url.searchParams.get("url") || "");
    conditionalHeader = String(request.headers["if-none-match"] || conditionalHeader);
    if (target.includes("/provider-error")) {
      response.writeHead(401, { "content-type": "application/json" });
      response.end(JSON.stringify({ error: "provider_unauthorized" }));
      return;
    }
    const headers = {
      "content-type": "application/octet-stream",
      etag: '"tile-v1"',
      "x-nodedc-map-cache": target.includes("nodedc_cache_refresh=1") ? "live-stale-upstream-error" : "live-cache-hit",
    };
    if (target.includes("/headers-slow")) {
      const timer = setTimeout(() => {
        if (response.destroyed) return;
        response.writeHead(200, { ...headers, "content-length": "4" });
        response.end("tile");
      }, 180);
      response.once("close", () => clearTimeout(timer));
      return;
    }
    if (target.includes("/body-idle")) {
      response.writeHead(200, headers);
      response.write("start");
      const timer = setTimeout(() => response.end("late"), 500);
      response.once("close", () => {
        clearTimeout(timer);
        resolveIdleClosed();
      });
      return;
    }
    if (target.includes("/progress")) {
      response.writeHead(200, { ...headers, "content-length": String(8 * 256) });
      let chunks = 0;
      const interval = setInterval(() => {
        chunks += 1;
        response.write(Buffer.alloc(256, chunks));
        if (chunks === 8) {
          clearInterval(interval);
          response.end();
        }
      }, 30);
      response.once("close", () => clearInterval(interval));
      return;
    }
    if (target.includes("/slow")) {
      response.writeHead(200, headers);
      response.write(Buffer.alloc(1024, 1));
      const interval = setInterval(() => response.write(Buffer.alloc(1024, 2)), 20);
      response.once("close", () => {
        clearInterval(interval);
        resolveSlowClosed();
      });
      return;
    }
    response.writeHead(200, { ...headers, "content-length": "4" });
    response.end("tile");
  });
  let foundry;

  try {
    gateway.listen(gatewayPort, "127.0.0.1");
    await once(gateway, "listening");
    foundry = spawn(process.execPath, ["server/catalog-server.mjs"], {
      cwd: foundryRoot,
      env: {
        ...process.env,
        NODE_ENV: "development",
        HOST: "127.0.0.1",
        PORT: String(foundryPort),
        FOUNDRY_RUNTIME_DIR: root,
        NODEDC_FOUNDRY_AUTH_REQUIRED: "false",
        NODEDC_MAP_GATEWAY_INTERNAL_URL: `http://127.0.0.1:${gatewayPort}`,
        NODEDC_MAP_GATEWAY_HEADERS_TIMEOUT_MS: "100",
        NODEDC_MAP_GATEWAY_BODY_IDLE_TIMEOUT_MS: "80",
      },
      stdio: ["ignore", "pipe", "pipe"],
    });
    await waitForService(foundryPort, foundry);
    const base = `http://127.0.0.1:${foundryPort}`;
    const proxied = (target) => `${base}/api/map-gateway/api/map/cache?url=${encodeURIComponent(target)}`;

    const hit = await fetch(proxied("https://assets.ion.cesium.com/1/tile.bin"));
    assert.equal(hit.status, 200);
    assert.equal(await hit.text(), "tile");
    assert.equal(hit.headers.get("cache-control"), "private, max-age=300, stale-while-revalidate=60");
    assert.equal(hit.headers.get("vary"), "Cookie");
    assert.equal(hit.headers.get("etag"), '"tile-v1"');

    const conditional = await fetch(proxied("https://assets.ion.cesium.com/1/tile.bin"), {
      headers: { "if-none-match": 'W/"tile-v1"' },
    });
    assert.equal(conditional.status, 304);
    assert.equal(conditionalHeader, 'W/"tile-v1"');
    assert.equal(conditional.headers.get("cache-control"), "private, max-age=300, stale-while-revalidate=60");

    const refresh = await fetch(proxied("https://assets.ion.cesium.com/1/tile.bin?nodedc_cache_refresh=1"));
    assert.equal(refresh.status, 200);
    assert.equal(refresh.headers.get("cache-control"), "no-store");

    const providerError = await fetch(proxied("https://assets.ion.cesium.com/1/provider-error"));
    assert.equal(providerError.status, 401);
    assert.equal(providerError.headers.get("cache-control"), "no-store");

    const endpoint = await fetch(`${base}/api/map-gateway/api/map/ion/assets/1/endpoint`);
    assert.equal(endpoint.status, 200);
    assert.equal(endpoint.headers.get("cache-control"), "no-store");
    const health = await fetch(`${base}/api/map-gateway/healthz`);
    assert.equal(health.status, 200);
    assert.equal(health.headers.get("cache-control"), "no-store");

    const progressing = await fetch(proxied("https://assets.ion.cesium.com/1/progress"));
    assert.equal(progressing.status, 200);
    assert.equal((await progressing.arrayBuffer()).byteLength, 8 * 256);

    const headersTimeout = await fetch(proxied("https://assets.ion.cesium.com/1/headers-slow"));
    assert.equal(headersTimeout.status, 504);
    assert.equal((await headersTimeout.json()).error, "map_gateway_headers_timeout");

    const idle = await fetch(proxied("https://assets.ion.cesium.com/1/body-idle"));
    assert.equal(idle.status, 200);
    await assert.rejects(idle.arrayBuffer());
    await Promise.race([
      idleClosed,
      new Promise((_, reject) => setTimeout(() => reject(new Error("upstream_body_idle_abort_timeout")), 2_000)),
    ]);

    await abortAfterFirstChunk(proxied("https://assets.ion.cesium.com/1/slow"));
    await Promise.race([
      slowClosed,
      new Promise((_, reject) => setTimeout(() => reject(new Error("upstream_abort_timeout")), 2_000)),
    ]);
    const stillHealthy = await fetch(`${base}/healthz`);
    assert.equal(stillHealthy.status, 200);
  } finally {
    await stop(foundry);
    if (gateway.listening) {
      gateway.close();
      await once(gateway, "close");
    }
    await rm(root, { recursive: true, force: true });
  }
});

function abortAfterFirstChunk(url) {
  return new Promise((resolveAbort, rejectAbort) => {
    const request = httpRequest(url, (response) => {
      response.once("data", () => {
        response.destroy();
        request.destroy();
        resolveAbort();
      });
    });
    request.once("error", (error) => {
      if (error?.code === "ECONNRESET") resolveAbort();
      else rejectAbort(error);
    });
    request.end();
  });
}

async function freePort() {
  const server = createNetServer();
  server.listen(0, "127.0.0.1");
  await once(server, "listening");
  const address = server.address();
  assert(address && typeof address === "object");
  const port = address.port;
  server.close();
  await once(server, "close");
  return port;
}

async function waitForService(port, child) {
  let output = "";
  child.stderr.on("data", (chunk) => { output += String(chunk); });
  for (let attempt = 0; attempt < 100; attempt += 1) {
    if (child.exitCode !== null) throw new Error(`foundry_exited:${child.exitCode}:${output}`);
    try {
      const response = await fetch(`http://127.0.0.1:${port}/healthz`);
      if (response.ok) return;
    } catch { /* service is still starting */ }
    await new Promise((resolveWait) => setTimeout(resolveWait, 50));
  }
  throw new Error(`foundry_start_timeout:${output}`);
}

async function stop(child) {
  if (!child || child.exitCode !== null) return;
  child.kill("SIGTERM");
  await once(child, "exit").catch(() => undefined);
}
