// Audit-chain medium M4 — regulator-facing self-verifying export [DIRECT-DEV].
//
// The export must be tamper-evident BY RECONSTRUCTION: a BoU examiner takes the bundle, recomputes
// every hash with their OWN code, confirms linkage and coverage, and trusts nothing the platform
// asserted. This test plays the examiner — it re-implements the hash recipe from scratch (node
// crypto, NOT the library's hasher) using ONLY the bundle's documented hashConstruction, and proves:
//   - a known-good chain: every content-verifiable entry's independently-recomputed hash matches
//     the stored hash, and linkage walks cleanly from genesis;
//   - a tampered entry is detected: mutating one entry's content makes the recompute diverge;
//   - the bundle is complete & self-documenting (coverage summary, baseline declaration, instructions).
//
// Requires DATABASE_URL + boot secrets (runs under test:integration).

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

const RUN = `export-${Date.now()}`;
const IDS = [`${RUN}-1`, `${RUN}-2`, `${RUN}-3`];
const ACT = `${RUN}-act`;

// INDEPENDENT re-implementation of the hash recipe, built ONLY from the bundle's documented
// hashConstruction (canonical key order; id=sourceAuditId; timestamp as-is; SHA-256 hex). This is
// deliberately a separate implementation from server/lib/audit-chain.ts's computeHash — if the two
// ever diverge, an examiner could not reproduce our hashes and this test must fail.
function examinerRecompute(e: AuditChainExportEntry): string {
  const preimage = JSON.stringify({
    id: e.sourceAuditId,
    previousHash: e.previousHash,
    timestamp: e.timestamp,
    action: e.action,
    userId: e.userId,
    resource: e.resource,
    details: e.details,
  });
  return createHash("sha256").update(preimage).digest("hex");
}

describe("M4 — regulator-facing self-verifying audit-chain export", () => {
  before(async () => {
    await auditDb.insert(auditLogs).values(
      IDS.map((id, i) => ({ id, timestamp: new Date(`2036-01-0${i + 1}T00:00:00.000Z`), action: ACT, resource: "test", userId: null, details: `payload-${i}` })),
    );
    for (let i = 0; i < IDS.length; i++) {
      await chainAuditLog({ id: IDS[i], action: ACT, userId: null, resource: "test", details: `payload-${i}`, timestamp: new Date(`2036-01-0${i + 1}T00:00:00.000Z`).toISOString() });
    }
  });
  after(async () => {
    try { await auditDb.delete(auditLogs).where(inArray(auditLogs.id, IDS)); } catch { /* throwaway DB */ }
  });

  test("bundle is self-documenting and complete (recipe, instructions, coverage, baseline)", async () => {
    const b = await buildAuditChainExport({ generatedAt: "2036-01-01T00:00:00.000Z" });
    assert.equal(b.exportFormat, "aegis-audit-chain-export/v1");
    assert.equal(b.genesisHash, "GENESIS_BLOCK_AEGIS_CYBER_2026");
    assert.deepEqual(b.hashConstruction.canonicalKeyOrder, ["id", "previousHash", "timestamp", "action", "userId", "resource", "details"]);
    assert.ok(b.hashConstruction.preimage.length > 0 && b.verificationInstructions.length >= 5);
    assert.ok(b.reconciliation && typeof b.reconciliation.reconciled === "boolean", "must carry the H6 coverage summary (completeness, not just integrity)");
    assert.ok(b.baselineNotes && typeof b.baselineNotes.legacyEntries === "number", "must declare its baseline honestly");
    assert.ok(b.entryCount >= IDS.length);
  });

  test("known-good chain: an independent recompute confirms every content-verifiable hash + linkage", async () => {
    const b = await buildAuditChainExport();
    // (a) Content: recompute every content-verifiable entry's hash independently and confirm match.
    let verified = 0;
    for (const e of b.entries) {
      if (!e.contentVerifiable) continue;
      assert.equal(examinerRecompute(e), e.hash, `entry index ${e.index} (${e.sourceAuditId}) must recompute to its stored hash`);
      verified++;
    }
    assert.ok(verified >= IDS.length, "all seeded entries must be independently re-verifiable");
    // (b) Linkage: walk the chain — each entry must commit to the prior entry's hash; genesis at index 0.
    for (const e of b.entries) {
      const expected = e.index === 0 ? b.genesisHash : b.entries[e.index - 1].hash;
      assert.equal(e.previousHash, expected, `entry index ${e.index} linkage must be unbroken`);
      assert.equal(e.expectedPreviousHash, expected, "export's expectedPreviousHash must match the walked linkage");
    }
  });

  test("tampered entry is detected: mutating content makes the independent recompute diverge", async () => {
    const b = await buildAuditChainExport();
    const mine = b.entries.find(e => e.contentVerifiable && e.sourceAuditId != null && IDS.includes(e.sourceAuditId));
    assert.ok(mine, "expected at least one of this run's content-verifiable entries in the export");
    // Examiner recompute of the untouched entry matches...
    assert.equal(examinerRecompute(mine!), mine!.hash);
    // ...but a tampered copy (one field altered) no longer matches the stored hash — tamper detected.
    const tampered: AuditChainExportEntry = { ...mine!, details: mine!.details + "_TAMPERED" };
    assert.notEqual(examinerRecompute(tampered), mine!.hash, "altering content must break the recomputed hash");
  });
});
