import assert from "node:assert/strict";
import { createHash, createHmac } from "node:crypto";
import { once } from "node:events";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createServer as createHttpServer } from "node:http";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawn } from "node:child_process";

const root = await mkdtemp(join(tmpdir(), "nodedc-map-gateway-admin-smoke-"));
const cacheDir = join(root, "cache");
const port = await freePort();
const egressPort = await freePort();
const adminSecret = "gateway-admin-smoke-secret";
const providerToken = "cesium-private-token-should-never-be-returned";
const rotatedProviderToken = "rotated-cesium-private-token-should-never-be-returned";
const egressProxyToken = "map-egress-private-token-should-never-be-returned";
const ionReferer = "https://foundry.example.test/page-library/";
const providerState = createProviderState();
let child;
let egress;
let childOutput = "";

try {
  const egressTokenFile = join(root, "map-egress-proxy-token");
  await writeFile(egressTokenFile, `${egressProxyToken}\n`, { mode: 0o600 });
  egress = createCesiumEgressMock(providerState, egressProxyToken);
  egress.listen(egressPort, "127.0.0.1");
  await once(egress, "listening");
  child = spawn(process.execPath, ["src/server.mjs"], {
    cwd: new URL("..", import.meta.url),
    env: {
      ...process.env,
      PORT: String(port),
      MAP_CACHE_DIR: cacheDir,
      MAP_GATEWAY_ALLOW_ANONYMOUS: "true",
      MAP_CACHE_MODE: "readwrite",
      NODE_ENV: "test",
      CESIUM_ION_TOKEN: "",
      // A malformed legacy override may add an approved asset, but must never
      // remove the three canonical Foundry map assets.
      CESIUM_ION_ASSET_ALLOWLIST: "999",
      CESIUM_ION_API_BASE_URL: "https://api.cesium.com/",
      MAP_GATEWAY_UPSTREAM_ALLOWLIST: "api.cesium.com,assets.ion.cesium.com,dev.virtualearth.net",
      MAP_GATEWAY_TEST_ALLOW_HTTP_LOOPBACK: "true",
      MAP_GATEWAY_EGRESS_URL: `http://127.0.0.1:${egressPort}/`,
      MAP_GATEWAY_EGRESS_PROXY_TOKEN_FILE: egressTokenFile,
      NODEDC_MAP_GATEWAY_ADMIN_SECRET: adminSecret,
      CESIUM_ION_ENDPOINT_TTL_SECONDS: "2",
      CESIUM_ION_ENDPOINT_REFRESH_AHEAD_SECONDS: "1",
    },
    stdio: ["ignore", "pipe", "pipe"],
  });
  child.stdout.on("data", (chunk) => { childOutput += String(chunk); });
  child.stderr.on("data", (chunk) => { childOutput += String(chunk); });

  await waitForGateway(port, child);
  const path = "/api/map/admin/cesium-ion";
  const unsigned = await fetch(`http://127.0.0.1:${port}${path}`);
  assert.equal(unsigned.status, 401);

  const initial = await requestAdmin(port, adminSecret, "GET", path, "user_root", "", ionReferer);
  assert.equal(initial.response.status, 200);
  assert.equal(initial.body.configured, false);

  const saved = await requestAdmin(port, adminSecret, "PUT", path, "user_root", JSON.stringify({ token: providerToken }), ionReferer);
  assert.equal(saved.response.status, 200);
  assert.equal(saved.body.configured, true);
  assert.equal(saved.body.verification, "verified");
  assert.equal(saved.raw.includes(providerToken), false);

  const terrainEndpoint = await fetch(`http://127.0.0.1:${port}/api/map/ion/assets/1/endpoint`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(terrainEndpoint.status, 200);
  assert.equal((await terrainEndpoint.json()).credentialMode, "gateway");
  const buildingsEndpoint = await fetch(`http://127.0.0.1:${port}/api/map/ion/assets/96188/endpoint`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(buildingsEndpoint.status, 200);
  const buildingsEndpointRaw = await buildingsEndpoint.text();
  const buildingsEndpointBody = JSON.parse(buildingsEndpointRaw);
  assert.equal(buildingsEndpointBody.credentialMode, "gateway");
  assert.equal(buildingsEndpointRaw.includes(providerState.credentials.v1.buildings), false);
  assert.equal(buildingsEndpointBody.url.endsWith("/tileset.json?v=production-shaped"), true);

  const endpointCallsBeforeCacheHit = providerState.endpointRequests.get("v1:96188");
  const cachedBuildingsEndpoint = await fetch(`http://127.0.0.1:${port}/api/map/ion/assets/96188/endpoint`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(cachedBuildingsEndpoint.status, 200);
  assert.equal(providerState.endpointRequests.get("v1:96188"), endpointCallsBeforeCacheHit);

  const terrainTile = encodeURIComponent("https://assets.ion.cesium.com/us-east-1/asset_depot/1/CesiumWorldTerrain/v1.2/0/0/0.terrain");
  const proxiedTerrain = await fetch(`http://127.0.0.1:${port}/api/map/cache?url=${terrainTile}`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(proxiedTerrain.status, 200);
  assert.equal(await proxiedTerrain.text(), "terrain-bytes");

  const buildingsRoot = await fetch(`http://127.0.0.1:${port}/api/map/cache?url=${encodeURIComponent(buildingsEndpointBody.url)}`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(buildingsRoot.status, 200);
  assert.equal((await buildingsRoot.json()).root.content.uri, "root.b3dm");
  const buildingsChildUrl = new URL("14/31780/12975.b3dm", buildingsEndpointBody.url).toString();
  const buildingsChild = await fetch(`http://127.0.0.1:${port}/api/map/cache?url=${encodeURIComponent(buildingsChildUrl)}`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(buildingsChild.status, 200);
  assert.equal(await buildingsChild.text(), "building-bytes-v1");

  const nestedEndpoint = await fetch(`http://127.0.0.1:${port}/api/map/ion/assets/999/endpoint`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  const nestedEndpointRaw = await nestedEndpoint.text();
  const nestedEndpointBody = JSON.parse(nestedEndpointRaw);
  assert.equal(nestedEndpoint.status, 200);
  assert.equal(nestedEndpointRaw.includes(providerState.credentials.v1.nested), false);
  const nestedChildUrl = new URL("nested-child.b3dm", nestedEndpointBody.url).toString();
  const nestedChild = await fetch(`http://127.0.0.1:${port}/api/map/cache?url=${encodeURIComponent(nestedChildUrl)}`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(nestedChild.status, 200);
  assert.equal(await nestedChild.text(), "nested-building-bytes-v1");

  const attackerToken = "browser-supplied-token-must-be-removed";
  const siblingUrl = `https://assets.ion.cesium.com/us-east-1/asset_depot/999/OtherAsset/root.b3dm?access_token=${attackerToken}`;
  const sibling = await fetch(`http://127.0.0.1:${port}/api/map/cache?url=${encodeURIComponent(siblingUrl)}`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(sibling.status, 401);
  assert.equal(providerState.siblingCredentialObserved, "");

  const rejected = await requestAdmin(port, adminSecret, "PUT", path, "user_root", JSON.stringify({ token: "invalid-private-token-should-not-replace-valid-value" }), ionReferer);
  assert.equal(rejected.response.status, 422);
  assert.equal(rejected.body.error, "cesium_ion_token_verification_failed");

  // Simulate metadata persisted by an older Gateway transport. A false failure
  // must be rechecked exactly once after the v4 scope/cache transport upgrade.
  await writeFile(join(cacheDir, "secrets", "cesium-ion-token.metadata.json"), JSON.stringify({
    updatedAt: "2026-07-15T00:00:00.000Z",
    updatedBy: "user_root",
    verification: "failed",
  }));
  const status = await requestAdmin(port, adminSecret, "GET", path, "user_root", "", ionReferer);
  assert.equal(status.response.status, 200);
  assert.equal(status.body.configured, true);
  assert.equal(status.body.verification, "verified");
  assert.equal(status.raw.includes(providerToken), false);
  const metadata = JSON.parse(await readFile(join(cacheDir, "secrets", "cesium-ion-token.metadata.json"), "utf8"));
  assert.equal(metadata.verificationTransportVersion, 4);

  // Refresh-ahead is cache-first and single-flight: concurrent callers receive
  // the still-valid cached endpoint while exactly one Ion request refreshes it.
  await delay(1_100);
  const callsBeforeRefreshAhead = providerState.endpointRequests.get("v1:96188");
  const refreshAheadResponses = await Promise.all(Array.from({ length: 8 }, () => fetch(
    `http://127.0.0.1:${port}/api/map/ion/assets/96188/endpoint`,
    { headers: { "x-nodedc-ion-referer": ionReferer } },
  )));
  assert.equal(refreshAheadResponses.every((response) => response.status === 200), true);
  await waitFor(() => providerState.endpointRequests.get("v1:96188") >= callsBeforeRefreshAhead + 1);
  await delay(100);
  assert.equal(providerState.endpointRequests.get("v1:96188"), callsBeforeRefreshAhead + 1);

  const rotated = await requestAdmin(port, adminSecret, "PUT", path, "user_root", JSON.stringify({ token: rotatedProviderToken }), ionReferer);
  assert.equal(rotated.response.status, 200);
  assert.equal(rotated.raw.includes(rotatedProviderToken), false);
  const rotatedEndpoint = await fetch(`http://127.0.0.1:${port}/api/map/ion/assets/96188/endpoint`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  const rotatedEndpointRaw = await rotatedEndpoint.text();
  const rotatedEndpointBody = JSON.parse(rotatedEndpointRaw);
  assert.equal(rotatedEndpoint.status, 200);
  assert.equal(rotatedEndpointRaw.includes(providerState.credentials.v1.buildings), false);
  assert.equal(rotatedEndpointRaw.includes(providerState.credentials.v2.buildings), false);
  const rotatedChildUrl = new URL("rotated.b3dm", rotatedEndpointBody.url).toString();
  const rotatedChild = await fetch(`http://127.0.0.1:${port}/api/map/cache?url=${encodeURIComponent(rotatedChildUrl)}`, { headers: { "x-nodedc-ion-referer": ionReferer } });
  assert.equal(rotatedChild.status, 200);
  assert.equal(await rotatedChild.text(), "building-bytes-v2");

  const indexRaw = await readFile(join(cacheDir, "index.json"), "utf8");
  for (const secret of [providerToken, rotatedProviderToken, ...Object.values(providerState.credentials.v1), ...Object.values(providerState.credentials.v2), attackerToken]) {
    assert.equal(indexRaw.includes(secret), false);
    assert.equal(childOutput.includes(secret), false);
  }

  const health = await fetch(`http://127.0.0.1:${port}/healthz`);
  const healthRaw = await health.text();
  assert.equal(health.ok, true);
  assert.equal(healthRaw.includes(providerToken), false);
  assert.equal(JSON.parse(healthRaw).ionConfigured, true);

  const stored = await readFile(join(cacheDir, "secrets", "cesium-ion-token"), "utf8");
  assert.equal(stored.trim(), rotatedProviderToken);
  console.log("ok: Ion scopes, refresh single-flight, token rotation, and secret boundaries hold");
} finally {
  if (child && !child.killed) {
    child.kill("SIGTERM");
    await once(child, "exit").catch(() => undefined);
  }
  if (egress?.listening) {
    egress.close();
    await once(egress, "close");
  }
  await rm(root, { recursive: true, force: true });
}

function createCesiumEgressMock(state, expectedProxyToken) {
  return createHttpServer((request, response) => {
    const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
    if (requestUrl.pathname !== "/proxy/cesium/fetch") {
      response.writeHead(404, { "content-type": "application/json" });
      response.end(JSON.stringify({ error: "not_found" }));
      return;
    }
    if (request.headers["x-proxy-token"] !== expectedProxyToken || request.headers.authorization) {
      response.writeHead(401, { "content-type": "application/json" });
      response.end(JSON.stringify({ error: "proxy_authorization_required" }));
      return;
    }
    const target = new URL(requestUrl.searchParams.get("url") || "https://invalid.example/");
    if (request.headers["x-nodedc-map-referer"] !== ionReferer) {
      response.writeHead(401, { "content-type": "application/json" });
      response.end(JSON.stringify({ error: "referer_required" }));
      return;
    }
    if (target.hostname === "assets.ion.cesium.com" && target.pathname.startsWith("/us-east-1/asset_depot/1/CesiumWorldTerrain/v1.2/")) {
      if (![state.credentials.v1.terrain, state.credentials.v2.terrain].includes(target.searchParams.get("access_token"))) {
        response.writeHead(401, { "content-type": "application/json" });
        response.end(JSON.stringify({ error: "terrain_authorization_required" }));
        return;
      }
      response.writeHead(200, { "content-type": "application/octet-stream" });
      response.end("terrain-bytes");
      return;
    }
    if (target.hostname === "assets.ion.cesium.com" && target.pathname.startsWith("/us-east-1/asset_depot/96188/OpenStreetMap/CWT/2025-04-01/nested/")) {
      const generation = target.searchParams.get("access_token") === state.credentials.v2.nested ? "v2"
        : target.searchParams.get("access_token") === state.credentials.v1.nested ? "v1" : "";
      if (!generation) {
        response.writeHead(401, { "content-type": "application/json" });
        response.end(JSON.stringify({ error: "nested_authorization_required" }));
        return;
      }
      response.writeHead(200, { "content-type": "application/octet-stream" });
      response.end(`nested-building-bytes-${generation}`);
      return;
    }
    if (target.hostname === "assets.ion.cesium.com" && target.pathname.startsWith("/us-east-1/asset_depot/96188/OpenStreetMap/CWT/2025-04-01/")) {
      const generation = target.searchParams.get("access_token") === state.credentials.v2.buildings ? "v2"
        : target.searchParams.get("access_token") === state.credentials.v1.buildings ? "v1" : "";
      if (!generation) {
        response.writeHead(401, { "content-type": "application/json" });
        response.end(JSON.stringify({ error: "buildings_authorization_required" }));
        return;
      }
      if (target.pathname.endsWith("/tileset.json")) {
        response.writeHead(200, { "content-type": "application/json" });
        response.end(JSON.stringify({ asset: { version: "1.1" }, geometricError: 1, root: { geometricError: 0, content: { uri: "root.b3dm" } } }));
        return;
      }
      response.writeHead(200, { "content-type": "application/octet-stream" });
      response.end(`building-bytes-${generation}`);
      return;
    }
    if (target.hostname === "assets.ion.cesium.com" && target.pathname.startsWith("/us-east-1/asset_depot/999/")) {
      state.siblingCredentialObserved = target.searchParams.get("access_token") || "";
      response.writeHead(401, { "content-type": "application/json" });
      response.end(JSON.stringify({ error: "scope_isolation" }));
      return;
    }
    const authorization = String(request.headers["x-nodedc-cesium-authorization"] || "");
    const generation = authorization === `Bearer ${providerToken}` ? "v1" : authorization === `Bearer ${rotatedProviderToken}` ? "v2" : "";
    if (target.hostname !== "api.cesium.com" || !generation) {
      response.writeHead(401, { "content-type": "application/json" });
      response.end(JSON.stringify({ error: "unauthorized" }));
      return;
    }
    const assetId = target.pathname.match(/^\/v1\/assets\/(\d+)\/endpoint$/)?.[1];
    state.endpointRequests.set(`${generation}:${assetId}`, (state.endpointRequests.get(`${generation}:${assetId}`) || 0) + 1);
    const body = assetId === "1"
      ? { type: "TERRAIN", url: "https://assets.ion.cesium.com/us-east-1/asset_depot/1/CesiumWorldTerrain/v1.2/", accessToken: state.credentials[generation].terrain }
      : assetId === "2"
      ? { type: "IMAGERY", externalType: "BING", options: { url: "https://dev.virtualearth.net/REST/v1/Imagery/Metadata/Aerial", key: state.credentials[generation].imagery, mapStyle: "Aerial" } }
      : assetId === "96188"
        ? { type: "3DTILES", url: "https://assets.ion.cesium.com/us-east-1/asset_depot/96188/OpenStreetMap/CWT/2025-04-01/tileset.json?v=production-shaped", accessToken: state.credentials[generation].buildings }
        : assetId === "999"
          ? { type: "3DTILES", url: "https://assets.ion.cesium.com/us-east-1/asset_depot/96188/OpenStreetMap/CWT/2025-04-01/nested/tileset.json?v=nested", accessToken: state.credentials[generation].nested }
        : null;
    if (!body) {
      response.writeHead(404, { "content-type": "application/json" });
      response.end(JSON.stringify({ error: "not_found" }));
      return;
    }
    response.writeHead(200, { "content-type": "application/json" });
    response.end(JSON.stringify(body));
  });
}

function createProviderState() {
  const expiresAt = Math.floor(Date.now() / 1000) + 3600;
  return {
    credentials: {
      v1: {
        terrain: fakeJwt("terrain-v1", expiresAt),
        imagery: "bing-private-endpoint-key-v1",
        buildings: fakeJwt("buildings-v1", expiresAt),
        nested: fakeJwt("nested-v1", expiresAt),
      },
      v2: {
        terrain: fakeJwt("terrain-v2", expiresAt),
        imagery: "bing-private-endpoint-key-v2",
        buildings: fakeJwt("buildings-v2", expiresAt),
        nested: fakeJwt("nested-v2", expiresAt),
      },
    },
    endpointRequests: new Map(),
    siblingCredentialObserved: null,
  };
}

function fakeJwt(subject, exp) {
  const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
  return `${encode({ alg: "HS256", typ: "JWT" })}.${encode({ sub: subject, exp })}.test-signature`;
}

async function requestAdmin(port, secret, method, pathname, actorId, body = "", referer = "") {
  const timestamp = String(Date.now());
  const bodyHash = createHash("sha256").update(body).digest("hex");
  const message = `nodedc.map-gateway.admin.v2\n${method}\n${pathname}\n${timestamp}\n${actorId}\n${bodyHash}\n${referer}`;
  const signature = createHmac("sha256", secret).update(message).digest("hex");
  const response = await fetch(`http://127.0.0.1:${port}${pathname}`, {
    method,
    headers: {
      ...(body ? { "content-type": "application/json" } : {}),
      "x-nodedc-admin-timestamp": timestamp,
      "x-nodedc-admin-actor": actorId,
      "x-nodedc-admin-signature": signature,
      ...(referer ? { "x-nodedc-ion-referer": referer } : {}),
    },
    body: body || undefined,
  });
  const raw = await response.text();
  return { response, raw, body: JSON.parse(raw) };
}

async function freePort() {
  const server = createServer();
  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 waitForGateway(port, child) {
  let output = "";
  child.stderr.on("data", (chunk) => { output += String(chunk); });
  for (let attempt = 0; attempt < 60; attempt += 1) {
    if (child.exitCode !== null) throw new Error(`gateway_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((resolve) => setTimeout(resolve, 50));
  }
  throw new Error(`gateway_start_timeout:${output}`);
}

async function waitFor(predicate) {
  for (let attempt = 0; attempt < 100; attempt += 1) {
    if (predicate()) return;
    await delay(20);
  }
  throw new Error("condition_timeout");
}

function delay(milliseconds) {
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
