// shutter-mempool-v2.test.js
//
// Full PoC test for AereShutterMempoolV2: a REAL 3-of-5 threshold-BLS committee
// generated by scripts/shutter-crypto.js (real ciphertexts, real shares, real
// Lagrange reconstruction) with every pairing verified on-chain via the local
// prague EIP-2537 pairing precompile (0x0f), the same precompile probed live on
// AERE chain 2800.
const { expect } = require("chai");
const { ethers } = require("hardhat");
const crypto = require("crypto");
const C = require("../scripts/shutter-crypto");

const T = 3, N = 5;

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

// 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 };
  });
  // execution position `pos` reveals env[order[pos]]
  const leaves = order.map((srcIdx, pos) => C.leafOf(epochId, pos, env[srcIdx].commitment));
  const { root, proofs } = C.buildMerkle(leaves);
  return { env, order, root, proofs };
}

async function deployAndRegister() {
  const [coordinator] = await ethers.getSigners();
  const com = C.setupCommittee(T, N);
  const F = await ethers.getContractFactory("AereShutterMempoolV2");
  const mempool = await F.deploy(coordinator.address, com.g2Gen);
  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 };
}

// Shares for keyper indices (0-based). x-coord = index+1.
function shareFor(com, ep, keyperIndex) {
  return C.makeShare(com.shares[keyperIndex].secret, ep.point);
}

