/**
 * T004a SCIM Provisions verification — full template (i) run
 * Checks: (a) round-trip, (b) ciphertext on disk + IV randomness,
 *          (c) plaintext-passthrough, (d) nullable columns,
 *          (e) filter-on-decrypted-plaintext (SCIM-specific)
 *
 * Run: npx tsx scripts/t004a-scim-verify.ts
 */

import { withBypassRls } from "../server/db";
import { scimProvisions } from "../shared/schema-integrations";
import { encryptPii, decryptPii, isEncrypted } from "../server/lib/pii-encryption";
import { eq, and } from "drizzle-orm";
import { randomBytes } from "crypto";

const TENANT_ID     = "t004a-scim-verify-tenant";
const TEST_USERNAME = "alice.nakamukwatwa@verify.test";
const TEST_DISPNAME = "Alice Nakamukwatwa";
const TEST_EMAIL    = "alice.nakamukwatwa@work.verify.test";
const ctx = { type: "tenant" as const, tenantId: TENANT_ID };

let passCount = 0;
let failCount = 0;

function pass(label: string, detail = "") {
  console.log(`  ✅ PASS: ${label}${detail ? " — " + detail : ""}`);
  passCount++;
}

function fail(label: string, detail = "") {
  console.error(`  ❌ FAIL: ${label}${detail ? " — " + detail : ""}`);
  failCount++;
}

const insertedIds: string[] = [];

async function cleanup() {
  await withBypassRls(async (bdb) => {
    for (const id of insertedIds) {
      await bdb.delete(scimProvisions).where(eq(scimProvisions.id, id));
    }
    // Belt-and-suspenders: remove any stragglers from the test tenant
    await bdb.delete(scimProvisions).where(eq(scimProvisions.tenantId, TENANT_ID));
  });
}

