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

// ---------------------------------------------------------------------------
// AerePQAggregate: constant-cost on-chain verification of a t-of-n POST-QUANTUM
// committee authorization via ONE succinct SP1 proof.
//
// WHAT IS UNDER TEST (real, on-chain): AerePQAggregateVerifier and the ERC-7579
// validator module AerePQAggregateModule. The verifier binds a committee root, t,
// n, scheme and the message digest into the SP1 public values and gates on a single
// gateway verifyProof call whose cost does not grow with n.
//
// TEST DOUBLE: MockSp1Verifier is the deployed SP1 gateway stand-in. Its verifyProof
// REVERTS on an invalid proof (exact production gateway semantics), so it faithfully
// exercises the fail-closed path. It accepts only (vkey, publicValues, proof) triples
// pre-registered via setValid(), so a "valid proof" in these tests means the gateway
// asserted the exact bound public values.
//
// HONEST BOUNDARY: the OFF-CHAIN SP1 aggregation guest circuit (verify n ML-DSA/Falcon
// signatures against the committee root inside the zkVM and emit these public values)
// is NOT implemented and is [MEASURE]. These tests prove the on-chain verifier + module
// only, against a mock gateway, exactly like AereComputeMarketV3 and the zk light clients.
// ---------------------------------------------------------------------------

const SCHEME_MLDSA65 = 5;
const VALIDATION_SUCCESS = 0n;
const SIG_VALIDATION_FAILED = 1n;
const EIP1271_MAGIC = "0x1626ba7e";
const EIP1271_FAIL = "0xffffffff";

function abi() {
  return ethers.AbiCoder.defaultAbiCoder();
}

// The fixed 192-byte SP1 public-values layout the aggregation circuit must emit.
function encodePublicValues({ root, threshold, size, scheme, digest, validCount }) {
  return abi().encode(
    ["bytes32", "uint256", "uint256", "uint256", "bytes32", "uint256"],
    [root, threshold, size, scheme, digest, validCount]
  );
}

// Marker the MockSp1Verifier keys a valid proof on: keccak256(abi.encode(vkey, publicValues, proof)).
function proofMarker(vkey, publicValues, proof) {
  return ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof]));
}

// A committee root: a commitment to the n authorized PQC public keys. The contract only
// compares a bytes32, so any binding commitment works; we use keccak over a label + n.
function committeeRoot(label, n) {
  return ethers.keccak256(ethers.toUtf8Bytes(`committee:${label}:n=${n}`));
}

