const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("AereShutterMempool", () => {
  let foundation, keyper, user, outsider;
  let m;
  const REVEAL_DELAY = 2;
  const KEYPER_SIZE = 13;
  const KEYPER_THRESHOLD = 9;
  const COMMITTEE_ROOT = ethers.keccak256(ethers.toUtf8Bytes("committee-v1"));
  const AGG_PUBKEY = ethers.keccak256(ethers.toUtf8Bytes("agg-pubkey-v1"));

  beforeEach(async () => {
    [foundation, keyper, user, outsider] = await ethers.getSigners();
    const F = await ethers.getContractFactory("AereShutterMempool");
    m = await F.deploy(foundation.address, REVEAL_DELAY, KEYPER_SIZE, KEYPER_THRESHOLD);
    await m.waitForDeployment();
  });

  async function mineBlocks(n) {
    for (let i = 0; i < n; i++) await ethers.provider.send("evm_mine", []);
  }

  it("only Foundation opens epochs; cannot reopen the same epoch", async () => {
    await expect(
      m.connect(user).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY)
    ).to.be.revertedWithCustomError(m, "NotFoundation");
    await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY);
    expect((await m.epochs(1n)).exists).to.equal(true);
    await expect(
      m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY)
    ).to.be.revertedWithCustomError(m, "EpochClosedAlready");
  });

  it("opening a new epoch closes the previous one", async () => {
    await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY);
    await m.connect(foundation).openEpoch(2n, COMMITTEE_ROOT, AGG_PUBKEY);
    expect((await m.epochs(1n)).endBlock).to.be.gt(0n);
    expect((await m.epochs(2n)).endBlock).to.equal(0n);
    expect(await m.currentEpochId()).to.equal(2n);
  });

  it("commit requires open epoch; envelope is recorded", async () => {
    const ch = ethers.keccak256(ethers.toUtf8Bytes("cipher-1"));
    await expect(
      m.connect(user).commit(ch, "ipfs://x")
    ).to.be.revertedWithCustomError(m, "EpochNotOpen");
    await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY);
    const tx = await m.connect(user).commit(ch, "ipfs://x");
    const rc = await tx.wait();
    const ev = rc.logs.map(l => { try { return m.interface.parseLog(l); } catch { return null; } }).find(x => x?.name === 'Committed');
    expect(ev).to.exist;
    const env = await m.envelopeOf(ev.args.envelopeId);
    expect(env.sender).to.equal(user.address);
    expect(env.ciphertextHash).to.equal(ch);
    expect(await m.envelopeCount(1n)).to.equal(1n);
  });

  it("releaseDecryptionKey rejected before REVEAL_DELAY; succeeds after", async () => {
    await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY);
    const kh = ethers.keccak256(ethers.toUtf8Bytes("key"));
    await expect(
      m.connect(foundation).releaseDecryptionKey(1n, kh, "ipfs://key")
    ).to.be.revertedWithCustomError(m, "RevealTooEarly");
    await mineBlocks(REVEAL_DELAY);
    await m.connect(foundation).releaseDecryptionKey(1n, kh, "ipfs://key");
    expect(await m.isKeyReleased(1n)).to.equal(true);
  });

  it("markRevealed: Foundation-only, plaintext-bound, idempotent (audit fix HIGH #3)", async () => {
    await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY);
    const plaintext = ethers.toUtf8Bytes("hello-world");
    const releasedKey = ethers.keccak256(ethers.toUtf8Bytes("k"));
    const ch = ethers.keccak256(ethers.solidityPacked(["bytes", "bytes32"], [plaintext, releasedKey]));
    const tx = await m.connect(user).commit(ch, "");
    const rc = await tx.wait();
    const ev = rc.logs.map(l => { try { return m.interface.parseLog(l); } catch { return null; } }).find(x => x?.name === 'Committed');
    const id = ev.args.envelopeId;

    // Non-Foundation can't markRevealed.
    await expect(m.connect(outsider).markRevealed(id, plaintext)).to.be.revertedWithCustomError(m, "NotFoundation");
    await mineBlocks(REVEAL_DELAY);
    // Key not yet released.
    await expect(m.connect(foundation).markRevealed(id, plaintext)).to.be.revertedWithCustomError(m, "KeyNotReleased");
    await m.connect(foundation).releaseDecryptionKey(1n, releasedKey, "");
    // Plaintext-mismatch rejected.
    await expect(m.connect(foundation).markRevealed(id, ethers.toUtf8Bytes("WRONG"))).to.be.revertedWithCustomError(m, "PlaintextMismatch");
    // Correct plaintext succeeds.
    await m.connect(foundation).markRevealed(id, plaintext);
    expect(await m.isRevealed(id)).to.equal(true);
    // Idempotent.
    await expect(m.connect(foundation).markRevealed(id, plaintext)).to.be.revertedWithCustomError(m, "AlreadyRevealed");
  });

  it("unknown envelope reveal reverts", async () => {
    const fake = ethers.keccak256(ethers.toUtf8Bytes("nonexistent"));
    await expect(
      m.connect(foundation).markRevealed(fake, "0x")
    ).to.be.revertedWithCustomError(m, "EnvelopeUnknown");
  });

  it("commit refuses to overwrite an existing envelope (audit fix HIGH #4)", async () => {
    await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY);
    const ch = ethers.keccak256(ethers.toUtf8Bytes("c1"));
    await m.connect(user).commit(ch, "");
    await expect(m.connect(user).commit(ch, "")).to.be.revertedWithCustomError(m, "EnvelopeAlreadyCommitted");
  });

  it("releaseDecryptionKey one-shot per epoch (audit fix HIGH #5)", async () => {
    await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY);
    await mineBlocks(REVEAL_DELAY);
    const k = ethers.keccak256(ethers.toUtf8Bytes("k"));
    await m.connect(foundation).releaseDecryptionKey(1n, k, "");
    await expect(m.connect(foundation).releaseDecryptionKey(1n, k, "")).to.be.revertedWithCustomError(m, "KeyAlreadyReleased");
  });

  it("constructor requires revealDelayBlocks >= 1 (audit fix MED #31)", async () => {
    const F = await ethers.getContractFactory("AereShutterMempool");
    await expect(F.deploy(foundation.address, 0, KEYPER_SIZE, KEYPER_THRESHOLD)).to.be.revertedWith("reveal-delay-zero");
  });

  it("zero ciphertext hash rejected", async () => {
    await m.connect(foundation).openEpoch(1n, COMMITTEE_ROOT, AGG_PUBKEY);
    await expect(
      m.connect(user).commit(ethers.ZeroHash, "")
    ).to.be.revertedWithCustomError(m, "EmptyCommitment");
  });
});
