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

// ---------------------------------------------------------------------------
// AereThresholdPQCRegistry -- on-chain t-of-n POST-QUANTUM threshold authorization.
//
// A committee of n PQC signers each hold their own NIST PQC key. A valid authorization
// requires >= t DISTINCT members to each PQC-sign the SAME domain-separated challenge,
// with every leg verified on-chain by the scheme's live precompile (0x0AE1..0x0AE4).
//
// HONEST label: this is a threshold MULTISIG of independent PQC signatures, NOT a single
// aggregate threshold-PQC signature (that is open research; see docs/THRESHOLD-PQC-*.md).
//
// Precompile modelling: Hardhat has no PQC precompile, so we install the repo's faithful
// MockPQCPrecompile at 0x0AE1..0x0AE4 (hardhat_setCode). It parses the input at the
// precompiles' SPEC offsets and accepts iff commitment == keccak256(pk || message) -- the
// same model the shipped AerePQCAttestation adversarial suite uses. Because the mock parses
// at the spec offsets (not offsets copied from the contract), a passing authorize() proves
// BOTH the threshold-counting logic AND that the contract built the precompile envelope at
// the correct offsets. The REAL envelope is separately proven bit-for-bit against the LIVE
// mainnet precompile via eth_call (scripts/pqc/verify-live-precompile.js).
// ---------------------------------------------------------------------------

const PRECOMPILE = {
  1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), // Falcon-512
  2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), // Falcon-1024
  3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), // ML-DSA-44
  4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), // SLH-DSA-128s
};

const META = {
  1: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true },
  2: { pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, falcon: true },
  3: { pkLen: 1312, sigLen: 2420, falcon: false },
  4: { pkLen: 32, sigLen: 7856, falcon: false },
};

// Deterministic seeded RNG (keccak hash-chain) so the whole suite is reproducible.
function makeRng(seedText) {
  let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
  function bytes(n) {
    const out = new Uint8Array(n);
    let i = 0;
    while (i < n) {
      seed = ethers.keccak256(seed);
      const chunk = ethers.getBytes(seed);
      for (let j = 0; j < chunk.length && i < n; j++, i++) out[i] = chunk[j];
    }
    return out;
  }
  return { bytes };
}

function makePubKey(scheme, rng) {
  const m = META[scheme];
  const raw = rng.bytes(m.pkLen);
  if (m.falcon) raw[0] = m.pkHeader; // Falcon requires the header byte
  return ethers.hexlify(raw);
}

// commitment the mock expects for a genuine signature over `message` by `pubKey`.
function commitmentFor(pubKey, message) {
  return ethers.keccak256(ethers.concat([pubKey, message]));
}

// Build a signature envelope carrying a chosen commitment (genuine or forged).
function envelope(scheme, commitment, rng) {
  const m = META[scheme];
  if (m.falcon) {
    const nonce = rng.bytes(40);
    const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]); // 33 bytes
    return ethers.hexlify(ethers.concat([nonce, esig]));
  } else {
    const sig = new Uint8Array(m.sigLen);
    sig.set(ethers.getBytes(commitment), 0);
    return ethers.hexlify(sig);
  }
}

// A genuine signature by `pubKey` over `message`.
function genuine(scheme, pubKey, message, rng) {
  return envelope(scheme, commitmentFor(pubKey, message), rng);
}

// Flip one byte inside the commitment region -> a genuine forgery the precompile rejects.
function tamperCommitment(scheme, sigHex) {
  const b = ethers.getBytes(sigHex);
  const idx = META[scheme].falcon ? 41 : 0;
  b[idx] ^= 0x01;
  return ethers.hexlify(b);
}

