// PII envelope migration T2a — async read-routing + scope fidelity + no-regression.
//
// GATE CONDITIONS (Moses):
//   1. SCOPE-FIDELITY (the unrecoverable-loss landmine): PiiContext→DekScope must be EXACT, and a
//      tenant scope with id="" must be a DIFFERENT DEK than platform (both scopeId="" but distinct
//      scopeType) — a mismatch is a permanent GCM cross-decrypt loss. Proven at the mapping level AND
//      behaviourally (a tenant-scoped v2 value must NOT decrypt under the platform scope).
//   2. NO-REGRESSION on live PII: existing v1 (direct-key) values still round-trip after async-ification
//      (writes stay v1 with the flag off), and a v1 value is NEVER orphaned (dual-read keeps it readable
//      even once v2 writes exist).
//
// Requires DATABASE_URL + PII_AES_MASTER_KEY (envelope DEKs live in data_keys). Cleans up its own rows.

import { test, describe, after } from "node:test";
import assert from "node:assert/strict";

if (!process.env.PII_AES_MASTER_KEY) {
  process.env.PII_AES_MASTER_KEY = "test-pii-aes-master-key-0123456789abcdef";
}

const { appPool } = await import("../server/db.js");
const { toDekScope, encryptPii, decryptPii } = await import("../server/lib/pii-encryption.js");

const RUN = `pii-t2a-${Date.now()}`;
const tenantA = { type: "tenant" as const, tenantId: `${RUN}-A` };
const tenantB = { type: "tenant" as const, tenantId: `${RUN}-B` };
const platform = { type: "platform" as const };

describe("PII envelope migration T2a — read-routing, scope fidelity, no-regression", () => {
  after(async () => {
    delete process.env.PII_ENVELOPE_WRITE;
    await appPool.query(`DELETE FROM data_keys WHERE scope_id LIKE $1`, [`${RUN}-%`]);
  });

  test("SCOPE-FIDELITY: PiiContext→DekScope is exact, and tenant-'' is DISTINCT from platform (the landmine)", () => {
    assert.deepEqual(toDekScope({ type: "tenant", tenantId: "t1" }), { type: "tenant", id: "t1" });
    assert.deepEqual(toDekScope({ type: "user", userId: "u1" }), { type: "user", id: "u1" });
    assert.deepEqual(toDekScope({ type: "platform" }), { type: "platform" });
    // The unrecoverable-loss landmine: a tenant scope with id "" must NOT collapse into platform.
    const tenantEmpty = toDekScope({ type: "tenant", tenantId: "" });
    const plat = toDekScope({ type: "platform" });
    assert.notDeepEqual(tenantEmpty, plat, "tenant-'' must map to a DIFFERENT DekScope than platform");
    assert.equal(tenantEmpty.type, "tenant");
    assert.equal(plat.type, "platform");
  });

  test("NO-REGRESSION: v1 round-trips with the write flag OFF (default) — existing format unchanged", async () => {
    delete process.env.PII_ENVELOPE_WRITE;
    const ct = await encryptPii("v1-secret", tenantA);
    assert.ok(ct.startsWith("ENC:v1:"), "flag off must still emit the v1 direct-key format");
    assert.equal(await decryptPii(ct, tenantA), "v1-secret");
  });

  test("v2 write + read: with the flag ON, encrypt emits ENC:v2 (envelope) and decrypt round-trips", async () => {
    process.env.PII_ENVELOPE_WRITE = "on";
    const ct = await encryptPii("v2-secret", tenantA);
    assert.ok(ct.startsWith("ENC:v2:"), "flag on must emit the v2 envelope format");
    assert.equal(await decryptPii(ct, tenantA), "v2-secret");
    delete process.env.PII_ENVELOPE_WRITE;
  });

  test("DUAL-READ: a v1 value AND a v2 value both decrypt — v1 is never orphaned", async () => {
    delete process.env.PII_ENVELOPE_WRITE;
    const v1 = await encryptPii("dual-v1", tenantA);
    process.env.PII_ENVELOPE_WRITE = "on";
    const v2 = await encryptPii("dual-v2", tenantA);
    delete process.env.PII_ENVELOPE_WRITE;
    assert.ok(v1.startsWith("ENC:v1:") && v2.startsWith("ENC:v2:"));
    // Read-routing: after the flag flips back to v1-writes, BOTH still decrypt.
    assert.equal(await decryptPii(v1, tenantA), "dual-v1");
    assert.equal(await decryptPii(v2, tenantA), "dual-v2");
  });

  test("SCOPE ISOLATION (v2): cross-scope decrypt fails — tenant A ≠ tenant B, and tenant ≠ platform", async () => {
    process.env.PII_ENVELOPE_WRITE = "on";
    const ctA = await encryptPii("A-only", tenantA);
    await assert.rejects(() => decryptPii(ctA, tenantB), "tenant B must not decrypt tenant A's v2 value");
    // The landmine, behaviourally: a tenant-scoped value must not decrypt under the platform scope.
    await assert.rejects(() => decryptPii(ctA, platform), "platform must not decrypt a tenant-scoped v2 value");
    delete process.env.PII_ENVELOPE_WRITE;
  });

  test("passthrough: non-ENC values pass through unchanged (legacy plaintext + erasure sentinels)", async () => {
    assert.equal(await decryptPii("legacy-plaintext", tenantA), "legacy-plaintext");
    assert.equal(await decryptPii("ubo_hash:v1:abc123", tenantA), "ubo_hash:v1:abc123");
  });
});
