import assert from "node:assert/strict";
import {
  generateKeyPairSync,
  randomBytes,
  sign,
} from "node:crypto";
import { chmod, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
  createManagedProvisionerRequestVerifier,
  loadManagedProvisionerPublicKeyFile,
  MANAGED_PROVISIONER_HEADERS,
  managedProvisionerSigningPayload,
  ManagedProvisionerReplayCache,
  sha256RawBody,
} from "../src/managed-provisioner-auth.mjs";

const serviceId = "nodedc-engine";
const keyId = "engine-edp-managed-provisioner-v1";
const audience = "nodedc-external-data-plane.managed-provisioning.v1";
const nowMs = Date.parse("2026-07-16T12:00:00.000Z");
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
let nonceSequence = 0;

const replayCache = new ManagedProvisionerReplayCache({ maxEntries: 100 });
const verify = createManagedProvisionerRequestVerifier({
  publicKey,
  serviceId,
  keyId,
  audience,
  maxSkewMs: 60_000,
  replayCache,
  now: () => nowMs,
});

const valid = signedRequest();
assert.deepEqual(verify(valid), { serviceId, keyId });
assert.throws(() => verify(valid), hasCode("managed_provisioner_request_replayed", 409));

const whitespaceTamper = signedRequest();
whitespaceTamper.rawBody = Buffer.from(`${whitespaceTamper.rawBody.toString("utf8")} `, "utf8");
assert.throws(() => verify(whitespaceTamper), hasCode("managed_provisioner_body_hash_mismatch", 401));

const methodTamper = signedRequest();
methodTamper.method = "POST";
assert.throws(() => verify(methodTamper), hasCode("managed_provisioner_signature_invalid", 401));

const pathTamper = signedRequest();
pathTamper.originalUrl = `${pathTamper.originalUrl}?unexpected=true`;
assert.throws(() => verify(pathTamper), hasCode("managed_provisioner_signature_invalid", 401));

const audienceTamper = signedRequest();
setHeader(audienceTamper, MANAGED_PROVISIONER_HEADERS.audience, "other-audience");
assert.throws(() => verify(audienceTamper), hasCode("managed_provisioner_identity_mismatch", 401));

const serviceTamper = signedRequest();
setHeader(serviceTamper, MANAGED_PROVISIONER_HEADERS.serviceId, "other-engine");
assert.throws(() => verify(serviceTamper), hasCode("managed_provisioner_identity_mismatch", 401));
const keyTamper = signedRequest();
setHeader(keyTamper, MANAGED_PROVISIONER_HEADERS.keyId, "other-key-v1");
assert.throws(() => verify(keyTamper), hasCode("managed_provisioner_identity_mismatch", 401));

const forbiddenBearer = signedRequest();
forbiddenBearer.headers.authorization = "Bearer value-that-must-not-appear-in-errors";
forbiddenBearer.rawHeaders.push("Authorization", forbiddenBearer.headers.authorization);
assert.throws(
  () => verify(forbiddenBearer),
  (error) => hasCode("managed_provisioner_authorization_header_forbidden", 401)(error)
    && !error.message.includes("value-that-must-not-appear-in-errors"),
);

const stale = signedRequest({ timestamp: new Date(nowMs - 60_000).toISOString() });
assert.throws(() => verify(stale), hasCode("managed_provisioner_timestamp_outside_window", 401));
const future = signedRequest({ timestamp: new Date(nowMs + 60_000).toISOString() });
assert.throws(() => verify(future), hasCode("managed_provisioner_timestamp_outside_window", 401));
const invalidTimestamp = signedRequest();
setHeader(invalidTimestamp, MANAGED_PROVISIONER_HEADERS.timestamp, "2026-07-16T12:00:00Z");
assert.throws(() => verify(invalidTimestamp), hasCode("managed_provisioner_timestamp_invalid", 401));

const duplicateHeader = signedRequest();
duplicateHeader.rawHeaders.push(MANAGED_PROVISIONER_HEADERS.nonce, header(duplicateHeader, MANAGED_PROVISIONER_HEADERS.nonce));
assert.throws(() => verify(duplicateHeader), hasCode("managed_provisioner_header_invalid", 401));

const uncaptured = signedRequest();
uncaptured.rawBodyCaptured = false;
uncaptured.headers["content-length"] = String(uncaptured.rawBody.length);
assert.throws(() => verify(uncaptured), hasCode("managed_provisioner_raw_body_unavailable", 401));

const badSignature = signedRequest();
setHeader(badSignature, MANAGED_PROVISIONER_HEADERS.signature, randomBytes(64).toString("base64url"));
assert.throws(() => verify(badSignature), hasCode("managed_provisioner_signature_invalid", 401));
const paddedSignature = signedRequest();
setHeader(
  paddedSignature,
  MANAGED_PROVISIONER_HEADERS.signature,
  `${header(paddedSignature, MANAGED_PROVISIONER_HEADERS.signature)}=`,
);
assert.throws(() => verify(paddedSignature), hasCode("managed_provisioner_signature_invalid", 401));
const uppercaseHash = signedRequest();
setHeader(
  uppercaseHash,
  MANAGED_PROVISIONER_HEADERS.bodySha256,
  header(uppercaseHash, MANAGED_PROVISIONER_HEADERS.bodySha256).toUpperCase(),
);
assert.throws(() => verify(uppercaseHash), hasCode("managed_provisioner_body_hash_invalid", 401));