describe("AereShutterMempoolV2 (real threshold-BLS anti-MEV mempool)", function () {
  let ctx;
  before(async function () {
    this.timeout(120000);
    // EIP-2537 BLS12-381 precompiles (incl. pairing 0x0f) activate in PRAGUE.
    // The repo's default `hardhat` network is pinned to cancun (to preserve the
    // Falcon-1024 gas-cap behaviour), so run this suite under a prague config:
    //   npx hardhat test test/shutter-mempool-v2.test.js --config hardhat.config.shutter-v2.js
    // If the local node lacks the precompile, skip cleanly instead of failing.
    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-v2.js (prague).");
      this.skip();
    }
    ctx = await deployAndRegister();
  });

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

  it("full happy path: encrypt -> order -> 3 valid shares -> finalize -> reveal both in order", async function () {
    this.timeout(120000);
    const { mempool, com } = ctx;
    const epochId = 100;
    const ep = C.epochIdentity(epochId);
    const plaintexts = [
      Buffer.from("transfer(0xAbCd, 100 AERE) nonce=7", "utf8"),
      Buffer.from("swap 5 USDC.e -> AERE minOut=4.98", "utf8"),
    ];
    const built = buildEnvelopes(com, epochId, plaintexts);

    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();

    let info = await mempool.getEpoch(epochId);
    expect(info.phase).to.equal(1); // Open
    expect(info.envelopeCount).to.equal(2);

    // decryption share BEFORE ordering must revert (prevents early-decrypt MEV)
    const s0 = shareFor(com, ep, 0);
    await expect(mempool.submitDecryptionShare(epochId, 0, s0))
      .to.be.revertedWithCustomError(mempool, "OrderingNotCommitted");

    // sequencer commits the ordering
    await (await mempool.commitOrdering(epochId, built.root)).wait();
    info = await mempool.getEpoch(epochId);
    expect(info.phase).to.equal(2); // Ordered

    // 3 valid shares (keypers 0,1,2 -> x = 1,2,3), each pairing-verified on-chain
    for (const k of [0, 1, 2]) {
      await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait();
    }
    info = await mempool.getEpoch(epochId);
    expect(info.shareCount).to.equal(3);

    // reconstruct DK off-chain via Lagrange from subset {1,2,3}; verify on-chain
    const { DK, point: dkPoint } = C.reconstructDK(ep.point, com, [1, 2, 3]);
    await (await mempool.finalizeEpoch(epochId, DK)).wait();
    expect(await mempool.isFinalized(epochId)).to.equal(true);

    // sanity: DK actually decrypts the real ciphertexts to the original plaintexts
    for (let i = 0; i < plaintexts.length; i++) {
      const dec = C.decrypt(dkPoint, built.env[i].ciphertext);
      expect(dec.equals(plaintexts[i])).to.equal(true);
    }

    // reveal both, strictly in committed order
    for (let pos = 0; pos < built.order.length; pos++) {
      const src = built.order[pos];
      const e = built.env[src];
      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("rejects a share that is inconsistent with its VSS commitment (wrong share)", async () => {
    const { mempool, com } = ctx;
    const epochId = 101;
    const ep = C.epochIdentity(epochId);
    await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait();
    const e = C.encrypt(com, epochId, Buffer.from("x", "utf8"));
    const commitment = C.commitmentOf("0x1234", rand32());
    await (await mempool.submitEncrypted(epochId, e.ciphertext, commitment)).wait();
    await (await mempool.commitOrdering(epochId, ethers.keccak256("0xabcd"))).wait();

    // keyper 0's real share, but submitted AS keyper index 1 -> pairing must fail
    const wrong = shareFor(com, ep, 0);
    await expect(mempool.submitDecryptionShare(epochId, 1, wrong))
      .to.be.revertedWithCustomError(mempool, "BadShare");

    // a truncated / malformed point is also rejected
    await expect(mempool.submitDecryptionShare(epochId, 1, "0x1234"))
      .to.be.revertedWithCustomError(mempool, "BadPointLength");

    // the correct share for keyper 1 passes
    await expect(mempool.submitDecryptionShare(epochId, 1, shareFor(com, ep, 1)))
      .to.emit(mempool, "ShareSubmitted");
  });

  it("rejects a forged/incorrect DK reconstruction at finalize", async () => {
    const { mempool, com } = ctx;
    const epochId = 102;
    const ep = C.epochIdentity(epochId);
    await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait();
    const e = C.encrypt(com, epochId, Buffer.from("y", "utf8"));
    await (await mempool.submitEncrypted(epochId, e.ciphertext, C.commitmentOf("0x22", rand32()))).wait();
    await (await mempool.commitOrdering(epochId, ethers.keccak256("0xbeef"))).wait();
    for (const k of [0, 1, 2]) await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait();

    // a DK from a WRONG epoch's identity does not satisfy e(DK,g2)==e(H1,PK)
    const wrongEp = C.epochIdentity(999);
    const { DK: wrongDK } = C.reconstructDK(wrongEp.point, com, [1, 2, 3]);
    await expect(mempool.finalizeEpoch(epochId, wrongDK))
      .to.be.revertedWithCustomError(mempool, "BadReconstruction");

    // the correct DK finalizes
    const { DK } = C.reconstructDK(ep.point, com, [1, 2, 3]);
    await expect(mempool.finalizeEpoch(epochId, DK)).to.emit(mempool, "EpochFinalized");
  });

  it("reverts finalize with fewer than t shares", async () => {
    const { mempool, com } = ctx;
    const epochId = 103;
    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("z")).ciphertext, C.commitmentOf("0x33", rand32()))).wait();
    await (await mempool.commitOrdering(epochId, ethers.keccak256("0xcafe"))).wait();
    for (const k of [0, 1]) await (await mempool.submitDecryptionShare(epochId, k, shareFor(com, ep, k))).wait(); // only 2
    const { DK } = C.reconstructDK(ep.point, com, [1, 2, 3]);
    await expect(mempool.finalizeEpoch(epochId, DK)).to.be.revertedWithCustomError(mempool, "NotEnoughShares");
  });

  it("reverts revealAndExecute with a plaintext that does not open the commitment", async () => {
    const { mempool, com } = ctx;
    const epochId = 104;
    const ep = C.epochIdentity(epochId);
    const plaintexts = [Buffer.from("real-payload-A", "utf8"), Buffer.from("real-payload-B", "utf8")];
    const built = buildEnvelopes(com, epochId, plaintexts);
    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();

    // tampered plaintext at position 0 with the real opening/proof -> commitment mismatch
    const tampered = toHex(Buffer.from("EVIL-payload-A", "utf8"));
    await expect(mempool.revealAndExecute(epochId, 0, tampered, built.env[0].opening, built.proofs[0]))
      .to.be.revertedWithCustomError(mempool, "UnknownCommitment");

    // correct reveal at position 0 works
    await expect(mempool.revealAndExecute(epochId, 0, built.env[0].plaintextHex, built.env[0].opening, built.proofs[0]))
      .to.emit(mempool, "Executed");
  });

  it("reverts revealAndExecute out of committed order", async () => {
    const { mempool, com } = ctx;
    const epochId = 105;
    const ep = C.epochIdentity(epochId);
    const plaintexts = [Buffer.from("order-0", "utf8"), Buffer.from("order-1", "utf8")];
    const built = buildEnvelopes(com, epochId, plaintexts);
    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();

    // trying to execute position 1 before position 0 must revert
    await expect(mempool.revealAndExecute(epochId, 1, built.env[1].plaintextHex, built.env[1].opening, built.proofs[1]))
      .to.be.revertedWithCustomError(mempool, "OutOfOrder");

    // in-order execution then succeeds
    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");
  });

  it("flags a keyper whose alleged share is inconsistent with its commitment", async () => {
    const { mempool, com } = ctx;
    const epochId = 106;
    const ep = C.epochIdentity(epochId);
    await (await mempool.startEpoch(epochId, com.PK, ep.H1, ep.H1neg)).wait();
    // report keyper 1 using keyper 0's share -> inconsistent -> flag emitted
    await expect(mempool.reportInconsistentShare(epochId, 1, shareFor(com, ep, 0)))
      .to.emit(mempool, "KeyperFlagged");
    // a genuinely consistent share cannot be used to falsely accuse
    await expect(mempool.reportInconsistentShare(epochId, 1, shareFor(com, ep, 1)))
      .to.be.revertedWithCustomError(mempool, "ShareIsConsistent");
  });
});
