// Property-based fuzz of AereThresholdAccount.validateUserOp: on top of the halmos proofs and the
// unit tests, hammer the counting logic with hundreds of random leg combinations (genuine / forged
// / junk / duplicate / out-of-range) over fresh challenges, and assert the account authorizes iff
// the number of DISTINCT in-range members with a genuine signature is >= threshold. Deterministic
// (seeded) so any failure reproduces.
const { expect } = require("chai");
const { ethers, network } = require("hardhat");

const PRECOMPILE = {
  1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"),
  2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"),
  3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"),
  4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"),
};
const M = { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29 };

function makeRng(seedText) {
  let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
  return {
    next() { seed = ethers.keccak256(seed); return BigInt(seed); },
    int(n) { return Number(this.next() % BigInt(n)); },
    bytes(n) {
      const out = new Uint8Array(n); let i = 0;
      while (i < n) { seed = ethers.keccak256(seed); const c = ethers.getBytes(seed); for (let j = 0; j < c.length && i < n; j++, i++) out[i] = c[j]; }
      return out;
    },
  };
}
function makePubKey(rng) { const raw = rng.bytes(M.pkLen); raw[0] = M.pkHeader; return ethers.hexlify(raw); }
function genuine(pubKey, message, rng) {
  const commitment = ethers.keccak256(ethers.concat([pubKey, message]));
  return ethers.hexlify(ethers.concat([rng.bytes(40), new Uint8Array([M.esigHeader]), ethers.getBytes(commitment)]));
}
function forged(pubKey, message, rng) { const b = ethers.getBytes(genuine(pubKey, message, rng)); b[41] ^= 0x01; return ethers.hexlify(b); }
function junk(rng) { return ethers.hexlify(rng.bytes(41 + rng.int(60))); }
function encodeLegs(legs) {
  return ethers.AbiCoder.defaultAbiCoder().encode(
    ["tuple(uint8 memberIndex, bytes signature)[]"], [legs.map((l) => [l.idx, l.sig])]
  );
}

const SCHEME = 1, N = 6, T = 3, ITERS = 300;

describe("AereThresholdAccount — property-based fuzz (300 random leg combos)", function () {
  this.timeout(120000);
  let mockCode, account, entryPoint, echo, pubKeys, rng;

  before(async function () {
    mockCode = {};
    const F = await ethers.getContractFactory("MockPQCPrecompile");
    for (const s of [1, 2, 3, 4]) { const m = await F.deploy(s); await m.waitForDeployment(); mockCode[s] = await ethers.provider.getCode(await m.getAddress()); }
    for (const s of [1, 2, 3, 4]) await network.provider.send("hardhat_setCode", [PRECOMPILE[s], mockCode[s]]);
    [entryPoint] = await ethers.getSigners();
    rng = makeRng("fuzz-seed-v1");
    pubKeys = []; for (let i = 0; i < N; i++) pubKeys.push(makePubKey(rng));
    const Fac = await ethers.getContractFactory("AereThresholdAccountFactory");
    const factory = await Fac.deploy(); await factory.waitForDeployment();
    const salt = ethers.id("fuzz");
    const addr = await factory.computeAddress(entryPoint.address, SCHEME, T, pubKeys, salt);
    await (await factory.createAccount(entryPoint.address, SCHEME, T, pubKeys, salt)).wait();
    account = await ethers.getContractAt("AereThresholdAccount", addr);
    echo = await (await ethers.getContractFactory("EchoTarget")).deploy(); await echo.waitForDeployment();
  });

  it(`authorizes iff distinct in-range genuine signers >= threshold, across ${ITERS} random inputs`, async function () {
    const acctAddr = await account.getAddress();
    let checked = 0;
    for (let iter = 0; iter < ITERS; iter++) {
      const userOpHash = ethers.id("fuzz-op-" + iter);
      const challenge = await account.userOpChallenge(userOpHash);
      const numLegs = 1 + rng.int(8);
      const legs = [];
      const distinctValid = new Set();
      for (let k = 0; k < numLegs; k++) {
        const idx = rng.int(N + 2); // 0..N+1 → indices N,N+1 are out-of-range
        const kind = rng.int(3); // 0 genuine, 1 forged, 2 junk
        let sig;
        if (kind === 0) { sig = genuine(pubKeys[idx % N], challenge, rng); if (idx < N) distinctValid.add(idx); }
        else if (kind === 1) sig = forged(pubKeys[idx % N], challenge, rng);
        else sig = junk(rng);
        legs.push({ idx, sig });
      }
      const expected = distinctValid.size >= T ? 0n : 1n;
      const op = {
        sender: acctAddr, nonce: 0n, initCode: "0x", callData: "0x",
        accountGasLimits: ethers.ZeroHash, preVerificationGas: 0n, gasFees: ethers.ZeroHash,
        paymasterAndData: "0x", signature: encodeLegs(legs),
      };
      const got = await account.connect(entryPoint).validateUserOp.staticCall(op, userOpHash, 0);
      if (got !== expected) {
        throw new Error(`FUZZ MISMATCH iter ${iter}: distinct-valid=${distinctValid.size} t=${T} expected=${expected} got=${got} legs=${JSON.stringify(legs.map((l) => l.idx))}`);
      }
      checked++;
    }
    expect(checked).to.equal(ITERS);
    console.log(`\n      fuzz: ${ITERS}/${ITERS} random leg combinations matched the threshold invariant exactly.`);
  });
});
