// AereDACommittee — unit tests (pure ECDSA M-of-N, runs fully local).

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

describe("AereDACommittee", function () {
  let dac, admin, m1, m2, m3, outsider;
  let members; // sorted ascending by address

  const rollupId = ethers.id("rollup:demo");
  const epoch = 7n;
  const dataCommitment = ethers.id("batch-data-commitment");

  beforeEach(async function () {
    [admin, m1, m2, m3, outsider] = await ethers.getSigners();
    // sort committee members ascending (attest requires ascending recovered addr)
    members = [m1, m2, m3].sort((a, b) =>
      a.address.toLowerCase() < b.address.toLowerCase() ? -1 : 1
    );
    const F = await ethers.getContractFactory("AereDACommittee");
    dac = await F.deploy(admin.address, members.map((m) => m.address), 2);
    await dac.waitForDeployment();
  });

  async function sign(wallet, rId = rollupId, ep = epoch, dc = dataCommitment) {
    // members sign the raw inner hash via personal_sign; the contract wraps with
    // the EIP-191 prefix in attestationDigest.
    const inner = ethers.keccak256(
      ethers.AbiCoder.defaultAbiCoder().encode(
        ["string", "uint256", "address", "bytes32", "uint256", "bytes32"],
        ["AereDACommittee:attest", 31337n, await dac.getAddress(), rId, ep, dc]
      )
    );
    return wallet.signMessage(ethers.getBytes(inner));
  }

  it("constructs with the committee and threshold", async function () {
    expect(await dac.memberCount()).to.equal(3n);
    expect(await dac.threshold()).to.equal(2n);
    expect(await dac.isMember(members[0].address)).to.equal(true);
    expect(await dac.isMember(outsider.address)).to.equal(false);
    expect((await dac.getMembers()).length).to.equal(3);
  });

  it("records availability with M distinct member signatures", async function () {
    const sigs = [await sign(members[0]), await sign(members[1])]; // ascending
    const tx = await dac.attest(rollupId, epoch, dataCommitment, sigs);
    await tx.wait();
    expect(await dac.isAvailable(rollupId, epoch)).to.equal(true);
    const [available, dc, , n] = await dac.getAttestation(rollupId, epoch);
    expect(available).to.equal(true);
    expect(dc).to.equal(dataCommitment);
    expect(n).to.equal(2n);
  });

  it("rejects M-1 signatures", async function () {
    const sigs = [await sign(members[0])];
    await expect(dac.attest(rollupId, epoch, dataCommitment, sigs))
      .to.be.revertedWithCustomError(dac, "ThresholdNotMet")
      .withArgs(1, 2);
    expect(await dac.isAvailable(rollupId, epoch)).to.equal(false);
  });

  it("rejects a non-member signature", async function () {
    const sigs = [await sign(members[0]), await sign(outsider)];
    // ordering may or may not place outsider second; either way it must revert.
    await expect(dac.attest(rollupId, epoch, dataCommitment, sigs)).to.be.reverted;
    expect(await dac.isAvailable(rollupId, epoch)).to.equal(false);
  });

  it("rejects duplicate / unsorted signatures", async function () {
    const dup = await sign(members[0]);
    await expect(dac.attest(rollupId, epoch, dataCommitment, [dup, dup]))
      .to.be.revertedWithCustomError(dac, "UnsortedOrDuplicate");
  });

  it("rejects re-recording the same epoch", async function () {
    const sigs = [await sign(members[0]), await sign(members[1])];
    await dac.attest(rollupId, epoch, dataCommitment, sigs);
    await expect(dac.attest(rollupId, epoch, dataCommitment, sigs))
      .to.be.revertedWithCustomError(dac, "AlreadyRecorded");
  });

  it("admin governance cannot forge availability, and freeze locks it", async function () {
    // no admin function can set availability; only attest can.
    await dac.connect(admin).freeze();
    expect(await dac.frozen()).to.equal(true);
    await expect(dac.connect(admin).addMember(outsider.address)).to.be.revertedWithCustomError(dac, "IsFrozen");
    // attest still works post-freeze
    const sigs = [await sign(members[0]), await sign(members[1])];
    await dac.attest(rollupId, epoch, dataCommitment, sigs);
    expect(await dac.isAvailable(rollupId, epoch)).to.equal(true);
  });

  it("only admin can manage the committee", async function () {
    await expect(dac.connect(outsider).setThreshold(1)).to.be.revertedWithCustomError(dac, "NotAdmin");
  });
});
