import assert from "node:assert/strict";
import test from "node:test";

import { createFoundryAuth } from "./nodedc-auth.mjs";

const HANDOFF_TOKEN = "handoff-token-must-never-appear-in-diagnostics";
const LAUNCHER_SESSION_ID = "launcher-session-must-never-appear-in-diagnostics";
const INTERNAL_ACCESS_TOKEN = "internal-access-token-must-never-appear-in-diagnostics";
const USER = Object.freeze({
  id: "user_root",
  email: "root@example.test",
  name: "DC SUDO",
  groups: ["nodedc:superadmin"],
});

test("Foundry session validation is burst-safe, failure-tolerant and revocation-bounded", async (t) => {
  const restoreEnvironment = setEnvironment({
    NODE_ENV: "production",
    NODEDC_FOUNDRY_AUTH_REQUIRED: "true",
    NODEDC_FOUNDRY_COOKIE_SECURE: "false",
    NODEDC_FOUNDRY_SESSION_TTL_MS: String(60 * 60 * 1000),
    NODEDC_LAUNCHER_BASE_URL: "https://launcher.example.test",
    NODEDC_LAUNCHER_INTERNAL_URL: "http://launcher.internal.test",
    NODEDC_INTERNAL_ACCESS_TOKEN: INTERNAL_ACCESS_TOKEN,
  });
  t.after(restoreEnvironment);

  let clock = 1_000_000;
  let validationMode = "active";
  let validationCalls = 0;
  const diagnostics = [];
  const fetchStub = async (url, init) => {
    assert.equal(init.headers.Authorization, `Bearer ${INTERNAL_ACCESS_TOKEN}`);
    const pathname = new URL(url).pathname;
    const payload = JSON.parse(init.body);
    if (pathname === "/api/internal/handoff/consume") {
      assert.equal(payload.token, HANDOFF_TOKEN);
      return jsonResponse(200, {
        ok: true,
        launcherSessionId: LAUNCHER_SESSION_ID,
        user: USER,
      });
    }
    assert.equal(pathname, "/api/internal/session/validate");
    assert.equal(payload.launcherSessionId, LAUNCHER_SESSION_ID);
    validationCalls += 1;
    // Let every request in a simulated Cesium tile burst reach the shared
    // validation promise before it resolves.
    await new Promise((resolve) => setImmediate(resolve));
    if (validationMode === "transient") {
      return jsonResponse(503, { ok: false, active: false, error: "launcher_temporarily_unavailable" });
    }
    if (validationMode === "network") {
      throw new TypeError(`network failed ${INTERNAL_ACCESS_TOKEN}`);
    }
    if (validationMode === "timeout") {
      const error = new Error(`timeout ${LAUNCHER_SESSION_ID}`);
      error.name = "TimeoutError";
      throw error;
    }
    if (validationMode === "inactive") {
      return jsonResponse(200, { ok: true, active: false, error: "service_access_denied" });
    }
    return jsonResponse(200, { ok: true, active: true, user: USER });
  };

  const auth = createFoundryAuth({
    now: () => clock,
    fetch: fetchStub,
    validationTtlMs: 20,
    validationGraceMs: 30,
    validationRetryMs: 5,
    onDiagnostic: (record) => diagnostics.push(record),
  });
  const cookie = await createSessionCookie(auth);

  await t.test("concurrent tile burst performs one Launcher validation", async () => {
    clock += 21;
    const results = await Promise.all(
      Array.from({ length: 64 }, (_, index) => authorize(auth, cookie, `/api/map-gateway/api/map/cache?tile=${index}`)),
    );
    assert.equal(validationCalls, 1);
    assert.ok(results.every(({ handled }) => handled === false));
    assert.ok(results.every(({ response }) => response.getHeader("set-cookie") === undefined));

    const withinTtl = await Promise.all(
      Array.from({ length: 64 }, (_, index) => authorize(auth, cookie, `/api/map-gateway/api/map/cache?warm=${index}`)),
    );
    assert.equal(validationCalls, 1);
    assert.ok(withinTtl.every(({ handled }) => handled === false));
  });

  await t.test("transient Launcher failure keeps the cookie and uses bounded read-only grace", async () => {
    validationMode = "transient";
    clock += 21;
    const graceResults = await Promise.all(
      Array.from({ length: 32 }, (_, index) => authorize(auth, cookie, `/api/map-gateway/api/map/cache?grace=${index}`)),
    );
    assert.equal(validationCalls, 2);
    assert.ok(graceResults.every(({ handled }) => handled === false));
    assert.ok(graceResults.every(({ response }) => response.getHeader("set-cookie") === undefined));

    const mutation = await authorize(auth, cookie, "/api/platform-settings/cesium-ion", "PUT");
    assert.equal(mutation.handled, true);
    assert.equal(mutation.response.statusCode, 503);
    assert.equal(JSON.parse(mutation.response.body).error, "module_foundry_auth_unavailable");
    assert.equal(mutation.response.getHeader("set-cookie"), undefined);
    // The short retry backoff shares the transient result instead of creating a
    // control-plane retry storm from the same burst.
    assert.equal(validationCalls, 2);

    clock += 31;
    const outsideGrace = await authorize(auth, cookie, "/api/map/runtime-config");
    assert.equal(outsideGrace.handled, true);
    assert.equal(outsideGrace.response.statusCode, 503);
    assert.equal(JSON.parse(outsideGrace.response.body).error, "module_foundry_auth_unavailable");
    assert.equal(outsideGrace.response.getHeader("set-cookie"), undefined);
    assert.equal(validationCalls, 3);
  });

  await t.test("network errors and timeouts keep the last-known-good read-only session", async () => {
    validationMode = "active";
    clock += 6;
    const recovered = await authorize(auth, cookie, "/api/map/runtime-config");
    assert.equal(recovered.handled, false);
    assert.equal(validationCalls, 4);

    validationMode = "network";
    clock += 21;
    const networkFailure = await authorize(auth, cookie, "/api/map/runtime-config");
    assert.equal(networkFailure.handled, false);
    assert.equal(networkFailure.response.getHeader("set-cookie"), undefined);
    assert.equal(validationCalls, 5);

    validationMode = "timeout";
    clock += 6;
    const timeout = await authorize(auth, cookie, "/api/map/runtime-config");
    assert.equal(timeout.handled, false);
    assert.equal(timeout.response.getHeader("set-cookie"), undefined);
    assert.equal(validationCalls, 6);
  });

  await t.test("explicit Launcher inactive response revokes the local session", async () => {
    validationMode = "inactive";
    clock += 6;
    const revoked = await authorize(auth, cookie, "/api/map/runtime-config");
    assert.equal(revoked.handled, true);
    assert.equal(revoked.response.statusCode, 401);
    assert.equal(JSON.parse(revoked.response.body).error, "module_foundry_auth_required");
    assert.match(String(revoked.response.getHeader("set-cookie")), /Max-Age=0/);
    assert.equal(validationCalls, 7);

    const afterRevoke = await authorize(auth, cookie, "/api/map/runtime-config");
    assert.equal(afterRevoke.handled, true);
    assert.equal(afterRevoke.response.statusCode, 401);
    assert.equal(validationCalls, 7);
  });

  await t.test("diagnostics contain outcomes but no cookie, Launcher id or token", () => {
    assert.ok(diagnostics.some((record) => record.outcome === "active"));
    assert.ok(diagnostics.some((record) => record.outcome === "transient"));
    assert.ok(diagnostics.some((record) => record.outcome === "inactive"));
    const serialized = JSON.stringify(diagnostics);
    assert.equal(serialized.includes(cookie), false);
    assert.equal(serialized.includes(HANDOFF_TOKEN), false);
    assert.equal(serialized.includes(LAUNCHER_SESSION_ID), false);
    assert.equal(serialized.includes(INTERNAL_ACCESS_TOKEN), false);
    assert.equal(serialized.includes(USER.email), false);
  });

  await t.test("a process restart expires an opaque process-local cookie safely", async () => {
    let restartedValidationCalls = 0;
    const restartDiagnostics = [];
    const restartedAuth = createFoundryAuth({
      now: () => clock,
      fetch: async () => {
        restartedValidationCalls += 1;
        throw new Error("must not validate an unrecoverable local cookie");
      },
      validationTtlMs: 20,
      validationGraceMs: 30,
      validationRetryMs: 5,
      onDiagnostic: (record) => restartDiagnostics.push(record),
    });
    const result = await authorize(restartedAuth, cookie, "/api/map/runtime-config");
    assert.equal(result.handled, true);
    assert.equal(result.response.statusCode, 401);
    assert.match(String(result.response.getHeader("set-cookie")), /Max-Age=0/);
    assert.equal(restartedValidationCalls, 0);
    assert.ok(restartDiagnostics.some((record) => record.reason === "local_session_missing_or_expired"));
  });
});

