// Audit-chain medium M5 — signed chain-head snapshot forgery countermeasure [DIRECT-DEV].
//
// The chain is unkeyed SHA-256, so a DB superuser can alter a historical row and re-hash the chain
// FORWARD — and verifyChainIntegrity still passes (the chain is internally consistent again). That
// is the forgery the plain chain cannot catch. This test performs exactly that forgery and proves:
//   - verifyChainIntegrity() still returns valid=true (the plain chain is fooled), AND
//   - a signed snapshot taken beforehand now reports headMatches=false (the forgery IS caught),
//   - and a forger who edits the snapshot to match cannot, because the signature (key outside the
//     DB) no longer validates.
//
// Requires DATABASE_URL + boot secrets. Sets AUDIT_SNAPSHOT_SIGNING_KEY within this process only.

import { test, describe, before, after } from "node:test";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { inArray, sql } from "drizzle-orm";
import { auditDb } from "../server/db.js";
import { auditLogs } from "../shared/schema.js";
import { chainAuditLog, createAuditChainSnapshot, verifyAuditChainSnapshots, verifyChainIntegrity } from "../server/lib/audit-chain.js";

const RUN = `snap-${Date.now()}`;
const IDS = [`${RUN}-A`, `${RUN}-B`, `${RUN}-C`];
const ACT = `${RUN}-act`;
let snapshotEntryCount = 0;

// The library's exact hash recipe, re-implemented here so we can forge the chain the way a DB
// superuser would (re-hash forward) — proving the snapshot catches what verifyChainIntegrity misses.
function chainHash(e: { sourceAuditId: string; previousHash: string; timestamp: string; action: string; userId: string | null; resource: string; details: string }): string {
  return createHash("sha256").update(JSON.stringify({
    id: e.sourceAuditId, previousHash: e.previousHash, timestamp: e.timestamp,
    action: e.action, userId: e.userId, resource: e.resource, details: e.details,
  })).digest("hex");
}

describe("M5 — signed chain-head snapshot forgery countermeasure", () => {
  before(async () => {
    process.env.AUDIT_SNAPSHOT_SIGNING_KEY = `${RUN}-signing-key-held-outside-the-db`;
    await auditDb.insert(auditLogs).values(
      IDS.map((id, i) => ({ id, timestamp: new Date(`2037-02-0${i + 1}T00:00:00.000Z`), action: ACT, resource: "test", userId: null, details: `orig-${i}` })),
    );
    for (let i = 0; i < IDS.length; i++) {
      await chainAuditLog({ id: IDS[i], action: ACT, userId: null, resource: "test", details: `orig-${i}`, timestamp: new Date(`2037-02-0${i + 1}T00:00:00.000Z`).toISOString() });
    }
    const snap = await createAuditChainSnapshot();
    assert.equal(snap.created, true, "snapshot must be created when the signing key is configured");
    snapshotEntryCount = snap.entryCount!;
  });
  after(async () => {
    try {
      await auditDb.delete(auditLogs).where(inArray(auditLogs.id, IDS));
      await auditDb.execute(sql`DELETE FROM audit_chain_entries WHERE source_audit_id = ANY(${IDS})`);
      await auditDb.execute(sql`DELETE FROM audit_chain_snapshots WHERE entry_count = ${snapshotEntryCount}`);
    } catch { /* throwaway DB */ }
    delete process.env.AUDIT_SNAPSHOT_SIGNING_KEY;
  });

  test("a fresh snapshot verifies a known-good chain (headMatches + signatureValid)", async () => {
    const v = await verifyAuditChainSnapshots();
    const mine = v.snapshots.find(s => s.entryCount === snapshotEntryCount);
    assert.ok(mine, "this run's snapshot must be present");
    assert.equal(mine!.headMatches, true, "head must match a non-forged chain");
    assert.equal(mine!.signatureValid, true, "signature must validate with the configured key");
    assert.ok(v.threatBoundary.includes("superuser") && v.threatBoundary.includes("host"), "must state the threat boundary honestly");
  });

  test("a re-hash forgery the plain chain MISSES is CAUGHT by the snapshot", async () => {
    // Forge like a DB superuser: alter a historical entry (B) and re-hash B and everything after it
    // forward, so the chain re-verifies as internally valid.
    const bId = IDS[1];
    const rowsRes: any = await auditDb.execute(sql`
      SELECT id, source_audit_id, previous_hash, entry_timestamp, action, user_id, resource, details
      FROM audit_chain_entries
      WHERE id >= (SELECT id FROM audit_chain_entries WHERE source_audit_id = ${bId})
      ORDER BY id ASC`);
    const rows = (rowsRes?.rows ?? rowsRes) as any[];
    let prevHash: string | null = null;
    for (let i = 0; i < rows.length; i++) {
      const r = rows[i];
      const previousHash = i === 0 ? String(r.previous_hash) : prevHash!;       // B keeps its link to A
      const details = i === 0 ? "FORGED-BY-DB-SUPERUSER" : String(r.details);   // alter B's content
      const newHash = chainHash({
        sourceAuditId: String(r.source_audit_id), previousHash,
        timestamp: new Date(r.entry_timestamp).toISOString(),
        action: String(r.action), userId: r.user_id ?? null, resource: String(r.resource), details,
      });
      await auditDb.execute(sql`UPDATE audit_chain_entries SET previous_hash = ${previousHash}, details = ${details}, hash = ${newHash} WHERE id = ${r.id}`);
      prevHash = newHash;
    }

    // The plain chain is fooled — internal integrity still passes.
    const integrity = await verifyChainIntegrity();
    assert.equal(integrity.valid, true, "the re-hashed chain re-verifies as internally valid — the plain chain MISSES the forgery");

    // The snapshot catches it — the head at the snapshot's entryCount no longer matches the signed head.
    const v = await verifyAuditChainSnapshots();
    const mine = v.snapshots.find(s => s.entryCount === snapshotEntryCount);
    assert.ok(mine, "this run's snapshot must still be present");
    assert.equal(mine!.headMatches, false, "the snapshot MUST detect the re-hashed history (forgery caught)");
    assert.equal(v.allValid, false, "overall notarization must report invalid once a forgery is present");
  });

  test("a forger cannot repair detection by editing the snapshot (signature fails without the key)", async () => {
    // The forger overwrites the snapshot's head to match the forged chain — but cannot re-sign it.
    const headRes: any = await auditDb.execute(sql`SELECT hash FROM audit_chain_entries ORDER BY id ASC LIMIT 1 OFFSET ${snapshotEntryCount - 1}`);
    const forgedHead = String(((headRes?.rows ?? headRes) as any[])[0].hash);
    await auditDb.execute(sql`UPDATE audit_chain_snapshots SET head_hash = ${forgedHead} WHERE entry_count = ${snapshotEntryCount}`);

    const v = await verifyAuditChainSnapshots();
    const mine = v.snapshots.find(s => s.entryCount === snapshotEntryCount);
    assert.ok(mine, "snapshot present");
    assert.equal(mine!.headMatches, true, "head now matches the forged chain (forger edited the snapshot)...");
    assert.equal(mine!.signatureValid, false, "...but the signature no longer validates — the edit is detected");
    assert.equal(v.allValid, false, "notarization still reports invalid");
  });
});