describe("AereThresholdPQCRegistry (post-quantum threshold multisig)", function () {
  let reg, mockCode;

  before(async function () {
    // Deploy one mock per scheme; capture its runtime bytecode (immutable scheme baked in).
    mockCode = {};
    const F = await ethers.getContractFactory("MockPQCPrecompile");
    for (const scheme of [1, 2, 3, 4]) {
      const mock = await F.deploy(scheme);
      await mock.waitForDeployment();
      mockCode[scheme] = await ethers.provider.getCode(await mock.getAddress());
    }
  });

  async function installMocks() {
    for (const scheme of [1, 2, 3, 4]) {
      await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]);
    }
  }

  beforeEach(async function () {
    await installMocks();
    const R = await ethers.getContractFactory("AereThresholdPQCRegistry");
    reg = await R.deploy();
    await reg.waitForDeployment();
  });

  // Register a fresh n-member committee of `scheme`, threshold t. Returns the committee id,
  // its member public keys, and a shared rng.
  async function registerCommittee(scheme, n, t, seed) {
    const rng = makeRng(seed || `c-${scheme}-${n}-${t}`);
    const pubKeys = [];
    for (let i = 0; i < n; i++) pubKeys.push(makePubKey(scheme, rng));
    const tx = await reg.registerCommittee(scheme, t, pubKeys);
    const rc = await tx.wait();
    const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "CommitteeRegistered");
    return { id: Number(log.args.committeeId), pubKeys, rng, scheme, n, t };
  }

  // Build the `sigs` array of {memberIndex, signature} for the given member indices, each a
  // genuine PQC signature over the committee's current authorization challenge.
  async function legsFor(c, messageHash, memberIndices) {
    const nonce = await reg.nonceOf(c.id);
    const challenge = await reg.authChallenge(c.id, nonce, messageHash);
    return memberIndices.map((idx) => ({
      memberIndex: idx,
      signature: genuine(c.scheme, c.pubKeys[idx], challenge, c.rng),
    }));
  }

  // -----------------------------------------------------------------------
  // Registration validation
  // -----------------------------------------------------------------------
  describe("registerCommittee", function () {
    it("registers a committee for every scheme and stores scheme/t/n + members commitment", async function () {
      for (const scheme of [1, 2, 3, 4]) {
        const c = await registerCommittee(scheme, 3, 2, `reg-${scheme}`);
        const got = await reg.getCommittee(c.id);
        expect(got.exists).to.equal(true);
        expect(Number(got.scheme)).to.equal(scheme);
        expect(Number(got.threshold)).to.equal(2);
        expect(Number(got.size)).to.equal(3);
        expect(Number(got.authNonce)).to.equal(0);
        // members commitment matches abi.encode(pubKeys)
        const expectHash = ethers.keccak256(
          ethers.AbiCoder.defaultAbiCoder().encode(["bytes[]"], [c.pubKeys]),
        );
        expect(got.membersHash).to.equal(expectHash);
        // stored keys are retrievable
        expect(await reg.getMemberPubKey(c.id, 0)).to.equal(c.pubKeys[0]);
        expect(await reg.precompileFor(scheme)).to.equal(PRECOMPILE[scheme]);
      }
    });

    it("rejects bad committee params (t=0, t>n, empty, n>255)", async function () {
      const rng = makeRng("bad");
      const three = [0, 1, 2].map(() => makePubKey(1, rng));
      await expect(reg.registerCommittee(1, 0, three)).to.be.revertedWithCustomError(reg, "BadCommitteeParams");
      await expect(reg.registerCommittee(1, 4, three)).to.be.revertedWithCustomError(reg, "BadCommitteeParams");
      await expect(reg.registerCommittee(1, 1, [])).to.be.revertedWithCustomError(reg, "BadCommitteeParams");
    });

    it("rejects an unknown scheme and malformed member keys", async function () {
      const rng = makeRng("mal");
      const goodF = makePubKey(1, rng);
      // unknown scheme
      await expect(reg.registerCommittee(5, 1, [goodF])).to.be.revertedWithCustomError(reg, "InvalidScheme");
      // Falcon-512 wrong header
      const badHeader = ethers.getBytes(makePubKey(1, rng));
      badHeader[0] = 0x0a;
      await expect(reg.registerCommittee(1, 1, [ethers.hexlify(badHeader)])).to.be.revertedWithCustomError(
        reg,
        "InvalidPubKey",
      );
      // ML-DSA-44 wrong length
      await expect(reg.registerCommittee(3, 1, ["0x" + "00".repeat(1311)])).to.be.revertedWithCustomError(
        reg,
        "InvalidPubKey",
      );
      // the offending member index is reported
      await expect(reg.registerCommittee(1, 2, [goodF, "0x" + "09" + "00".repeat(10)]))
        .to.be.revertedWithCustomError(reg, "InvalidPubKey")
        .withArgs(1);
    });

    it("rejects a committee with duplicate keys (distinctness is by KEY, not index)", async function () {
      // Authorization counts distinct signers by member index, so a repeated key would let one
      // keyholder fill multiple slots and reach threshold alone. registerCommittee must reject it.
      const rng = makeRng("dup-key-registry");
      const keys = [];
      for (let i = 0; i < 4; i++) keys.push(makePubKey(1, rng));
      keys[2] = keys[0]; // member 2 reuses member 0's key
      await expect(reg.registerCommittee(1, 2, keys))
        .to.be.revertedWithCustomError(reg, "DuplicatePubKey")
        .withArgs(2);
    });
  });

  // -----------------------------------------------------------------------
  // t-of-n happy path + threshold gate
  // -----------------------------------------------------------------------
  describe("authorize: t-of-n happy path and threshold gate", function () {
    it("authorizes with EXACTLY t distinct valid PQC legs (2-of-3, Falcon-512)", async function () {
      const c = await registerCommittee(1, 3, 2, "happy-f512");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("release vault payment #1"));
      const sigs = await legsFor(c, messageHash, [0, 2]); // any 2 distinct members

      const before = await reg.authorizationCount();
      await expect(reg.authorize(c.id, messageHash, sigs))
        .to.emit(reg, "Authorized")
        .withArgs(c.id, 0, messageHash, await reg.authChallenge(c.id, 0, messageHash), 2, anyBlock());
      expect(await reg.authorizationCount()).to.equal(before + 1n);
      expect(Number(await reg.nonceOf(c.id))).to.equal(1);

      const a = await reg.getAuthorization(c.id, 0);
      expect(a.exists).to.equal(true);
      expect(a.messageHash).to.equal(messageHash);
      expect(Number(a.quorum)).to.equal(2);
      expect(await reg.isAuthorized(c.id, 0, messageHash)).to.equal(true);
      expect(await reg.isAuthorized(c.id, 0, ethers.ZeroHash)).to.equal(false);
    });

    it("authorizes for EVERY scheme with a t-of-n quorum", async function () {
      for (const scheme of [1, 2, 3, 4]) {
        const c = await registerCommittee(scheme, 4, 3, `all-${scheme}`);
        const messageHash = ethers.keccak256(ethers.toUtf8Bytes(`msg-${scheme}`));
        const sigs = await legsFor(c, messageHash, [0, 1, 3]);
        await expect(reg.authorize(c.id, messageHash, sigs)).to.emit(reg, "Authorized");
        expect(Number(await reg.nonceOf(c.id))).to.equal(1);
      }
    });

    it("REJECTS a below-threshold quorum (t-1 valid legs)", async function () {
      const c = await registerCommittee(1, 3, 2, "below");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("only one signer"));
      const sigs = await legsFor(c, messageHash, [1]); // 1 < t=2
      await expect(reg.authorize(c.id, messageHash, sigs))
        .to.be.revertedWithCustomError(reg, "BelowThreshold")
        .withArgs(1, 2);
      // nothing recorded, nonce frozen
      expect(await reg.authorizationCount()).to.equal(0n);
      expect(Number(await reg.nonceOf(c.id))).to.equal(0);
      expect((await reg.getAuthorization(c.id, 0)).exists).to.equal(false);
    });

    it("previewAuthorize reports quorum without recording (lenient view)", async function () {
      const c = await registerCommittee(2, 5, 3, "preview");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("preview-msg"));
      const nonce = await reg.nonceOf(c.id);
      const three = await legsFor(c, messageHash, [0, 2, 4]);
      const two = three.slice(0, 2);
      expect((await reg.previewAuthorize(c.id, nonce, messageHash, three)).would).to.equal(true);
      expect((await reg.previewAuthorize(c.id, nonce, messageHash, two)).would).to.equal(false);
      // view did not mutate state
      expect(Number(await reg.nonceOf(c.id))).to.equal(0);
    });
  });

  // -----------------------------------------------------------------------
  // Distinctness, membership, message binding, replay
  // -----------------------------------------------------------------------
  describe("authorize: distinctness / membership / binding / replay", function () {
    it("does NOT double-count a duplicate member (reverts DuplicateMember)", async function () {
      const c = await registerCommittee(1, 3, 2, "dup");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("dup-msg"));
      const nonce = await reg.nonceOf(c.id);
      const challenge = await reg.authChallenge(c.id, nonce, messageHash);
      // member 0 signs twice: two legs, both valid, but same index -> not a 2-of-3 quorum
      const m0a = { memberIndex: 0, signature: genuine(c.scheme, c.pubKeys[0], challenge, c.rng) };
      const m0b = { memberIndex: 0, signature: genuine(c.scheme, c.pubKeys[0], challenge, c.rng) };
      await expect(reg.authorize(c.id, messageHash, [m0a, m0b]))
        .to.be.revertedWithCustomError(reg, "DuplicateMember")
        .withArgs(0);
      expect(await reg.authorizationCount()).to.equal(0n);
    });

    it("rejects an out-of-range member index (non-member)", async function () {
      const c = await registerCommittee(1, 3, 2, "oor");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("oor-msg"));
      const good = await legsFor(c, messageHash, [0]);
      const bogus = { memberIndex: 9, signature: good[0].signature };
      await expect(reg.authorize(c.id, messageHash, [good[0], bogus]))
        .to.be.revertedWithCustomError(reg, "MemberOutOfRange")
        .withArgs(9);
    });

    it("rejects a leg signed by a NON-member key at a valid index", async function () {
      const c = await registerCommittee(1, 3, 2, "foreign");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("foreign-msg"));
      const nonce = await reg.nonceOf(c.id);
      const challenge = await reg.authChallenge(c.id, nonce, messageHash);
      const outsiderKey = makePubKey(1, c.rng); // never registered
      const legGood = { memberIndex: 0, signature: genuine(c.scheme, c.pubKeys[0], challenge, c.rng) };
      const legForeign = { memberIndex: 1, signature: genuine(c.scheme, outsiderKey, challenge, c.rng) };
      await expect(reg.authorize(c.id, messageHash, [legGood, legForeign]))
        .to.be.revertedWithCustomError(reg, "LegVerificationFailed")
        .withArgs(1);
    });

    it("rejects a leg that is a tampered / invalid PQC signature", async function () {
      const c = await registerCommittee(2, 3, 2, "tamper");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("tamper-msg"));
      const sigs = await legsFor(c, messageHash, [0, 1]);
      sigs[1].signature = tamperCommitment(c.scheme, sigs[1].signature);
      await expect(reg.authorize(c.id, messageHash, sigs))
        .to.be.revertedWithCustomError(reg, "LegVerificationFailed")
        .withArgs(1);
    });

    it("rejects legs that are genuine over a DIFFERENT message", async function () {
      const c = await registerCommittee(1, 3, 2, "wrongmsg");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("intended"));
      const otherHash = ethers.keccak256(ethers.toUtf8Bytes("attacker-substituted"));
      // sign the challenge for `otherHash`, then submit against `messageHash`
      const nonce = await reg.nonceOf(c.id);
      const otherChallenge = await reg.authChallenge(c.id, nonce, otherHash);
      const sigs = [0, 1].map((idx) => ({
        memberIndex: idx,
        signature: genuine(c.scheme, c.pubKeys[idx], otherChallenge, c.rng),
      }));
      await expect(reg.authorize(c.id, messageHash, sigs)).to.be.revertedWithCustomError(reg, "LegVerificationFailed");
      expect(await reg.authorizationCount()).to.equal(0n);
    });

    it("prevents REPLAY of a used authorization (nonce advances, same sigs no longer verify)", async function () {
      const c = await registerCommittee(1, 3, 2, "replay");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("pay once"));
      const sigs = await legsFor(c, messageHash, [0, 1]); // legs bound to nonce 0
      await reg.authorize(c.id, messageHash, sigs); // consumes nonce 0
      expect(Number(await reg.nonceOf(c.id))).to.equal(1);
      // exact replay: challenge now uses nonce 1, so the nonce-0 legs fail verification
      await expect(reg.authorize(c.id, messageHash, sigs)).to.be.revertedWithCustomError(reg, "LegVerificationFailed");
      expect(await reg.authorizationCount()).to.equal(1n);
      expect(Number(await reg.nonceOf(c.id))).to.equal(1);
    });

    it("allows a fresh authorization at the new nonce after a successful one", async function () {
      const c = await registerCommittee(3, 4, 3, "second");
      const msg1 = ethers.keccak256(ethers.toUtf8Bytes("first"));
      await reg.authorize(c.id, msg1, await legsFor(c, msg1, [0, 1, 2]));
      expect(Number(await reg.nonceOf(c.id))).to.equal(1);
      const msg2 = ethers.keccak256(ethers.toUtf8Bytes("second"));
      await reg.authorize(c.id, msg2, await legsFor(c, msg2, [1, 2, 3]));
      expect(Number(await reg.nonceOf(c.id))).to.equal(2);
      expect(await reg.isAuthorized(c.id, 0, msg1)).to.equal(true);
      expect(await reg.isAuthorized(c.id, 1, msg2)).to.equal(true);
    });

    it("reverts on an unknown committee", async function () {
      await expect(reg.authorize(999, ethers.ZeroHash, [])).to.be.revertedWithCustomError(reg, "UnknownCommittee");
    });

    it("treats an empty (pre-fork / wiped) precompile as an invalid leg, not an authorization", async function () {
      const c = await registerCommittee(2, 3, 2, "wiped");
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("wiped-msg"));
      const sigs = await legsFor(c, messageHash, [0, 1]);
      await network.provider.send("hardhat_setCode", [PRECOMPILE[2], "0x"]); // wipe Falcon-1024 precompile
      await expect(reg.authorize(c.id, messageHash, sigs)).to.be.revertedWithCustomError(reg, "LegVerificationFailed");
      expect(await reg.authorizationCount()).to.equal(0n);
    });
  });

  // -----------------------------------------------------------------------
  // Gas ceiling under EIP-7825: measure the marginal cost per additional leg.
  // NB: the mock verify is cheaper than the live precompile's FIXED verify gas
  // (Falcon-512 40k, Falcon-1024 75k, ML-DSA-44 55k, SLH-DSA-128s 350k). So the
  // REAL per-leg cost = (measured marginal here, which is calldata + envelope build +
  // distinct-member bookkeeping + cheap mock verify) + the precompile's fixed verify gas.
  // This test reports the Solidity/calldata overhead per leg so the ceiling in
  // docs/THRESHOLD-PQC-2026-07-12.md is grounded in a measurement, not a guess.
  // -----------------------------------------------------------------------
  describe("gas: per-leg marginal cost (informs the EIP-7825 (scheme, t) ceiling)", function () {
    const CAP = 16_777_216n;
    const PRECOMPILE_VERIFY_GAS = { 1: 40000n, 2: 75000n, 3: 55000n, 4: 350000n };

    async function authGas(scheme, t, seed) {
      const n = t; // exactly-t quorum
      const c = await registerCommittee(scheme, n, t, seed);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes(seed));
      const sigs = await legsFor(
        c,
        messageHash,
        Array.from({ length: t }, (_, i) => i),
      );
      const tx = await reg.authorize(c.id, messageHash, sigs);
      const rc = await tx.wait();
      return rc.gasUsed;
    }

    it("reports marginal Solidity+calldata overhead per leg and the projected real ceiling", async function () {
      for (const scheme of [1, 2, 3, 4]) {
        const g2 = await authGas(scheme, 2, `gas-${scheme}-2`);
        const g6 = await authGas(scheme, 6, `gas-${scheme}-6`);
        const overheadPerLeg = (g6 - g2) / 4n; // 4 extra legs between t=2 and t=6

        // Real per-leg = measured Solidity/calldata overhead (which already includes the
        // cheap mock verify) + the precompile's fixed verify gas. Slightly conservative
        // (double-counts the tiny mock verify, so the ceiling below is an under-estimate).
        const realPerLeg = overheadPerLeg + PRECOMPILE_VERIFY_GAS[scheme];
        const fixedOverhead = g2 - overheadPerLeg * 2n; // tx base + committee load + record
        const tMax = (CAP - fixedOverhead) / realPerLeg;

        console.log(
          `      scheme ${scheme}: mock authorize t=2 ${g2} gas, t=6 ${g6} gas; ` +
            `Solidity/calldata overhead/leg ~${overheadPerLeg}; ` +
            `+precompile ${PRECOMPILE_VERIFY_GAS[scheme]} => real/leg ~${realPerLeg}; ` +
            `projected t_max under 2^24 ~= ${tMax}`,
        );

        // Sanity: the per-leg cost is positive and the projected ceiling is a real bound.
        expect(overheadPerLeg > 0n).to.equal(true);
        expect(tMax > 0n).to.equal(true);
        // Falcon-512 (cheapest verify) must admit a larger quorum than SLH-DSA-128s.
        if (scheme === 1) expect(tMax > 20n).to.equal(true);
      }
    });
  });
});

// chai matcher helper: accept any block number in withArgs.
function anyBlock() {
  return (x) => typeof x === "bigint" || typeof x === "number";
}
