// AerePQCSocialRecoveryModule — quantum-durable M-of-N recovery for AereModularAccount.
//
// Guardians are NIST PQC public keys registered (with on-chain proof-of-possession) in
// AerePQCKeyRegistry; a recovery is authorized by >= threshold DISTINCT guardian PQC signatures
// over a domain-separated challenge (account + newOwner + round + chainid), verified on-chain by
// registry.verifyWithKey, behind the same 48h timelock as the ECDSA social-recovery module.
//
// Two verification backends are exercised at 0x0AE1..0x0AE4:
//   1. MockPQCPrecompile  — the repo's faithful mock (accept iff commitment==keccak256(pk||msg)),
//      used for the threshold/timelock/replay/config LOGIC (accept + reject branches). This is the
//      same stand-in AereThresholdAccount and AerePQCKeyRegistry are tested against.
//   2. RealFalcon512PrecompileAdapter — routes the precompile staticcall into the NIST-KAT-proven
//      AereFalcon512Verifier, so a subset of tests drive GENUINE Falcon-512 signatures (produced by
//      @noble/post-quantum, byte-compatible with AERE's live precompile) through the full module.
//
// Run: npx hardhat test test/AerePQCSocialRecoveryModule.test.js
const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const path = require("path");
const { pathToFileURL } = require("url");

const coder = ethers.AbiCoder.defaultAbiCoder();

const PRECOMPILE = {
  1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"),
  2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"),
  3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"),
  4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"),
};
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 },
};
const DAY = 24 * 3600;

// ---- deterministic seeded RNG (keccak hash-chain) --------------------------
function makeRng(seedText) {
  let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
  return {
    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;
    },
  };
}
function makePubKey(scheme, rng) {
  const m = META[scheme];
  const raw = rng.bytes(m.pkLen);
  if (m.falcon) raw[0] = m.pkHeader;
  return ethers.hexlify(raw);
}
function commitmentFor(pubKey, message) {
  return ethers.keccak256(ethers.concat([pubKey, message]));
}
// A genuine MOCK envelope binding to `message` under `pubKey` (mock accepts it).
function genuineMock(scheme, pubKey, message, rng) {
  const m = META[scheme];
  const commitment = commitmentFor(pubKey, message);
  if (m.falcon) {
    const nonce = rng.bytes(40);
    const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]);
    return ethers.hexlify(ethers.concat([nonce, esig]));
  }
  const sig = new Uint8Array(m.sigLen);
  sig.set(ethers.getBytes(commitment), 0);
  return ethers.hexlify(sig);
}
// Flip one byte in the commitment region -> a forgery (mock rejects it).
function tamperMock(scheme, sigHex) {
  const b = ethers.getBytes(sigHex);
  b[META[scheme].falcon ? 41 : 0] ^= 0x01;
  return ethers.hexlify(b);
}
function encodeInit(keyIds, threshold) {
  return coder.encode(["uint256[]", "uint256"], [keyIds, threshold]);
}