const cache = new ManagedProvisionerReplayCache({ maxEntries: 1 });
cache.consume("first", nowMs + 1000, nowMs);
assert.throws(() => cache.consume("second", nowMs + 1000, nowMs), hasCode("managed_provisioner_replay_cache_exhausted", 503));
assert.throws(() => cache.consume("first", nowMs + 1000, nowMs), hasCode("managed_provisioner_request_replayed", 409));
cache.consume("second", nowMs + 2000, nowMs + 1000);

const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-managed-auth-"));
try {
  const publicKeyPath = join(directory, "engine-public-key.pem");
  const rsaPublicKeyPath = join(directory, "rsa-public-key.pem");
  const privateKeyPath = join(directory, "engine-private-key.pem");
  const symlinkPath = join(directory, "engine-public-key-link.pem");
  await writeFile(publicKeyPath, publicKey.export({ type: "spki", format: "pem" }), { mode: 0o600 });
  assert.equal(loadManagedProvisionerPublicKeyFile(publicKeyPath).asymmetricKeyType, "ed25519");
  assert.throws(
    () => loadManagedProvisionerPublicKeyFile("relative-public-key.pem"),
    /external_data_plane_managed_provisioner_public_key_file_must_be_absolute/,
  );
  assert.throws(
    () => loadManagedProvisionerPublicKeyFile(join(directory, "missing.pem")),
    /external_data_plane_managed_provisioner_public_key_file_unreadable/,
  );

  await symlink(publicKeyPath, symlinkPath);
  assert.throws(
    () => loadManagedProvisionerPublicKeyFile(symlinkPath),
    /external_data_plane_managed_provisioner_public_key_file_must_be_regular/,
  );

  await chmod(publicKeyPath, 0o622);
  assert.throws(
    () => loadManagedProvisionerPublicKeyFile(publicKeyPath),
    /external_data_plane_managed_provisioner_public_key_file_permissions_invalid/,
  );
  await chmod(publicKeyPath, 0o600);

  const { publicKey: rsaPublicKey } = generateKeyPairSync("rsa", { modulusLength: 2048 });
  await writeFile(rsaPublicKeyPath, rsaPublicKey.export({ type: "spki", format: "pem" }), { mode: 0o600 });
  assert.throws(
    () => loadManagedProvisionerPublicKeyFile(rsaPublicKeyPath),
    /external_data_plane_managed_provisioner_public_key_algorithm_invalid/,
  );

  await writeFile(privateKeyPath, privateKey.export({ type: "pkcs8", format: "pem" }), { mode: 0o600 });
  assert.throws(
    () => loadManagedProvisionerPublicKeyFile(privateKeyPath),
    /external_data_plane_managed_provisioner_public_key_file_format_invalid/,
  );
} finally {
  await rm(directory, { recursive: true, force: true });
}

console.log("external-data-plane managed provisioner auth: ok");

function signedRequest({
  method = "PUT",
  path = "/internal/data-plane/v1/writer-bindings/by-key/engine.test.positions",
  rawBody = Buffer.from('{"generation":1}', "utf8"),
  timestamp = new Date(nowMs).toISOString(),
  nonce = nextNonce(),
} = {}) {
  const bodySha256 = sha256RawBody(rawBody);
  const signature = sign(null, Buffer.from(managedProvisionerSigningPayload({
    audience,
    serviceId,
    keyId,
    method,
    path,
    timestamp,
    nonce,
    bodySha256,
  }), "utf8"), privateKey).toString("base64url");
  const headers = {
    [MANAGED_PROVISIONER_HEADERS.serviceId]: serviceId,
    [MANAGED_PROVISIONER_HEADERS.keyId]: keyId,
    [MANAGED_PROVISIONER_HEADERS.audience]: audience,
    [MANAGED_PROVISIONER_HEADERS.timestamp]: timestamp,
    [MANAGED_PROVISIONER_HEADERS.nonce]: nonce,
    [MANAGED_PROVISIONER_HEADERS.bodySha256]: bodySha256,
    [MANAGED_PROVISIONER_HEADERS.signature]: signature,
    "content-length": String(rawBody.length),
  };
  return {
    method,
    originalUrl: path,
    headers,
    rawHeaders: Object.entries(headers).flat(),
    rawBody,
    rawBodyCaptured: true,
  };
}

function nextNonce() {
  nonceSequence += 1;
  return Buffer.from(`managed-auth-test-${String(nonceSequence).padStart(6, "0")}`).toString("base64url");
}

function header(req, name) {
  return req.headers[name];
}

function setHeader(req, name, value) {
  req.headers[name] = value;
  const index = req.rawHeaders.findIndex((candidate) => String(candidate).toLowerCase() === name);
  req.rawHeaders[index + 1] = value;
}

function hasCode(code, status) {
  return (error) => error?.code === code && error?.status === status;
}
