// shutter-mempool-v3.test.js
//
// Audit-fix suite for AereShutterMempoolV3: reveal-timeout skip that restores
// liveness. Follows the repo's V-reproduces / V-corrects pattern — every new
// behaviour is shown STALLING on the live V2 design and RECOVERING on V3, using
// the SAME real 3-of-5 threshold-BLS committee produced by scripts/shutter-crypto.js
// (real ciphertexts, real shares, real Lagrange DK), all pairings verified
// on-chain via the local prague EIP-2537 pairing precompile (0x0f), the same
// precompile probed live on AERE chain 2800.
//
//   npx hardhat test test/shutter-mempool-v3.test.js --config hardhat.config.shutter-v3.js
const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
const crypto = require("crypto");
const C = require("../scripts/shutter-crypto");

const T = 3, N = 5;
const REVEAL_WINDOW = 3600; // seconds; short enough to fast-forward in tests

function rand32() { return "0x" + crypto.randomBytes(32).toString("hex"); }
function toHex(buf) { return "0x" + buf.toString("hex"); }

async function increaseTime(seconds) {
  await network.provider.send("evm_increaseTime", [seconds]);
  await network.provider.send("evm_mine");
}

// Build the encrypted envelopes + sequencer ordering (execution order == the
// given permutation of submission order; default identity).
function buildEnvelopes(com, epochId, plaintexts, order) {
  order = order || plaintexts.map((_, i) => i);
  const env = plaintexts.map((pt) => {
    const e = C.encrypt(com, epochId, pt);
    const opening = rand32();
    const plaintextHex = toHex(pt);
    const commitment = C.commitmentOf(plaintextHex, opening);
    return { ciphertext: e.ciphertext, opening, plaintextHex, commitment, pt };
  });
  const leaves = order.map((srcIdx, pos) => C.leafOf(epochId, pos, env[srcIdx].commitment));
  const { root, proofs } = C.buildMerkle(leaves);
  return { env, order, root, proofs };
}

// Deploy + register a fresh committee against a given contract name.
// V2 ctor = (coordinator, g2Gen); V3 ctor = (coordinator, g2Gen, revealWindow).
async function deployAndRegister(name) {
  const [coordinator] = await ethers.getSigners();
  const com = C.setupCommittee(T, N);
  const F = await ethers.getContractFactory(name);
  const args = name === "AereShutterMempoolV3"
    ? [coordinator.address, com.g2Gen, REVEAL_WINDOW]
    : [coordinator.address, com.g2Gen];
  const mempool = await F.deploy(...args);
  await mempool.waitForDeployment();
  const keyperAddrs = Array.from({ length: N }, () => ethers.Wallet.createRandom().address);
  await (await mempool.registerKeypers(keyperAddrs, com.pubkeys, T)).wait();
  return { mempool, com, coordinator, keyperAddrs };
}

function shareFor(com, ep, keyperIndex) {
  return C.makeShare(com.shares[keyperIndex].secret, ep.point);
}

// Drive an epoch through: start -> submit -> commit ordering -> 3 shares -> finalize.
// Returns the built envelopes/order/proofs so the caller can reveal/skip.
async function runToFinalized(mempool, com, epochId, plaintexts, order) {
  const ep = C.epochIdentity(epochId);
  const built = buildEnvelopes(com, epochId, plaintexts, order);
  await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait();
  for (const e of built.env) await (await mempool.submitEncrypted(epochId, e.ciphertext, e.commitment)).wait();
  await (await mempool.commitOrdering(epochId, built.root)).wait();
  for (const k of [0, 1, 2]) await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait();
  await (await mempool.finalizeEpoch(epochId, C.reconstructDK(ep.point, com, [1, 2, 3]).DK)).wait();
  return { ep, built };
}