describe("AerePQCSocialRecoveryModule (quantum-durable M-of-N recovery)", function () {
  let mockCode;
  let deployer, root, newOwner, keyOwner, relayer, stranger, ep;

  before(async function () {
    [deployer, root, newOwner, keyOwner, relayer, stranger, ep] = await ethers.getSigners();
    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());
    }
  });

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

  // Deploy registry + module + account, register `schemes.length` guardian keys under keyOwner,
  // install the module as executor with the given threshold. Returns everything the tests need.
  async function deployStack(schemes, threshold, rng) {
    await installMocks();

    const Reg = await ethers.getContractFactory("AerePQCKeyRegistry");
    const reg = await Reg.deploy();
    await reg.waitForDeployment();

    const Mod = await ethers.getContractFactory("AerePQCSocialRecoveryModule");
    const mod = await Mod.deploy(await reg.getAddress());
    await mod.waitForDeployment();

    const Fac = await ethers.getContractFactory("AereModularAccountFactory");
    const factory = await Fac.deploy(ep.address); // EntryPoint unused by the recovery path
    await factory.waitForDeployment();

    const salt = 1n;
    const acctAddr = await factory["getAddress(address,uint256)"](root.address, salt);
    await (await factory.createAccount(root.address, salt)).wait();
    const account = await ethers.getContractAt("AereModularAccount", acctAddr);

    // Register guardian keys (each with a genuine mock PoP), owned by keyOwner (!= account).
    const guardians = [];
    for (let i = 0; i < schemes.length; i++) {
      const scheme = schemes[i];
      const pk = makePubKey(scheme, rng);
      const nonce = await reg.identityNonce(keyOwner.address);
      const ch = await reg.popChallenge(keyOwner.address, scheme, pk, nonce);
      const sig = genuineMock(scheme, pk, ch, rng);
      const rc = await (await reg.connect(keyOwner).registerKey(scheme, pk, sig)).wait();
      const log = rc.logs.map((l) => { try { return reg.interface.parseLog(l); } catch { return null; } })
        .find((p) => p && p.name === "KeyRegistered");
      guardians.push({ keyId: BigInt(log.args.keyId), scheme, pk });
    }

    const keyIds = guardians.map((g) => g.keyId);
    const initData = encodeInit(keyIds, threshold);
    await (await account.connect(root).installModule(2, await mod.getAddress(), initData)).wait();

    return { reg, mod, factory, account, acctAddr, guardians, keyIds };
  }

  // Build genuine mock legs for the given guardian indices over the current-round challenge.
  async function mockLegs(mod, guardians, acctAddr, proposedOwner, indices, rng, roundOverride) {
    const round = roundOverride !== undefined ? roundOverride : await mod.recoveryNonce(acctAddr);
    const challenge = await mod.recoveryChallenge(acctAddr, proposedOwner, round);
    return indices.map((idx) => [idx, genuineMock(guardians[idx].scheme, guardians[idx].pk, challenge, rng)]);
  }

  // =========================================================================
  //  Config / install
  // =========================================================================
  describe("install + guardian config", function () {
    it("installs as executor, wiring guardian keyIds and threshold", async function () {
      const rng = makeRng("cfg-1");
      const { mod, account, acctAddr, keyIds } = await deployStack([1, 1, 3], 2, rng);
      expect(await account.isModuleInstalled(2, await mod.getAddress(), "0x")).to.equal(true);
      const [t, size, round, pending] = await mod.committee(acctAddr);
      expect(t).to.equal(2n);
      expect(size).to.equal(3n);
      expect(round).to.equal(0n);
      expect(pending).to.equal(false);
      expect((await mod.guardianKeysOf(acctAddr)).map((x) => x)).to.deep.equal(keyIds);
      for (const id of keyIds) expect(await mod.isGuardianKey(acctAddr, id)).to.equal(true);
    });

    it("SDK challenge derivation matches the contract byte-for-byte", async function () {
      // Reproduces AerePQCSocialRecoveryClient.recoveryChallenge exactly; asserting equality with
      // the on-chain view proves SDK<->contract parity (no drift).
      const rng = makeRng("cfg-parity");
      const { mod, acctAddr } = await deployStack([1, 1, 1], 2, rng);
      const RECOVERY_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes("AerePQCSocialRecoveryModule.v1.recovery"));
      const chainId = (await ethers.provider.getNetwork()).chainId;
      const modAddr = await mod.getAddress();
      for (const round of [0n, 3n, 42n]) {
        const sdk = ethers.keccak256(
          coder.encode(
            ["bytes32", "uint256", "address", "address", "address", "uint256"],
            [RECOVERY_DOMAIN, chainId, modAddr, acctAddr, newOwner.address, round]
          )
        );
        expect(sdk).to.equal(await mod.recoveryChallenge(acctAddr, newOwner.address, round));
      }
    });

    it("rejects duplicate guardian keyId at install (distinctness must be by KEY)", async function () {
      const rng = makeRng("cfg-dup");
      await installMocks();
      const Reg = await ethers.getContractFactory("AerePQCKeyRegistry");
      const reg = await Reg.deploy(); await reg.waitForDeployment();
      const Mod = await ethers.getContractFactory("AerePQCSocialRecoveryModule");
      const mod = await Mod.deploy(await reg.getAddress()); await mod.waitForDeployment();
      const Fac = await ethers.getContractFactory("AereModularAccountFactory");
      const factory = await Fac.deploy(ep.address); await factory.waitForDeployment();
      const acctAddr = await factory["getAddress(address,uint256)"](root.address, 7n);
      await (await factory.createAccount(root.address, 7n)).wait();
      const account = await ethers.getContractAt("AereModularAccount", acctAddr);

      // one registered key, referenced twice
      const pk = makePubKey(1, rng);
      const nonce = await reg.identityNonce(keyOwner.address);
      const ch = await reg.popChallenge(keyOwner.address, 1, pk, nonce);
      await (await reg.connect(keyOwner).registerKey(1, pk, genuineMock(1, pk, ch, rng))).wait();
      const initData = encodeInit([0n, 0n], 1);
      await expect(account.connect(root).installModule(2, await mod.getAddress(), initData))
        .to.be.revertedWithCustomError(mod, "DuplicateGuardianKey");
    });

    it("rejects a guardian key OWNED BY the account itself", async function () {
      const rng = makeRng("cfg-self");
      await installMocks();
      const Reg = await ethers.getContractFactory("AerePQCKeyRegistry");
      const reg = await Reg.deploy(); await reg.waitForDeployment();
      const Mod = await ethers.getContractFactory("AerePQCSocialRecoveryModule");
      const mod = await Mod.deploy(await reg.getAddress()); await mod.waitForDeployment();
      const Fac = await ethers.getContractFactory("AereModularAccountFactory");
      const factory = await Fac.deploy(ep.address); await factory.waitForDeployment();
      const acctAddr = await factory["getAddress(address,uint256)"](root.address, 9n);
      await (await factory.createAccount(root.address, 9n)).wait();
      const account = await ethers.getContractAt("AereModularAccount", acctAddr);

      // The account "self-registers" a key: impersonate the account to call registerKey so ownerOf==account.
      await network.provider.request({ method: "hardhat_impersonateAccount", params: [acctAddr] });
      await network.provider.send("hardhat_setBalance", [acctAddr, "0x56bc75e2d63100000"]);
      const acctSigner = await ethers.getSigner(acctAddr);
      const pk = makePubKey(1, rng);
      const nonce = await reg.identityNonce(acctAddr);
      const ch = await reg.popChallenge(acctAddr, 1, pk, nonce);
      await (await reg.connect(acctSigner).registerKey(1, pk, genuineMock(1, pk, ch, rng))).wait();
      await network.provider.request({ method: "hardhat_stopImpersonatingAccount", params: [acctAddr] });

      const initData = encodeInit([0n], 1);
      await expect(account.connect(root).installModule(2, await mod.getAddress(), initData))
        .to.be.revertedWithCustomError(mod, "InvalidGuardianKey");
    });

    it("rejects threshold 0 or > n at install", async function () {
      const rng = makeRng("cfg-thr");
      await installMocks();
      const Reg = await ethers.getContractFactory("AerePQCKeyRegistry");
      const reg = await Reg.deploy(); await reg.waitForDeployment();
      const Mod = await ethers.getContractFactory("AerePQCSocialRecoveryModule");
      const mod = await Mod.deploy(await reg.getAddress()); await mod.waitForDeployment();
      const Fac = await ethers.getContractFactory("AereModularAccountFactory");
      const factory = await Fac.deploy(ep.address); await factory.waitForDeployment();
      const acctAddr = await factory["getAddress(address,uint256)"](root.address, 11n);
      await (await factory.createAccount(root.address, 11n)).wait();
      const account = await ethers.getContractAt("AereModularAccount", acctAddr);
      const pk = makePubKey(1, rng);
      const nonce = await reg.identityNonce(keyOwner.address);
      const ch = await reg.popChallenge(keyOwner.address, 1, pk, nonce);
      await (await reg.connect(keyOwner).registerKey(1, pk, genuineMock(1, pk, ch, rng))).wait();
      await expect(account.connect(root).installModule(2, await mod.getAddress(), encodeInit([0n], 2)))
        .to.be.revertedWithCustomError(mod, "InvalidThreshold");
      await expect(account.connect(root).installModule(2, await mod.getAddress(), encodeInit([0n], 0)))
        .to.be.revertedWithCustomError(mod, "InvalidThreshold");
    });

    it("rejects a non-active (nonexistent) guardian keyId", async function () {
      const rng = makeRng("cfg-nonexist");
      await installMocks();
      const Reg = await ethers.getContractFactory("AerePQCKeyRegistry");
      const reg = await Reg.deploy(); await reg.waitForDeployment();
      const Mod = await ethers.getContractFactory("AerePQCSocialRecoveryModule");
      const mod = await Mod.deploy(await reg.getAddress()); await mod.waitForDeployment();
      const Fac = await ethers.getContractFactory("AereModularAccountFactory");
      const factory = await Fac.deploy(ep.address); await factory.waitForDeployment();
      const acctAddr = await factory["getAddress(address,uint256)"](root.address, 13n);
      await (await factory.createAccount(root.address, 13n)).wait();
      const account = await ethers.getContractAt("AereModularAccount", acctAddr);
      await expect(account.connect(root).installModule(2, await mod.getAddress(), encodeInit([42n], 1)))
        .to.be.revertedWithCustomError(mod, "GuardianKeyNotActive");
    });

    it("uninstall clears config but keeps recoveryNonce monotone", async function () {
      const rng = makeRng("cfg-uninstall");
      const { mod, account, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      // schedule then execute one recovery so recoveryNonce advances to 1
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng);
      await (await mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs)).wait();
      await time.increase(2 * DAY + 60);
      await (await mod.connect(relayer).executeRecovery(acctAddr)).wait();
      expect(await mod.recoveryNonce(acctAddr)).to.equal(1n);

      // new owner uninstalls
      await (await account.connect(newOwner).uninstallModule(2, await mod.getAddress(), "0x")).wait();
      const [t, size] = await mod.committee(acctAddr);
      expect(t).to.equal(0n);
      expect(size).to.equal(0n);
      expect(await mod.recoveryNonce(acctAddr)).to.equal(1n); // NOT reset
    });
  });

  // =========================================================================
  //  Recovery flow — mock verifier accept + reject branches
  // =========================================================================
  describe("recovery flow (mock verifier)", function () {
    it("ACCEPT: >= threshold distinct legs schedule; after 48h the root owner is replaced", async function () {
      const rng = makeRng("flow-happy");
      const { mod, account, acctAddr, guardians } = await deployStack([1, 3, 1], 2, rng);
      expect(await account.rootOwner()).to.equal(root.address);

      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 2], rng);
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.emit(mod, "RecoveryScheduled");
      const [, , , pending] = await mod.committee(acctAddr);
      expect(pending).to.equal(true);

      // premature execution reverts
      await expect(mod.connect(relayer).executeRecovery(acctAddr))
        .to.be.revertedWithCustomError(mod, "TimelockNotElapsed");

      await time.increase(2 * DAY + 60);
      await expect(mod.connect(relayer).executeRecovery(acctAddr)).to.emit(mod, "RecoveryExecuted");
      expect(await account.rootOwner()).to.equal(newOwner.address);
      expect(await mod.recoveryNonce(acctAddr)).to.equal(1n);
    });

    it("REJECT: below threshold reverts (only 1 valid leg for a 2-of-3)", async function () {
      const rng = makeRng("flow-below");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [1], rng);
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(1, 2);
    });

    it("REJECT: a forged (tampered) leg is not counted", async function () {
      const rng = makeRng("flow-forge");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const round = await mod.recoveryNonce(acctAddr);
      const challenge = await mod.recoveryChallenge(acctAddr, newOwner.address, round);
      const good0 = genuineMock(1, guardians[0].pk, challenge, rng);
      const forged2 = tamperMock(1, genuineMock(1, guardians[2].pk, challenge, rng));
      const legs = [[0, good0], [2, forged2]]; // 1 valid + 1 forged < 2
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(1, 2);
    });

    it("REJECT: duplicate guardian index counts once (2x guardian 0 for a 2-of-3 fails)", async function () {
      const rng = makeRng("flow-dupidx");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const round = await mod.recoveryNonce(acctAddr);
      const challenge = await mod.recoveryChallenge(acctAddr, newOwner.address, round);
      const s0 = genuineMock(1, guardians[0].pk, challenge, rng);
      const legs = [[0, s0], [0, s0]]; // same guardian twice -> 1 distinct
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(1, 2);
    });

    it("REJECT: wrong-challenge legs (signed for a different newOwner) do not authorize", async function () {
      const rng = makeRng("flow-wrongchal");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const round = await mod.recoveryNonce(acctAddr);
      // guardians sign the challenge for `stranger`, but we submit to recover to `newOwner`
      const wrongChallenge = await mod.recoveryChallenge(acctAddr, stranger.address, round);
      const legs = [0, 1].map((idx) => [idx, genuineMock(1, guardians[idx].pk, wrongChallenge, rng)]);
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(0, 2);
    });

    it("REJECT: legs signed for a different ROUND do not authorize (replay barrier)", async function () {
      const rng = makeRng("flow-winground");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      // legs for round 5 while the account is at round 0
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng, 5n);
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(0, 2);
    });

    it("REJECT: out-of-range guardian index is ignored", async function () {
      const rng = makeRng("flow-oor");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const round = await mod.recoveryNonce(acctAddr);
      const challenge = await mod.recoveryChallenge(acctAddr, newOwner.address, round);
      const legs = [
        [0, genuineMock(1, guardians[0].pk, challenge, rng)],
        [99, genuineMock(1, guardians[0].pk, challenge, rng)], // idx 99 >= size 3
      ];
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(1, 2);
    });

    it("REJECT: a REVOKED guardian key can no longer authorize", async function () {
      const rng = makeRng("flow-revoke");
      const { reg, mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      // keyOwner revokes guardian 2's key
      await (await reg.connect(keyOwner).revokeKey(guardians[2].keyId)).wait();
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 2], rng);
      // guardian 2 revoked -> only guardian 0 counts -> below 2
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(1, 2);
      // but 0 + 1 still works
      const ok = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng);
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, ok))
        .to.emit(mod, "RecoveryScheduled");
    });

    it("mixed-scheme committee (Falcon-512 + ML-DSA-44) authorizes", async function () {
      const rng = makeRng("flow-mixed");
      const { mod, account, acctAddr, guardians } = await deployStack([1, 3, 4], 2, rng); // Falcon512, ML-DSA-44, SLH-DSA-128s
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng);
      await (await mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs)).wait();
      await time.increase(2 * DAY + 60);
      await (await mod.connect(relayer).executeRecovery(acctAddr)).wait();
      expect(await account.rootOwner()).to.equal(newOwner.address);
    });
  });

  // =========================================================================
  //  Timelock, cancel, replay, permissions
  // =========================================================================
  describe("timelock / cancel / replay", function () {
    it("owner can cancel a pending recovery; execution then impossible; nonce advanced", async function () {
      const rng = makeRng("cancel-owner");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng);
      await (await mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs)).wait();
      await expect(mod.connect(root).cancelRecovery(acctAddr)).to.emit(mod, "RecoveryCancelled");
      expect(await mod.recoveryNonce(acctAddr)).to.equal(1n);
      await time.increase(2 * DAY + 60);
      await expect(mod.connect(relayer).executeRecovery(acctAddr))
        .to.be.revertedWithCustomError(mod, "NoActiveRecovery");
    });

    it("stranger cannot cancel", async function () {
      const rng = makeRng("cancel-stranger");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng);
      await (await mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs)).wait();
      await expect(mod.connect(stranger).cancelRecovery(acctAddr))
        .to.be.revertedWithCustomError(mod, "NotOwner");
    });

    it("cannot schedule a second recovery while one is pending", async function () {
      const rng = makeRng("double-sched");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng);
      await (await mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs)).wait();
      const legs2 = await mockLegs(mod, guardians, acctAddr, stranger.address, [0, 1], rng);
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, stranger.address, legs2))
        .to.be.revertedWithCustomError(mod, "RecoveryAlreadyActive");
    });

    it("legs are single-use: after execute, re-submitting the same legs fails (round advanced)", async function () {
      const rng = makeRng("single-use");
      const { mod, account, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng, 0n);
      await (await mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs)).wait();
      await time.increase(2 * DAY + 60);
      await (await mod.connect(relayer).executeRecovery(acctAddr)).wait();
      expect(await account.rootOwner()).to.equal(newOwner.address);
      // round is now 1; the same round-0 legs no longer verify
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(0, 2);
    });

    it("scheduleRecovery on an unconfigured account reverts NotConfigured", async function () {
      await installMocks();
      const Reg = await ethers.getContractFactory("AerePQCKeyRegistry");
      const reg = await Reg.deploy(); await reg.waitForDeployment();
      const Mod = await ethers.getContractFactory("AerePQCSocialRecoveryModule");
      const mod = await Mod.deploy(await reg.getAddress()); await mod.waitForDeployment();
      await expect(mod.connect(relayer).scheduleRecovery(stranger.address, newOwner.address, []))
        .to.be.revertedWithCustomError(mod, "NotConfigured");
    });

    it("execute fires only after a FULL 48h (one hour short still reverts)", async function () {
      const rng = makeRng("timelock-exact");
      const { mod, acctAddr, guardians } = await deployStack([1, 1, 1], 2, rng);
      const legs = await mockLegs(mod, guardians, acctAddr, newOwner.address, [0, 1], rng);
      await (await mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs)).wait();
      await time.increase(2 * DAY - 3600); // 1h short
      await expect(mod.connect(relayer).executeRecovery(acctAddr))
        .to.be.revertedWithCustomError(mod, "TimelockNotElapsed");
      await time.increase(3600 + 5);
      await (await mod.connect(relayer).executeRecovery(acctAddr)).wait();
    });
  });

  // =========================================================================
  //  REAL Falcon-512 crypto: genuine signatures via @noble, verified by the
  //  NIST-KAT-proven AereFalcon512Verifier behind a precompile-format adapter.
  // =========================================================================
  describe("real Falcon-512 legs (KAT-proven verifier, no mock)", function () {
    let falcon512, verifier, verifierAddr, adapterCode;
    const kat = require("./falcon512_kat0.json");

    before(async function () {
      const nobleFalcon = path.resolve(
        __dirname, "../../sdk-js/node_modules/@noble/post-quantum/falcon.js"
      );
      ({ falcon512 } = await import(pathToFileURL(nobleFalcon).href));

      const V = await ethers.getContractFactory("AereFalcon512Verifier");
      verifier = await V.deploy();
      await verifier.waitForDeployment();
      verifierAddr = await verifier.getAddress();

      const A = await ethers.getContractFactory("RealFalcon512PrecompileAdapter");
      const adapter = await A.deploy(verifierAddr);
      await adapter.waitForDeployment();
      adapterCode = await ethers.provider.getCode(await adapter.getAddress());
    });

    // The pure-Solidity AereFalcon512Verifier standing in for the native precompile costs ~10.7M
    // gas PER leg (on mainnet the native precompile is cheap). With that stand-in, ethers' auto
    // gas-estimation does not leave the 63/64 headroom the nested precompile staticcalls need, so a
    // leg can soft-fail. Pass an explicit generous gasLimit for the heavy scheduleRecovery calls
    // (the hardhat network already permits a 2e9 block gas limit). This is a TEST HARNESS
    // accommodation for the expensive stand-in, not a contract concern.
    const HEAVY = { gasLimit: 400_000_000 };

    // noble detached (0x39 || nonce(40) || compressed) -> AERE envelope (nonce(40) || 0x29 || compressed)
    function realEnvelope(detached) {
      const nonce = detached.slice(1, 41);
      const compressed = detached.slice(41);
      const esig = new Uint8Array(1 + compressed.length);
      esig[0] = 0x29;
      esig.set(compressed, 1);
      return ethers.hexlify(ethers.concat([nonce, esig]));
    }

    async function installRealFalconAt0AE1() {
      await network.provider.send("hardhat_setCode", [PRECOMPILE[1], adapterCode]);
    }

    // Produce a genuine Falcon-512 leg over `challenge` for guardian `idx`, and confirm (via an
    // eth_call with ample gas) that the FULL registry->adapter->verifier path accepts it before we
    // rely on it. Falcon signing is randomized, so on the rare miss we re-sign (a real client does
    // the same). Returns [idx, envelope].
    async function signValidRealLeg(reg, keyId, kp, idx, challenge) {
      for (let attempt = 0; attempt < 6; attempt++) {
        const env = realEnvelope(falcon512.sign(ethers.getBytes(challenge), kp.secretKey));
        if (await reg.verifyWithKey(keyId, challenge, env)) return [idx, env];
      }
      throw new Error(`could not produce a verifying Falcon leg for guardian ${idx}`);
    }

    it("the precompile adapter accepts the OFFICIAL NIST Falcon-512 KAT vector 0", async function () {
      await installRealFalconAt0AE1();
      // raw precompile call: input = pk(897) || sm ; returns 32-byte 0x..01 on accept
      const input = ethers.concat([kat.pk, kat.sm]);
      const ret = await ethers.provider.call({ to: PRECOMPILE[1], data: input });
      expect(BigInt(ret)).to.equal(1n);
      // sanity: the backing verifier accepts it directly too
      expect(await verifier.verifySignedMessage(kat.pk, kat.sm)).to.equal(true);
    });

    it("end-to-end 2-of-3 recovery with GENUINE Falcon-512 signatures over the challenge", async function () {
      await installRealFalconAt0AE1();

      const Reg = await ethers.getContractFactory("AerePQCKeyRegistry");
      const reg = await Reg.deploy(); await reg.waitForDeployment();
      const Mod = await ethers.getContractFactory("AerePQCSocialRecoveryModule");
      const mod = await Mod.deploy(await reg.getAddress()); await mod.waitForDeployment();
      const Fac = await ethers.getContractFactory("AereModularAccountFactory");
      const factory = await Fac.deploy(ep.address); await factory.waitForDeployment();
      const acctAddr = await factory["getAddress(address,uint256)"](root.address, 100n);
      await (await factory.createAccount(root.address, 100n)).wait();
      const account = await ethers.getContractAt("AereModularAccount", acctAddr);

      // Generate 3 real Falcon-512 guardian keypairs, register each with a GENUINE PoP.
      const kps = [];
      const keyIds = [];
      for (let i = 0; i < 3; i++) {
        const seed = new Uint8Array(48).fill(100 + i);
        const kp = falcon512.keygen(seed);
        const pk = ethers.hexlify(kp.publicKey);
        const nonce = await reg.identityNonce(keyOwner.address);
        const ch = await reg.popChallenge(keyOwner.address, 1, pk, nonce);
        const detached = falcon512.sign(ethers.getBytes(ch), kp.secretKey);
        const rc = await (await reg.connect(keyOwner).registerKey(1, pk, realEnvelope(detached), HEAVY)).wait();
        const log = rc.logs.map((l) => { try { return reg.interface.parseLog(l); } catch { return null; } })
          .find((p) => p && p.name === "KeyRegistered");
        kps.push(kp);
        keyIds.push(BigInt(log.args.keyId));
      }

      await (await account.connect(root).installModule(2, await mod.getAddress(), encodeInit(keyIds, 2))).wait();

      // Two guardians (0 and 2) sign the CURRENT-round recovery challenge with real Falcon keys.
      const round = await mod.recoveryNonce(acctAddr);
      const challenge = await mod.recoveryChallenge(acctAddr, newOwner.address, round);
      const legs = [
        await signValidRealLeg(reg, keyIds[0], kps[0], 0, challenge),
        await signValidRealLeg(reg, keyIds[2], kps[2], 2, challenge),
      ];

      const tx = await mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs, HEAVY);
      const rc = await tx.wait();
      console.log("        gas — scheduleRecovery (2 real Falcon-512 legs):", rc.gasUsed.toString());

      await time.increase(2 * DAY + 60);
      await (await mod.connect(relayer).executeRecovery(acctAddr)).wait();
      expect(await account.rootOwner()).to.equal(newOwner.address);
    });

    it("REJECT: a genuine Falcon-512 leg for the WRONG newOwner does not authorize", async function () {
      await installRealFalconAt0AE1();
      const Reg = await ethers.getContractFactory("AerePQCKeyRegistry");
      const reg = await Reg.deploy(); await reg.waitForDeployment();
      const Mod = await ethers.getContractFactory("AerePQCSocialRecoveryModule");
      const mod = await Mod.deploy(await reg.getAddress()); await mod.waitForDeployment();
      const Fac = await ethers.getContractFactory("AereModularAccountFactory");
      const factory = await Fac.deploy(ep.address); await factory.waitForDeployment();
      const acctAddr = await factory["getAddress(address,uint256)"](root.address, 101n);
      await (await factory.createAccount(root.address, 101n)).wait();
      const account = await ethers.getContractAt("AereModularAccount", acctAddr);

      const kps = []; const keyIds = [];
      for (let i = 0; i < 2; i++) {
        const seed = new Uint8Array(48).fill(200 + i);
        const kp = falcon512.keygen(seed);
        const pk = ethers.hexlify(kp.publicKey);
        const nonce = await reg.identityNonce(keyOwner.address);
        const ch = await reg.popChallenge(keyOwner.address, 1, pk, nonce);
        const detached = falcon512.sign(ethers.getBytes(ch), kp.secretKey);
        const rc = await (await reg.connect(keyOwner).registerKey(1, pk, realEnvelope(detached), HEAVY)).wait();
        const log = rc.logs.map((l) => { try { return reg.interface.parseLog(l); } catch { return null; } })
          .find((p) => p && p.name === "KeyRegistered");
        kps.push(kp); keyIds.push(BigInt(log.args.keyId));
      }
      await (await account.connect(root).installModule(2, await mod.getAddress(), encodeInit(keyIds, 2))).wait();

      const round = await mod.recoveryNonce(acctAddr);
      // GENUINE Falcon-512 signatures, but over the challenge for `stranger`; submitted to recover
      // to `newOwner`. Each is well-formed (verifies over the wrong challenge) but binds the wrong
      // target owner, so the module counts 0 over the real challenge.
      const wrong = await mod.recoveryChallenge(acctAddr, stranger.address, round);
      const legs = [
        await signValidRealLeg(reg, keyIds[0], kps[0], 0, wrong),
        await signValidRealLeg(reg, keyIds[1], kps[1], 1, wrong),
      ];
      await expect(mod.connect(relayer).scheduleRecovery(acctAddr, newOwner.address, legs, HEAVY))
        .to.be.revertedWithCustomError(mod, "BelowThreshold").withArgs(0, 2);
    });
  });
});
