import assert from "node:assert/strict";
import { createHash, generateKeyPairSync, verify } from "node:crypto";
import { chmod, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { createFoundryReaderGrantProvisioner } from "./foundry-reader-grant-provisioner.mjs";

const signatureSchema = "nodedc.external-data-plane.managed-provisioner-request/v1";
const audience = "nodedc-external-data-plane.managed-provisioning.v1";
const serviceId = "nodedc-module-foundry";
const keyId = "foundry-edp-managed-provisioner-v1";
const target = {
  application: { id: "11111111-1111-4111-8111-111111111111" },
  page: { id: "map" },
  binding: { id: "fleet-current", dataProductId: "fleet.positions.current.v1" },
};

test("managed Foundry grant provisioning signs digest-only requests and persists the token privately", async () => {
  const root = await mkdtemp(join(tmpdir(), "foundry-reader-grant-"));
  const privateKeyFile = join(root, "private-key.pem");
  const grantsDir = join(root, "grants");
  const { privateKey, publicKey } = generateKeyPairSync("ed25519");
  await writeFile(privateKeyFile, privateKey.export({ type: "pkcs8", format: "pem" }), { mode: 0o400 });
  await chmod(privateKeyFile, 0o400);
  const requestBodies = [];
  let capabilityDigest = null;
  const fetchImpl = async (input, options) => {
    const url = new URL(input);
    const body = String(options.body);
    const parsed = JSON.parse(body);
    requestBodies.push(body);
    const bodySha256 = createHash("sha256").update(body, "utf8").digest("hex");
    assert.equal(options.headers["x-nodedc-content-sha256"], bodySha256);
    const signedPayload = JSON.stringify({
      schemaVersion: signatureSchema,
      audience,
      serviceId,
      keyId,
      method: options.method,
      path: url.pathname,
      timestamp: options.headers["x-nodedc-request-timestamp"],
      nonce: options.headers["x-nodedc-request-nonce"],
      bodySha256,
    });
    assert.equal(verify(
      null,
      Buffer.from(signedPayload, "utf8"),
      publicKey,
      Buffer.from(options.headers["x-nodedc-request-signature"], "base64url"),
    ), true);
    assert.equal(options.headers["x-nodedc-engine-service-id"], serviceId);
    assert.equal(options.headers["x-nodedc-engine-key-id"], keyId);
    assert.equal(options.headers["x-nodedc-request-audience"], audience);
    assert.deepEqual(parsed.allowedDataProductIds, ["fleet.positions.current.v1"]);
    for (const forbidden of ["provider", "providerId", "tenant", "tenantId", "connection", "connectionId", "token", "capability"]) {
      assert.equal(Object.hasOwn(parsed, forbidden), false);
    }
    if (url.pathname.endsWith("/plan")) {
      assert.deepEqual(Object.keys(parsed), ["allowedDataProductIds"]);
      return Response.json({
        ok: true,
        sourceScope: "resolved-server-side",
        dataProducts: [{
          id: "fleet.positions.current.v1",
          version: "1.0.0",
          deliveryMode: "snapshot+patch",
          semanticTypes: ["map.moving_object"],
          active: true,
        }],
      });
    }
    assert.match(url.pathname, /\/consumer-reader-bindings\/by-key\/fndrc-[0-9a-f]{64}$/);
    assert.deepEqual(Object.keys(parsed).sort(), ["allowedDataProductIds", "capabilityDigest", "expiresAt", "generation"].sort());
    assert.match(parsed.capabilityDigest, /^[0-9a-f]{64}$/);
    capabilityDigest ||= parsed.capabilityDigest;
    assert.equal(parsed.capabilityDigest, capabilityDigest);
    const bindingKey = decodeURIComponent(url.pathname.split("/").at(-1));
    return Response.json({
      ok: true,
      idempotent: capabilityDigest === parsed.capabilityDigest,
      readerBinding: {
        bindingKey,
        generation: 1,
        active: true,
        expiresAt: null,
        sourceScope: "resolved-server-side",
        allowedDataProductIds: parsed.allowedDataProductIds,
      },
    });
  };
  const provisioner = createFoundryReaderGrantProvisioner({
    dataPlaneUrl: "http://edp.test",
    privateKeyFile,
    grantsDir,
    serviceId,
    keyId,
    audience,
    fetchImpl,
    now: () => new Date("2026-07-19T14:00:00.000Z"),
    randomBytesImpl: () => Buffer.alloc(32, 7),
    randomUUIDImpl: () => "22222222-2222-4222-8222-222222222222",
    production: false,
  });
  try {
    assert.equal(provisioner.configured, true);
    const planned = await provisioner.plan(target);
    assert.equal(planned.product.id, "fleet.positions.current.v1");
    const first = await provisioner.ensure(target);
    const second = await provisioner.ensure(target);
    assert.equal(first.ensured, true);
    assert.equal(second.ensured, true);
    const files = await readdir(grantsDir);
    assert.equal(files.length, 1);
    const tokenPath = join(grantsDir, files[0]);
    const token = (await readFile(tokenPath, "utf8")).trim();
    assert.match(token, /^ndc_edprb_/);
    assert.equal((await stat(tokenPath)).mode & 0o777, 0o400);
    assert.equal(createHash("sha256").update(token, "utf8").digest("hex"), capabilityDigest);
    assert.equal(await provisioner.readToken(target), token);
    assert.equal(requestBodies.some((body) => body.includes(token)), false);
    assert.equal(JSON.stringify([planned, first, second]).includes(token), false);
    assert.equal(requestBodies.some((body) => /provider|tenant|connection/i.test(body)), false);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});

test("managed Foundry grant provisioning keeps successor capabilities separate and revokes an exact predecessor generation", async () => {
  const root = await mkdtemp(join(tmpdir(), "foundry-reader-grant-generation-"));
  const privateKeyFile = join(root, "private-key.pem");
  const grantsDir = join(root, "grants");
  const { privateKey } = generateKeyPairSync("ed25519");
  await writeFile(privateKeyFile, privateKey.export({ type: "pkcs8", format: "pem" }), { mode: 0o400 });
  await chmod(privateKeyFile, 0o400);
  const requests = [];
  const fetchImpl = async (input, options) => {
    const url = new URL(input);
    const parsed = JSON.parse(String(options.body));
    requests.push({ method: options.method, path: url.pathname, body: parsed });
    const bindingKey = url.pathname.match(/by-key\/([^/]+)/)?.[1];
    if (url.pathname.endsWith("/plan")) {
      const id = parsed.allowedDataProductIds[0];
      return Response.json({
        ok: true,
        sourceScope: "resolved-server-side",
        dataProducts: [{
          id,
          version: id.endsWith(".v2") ? "2.0.0" : "1.0.0",
          deliveryMode: "snapshot+patch",
          semanticTypes: ["map.moving_object"],
          active: true,
        }],
      });
    }
    if (url.pathname.endsWith("/revoke")) {
      const generation = Number(url.pathname.split("/").at(-2));
      return Response.json({
        ok: true,
        idempotent: false,
        readerBinding: {
          bindingKey: decodeURIComponent(bindingKey),
          generation,
          active: false,
          expiresAt: null,
          sourceScope: "resolved-server-side",
          allowedDataProductIds: ["fleet.positions.current.v1"],
        },
      });
    }
    return Response.json({
      ok: true,
      idempotent: false,
      readerBinding: {
        bindingKey: decodeURIComponent(bindingKey),
        generation: parsed.generation,
        active: true,
        expiresAt: null,
        sourceScope: "resolved-server-side",
        allowedDataProductIds: parsed.allowedDataProductIds,
      },
    });
  };
  const provisioner = createFoundryReaderGrantProvisioner({
    dataPlaneUrl: "http://edp.test",
    privateKeyFile,
    grantsDir,
    serviceId,
    keyId,
    audience,
    fetchImpl,
    randomBytesImpl: (size) => Buffer.alloc(size, requests.length + 1),
    randomUUIDImpl: () => "33333333-3333-4333-8333-333333333333",
    production: false,
  });
  const successorTarget = structuredClone(target);
  successorTarget.binding.dataProductId = "fleet.positions.current.v2";
  try {
    const first = await provisioner.ensure(target, { generation: 1 });
    const second = await provisioner.ensure(successorTarget, { generation: 2 });
    assert.equal(first.generation, 1);
    assert.equal(second.generation, 2);
    const firstToken = await provisioner.readToken(target, { generation: 1 });
    const secondToken = await provisioner.readToken(successorTarget, { generation: 2 });
    assert.notEqual(firstToken, secondToken);
    const files = (await readdir(grantsDir)).sort();
    const baseFile = files.find((entry) => !entry.includes(".g"));
    assert.ok(baseFile);
    assert.deepEqual(files, [baseFile, `${baseFile}.g2`]);
    const revoked = await provisioner.revoke(target, { generation: 1 });
    assert.equal(revoked.generation, 1);
    assert.ok(requests.some((request) => request.method === "POST" && request.path.endsWith("/generations/1/revoke")));
    assert.equal(requests.some((request) => JSON.stringify(request.body).includes(firstToken)), false);
    assert.equal(requests.some((request) => JSON.stringify(request.body).includes(secondToken)), false);
  } finally {
    await rm(root, { recursive: true, force: true });
  }
});