async function main() {
  console.log("\n=== T004a SCIM Provisions — Template (i) Verification Suite ===\n");

  await cleanup();

  // ──────────────────────────────────────────────────────────────────────────
  // Check (a) + (b-ciphertext): New-write round-trip + stored as ENC:v1:
  // ──────────────────────────────────────────────────────────────────────────
  console.log("--- Check (a)+(b): Round-trip + ciphertext-on-disk ---");

  const [rowA] = await withBypassRls(async (bdb) =>
    bdb.insert(scimProvisions).values({
      tenantId:    TENANT_ID,
      externalId:  `ext-verify-a-${randomBytes(3).toString("hex")}`,
      userName:    encryptPii(TEST_USERNAME, ctx),
      displayName: encryptPii(TEST_DISPNAME, ctx),
      email:       encryptPii(TEST_EMAIL,    ctx),
      groups:      "[]",
      active:      true,
      syncStatus:  "synced",
    }).returning()
  );
  insertedIds.push(rowA.id);

  // Read raw stored values (Drizzle returns what is stored — not decrypted)
  const rawA = await withBypassRls(async (bdb) => {
    const [row] = await bdb.select().from(scimProvisions).where(eq(scimProvisions.id, rowA.id));
    return row;
  });

  if (!rawA) {
    fail("Row A not found after insert");
  } else {
    // (b) all three PII columns stored as ENC:v1: ciphertext
    if (rawA.userName.startsWith("ENC:v1:")) {
      pass(`(b) userName stored as ciphertext: "${rawA.userName.slice(0, 28)}…"`);
    } else {
      fail("(b) userName NOT stored as ciphertext", `value: "${rawA.userName}"`);
    }

    if (rawA.displayName?.startsWith("ENC:v1:")) {
      pass(`(b) displayName stored as ciphertext: "${rawA.displayName.slice(0, 28)}…"`);
    } else {
      fail("(b) displayName NOT stored as ciphertext", `value: "${rawA.displayName}"`);
    }

    if (rawA.email?.startsWith("ENC:v1:")) {
      pass(`(b) email stored as ciphertext: "${rawA.email.slice(0, 28)}…"`);
    } else {
      fail("(b) email NOT stored as ciphertext", `value: "${rawA.email}"`);
    }

    // isEncrypted() helper
    if (isEncrypted(rawA.userName) && isEncrypted(rawA.displayName!) && isEncrypted(rawA.email!)) {
      pass("(b) isEncrypted() returns true for all three stored PII columns");
    } else {
      fail("(b) isEncrypted() returned false for at least one column");
    }

    // (a) decrypt and confirm plaintext
    const decUserName = decryptPii(rawA.userName,     ctx);
    const decDispName = decryptPii(rawA.displayName!, ctx);
    const decEmail    = decryptPii(rawA.email!,       ctx);

    if (decUserName === TEST_USERNAME) {
      pass(`(a) userName round-trip correct ("${decUserName}")`);
    } else {
      fail("(a) userName round-trip wrong", `expected "${TEST_USERNAME}", got "${decUserName}"`);
    }

    if (decDispName === TEST_DISPNAME) {
      pass(`(a) displayName round-trip correct ("${decDispName}")`);
    } else {
      fail("(a) displayName round-trip wrong", `expected "${TEST_DISPNAME}", got "${decDispName}"`);
    }

    if (decEmail === TEST_EMAIL) {
      pass(`(a) email round-trip correct ("${decEmail}")`);
    } else {
      fail("(a) email round-trip wrong", `expected "${TEST_EMAIL}", got "${decEmail}"`);
    }

    // (b) IV randomness — two in-memory encryptions of same plaintext must differ
    const enc1 = encryptPii(TEST_USERNAME, ctx);
    const enc2 = encryptPii(TEST_USERNAME, ctx);
    if (enc1 !== enc2) {
      pass("(b) Two in-memory encryptions of same plaintext differ (random IV confirmed)");
    } else {
      fail("(b) IV randomness — two in-memory encryptions IDENTICAL — deterministic leak");
    }

    // (b) IV randomness on actual stored DB values — second write must differ
    const [rowA2] = await withBypassRls(async (bdb) =>
      bdb.insert(scimProvisions).values({
        tenantId:    TENANT_ID,
        externalId:  `ext-verify-a2-${randomBytes(3).toString("hex")}`,
        userName:    encryptPii(TEST_USERNAME, ctx),
        displayName: encryptPii(TEST_DISPNAME, ctx),
        email:       encryptPii(TEST_EMAIL,    ctx),
        groups:      "[]",
        active:      true,
        syncStatus:  "synced",
      }).returning()
    );
    insertedIds.push(rowA2.id);

    const rawA2 = await withBypassRls(async (bdb) => {
      const [row] = await bdb.select().from(scimProvisions).where(eq(scimProvisions.id, rowA2.id));
      return row;
    });

    if (rawA2 && rawA.userName !== rawA2.userName) {
      pass("(b) Two DB writes of same plaintext → different stored ciphertext (IV randomness verified on stored values)");
    } else {
      fail("(b) IV randomness on stored values", "Two DB rows have identical ciphertext for same plaintext");
    }
  }

  // ──────────────────────────────────────────────────────────────────────────
  // Check (c): Existing-plaintext-row passthrough (dual-mode reader)
  // ──────────────────────────────────────────────────────────────────────────
  console.log("\n--- Check (c): Plaintext passthrough (pre-T004b row simulation) ---");

  // Insert WITHOUT encryptPii() — simulates a row written before T004b migration
  const [rowC] = await withBypassRls(async (bdb) =>
    bdb.insert(scimProvisions).values({
      tenantId:    TENANT_ID,
      externalId:  `ext-verify-pt-${randomBytes(3).toString("hex")}`,
      userName:    TEST_USERNAME,   // raw plaintext — no encryptPii()
      displayName: TEST_DISPNAME,
      email:       TEST_EMAIL,
      groups:      "[]",
      active:      true,
      syncStatus:  "synced",
    }).returning()
  );
  insertedIds.push(rowC.id);

  const rawC = await withBypassRls(async (bdb) => {
    const [row] = await bdb.select().from(scimProvisions).where(eq(scimProvisions.id, rowC.id));
    return row;
  });

  if (!rawC) {
    fail("Row C not found after plaintext insert");
  } else {
    // Confirm raw stored value is plaintext
    if (!isEncrypted(rawC.userName)) {
      pass(`(c) Raw stored userName is plaintext (isEncrypted=false): "${rawC.userName}"`);
    } else {
      fail("(c) Raw stored userName should be plaintext", `got: "${rawC.userName}"`);
    }

    // decryptPii() must pass plaintext through unchanged
    const ptUserName = decryptPii(rawC.userName,     ctx);
    const ptDispName = decryptPii(rawC.displayName!, ctx);
    const ptEmail    = decryptPii(rawC.email!,       ctx);

    if (ptUserName === TEST_USERNAME) {
      pass(`(c) decryptPii passes plaintext userName through unchanged ("${ptUserName}")`);
    } else {
      fail("(c) Plaintext passthrough — userName", `got: "${ptUserName}"`);
    }

    if (ptDispName === TEST_DISPNAME) {
      pass("(c) decryptPii passes plaintext displayName through unchanged");
    } else {
      fail("(c) Plaintext passthrough — displayName", `got: "${ptDispName}"`);
    }

    if (ptEmail === TEST_EMAIL) {
      pass("(c) decryptPii passes plaintext email through unchanged");
    } else {
      fail("(c) Plaintext passthrough — email", `got: "${ptEmail}"`);
    }

    // Confirm read path did NOT mutate stored value
    const rawCPost = await withBypassRls(async (bdb) => {
      const [row] = await bdb.select().from(scimProvisions).where(eq(scimProvisions.id, rowC.id));
      return row;
    });

    if (rawCPost && !isEncrypted(rawCPost.userName)) {
      pass("(c) Stored value NOT mutated by read — still plaintext after decryptPii() call");
    } else {
      fail("(c) Read path mutated stored value", `now: "${rawCPost?.userName}"`);
    }
  }

  // ──────────────────────────────────────────────────────────────────────────
  // Check (d): Nullable columns — NULL displayName and email stored as NULL
  // ──────────────────────────────────────────────────────────────────────────
  console.log("\n--- Check (d): Nullable columns ---");

  const [rowD] = await withBypassRls(async (bdb) =>
    bdb.insert(scimProvisions).values({
      tenantId:   TENANT_ID,
      externalId: `ext-verify-null-${randomBytes(3).toString("hex")}`,
      userName:   encryptPii(TEST_USERNAME + ".nulltest", ctx),
      // displayName and email intentionally absent (nullable)
      groups:     "[]",
      active:     true,
      syncStatus: "synced",
    }).returning()
  );
  insertedIds.push(rowD.id);

  const rawD = await withBypassRls(async (bdb) => {
    const [row] = await bdb.select().from(scimProvisions).where(eq(scimProvisions.id, rowD.id));
    return row;
  });

  if (!rawD) {
    fail("Row D not found after nullable insert");
  } else {
    if (rawD.displayName === null) {
      pass("(d) NULL displayName stored as NULL (not encrypted empty string)");
    } else {
      fail("(d) NULL displayName", `got: "${rawD.displayName}"`);
    }

    if (rawD.email === null) {
      pass("(d) NULL email stored as NULL");
    } else {
      fail("(d) NULL email", `got: "${rawD.email}"`);
    }

    // decryptScimRow pattern must handle nulls without crashing
    const decDispName = rawD.displayName ? decryptPii(rawD.displayName, ctx) : rawD.displayName;
    const decEmail    = rawD.email       ? decryptPii(rawD.email, ctx)       : rawD.email;

    if (decDispName === null) {
      pass("(d) decryptScimRow pattern: null displayName → null (not crash)");
    } else {
      fail("(d) decryptScimRow pattern returned non-null for null displayName", `got: "${decDispName}"`);
    }

    if (decEmail === null) {
      pass("(d) decryptScimRow pattern: null email → null (not crash)");
    } else {
      fail("(d) decryptScimRow pattern returned non-null for null email", `got: "${decEmail}"`);
    }
  }

  // ──────────────────────────────────────────────────────────────────────────
  // Check (e): Filter-on-decrypted-plaintext (SCIM GET ?filter=userName eq "…")
  // ──────────────────────────────────────────────────────────────────────────
  console.log("\n--- Check (e): Filter-on-decrypted-plaintext (SCIM-specific) ---");

  // Fetch all provisions for our test tenant via bypass (simulates wired GET path)
  const allRaw = await withBypassRls(async (bdb) =>
    bdb.select().from(scimProvisions).where(eq(scimProvisions.tenantId, TENANT_ID))
  );

  // Apply decryptScimRow pattern — same as what the wired GET handler does
  const allDecrypted = allRaw.map(row => ({
    ...row,
    userName:    decryptPii(row.userName, ctx),
    displayName: row.displayName ? decryptPii(row.displayName, ctx) : row.displayName,
    email:       row.email       ? decryptPii(row.email, ctx)       : row.email,
  }));

  // Filter by userName (simulating SCIM filter GET ?filter=userName eq "alice…")
  const filtered = allDecrypted.filter(p => p.userName === TEST_USERNAME);

  // We inserted rowA, rowA2, and rowC with TEST_USERNAME (rowC = plaintext passthrough)
  if (filtered.length >= 2) {
    pass(`(e) Filter by decrypted userName returned ${filtered.length} rows (≥2 expected)`, `userName="${TEST_USERNAME}"`);
  } else {
    fail("(e) Filter by decrypted userName returned wrong count", `got ${filtered.length}, expected ≥2`);
  }

  // No filtered row should have an encrypted userName value
  const hasEncrypted = filtered.some(p => p.userName.startsWith("ENC:v1:"));
  if (!hasEncrypted) {
    pass("(e) All filtered rows have plaintext userName (decrypt ran before filter)");
  } else {
    fail("(e) Some filtered rows still have encrypted userName — decrypt not running before filter");
  }

  // Confirm the wrong-tenant filter returns nothing (cross-tenant isolation)
  const wrongTenantFiltered = allDecrypted.filter(
    p => (p as any).tenantId !== TENANT_ID && p.userName === TEST_USERNAME
  );
  if (wrongTenantFiltered.length === 0) {
    pass("(e) No cross-tenant leakage: filter only returns rows for the correct tenantId");
  } else {
    fail("(e) Cross-tenant leakage in filter result", `${wrongTenantFiltered.length} wrong-tenant rows matched`);
  }

  // ──────────────────────────────────────────────────────────────────────────
  // Cleanup + summary
  // ──────────────────────────────────────────────────────────────────────────
  await cleanup();
  console.log(`\n[cleanup] Deleted ${insertedIds.length} test row(s) for tenant ${TENANT_ID}`);

  console.log("\n══════════════════════════════════════════════════════");
  const allPass = failCount === 0;
  console.log(`T004a SCIM Provisions — RESULT: ${allPass ? "✅ ALL PASS" : "❌ SOME FAIL"}`);
  console.log(`  ${passCount}/${passCount + failCount} checks passed`);
  console.log("══════════════════════════════════════════════════════\n");

  if (!allPass) {
    process.exit(1);
  }
}

main().catch(err => {
  console.error("\nFATAL:", err);
  process.exit(1);
});