describe("AerePQAggregate (verifier + ERC-7579 module)", function () {
  let sp1, verifier, module;
  let deployer, account, relayer, other;
  const VKEY = ethers.keccak256(ethers.toUtf8Bytes("sp1-program:aere-pq-aggregate-mldsa65-v1"));

  beforeEach(async function () {
    [deployer, account, relayer, other] = await ethers.getSigners();

    sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
    await sp1.waitForDeployment();

    verifier = await (await ethers.getContractFactory("AerePQAggregateVerifier")).deploy(
      await sp1.getAddress(),
      VKEY
    );
    await verifier.waitForDeployment();

    module = await (await ethers.getContractFactory("AerePQAggregateModule")).deploy(
      await verifier.getAddress()
    );
    await module.waitForDeployment();
  });

  // Register a committee and return {id, root, threshold, size, scheme}.
  async function registerCommittee({ label = "A", threshold = 3, size = 5, scheme = SCHEME_MLDSA65 } = {}) {
    const root = committeeRoot(label, size);
    const idBefore = await verifier.committeeCount();
    await verifier.connect(deployer).registerCommittee(root, threshold, size, scheme);
    const id = idBefore + 1n;
    return { id, root, threshold, size, scheme };
  }

  // Build public values + a proof, and (optionally) register it valid in the mock gateway.
  async function makeProof(c, { digest, validCount, proof = "0xa1b2c3d4", register = true } = {}) {
    const publicValues = encodePublicValues({
      root: c.root,
      threshold: c.threshold,
      size: c.size,
      scheme: c.scheme,
      digest,
      validCount,
    });
    if (register) {
      await sp1.setValid(proofMarker(VKEY, publicValues, proof), true);
    }
    return { publicValues, proof };
  }

  // =========================================================================
  // 1. Registration
  // =========================================================================
  describe("registration", function () {
    it("registers a committee and rejects bad params", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      const info = await verifier.getCommittee(c.id);
      expect(info.exists).to.equal(true);
      expect(info.threshold).to.equal(3);
      expect(info.size).to.equal(5);
      expect(info.scheme).to.equal(SCHEME_MLDSA65);
      expect(info.root).to.equal(c.root);
      expect(await verifier.committeeExists(c.id)).to.equal(true);

      await expect(
        verifier.registerCommittee(ethers.ZeroHash, 3, 5, SCHEME_MLDSA65)
      ).to.be.revertedWithCustomError(verifier, "ZeroRoot");
      await expect(
        verifier.registerCommittee(committeeRoot("bad", 5), 0, 5, SCHEME_MLDSA65)
      ).to.be.revertedWithCustomError(verifier, "BadCommitteeParams");
      await expect(
        verifier.registerCommittee(committeeRoot("bad", 5), 6, 5, SCHEME_MLDSA65)
      ).to.be.revertedWithCustomError(verifier, "BadCommitteeParams");
      await expect(
        verifier.registerCommittee(committeeRoot("bad", 5), 3, 5, 0)
      ).to.be.revertedWithCustomError(verifier, "BadScheme");
    });

    // L1 (LOW) fix: verifyProof returns void, so a code-less gateway makes the call succeed silently
    // (fail-OPEN = post-quantum authorization bypass). The constructor must reject a code-less gateway,
    // exactly like the sibling AereFinalityCertificateVerifier.
    it("rejects a code-less SP1 gateway at construction (L1 fail-open guard)", async function () {
      const F = await ethers.getContractFactory("AerePQAggregateVerifier");
      // A non-zero EOA / empty address has no code.
      await expect(F.deploy(other.address, VKEY)).to.be.revertedWithCustomError(verifier, "VerifierHasNoCode");
      // A zero gateway still reverts on the earlier ZeroGateway check.
      await expect(F.deploy(ethers.ZeroAddress, VKEY)).to.be.revertedWithCustomError(verifier, "ZeroGateway");
      // Control: the real (code-bearing) mock gateway deploys fine.
      const ok = await F.deploy(await sp1.getAddress(), VKEY);
      await ok.waitForDeployment();
      expect(await ok.SP1_GATEWAY()).to.equal(await sp1.getAddress());
    });
  });

  // =========================================================================
  // 2. Verifier: valid proof authorizes; invalid / wrong-digest / wrong-root reject
  // =========================================================================
  describe("verifier fail-closed semantics", function () {
    it("a valid aggregate proof authorizes (view + recorded)", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      const digest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:approve-transfer"));
      const { publicValues, proof } = await makeProof(c, { digest, validCount: 3 });

      // Strict view path does not revert.
      expect(await verifier.verifyAggregate(c.id, digest, publicValues, proof)).to.equal(true);
      // Non-reverting path returns true.
      expect(await verifier.tryVerifyAggregate(c.id, digest, publicValues, proof)).to.equal(true);

      // State path records provenance and emits.
      await expect(verifier.connect(relayer).recordAggregate(c.id, digest, publicValues, proof))
        .to.emit(verifier, "AggregateVerified")
        .withArgs(c.id, digest, c.threshold, 3, anyUint());
      const att = await verifier.getAttestation(c.id, digest);
      expect(att.exists).to.equal(true);
      expect(att.validCount).to.equal(3);
      expect(await verifier.isAttested(c.id, digest)).to.equal(true);
    });

    it("rejects an invalid proof fail-closed (proof not registered in the gateway)", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      const digest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:1"));
      // register=false => the mock gateway will revert MockInvalidProof.
      const { publicValues, proof } = await makeProof(c, { digest, validCount: 3, register: false });

      await expect(
        verifier.verifyAggregate(c.id, digest, publicValues, proof)
      ).to.be.revertedWithCustomError(sp1, "MockInvalidProof");
      expect(await verifier.tryVerifyAggregate(c.id, digest, publicValues, proof)).to.equal(false);
      await expect(
        verifier.recordAggregate(c.id, digest, publicValues, proof)
      ).to.be.revertedWithCustomError(sp1, "MockInvalidProof");
      expect(await verifier.isAttested(c.id, digest)).to.equal(false);
    });

    it("rejects a proof for the WRONG message digest", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      const signedDigest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:the-committee-signed-this"));
      const claimedDigest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:but-caller-claims-this"));
      // The proof binds signedDigest and is even registered valid in the gateway...
      const { publicValues, proof } = await makeProof(c, { digest: signedDigest, validCount: 3 });

      // ...but verifying it against a different claimed digest is rejected BEFORE the gateway call.
      await expect(
        verifier.verifyAggregate(c.id, claimedDigest, publicValues, proof)
      ).to.be.revertedWithCustomError(verifier, "MessageDigestMismatch");
      expect(await verifier.tryVerifyAggregate(c.id, claimedDigest, publicValues, proof)).to.equal(false);
    });

    it("rejects a proof whose committee root does NOT match the registered committee", async function () {
      const c = await registerCommittee({ label: "real", threshold: 3, size: 5 });
      const digest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:x"));
      // Public values carry a DIFFERENT root than the registered committee, and are registered valid.
      const forgedRoot = committeeRoot("attacker-key-set", 5);
      const publicValues = encodePublicValues({
        root: forgedRoot,
        threshold: c.threshold,
        size: c.size,
        scheme: c.scheme,
        digest,
        validCount: 3,
      });
      const proof = "0xdeadbeef";
      await sp1.setValid(proofMarker(VKEY, publicValues, proof), true);

      await expect(
        verifier.verifyAggregate(c.id, digest, publicValues, proof)
      ).to.be.revertedWithCustomError(verifier, "CommitteeRootMismatch");
      expect(await verifier.tryVerifyAggregate(c.id, digest, publicValues, proof)).to.equal(false);
    });

    it("rejects when the proven valid-signature count is below threshold", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      const digest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:subquorum"));
      // Only 2 valid signatures proven, threshold is 3. Even a gateway-valid proof must be rejected.
      const { publicValues, proof } = await makeProof(c, { digest, validCount: 2 });

      await expect(
        verifier.verifyAggregate(c.id, digest, publicValues, proof)
      ).to.be.revertedWithCustomError(verifier, "BelowThreshold");
      expect(await verifier.tryVerifyAggregate(c.id, digest, publicValues, proof)).to.equal(false);
    });

    it("rejects on threshold / size / scheme mismatch and malformed public values", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5, scheme: SCHEME_MLDSA65 });
      const digest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:mismatch"));

      const wrongT = encodePublicValues({ root: c.root, threshold: 2, size: 5, scheme: SCHEME_MLDSA65, digest, validCount: 5 });
      await sp1.setValid(proofMarker(VKEY, wrongT, "0x01"), true);
      await expect(verifier.verifyAggregate(c.id, digest, wrongT, "0x01")).to.be.revertedWithCustomError(verifier, "ThresholdMismatch");

      const wrongN = encodePublicValues({ root: c.root, threshold: 3, size: 9, scheme: SCHEME_MLDSA65, digest, validCount: 3 });
      await sp1.setValid(proofMarker(VKEY, wrongN, "0x02"), true);
      await expect(verifier.verifyAggregate(c.id, digest, wrongN, "0x02")).to.be.revertedWithCustomError(verifier, "SizeMismatch");

      const wrongScheme = encodePublicValues({ root: c.root, threshold: 3, size: 5, scheme: 3, digest, validCount: 3 });
      await sp1.setValid(proofMarker(VKEY, wrongScheme, "0x03"), true);
      await expect(verifier.verifyAggregate(c.id, digest, wrongScheme, "0x03")).to.be.revertedWithCustomError(verifier, "SchemeMismatch");

      // Malformed (not 192 bytes) public values: strict reverts, non-reverting returns false.
      const malformed = "0x1234";
      await expect(verifier.verifyAggregate(c.id, digest, malformed, "0x04")).to.be.revertedWithCustomError(verifier, "BadPublicValues");
      expect(await verifier.tryVerifyAggregate(c.id, digest, malformed, "0x04")).to.equal(false);
    });

    it("rejects an unknown committee", async function () {
      const digest = ethers.keccak256(ethers.toUtf8Bytes("x"));
      const publicValues = encodePublicValues({
        root: committeeRoot("ghost", 5), threshold: 3, size: 5, scheme: SCHEME_MLDSA65, digest, validCount: 3,
      });
      await expect(
        verifier.verifyAggregate(999, digest, publicValues, "0x05")
      ).to.be.revertedWithCustomError(verifier, "UnknownCommittee");
      expect(await verifier.tryVerifyAggregate(999, digest, publicValues, "0x05")).to.equal(false);
    });
  });

  // =========================================================================
  // 3. Gas is INDEPENDENT of the committee size n (the whole point)
  // =========================================================================
  describe("constant on-chain cost regardless of committee size n", function () {
    it("recordAggregate costs the same for a tiny and a huge committee", async function () {
      const small = await registerCommittee({ label: "small", threshold: 3, size: 5 });
      const huge = await registerCommittee({ label: "huge", threshold: 3, size: 1_000_000 });

      const digest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:gas-parity"));
      const pSmall = await makeProof(small, { digest, validCount: 3, proof: "0xaa" });
      const pHuge = await makeProof(huge, { digest, validCount: 3, proof: "0xbb" });

      const rcSmall = await (await verifier.recordAggregate(small.id, digest, pSmall.publicValues, pSmall.proof)).wait();
      const rcHuge = await (await verifier.recordAggregate(huge.id, digest, pHuge.publicValues, pHuge.proof)).wait();

      // The only residual difference is the calldata encoding of the integer n itself (a few bytes),
      // which is O(log n), NOT the O(n) tens-of-thousands-of-gas-per-signature a linear verifier costs.
      const diff = rcSmall.gasUsed > rcHuge.gasUsed ? rcSmall.gasUsed - rcHuge.gasUsed : rcHuge.gasUsed - rcSmall.gasUsed;
      expect(diff).to.be.lessThan(500n);
      // Sanity: n grew 200,000x yet gas barely moved and stayed well under any per-member budget.
      expect(rcHuge.gasUsed).to.be.lessThan(200_000n);
      console.log(`        gas n=5: ${rcSmall.gasUsed}  gas n=1,000,000: ${rcHuge.gasUsed}  diff: ${diff}`);
    });
  });

  // =========================================================================
  // 4. ERC-7579 validator module
  // =========================================================================
  describe("AerePQAggregateModule (ERC-7579 validator, type 1)", function () {
    function makeUserOp(sender, signature) {
      return {
        sender,
        nonce: 0,
        initCode: "0x",
        callData: "0x",
        accountGasLimits: ethers.ZeroHash,
        preVerificationGas: 0,
        gasFees: ethers.ZeroHash,
        paymasterAndData: "0x",
        signature,
      };
    }

    // module signature = abi.encode(bytes publicValues, bytes proofBytes)
    function moduleSig(publicValues, proof) {
      return abi().encode(["bytes", "bytes"], [publicValues, proof]);
    }

    it("declares module type 1 (validator)", async function () {
      expect(await module.isModuleType(1)).to.equal(true);
      expect(await module.isModuleType(2)).to.equal(false);
      expect(await module.name()).to.equal("aere.pq-aggregate-validator.1.0.0");
    });

    it("install binds the account to a registered committee; unknown committee reverts", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      await expect(
        module.connect(account).onInstall(abi().encode(["uint256"], [999]))
      ).to.be.revertedWithCustomError(module, "UnknownCommittee");

      await module.connect(account).onInstall(abi().encode(["uint256"], [c.id]));
      expect(await module.committeeOf(account.address)).to.equal(c.id);
    });

    it("validateUserOp returns SUCCESS on a valid aggregate proof over the userOpHash", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      await module.connect(account).onInstall(abi().encode(["uint256"], [c.id]));

      const userOpHash = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:committee-approved"));
      const { publicValues, proof } = await makeProof(c, { digest: userOpHash, validCount: 4 });
      const userOp = makeUserOp(account.address, moduleSig(publicValues, proof));

      const res = await module.connect(account).validateUserOp.staticCall(userOp, userOpHash);
      expect(res).to.equal(VALIDATION_SUCCESS);
    });

    it("validateUserOp returns FAILED (no revert) on an invalid proof", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      await module.connect(account).onInstall(abi().encode(["uint256"], [c.id]));

      const userOpHash = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:forged"));
      // Not registered in the gateway => the proof is invalid.
      const { publicValues, proof } = await makeProof(c, { digest: userOpHash, validCount: 4, register: false });
      const userOp = makeUserOp(account.address, moduleSig(publicValues, proof));

      const res = await module.connect(account).validateUserOp.staticCall(userOp, userOpHash);
      expect(res).to.equal(SIG_VALIDATION_FAILED);
    });

    it("validateUserOp returns FAILED on a wrong-digest proof and on a malformed signature", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      await module.connect(account).onInstall(abi().encode(["uint256"], [c.id]));

      // Valid proof, but bound to a DIFFERENT digest than the userOpHash being validated.
      const otherDigest = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:other-op"));
      const { publicValues, proof } = await makeProof(c, { digest: otherDigest, validCount: 4 });
      const userOpHash = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:this-op"));
      const userOp = makeUserOp(account.address, moduleSig(publicValues, proof));
      expect(await module.connect(account).validateUserOp.staticCall(userOp, userOpHash)).to.equal(SIG_VALIDATION_FAILED);

      // Malformed signature blob must NOT revert; it returns FAILED.
      const junkOp = makeUserOp(account.address, "0x1234");
      expect(await module.connect(account).validateUserOp.staticCall(junkOp, userOpHash)).to.equal(SIG_VALIDATION_FAILED);
    });

    it("validateUserOp returns FAILED for an account with no committee configured", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      const userOpHash = ethers.keccak256(ethers.toUtf8Bytes("userOpHash:unconfigured"));
      const { publicValues, proof } = await makeProof(c, { digest: userOpHash, validCount: 3 });
      // `other` never installed the module.
      const userOp = makeUserOp(other.address, moduleSig(publicValues, proof));
      expect(await module.connect(other).validateUserOp.staticCall(userOp, userOpHash)).to.equal(SIG_VALIDATION_FAILED);
    });

    it("isValidSignatureWithSender returns the ERC-1271 magic on valid, fail on invalid", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      await module.connect(account).onInstall(abi().encode(["uint256"], [c.id]));

      const hash = ethers.keccak256(ethers.toUtf8Bytes("erc1271:committee-approved-message"));
      const good = await makeProof(c, { digest: hash, validCount: 3, proof: "0xc0ffee" });
      const okMagic = await module
        .connect(account)
        .isValidSignatureWithSender(other.address, hash, moduleSig(good.publicValues, good.proof));
      expect(okMagic).to.equal(EIP1271_MAGIC);

      // Invalid proof => fail magic, no revert.
      const bad = await makeProof(c, { digest: hash, validCount: 3, proof: "0xbadbad", register: false });
      const failMagic = await module
        .connect(account)
        .isValidSignatureWithSender(other.address, hash, moduleSig(bad.publicValues, bad.proof));
      expect(failMagic).to.equal(EIP1271_FAIL);

      // Malformed signature => fail magic, no revert.
      const junkMagic = await module.connect(account).isValidSignatureWithSender(other.address, hash, "0xabcd");
      expect(junkMagic).to.equal(EIP1271_FAIL);
    });

    it("uninstall clears the binding; validation then fails closed", async function () {
      const c = await registerCommittee({ threshold: 3, size: 5 });
      await module.connect(account).onInstall(abi().encode(["uint256"], [c.id]));
      await module.connect(account).onUninstall("0x");
      expect(await module.committeeOf(account.address)).to.equal(0n);

      const hash = ethers.keccak256(ethers.toUtf8Bytes("after-uninstall"));
      const p = await makeProof(c, { digest: hash, validCount: 3 });
      expect(
        await module.connect(account).isValidSignatureWithSender(other.address, hash, moduleSig(p.publicValues, p.proof))
      ).to.equal(EIP1271_FAIL);
    });
  });
});

// Matches any uint (for the block.number arg in the AggregateVerified event).
function anyUint() {
  const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
  return anyValue;
}
