// Audit-chain medium H6 — audit_logs <-> audit_chain_entries reconciliation [DIRECT-DEV].
//
// createAuditLog writes audit_logs always, then best-effort chains it; a chain-write failure is
// only logged (the deliberate atomic-or-fail "visible gap"). verifyChainIntegrity inspects only
// the chain rows that exist, so a MISSING chain entry for an audit_logs row is invisible to it.
// reconcileAuditChain closes that blind spot. This proves it: detects a seeded post-cutover gap,
// does NOT false-flag a pre-cutover (intentionally unchained) row, and stops reporting the gap
// once it is chained.
//
// Timestamps are chosen so the ordering holds regardless of any other audit data the suite writes:
//   PAST (2001) is before any real/other chained row -> always pre-cutover (expected unchained);
//   FUT1/FUT2 (2035) are after any other row -> reliably at/after the watermark.
// Assertions key on THIS run's ids (membership), never on global counts, so they are pollution-proof.
//
// Requires DATABASE_URL + boot secrets (runs under test:integration). Reads/writes via auditDb.

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

const RUN = `recon-${Date.now()}`;
const A = `${RUN}-A`; // chained -> the watermark
const B = `${RUN}-B`; // post-cutover, unchained -> THE GAP
const C = `${RUN}-C`; // pre-cutover, unchained -> expected, must NOT be flagged
const PAST = new Date("2001-01-01T00:00:00.000Z");
const FUT1 = new Date("2035-06-01T00:00:00.000Z");
const FUT2 = new Date("2035-06-02T00:00:00.000Z");
const ACT = `${RUN}-act`;

const sampleIds = (r: Awaited<ReturnType<typeof reconcileAuditChain>>) =>
  new Set(r.unchainedSample.map(x => x.id));

describe("H6 — audit_logs <-> audit_chain_entries reconciliation", () => {
  before(async () => {
    await auditDb.insert(auditLogs).values([
      { id: C, timestamp: PAST, action: ACT, resource: "test", userId: null, details: null },
      { id: A, timestamp: FUT1, action: ACT, resource: "test", userId: null, details: null },
      { id: B, timestamp: FUT2, action: ACT, resource: "test", userId: null, details: null },
    ]);
    // Chain ONLY A: B is left as a post-cutover gap; C as a pre-cutover (expected) unchained row.
    await chainAuditLog({ id: A, action: ACT, userId: null, resource: "test", details: "", timestamp: FUT1.toISOString() });
  });
  after(async () => {
    try { await auditDb.delete(auditLogs).where(inArray(auditLogs.id, [A, B, C])); } catch { /* throwaway DB */ }
  });

  test("detects the gap; does not false-flag chained or pre-cutover rows", async () => {
    const r = await reconcileAuditChain({ sampleLimit: 200 });
    const ids = sampleIds(r);
    assert.equal(r.reconciled, false, "an unchained post-cutover audit row must make reconciled=false");
    assert.ok(ids.has(B), "the unchained post-cutover row B must be reported as a coverage gap");
    assert.ok(!ids.has(A), "the chained row A must NOT be reported as a gap");
    assert.ok(!ids.has(C), "the pre-cutover row C (intentionally unchained) must NOT be false-flagged");
    assert.ok(r.chainWatermark !== null, "watermark must be set once at least one row is chained");
    assert.equal(typeof r.nonAuditBackedEntries, "number");
  });

  test("closing the gap: once B is chained it is no longer reported", async () => {
    await chainAuditLog({ id: B, action: ACT, userId: null, resource: "test", details: "", timestamp: FUT2.toISOString() });
    const r = await reconcileAuditChain({ sampleLimit: 200 });
    assert.ok(!sampleIds(r).has(B), "after chaining, B must no longer be reported as a gap");
  });
});