async function createSessionCookie(auth) {
  const request = {
    method: "GET",
    url: `/auth/nodedc/handoff?token=${encodeURIComponent(HANDOFF_TOKEN)}&returnTo=%2F`,
    headers: {},
  };
  const response = mockResponse();
  await auth.handleLauncherHandoff(request, response, new URL(request.url, "https://foundry.example.test"));
  assert.equal(response.statusCode, 302);
  const setCookie = String(response.getHeader("set-cookie"));
  assert.match(setCookie, /^nodedc_foundry_session=/);
  return setCookie.split(";", 1)[0];
}

async function authorize(auth, cookie, path, method = "GET") {
  const request = { method, url: path, headers: { cookie, accept: "application/json" } };
  const response = mockResponse();
  const handled = await auth.ensureRequestSession(
    request,
    response,
    new URL(path, "https://foundry.example.test"),
  );
  return { handled, request, response };
}

function mockResponse() {
  const headers = new Map();
  return {
    statusCode: 200,
    body: "",
    setHeader(name, value) {
      headers.set(String(name).toLowerCase(), value);
    },
    getHeader(name) {
      return headers.get(String(name).toLowerCase());
    },
    end(body = "") {
      this.body = String(body);
    },
  };
}

function jsonResponse(status, body) {
  return new Response(JSON.stringify(body), {
    status,
    headers: { "content-type": "application/json" },
  });
}

function setEnvironment(values) {
  const original = Object.fromEntries(Object.keys(values).map((key) => [key, process.env[key]]));
  for (const [key, value] of Object.entries(values)) process.env[key] = value;
  return () => {
    for (const [key, value] of Object.entries(original)) {
      if (value === undefined) delete process.env[key];
      else process.env[key] = value;
    }
  };
}