describe("AereShutterMempoolV3 (reveal-timeout skip restores liveness)", function () {
  let v3, v2;
  before(async function () {
    this.timeout(180000);
    // EIP-2537 BLS12-381 precompiles (incl. pairing 0x0f) activate in PRAGUE.
    // Run under hardhat.config.shutter-v3.js (prague). Skip cleanly if absent.
    const id = "0x" + [C.g1Hex(C.G1.BASE), C.g2Hex(C.G2.BASE), C.g1Hex(C.G1.BASE.negate()), C.g2Hex(C.G2.BASE)].map((h) => h.slice(2)).join("");
    let ok = false;
    try { ok = BigInt(await ethers.provider.call({ to: "0x000000000000000000000000000000000000000f", data: id })) === 1n; } catch (_) {}
    if (!ok) {
      console.warn("    [skip] local node has no EIP-2537 pairing precompile (0x0f). Run with --config hardhat.config.shutter-v3.js (prague).");
      this.skip();
    }
    v3 = await deployAndRegister("AereShutterMempoolV3");
    v2 = await deployAndRegister("AereShutterMempoolV2");
  });

  it("registers a 3-of-5 committee and exposes REVEAL_WINDOW", async () => {
    expect(await v3.mempool.committeeSet()).to.equal(true);
    expect(await v3.mempool.threshold()).to.equal(T);
    expect(await v3.mempool.keyperCount()).to.equal(N);
    expect(await v3.mempool.REVEAL_WINDOW()).to.equal(REVEAL_WINDOW);
  });

  it("constructor rejects a zero reveal window", async () => {
    const [coordinator] = await ethers.getSigners();
    const com = C.setupCommittee(T, N);
    const F = await ethers.getContractFactory("AereShutterMempoolV3");
    await expect(F.deploy(coordinator.address, com.g2Gen, 0))
      .to.be.revertedWithCustomError(F, "BadArgs");
  });

  it("happy path UNCHANGED: encrypt -> order -> 3 shares -> finalize -> reveal both in order (no skip)", async function () {
    this.timeout(120000);
    const { mempool, com } = v3;
    const epochId = 300;
    const plaintexts = [
      Buffer.from("transfer(0xAbCd, 100 AERE) nonce=7 || opening-embedded", "utf8"),
      Buffer.from("swap 5 USDC.e -> AERE minOut=4.98 || opening-embedded", "utf8"),
    ];
    const { built } = await runToFinalized(mempool, com, epochId, plaintexts);
    expect(await mempool.isFinalized(epochId)).to.equal(true);

    // deadline was stamped at finalize
    const info0 = await mempool.getEpoch(epochId);
    expect(info0.revealDeadline).to.be.greaterThan(0n);

    for (let pos = 0; pos < built.order.length; pos++) {
      const e = built.env[built.order[pos]];
      await expect(mempool.revealAndExecute(epochId, pos, e.plaintextHex, e.opening, built.proofs[pos]))
        .to.emit(mempool, "Executed");
    }
    const after = await mempool.getEpoch(epochId);
    expect(after.nextExecIndex).to.equal(2);
  });

  it("V2 STALL (reproduce): an unrevealed position 0 permanently blocks position 1 (no skip exists)", async function () {
    this.timeout(120000);
    const { mempool, com } = v2; // the live V2 design
    const epochId = 301;
    const plaintexts = [Buffer.from("stuck-position-0", "utf8"), Buffer.from("victim-position-1", "utf8")];
    const { built } = await runToFinalized(mempool, com, epochId, plaintexts);

    // position 0 is deliberately never revealed. position 1 cannot execute:
    await expect(mempool.revealAndExecute(epochId, 1, built.env[1].plaintextHex, built.env[1].opening, built.proofs[1]))
      .to.be.revertedWithCustomError(mempool, "OutOfOrder");

    // there is NO skip path in V2 (function does not exist on the ABI) -> permanent stall
    expect(typeof mempool.skipUnrevealed).to.equal("undefined");
    const info = await mempool.getEpoch(epochId);
    expect(info.nextExecIndex).to.equal(0); // cursor frozen at the stuck head forever
  });

  it("V3 FIX: unrevealed position 0 -> skipUnrevealed after deadline advances -> position 1 executes", async function () {
    this.timeout(120000);
    const { mempool, com } = v3;
    const epochId = 302;
    const plaintexts = [Buffer.from("stuck-position-0", "utf8"), Buffer.from("victim-position-1", "utf8")];
    const { built } = await runToFinalized(mempool, com, epochId, plaintexts);

    // position 1 still blocked while position 0 is the (unrevealed) head
    await expect(mempool.revealAndExecute(epochId, 1, built.env[1].plaintextHex, built.env[1].opening, built.proofs[1]))
      .to.be.revertedWithCustomError(mempool, "OutOfOrder");

    // BEFORE the deadline the skip reverts
    await expect(mempool.skipUnrevealed(epochId, 0))
      .to.be.revertedWithCustomError(mempool, "SkipTooEarly");

    // fast-forward past the reveal window, then skip the stuck head (permissionless)
    await increaseTime(REVEAL_WINDOW + 5);
    const caller = (await ethers.getSigners())[0].address;
    await expect(mempool.skipUnrevealed(epochId, 0))
      .to.emit(mempool, "PositionSkipped").withArgs(epochId, 0, anyValue, caller);

    const mid = await mempool.getEpoch(epochId);
    expect(mid.nextExecIndex).to.equal(1); // cursor advanced past the skipped position
    expect(await mempool.skippedPosition(epochId, 0)).to.equal(true);

    // now position 1 executes -> liveness restored
    await expect(mempool.revealAndExecute(epochId, 1, built.env[1].plaintextHex, built.env[1].opening, built.proofs[1]))
      .to.emit(mempool, "Executed");
    const after = await mempool.getEpoch(epochId);
    expect(after.nextExecIndex).to.equal(2);
  });

  it("skipUnrevealed BEFORE the deadline reverts SkipTooEarly", async function () {
    this.timeout(120000);
    const { mempool, com } = v3;
    const epochId = 303;
    const plaintexts = [Buffer.from("head-A", "utf8"), Buffer.from("tail-B", "utf8")];
    await runToFinalized(mempool, com, epochId, plaintexts);
    // no time advance: still inside the window
    await expect(mempool.skipUnrevealed(epochId, 0))
      .to.be.revertedWithCustomError(mempool, "SkipTooEarly");
  });

  it("skipping a position that WAS revealed reverts (cursor already past it)", async function () {
    this.timeout(120000);
    const { mempool, com } = v3;
    const epochId = 304;
    const plaintexts = [Buffer.from("revealed-0", "utf8"), Buffer.from("later-1", "utf8")];
    const { built } = await runToFinalized(mempool, com, epochId, plaintexts);

    // reveal position 0 legitimately -> cursor -> 1
    await (await mempool.revealAndExecute(epochId, 0, built.env[0].plaintextHex, built.env[0].opening, built.proofs[0])).wait();

    // even after the window closes, position 0 can NOT be skipped: it is behind the head
    await increaseTime(REVEAL_WINDOW + 5);
    await expect(mempool.skipUnrevealed(epochId, 0))
      .to.be.revertedWithCustomError(mempool, "OutOfOrder");
    expect(await mempool.skippedPosition(epochId, 0)).to.equal(false);
  });

  it("skipUnrevealed reverts on a non-finalized epoch and past the last position", async function () {
    this.timeout(120000);
    const { mempool, com } = v3;
    const epochId = 305;
    // not finalized yet: only start + submit + order
    const ep = C.epochIdentity(epochId);
    const built = buildEnvelopes(com, epochId, [Buffer.from("only-0", "utf8")]);
    await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait();
    await (await mempool.submitEncrypted(epochId, built.env[0].ciphertext, built.env[0].commitment)).wait();
    await (await mempool.commitOrdering(epochId, built.root)).wait();
    await increaseTime(REVEAL_WINDOW + 5);
    // Ordered (not Finalized) -> revealDeadline is 0 but the phase guard fires first
    await expect(mempool.skipUnrevealed(epochId, 0))
      .to.be.revertedWithCustomError(mempool, "NotFinalized");

    // finalize, then skip the single position, then a further skip past the end reverts NothingToSkip
    for (const k of [0, 1, 2]) await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait();
    await (await mempool.finalizeEpoch(epochId, C.reconstructDK(ep.point, com, [1, 2, 3]).DK)).wait();
    await increaseTime(REVEAL_WINDOW + 5);
    await (await mempool.skipUnrevealed(epochId, 0)).wait(); // envelopeCount==1 -> index 0 skippable
    await expect(mempool.skipUnrevealed(epochId, 1))
      .to.be.revertedWithCustomError(mempool, "NothingToSkip"); // 1 >= envelopeCount(1)
  });

  it("no early decrypt: a decryption share before the ordering is committed still reverts", async function () {
    this.timeout(120000);
    const { mempool, com } = v3;
    const epochId = 306;
    const ep = C.epochIdentity(epochId);
    await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait();
    await (await mempool.submitEncrypted(epochId, C.encrypt(com, epochId, Buffer.from("x")).ciphertext, C.commitmentOf("0x1234", rand32()))).wait();
    // ordering NOT yet committed -> share must revert (prevents early-decrypt MEV)
    await expect(mempool.submitDecryptionShare(epochId, 0, shareFor(com, ep, 0)))
      .to.be.revertedWithCustomError(mempool, "OrderingNotCommitted");
  });

  it("out-of-order reveal still rejected (ordering guarantee preserved)", async function () {
    this.timeout(120000);
    const { mempool, com } = v3;
    const epochId = 307;
    const plaintexts = [Buffer.from("ord-0", "utf8"), Buffer.from("ord-1", "utf8")];
    const { built } = await runToFinalized(mempool, com, epochId, plaintexts);
    await expect(mempool.revealAndExecute(epochId, 1, built.env[1].plaintextHex, built.env[1].opening, built.proofs[1]))
      .to.be.revertedWithCustomError(mempool, "OutOfOrder");
    await (await mempool.revealAndExecute(epochId, 0, built.env[0].plaintextHex, built.env[0].opening, built.proofs[0])).wait();
    await expect(mempool.revealAndExecute(epochId, 1, built.env[1].plaintextHex, built.env[1].opening, built.proofs[1]))
      .to.emit(mempool, "Executed");
  });
});
