// M5 phase-2 core — transport-agnostic BoU anchor package [DIRECT-DEV].
//
// The anchor lodges a signed chain-head with an external custodian (BoU) so that even a full-host
// attacker (who can read the signing key and forge a fresh snapshot) cannot rewrite history before
// time T undetectably — because BoU RETAINS a copy the bank cannot alter. The dispute-resolution
// procedure is the acceptance test for the design: BoU holds the anchor (entryCount, headHash) from
// T; later, recompute the chain head at entryCount from a current export and compare to BoU's
// retained headHash. MATCH = no forgery before T; MISMATCH = tampering. The comparison is KEY-FREE.
//
// This proves exactly that procedure end-to-end: it confirms a known-good chain, and CATCHES a
// re-hash-forward forgery that the bank's own verifyChainIntegrity still reports valid.
//
// Requires DATABASE_URL + boot secrets. Sets AUDIT_SNAPSHOT_SIGNING_KEY in 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, buildAuditChainAnchor, verifyChainIntegrity } from "../server/lib/audit-chain.js";

const RUN = `anchor-${Date.now()}`;
const IDS = [`${RUN}-A`, `${RUN}-B`, `${RUN}-C`];
const ACT = `${RUN}-act`;
let anchorEntryCount = 0;
let anchorHeadHash = "";
let anchorSnapshotId = 0;

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");
}

// BoU's side of the dispute procedure: the chain head at `entryCount` = the entryCount-th entry's
// hash in id order. KEY-FREE — just a read of the current chain.
async function recomputeHeadAt(entryCount: number): Promise<string | null> {
  const r: any = await auditDb.execute(sql`SELECT hash FROM audit_chain_entries ORDER BY id ASC LIMIT 1 OFFSET ${entryCount - 1}`);
  return (r.rows ?? r)[0]?.hash ?? null;
}

describe("M5 phase-2 — transport-agnostic BoU anchor package", () => {
  before(async () => {
    process.env.AUDIT_SNAPSHOT_SIGNING_KEY = `${RUN}-key-outside-the-db`;
    await auditDb.insert(auditLogs).values(
      IDS.map((id, i) => ({ id, timestamp: new Date(`2038-03-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(`2038-03-0${i + 1}T00:00:00.000Z`).toISOString() });
    }
    const anchor = await buildAuditChainAnchor();
    // assert.ok is an assertion function (`asserts value`), so this both checks the status AND
    // narrows the discriminated union — commitment/sequence become non-optional below, with no
    // non-null assertions bridging a contract the compiler cannot see.
    assert.ok(anchor.status === "anchored", "anchor must be issued when the signing key is configured");
    anchorEntryCount = anchor.commitment.entryCount;
    anchorHeadHash = anchor.commitment.headHash;
    anchorSnapshotId = anchor.sequence.snapshotId;
  });
  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})`);
    } catch { /* throwaway DB */ }
    delete process.env.AUDIT_SNAPSHOT_SIGNING_KEY;
  });

  test("self-documenting & transport-agnostic: carries the recipe, dispute procedure, genesis, signature", async () => {
    const a = await buildAuditChainAnchor();
    assert.equal(a.anchorFormat, "aegis-audit-chain-anchor/v1");
    assert.equal(a.verification.genesisHash, "GENESIS_BLOCK_AEGIS_CYBER_2026");
    assert.deepEqual(a.verification.hashConstruction.canonicalKeyOrder, ["id", "previousHash", "timestamp", "action", "userId", "resource", "details"]);
    assert.ok(a.verification.procedure.length >= 5, "must carry the end-to-end dispute procedure");
    assert.match(a.verification.transport, /TRANSPORT-AGNOSTIC/i, "must declare it does not encode a BoU channel");
    assert.match(a.verification.threatBoundaryClosed, /full-host|host\/environment/i, "must state which threat it closes");
    // This test previously reached commitment through a non-null assertion WITHOUT ever checking
    // status — it asserted the commitment existed rather than verifying it. Now the status check
    // is explicit and narrows the union, so the commitment access is checked, not asserted.
    assert.ok(a.status === "anchored", "anchor must be issued when the signing key is configured");
    assert.ok(a.commitment.signature.length > 0 && a.commitment.headHash.length > 0, "must carry the signed head commitment");
  });

  test("dispute procedure on a KNOWN-GOOD chain: recomputed head equals BoU's retained headHash", async () => {
    const head = await recomputeHeadAt(anchorEntryCount);
    assert.equal(head, anchorHeadHash, "an unforged chain's head at entryCount must equal the anchor BoU retains");
  });

  test("dispute procedure CATCHES a full-host re-hash forgery the bank's own chain still reports valid", async () => {
    // Forge like an attacker who owns the host + key: alter a historical entry (B) and re-hash B and
    // everything after it forward, so the bank's own verifyChainIntegrity still passes.
    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!;
      const details = i === 0 ? "FORGED-BY-FULL-HOST-ATTACKER" : String(r.details);
      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 bank's own chain is fooled — it re-verifies as internally valid.
    const integrity = await verifyChainIntegrity();
    assert.equal(integrity.valid, true, "the re-hashed chain self-verifies — the bank's chain alone MISSES the forgery");

    // BoU's anchor catches it: the head at entryCount no longer matches the retained headHash.
    const head = await recomputeHeadAt(anchorEntryCount);
    assert.notEqual(head, anchorHeadHash, "the anchor BoU retains MUST detect the re-hashed history (full-host forgery caught)");
    assert.ok(anchorSnapshotId > 0, "anchor carries a sequence id for gap detection");
  });
});
