// =============================================================================
// SEEDED RANDOMIZED PROPERTY / STATEFUL-INVARIANT fuzzing for the LIVE agentic
// settlement facilitator: AERE402FacilitatorV2 (canonical, cross-wired to
// AereAgentV2). Matched to the live addresses in sdk-js/src/addresses.ts:
//     AereAgentV2                 0x3FAcb997eb4252341052e7c84a86fCa0513c4490 (CANONICAL registry)
//     AERE402FacilitatorV2        cross-wired settlement facilitator (25bps -> Sink)
//   (the V1 pair 0xbA6e...4E56 / 0xE963...3536 is DEPRECATED for finding F13 and is
//    NOT under test here.)
//
// Source under test:
//     contracts/agentic/AERE402FacilitatorV2.sol   (settle + fee split + replay)
//     contracts/agentic/AereAgentV2.sol             (prepaid registry, debit gate)
//
// TOOLCHAIN: hardhat-based randomized invariant fuzzing (NOT Foundry - `forge`
// is not installed; the repo builds through hardhat with OZ 4.9.6 + viaIR). Every
// random choice comes from mulberry32(seed); the base seed is printed at suite
// start and pinnable via FUZZ_SEED. On any break, the failing
// (campaign, step, actor, action, values) is embedded in the assertion message
// for a minimal repro.
//
// SCOPE: run this file in ISOLATION (the full suite OOM/segfaults on the
// unrelated ML-DSA PQC KAT tests):
//     npx hardhat test test/aere402-facilitator-invariant-property.test.js
// No deploy to any live node, no contract-logic change, one new test file only.
// Reuses existing mocks: MockERC20Lending (settlement token), MockSinkSimple
// (fee-accepting sink). One minimal in-file reverting-sink mock for the
// liveness/residual path.
//
// INVARIANTS FUZZED (exactly the AERE402Facilitator brief):
//   * CONSERVATION: for every settlement, the agent's registry debit == payee
//     credit + protocol fee, with fee == floor(amount * 25 / 10000) routed to the
//     Sink EXACTLY. No wei is created or lost (payeeDelta + sinkDelta == amount;
//     facilitator residual == 0 when the sink accepts).
//   * REGISTRY SOLVENCY: the registry's real token balance always equals the sum
//     of its internal prepaid balances[agentId][token] (no phantom credit).
//   * NO REPLAY: an EIP-712 authorization cannot be replayed - re-submitting the
//     same (agentId, payee, nonce) reverts NonceAlreadyConsumed, even re-signed
//     with a fresh deadline; a passed deadline reverts DeadlinePassed.
//   * NO CROSS-CONTRACT / CROSS-CHAIN REPLAY: a signature bound to a different
//     verifyingContract or a different chainId reverts InvalidSignature (domain
//     separation).
//   * NO SELF-DEAL: payee == the agent's signer key reverts SelfDealForbidden, so
//     a leaked signer key cannot pay itself to drain the prepaid balance.
//   * ONLY AUTHORIZED FUNDS MOVE: a wrong signer (InvalidSignature), a tampered
//     field (InvalidSignature), or a mismatched submitter (PayeeMismatch) all
//     revert and move zero funds; global conservation is unchanged after a burst
//     of such rejected attempts.
//   * NO OVERFLOW/UNDERFLOW: fee/split math is exact across dust..1e24 amounts;
//     dust amounts whose fee floors to 0 route the whole amount to the payee.
//   * LIVENESS: a reverting sink never blocks a user settlement - the payee is
//     still paid in full-minus-fee and the fee is retained as a sweepable
//     residual (debit == payee + residual), so no wei is lost.
//
// No em-dashes anywhere in this file.
// =============================================================================

const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-network-helpers");

/* ------------------------------- seeded PRNG ------------------------------ */
function mulberry32(a) {
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
const BASE_SEED = process.env.FUZZ_SEED ? Number(process.env.FUZZ_SEED) : 0x402ABCE1;
function rngFor(tag, campaign) {
  let h = BASE_SEED ^ (campaign * 0x9e3779b1);
  for (let i = 0; i < tag.length; i++) h = (Math.imul(h, 31) + tag.charCodeAt(i)) | 0;
  return mulberry32(h >>> 0);
}
const ri = (rng, min, max) => min + Math.floor(rng() * (max - min + 1)); // inclusive
const pick = (rng, arr) => arr[Math.floor(rng() * arr.length)];
const chance = (rng, p) => rng() < p;

/* ------------------------------- misc helpers ----------------------------- */
const ONE = 10n ** 18n;
const FEE_BPS = 25n;
const BPS_DEN = 10_000n;
const feeOf = (amount) => (amount * FEE_BPS) / BPS_DEN;

const PAYMENT_AUTH_TYPES = {
  PaymentAuth: [
    { name: "agentId", type: "uint256" },
    { name: "token", type: "address" },
    { name: "payee", type: "address" },
    { name: "amount", type: "uint256" },
    { name: "resourceId", type: "bytes32" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" },
  ],
};

async function domainFor(facAddr, chainId) {
  return {
    name: "AERE402Facilitator",
    version: "1",
    chainId,
    verifyingContract: facAddr,
  };
}

async function setBal(addr, amountWei) {
  await network.provider.send("hardhat_setBalance", [addr, "0x" + amountWei.toString(16)]);
}
async function nowTs() {
  return BigInt((await ethers.provider.getBlock("latest")).timestamp);
}

// A random settlement amount: mix of fee-flooring dust (< 400 wei -> fee==0),
// small, and large fractional values so the split math is stressed hard.
function rndAmount(rng, cap) {
  const k = rng();
  let a;
  if (k < 0.18) a = BigInt(ri(rng, 1, 399)); // dust: fee floors to 0
  else if (k < 0.45) a = BigInt(ri(rng, 400, 100_000)); // tiny non-zero fee
  else if (k < 0.75) a = ONE * BigInt(ri(rng, 1, 500)) / BigInt(ri(rng, 1, 997)); // fractional
  else a = ONE * BigInt(ri(rng, 1, 1_000_000)); // large
  if (a < 1n) a = 1n;
  if (cap !== undefined && a > cap) a = cap; // never exceed available prepaid balance
  return a < 1n ? 1n : a;
}

// Iteration counts (env-overridable so a quick smoke can shrink them).
const N = {
  CAMPAIGNS: Number(process.env.F402_CAMPAIGNS || 5),
  STEPS: Number(process.env.F402_STEPS || 60),
  SELFDEAL_TRIALS: Number(process.env.F402_SELFDEAL || 24),
  EDGE_TRIALS: Number(process.env.F402_EDGE || 40),
};

// =============================================================================
describe("INVARIANT: AERE402FacilitatorV2 agentic settlement (25bps -> Sink)", function () {
  this.timeout(0);

  let signers, chainId, TokenF, SinkF, AgentF, FacF;

  before(async function () {
    signers = await ethers.getSigners();
    chainId = (await ethers.provider.getNetwork()).chainId;
    TokenF = await ethers.getContractFactory("MockERC20Lending");
    SinkF = await ethers.getContractFactory("MockSinkSimple");
    AgentF = await ethers.getContractFactory("AereAgentV2");
    FacF = await ethers.getContractFactory("AERE402FacilitatorV2");
    console.log(
      `      [402] base seed 0x${BASE_SEED.toString(16)} | chainId=${chainId} | ` +
        `${N.CAMPAIGNS}x${N.STEPS} main + ${N.SELFDEAL_TRIALS} self-deal + ${N.EDGE_TRIALS} edge`
    );
  });

  // Deploy a fresh, cross-wired stack: token + sink + AereAgentV2 registry +
  // AERE402FacilitatorV2, mirroring the production companion-redeploy wiring
  // (registry.AERE402_FACILITATOR == facilitator, facilitator.AGENT_REGISTRY ==
  // registry). Returns handles + addresses.
  async function deployStack(deployer, sinkAddrOverride) {
    const token = await TokenF.connect(deployer).deploy("SettleToken", "STL", 18);
    await token.waitForDeployment();
    // Default sink is MockSinkSimple (accepts flush by pulling the fee). A test
    // may override with a NON-ACCEPTING sink address (a contract lacking a
    // flush(address,uint256) selector), which makes the facilitator's low-level
    // sink call return false - the "sink does not accept" branch that leaves the
    // fee as a sweepable residual.
    let sink, sinkAddr;
    if (sinkAddrOverride) {
      sink = null;
      sinkAddr = sinkAddrOverride;
    } else {
      sink = await SinkF.connect(deployer).deploy();
      await sink.waitForDeployment();
      sinkAddr = await sink.getAddress();
    }

    // Pre-compute the facilitator's future address so the registry can pin it.
    const nonce = await ethers.provider.getTransactionCount(deployer.address);
    const futureFac = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 });
    const agent = await AgentF.connect(deployer).deploy(futureFac);
    await agent.waitForDeployment();
    const fac = await FacF.connect(deployer).deploy(await agent.getAddress(), sinkAddr);
    await fac.waitForDeployment();

    expect((await fac.getAddress()).toLowerCase(), "facilitator address prediction drifted")
      .to.equal(futureFac.toLowerCase());
    expect(await agent.AERE402_FACILITATOR(), "cross-wire agent->fac").to.equal(await fac.getAddress());
    expect(await fac.AGENT_REGISTRY(), "cross-wire fac->agent").to.equal(await agent.getAddress());

    return {
      token,
      sink,
      agent,
      fac,
      tokenAddr: await token.getAddress(),
      sinkAddr,
      agentAddr: await agent.getAddress(),
      facAddr: await fac.getAddress(),
    };
  }

  // Register an agent whose signer is a fresh random secp256k1 wallet (the SDK
  // key that signs payment auths). Unlimited caps so the conservation loop is
  // never masked by a registry rate-limit revert. Funds the prepaid balance.
  async function registerAndFund(stack, operator, topUpAmount) {
    const signerWallet = ethers.Wallet.createRandom();
    const tx = await stack.agent
      .connect(operator)
      .registerAgent(signerWallet.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, "");
    const agentId = (await tx.wait()).logs.find((l) => l.fragment?.name === "AgentRegistered").args.agentId;
    await topUp(stack, operator, agentId, topUpAmount);
    return { signerWallet, agentId, operator };
  }

  async function topUp(stack, operator, agentId, amount) {
    await (await stack.token.mint(operator.address, amount)).wait();
    await (await stack.token.connect(operator).approve(stack.agentAddr, amount)).wait();
    await (await stack.agent.connect(operator).topUp(agentId, stack.tokenAddr, amount)).wait();
  }

  async function signAuth(signerWallet, facAddr, cid, v) {
    const domain = await domainFor(facAddr, cid);
    return signerWallet.signTypedData(domain, PAYMENT_AUTH_TYPES, v);
  }

  // The global conservation battery, asserted after EVERY step.
  async function assertGlobal(ctx, stack, agentIds, state) {
    // ---- REGISTRY SOLVENCY: registry token balance == sum of internal prepaid.
    let sumInternal = 0n;
    for (const id of agentIds) sumInternal += await stack.agent.balances(id, stack.tokenAddr);
    const regBal = await stack.token.balanceOf(stack.agentAddr);
    expect(regBal, `[402 SOLVENCY] registry balance != sum(internal prepaid) ${ctx}`).to.equal(sumInternal);

    // ---- CONSERVATION (cumulative): everything debited landed at a payee or the
    //      sink; nothing created or lost, and the facilitator never accretes a
    //      residual when the sink accepts (MockSinkSimple always accepts).
    expect(
      state.toPayees + state.toSink,
      `[402 CONSERVE] payees(${state.toPayees})+sink(${state.toSink}) != debited(${state.debited}) ${ctx}`
    ).to.equal(state.debited);
    expect(state.toSink, `[402 FEE-SUM] cumulative sink != cumulative fee ${ctx}`).to.equal(state.fees);
    const facResid = await stack.token.balanceOf(stack.facAddr);
    expect(facResid, `[402 NO-RESIDUAL] facilitator retained dust with a live sink ${ctx}`).to.equal(0n);

    // ---- NO VALUE CREATION: nothing moved out beyond what was ever funded in.
    expect(state.debited, `[402 NO-CREATE] debited(${state.debited}) > funded(${state.funded}) ${ctx}`)
      .to.be.lte(state.funded);

    // ---- TOKEN CONSERVATION: total supply only ever grew via explicit mints.
    expect(await stack.token.totalSupply(), `[402 SUPPLY] token supply drifted ${ctx}`).to.equal(state.minted);
  }

  // -------------------------------------------------------------------------
  it("main interleaving: conservation, exact 25bps fee, replay + auth rejection, registry solvency", async function () {
    let steps = 0, settled = 0, replays = 0, expired = 0, wrongPayee = 0, badSig = 0,
      tampered = 0, topups = 0, regs = 0, rotations = 0, pauses = 0, advances = 0, dustSettles = 0;

    const deployer = signers[0];
    const operators = signers.slice(1, 6); // 5 operators
    const payees = signers.slice(10, 18); // 8 distinct payee/submitter accounts

    for (let c = 0; c < N.CAMPAIGNS; c++) {
      const rng = rngFor("main", c);
      const stack = await deployStack(deployer);

      const state = { debited: 0n, toPayees: 0n, toSink: 0n, fees: 0n, funded: 0n, minted: 0n };
      const agentIds = [];
      const agentInfo = {}; // agentId -> { signerWallet, operator, paused }
      const usedTuples = []; // { agentId, payee, nonce }
      const nonceCursor = {}; // `${agentId}:${payee}` -> next fresh nonce

      // Seed 2 funded agents.
      for (let i = 0; i < 2; i++) {
        const op = pick(rng, operators);
        const amt = ONE * BigInt(ri(rng, 100, 5000));
        state.minted += amt;
        state.funded += amt;
        const info = await registerAndFund(stack, op, amt);
        agentIds.push(info.agentId);
        agentInfo[info.agentId] = { ...info, paused: false };
        regs++;
      }
      await assertGlobal(`c${c} init`, stack, agentIds, state);

      for (let s = 0; s < N.STEPS; s++) {
        const action = pick(rng, [
          "settle", "settle", "settle", "settle", "settle",
          "replay", "expired", "wrongPayee", "badSig", "tampered",
          "topup", "register", "rotate", "pause", "advance",
        ]);
        const ctx = `c${c} s${s} action=${action}`;

        try {
          if (action === "settle") {
            const liveAgents = agentIds.filter((id) => !agentInfo[id].paused);
            if (liveAgents.length === 0) { steps++; continue; }
            const agentId = pick(rng, liveAgents);
            const info = agentInfo[agentId];
            const bal = await stack.agent.balances(agentId, stack.tokenAddr);
            if (bal === 0n) { steps++; continue; }
            const amount = rndAmount(rng, bal);
            // Pick a payee distinct from the agent's signer (self-deal is a
            // separate negative case).
            let payee = pick(rng, payees);
            if (payee.address === info.signerWallet.address) { steps++; continue; }
            const key = `${agentId}:${payee.address}`;
            const nonce = BigInt(nonceCursor[key] || 0);
            nonceCursor[key] = Number(nonce) + 1;
            const resourceId = ethers.id(`res:${ctx}:${nonce}`);
            const deadline = (await nowTs()) + 3600n;
            const sig = await signAuth(info.signerWallet, stack.facAddr, chainId, {
              agentId, token: stack.tokenAddr, payee: payee.address, amount, resourceId, nonce, deadline,
            });

            // ---- per-settle measured deltas ----
            const fee = feeOf(amount);
            const toPayee = amount - fee;
            const regIntBefore = await stack.agent.balances(agentId, stack.tokenAddr);
            const regTokBefore = await stack.token.balanceOf(stack.agentAddr);
            const payeeBefore = await stack.token.balanceOf(payee.address);
            const sinkBefore = await stack.token.balanceOf(stack.sinkAddr);
            const facBefore = await stack.token.balanceOf(stack.facAddr);

            await (await stack.fac
              .connect(payee)
              .settle(agentId, stack.tokenAddr, payee.address, amount, resourceId, nonce, deadline, sig)).wait();

            const regIntAfter = await stack.agent.balances(agentId, stack.tokenAddr);
            const regTokAfter = await stack.token.balanceOf(stack.agentAddr);
            const payeeAfter = await stack.token.balanceOf(payee.address);
            const sinkAfter = await stack.token.balanceOf(stack.sinkAddr);
            const facAfter = await stack.token.balanceOf(stack.facAddr);

            // debit exactly amount, from both internal accounting and real balance.
            expect(regIntBefore - regIntAfter, `[402 DEBIT-INT] internal debit != amount ${ctx} amt=${amount}`).to.equal(amount);
            expect(regTokBefore - regTokAfter, `[402 DEBIT-TOK] registry token debit != amount ${ctx} amt=${amount}`).to.equal(amount);
            // payee credited exactly amount - fee.
            expect(payeeAfter - payeeBefore, `[402 CREDIT] payee credit != amount-fee ${ctx} amt=${amount} fee=${fee}`).to.equal(toPayee);
            // sink credited EXACTLY the 25bps fee.
            expect(sinkAfter - sinkBefore, `[402 FEE] sink credit != 25bps fee ${ctx} amt=${amount} fee=${fee}`).to.equal(fee);
            // fee is exactly floor(amount*25/10000).
            expect(fee, `[402 FEE-MATH] fee != floor(amount*25/10000) ${ctx}`).to.equal((amount * 25n) / 10_000n);
            // no wei created or lost: payee + sink == amount; facilitator net 0.
            expect((payeeAfter - payeeBefore) + (sinkAfter - sinkBefore), `[402 NO-LOSS] payee+sink != amount ${ctx}`).to.equal(amount);
            expect(facAfter - facBefore, `[402 FAC-NET] facilitator retained wei ${ctx}`).to.equal(0n);
            // nonce is now consumed in the per-payee namespace.
            expect(await stack.fac.consumed(agentId, payee.address, nonce), `[402 CONSUMED] nonce not marked consumed ${ctx}`).to.equal(true);

            state.debited += amount;
            state.toPayees += toPayee;
            state.toSink += fee;
            state.fees += fee;
            usedTuples.push({ agentId, payee, nonce });
            settled++;
            if (fee === 0n) dustSettles++;

          } else if (action === "replay") {
            if (usedTuples.length === 0) { steps++; continue; }
            const t = pick(rng, usedTuples);
            const info = agentInfo[t.agentId];
            // Re-sign the SAME (agentId, payee, nonce) with a FRESH deadline +
            // amount so only the nonce collision can be the cause. Must still
            // revert NonceAlreadyConsumed (true replay is always blocked).
            const bal = await stack.agent.balances(t.agentId, stack.tokenAddr);
            const amount = bal > 0n ? (bal > ONE ? ONE : bal) : 1n;
            const deadline = (await nowTs()) + 3600n;
            const resourceId = ethers.id(`replay:${ctx}`);
            const sig = await signAuth(info.signerWallet, stack.facAddr, chainId, {
              agentId: t.agentId, token: stack.tokenAddr, payee: t.payee.address, amount, resourceId, nonce: t.nonce, deadline,
            });
            await expect(
              stack.fac.connect(t.payee).settle(t.agentId, stack.tokenAddr, t.payee.address, amount, resourceId, t.nonce, deadline, sig),
              `[402 REPLAY] true replay not rejected ${ctx}`
            ).to.be.revertedWithCustomError(stack.fac, "NonceAlreadyConsumed");
            replays++;

          } else if (action === "expired") {
            const agentId = pick(rng, agentIds);
            const info = agentInfo[agentId];
            const payee = pick(rng, payees);
            if (payee.address === info.signerWallet.address) { steps++; continue; }
            const key = `${agentId}:${payee.address}`;
            const nonce = BigInt(nonceCursor[key] || 0); // fresh, so DeadlinePassed fires first
            const deadline = 1n; // long past
            const resourceId = ethers.id(`exp:${ctx}`);
            const sig = await signAuth(info.signerWallet, stack.facAddr, chainId, {
              agentId, token: stack.tokenAddr, payee: payee.address, amount: ONE, resourceId, nonce, deadline,
            });
            await expect(
              stack.fac.connect(payee).settle(agentId, stack.tokenAddr, payee.address, ONE, resourceId, nonce, deadline, sig),
              `[402 EXPIRED] expired auth not rejected ${ctx}`
            ).to.be.revertedWithCustomError(stack.fac, "DeadlinePassed");
            expired++;

          } else if (action === "wrongPayee") {
            const agentId = pick(rng, agentIds);
            const info = agentInfo[agentId];
            const payee = pick(rng, payees);
            let submitter = pick(rng, payees);
            if (submitter.address === payee.address) { steps++; continue; }
            if (payee.address === info.signerWallet.address) { steps++; continue; }
            const key = `${agentId}:${payee.address}`;
            const nonce = BigInt(nonceCursor[key] || 0);
            const deadline = (await nowTs()) + 3600n;
            const resourceId = ethers.id(`wp:${ctx}`);
            const sig = await signAuth(info.signerWallet, stack.facAddr, chainId, {
              agentId, token: stack.tokenAddr, payee: payee.address, amount: ONE, resourceId, nonce, deadline,
            });
            // A third party submits an auth intended for `payee`.
            await expect(
              stack.fac.connect(submitter).settle(agentId, stack.tokenAddr, payee.address, ONE, resourceId, nonce, deadline, sig),
              `[402 WRONGPAYEE] submitter != payee not rejected ${ctx}`
            ).to.be.revertedWithCustomError(stack.fac, "PayeeMismatch");
            wrongPayee++;

          } else if (action === "badSig") {
            const agentId = pick(rng, agentIds);
            const info = agentInfo[agentId];
            const payee = pick(rng, payees);
            if (payee.address === info.signerWallet.address) { steps++; continue; }
            const key = `${agentId}:${payee.address}`;
            const nonce = BigInt(nonceCursor[key] || 0); // fresh -> not blocked by nonce
            const deadline = (await nowTs()) + 3600n;
            const resourceId = ethers.id(`bs:${ctx}`);
            // Signed by an UNREGISTERED random key, not the agent's signer.
            const rogue = ethers.Wallet.createRandom();
            const sig = await signAuth(rogue, stack.facAddr, chainId, {
              agentId, token: stack.tokenAddr, payee: payee.address, amount: ONE, resourceId, nonce, deadline,
            });
            await expect(
              stack.fac.connect(payee).settle(agentId, stack.tokenAddr, payee.address, ONE, resourceId, nonce, deadline, sig),
              `[402 BADSIG] unauthorized signer moved funds ${ctx}`
            ).to.be.revertedWithCustomError(stack.fac, "InvalidSignature");
            badSig++;

          } else if (action === "tampered") {
            const liveAgents = agentIds.filter((id) => !agentInfo[id].paused);
            if (liveAgents.length === 0) { steps++; continue; }
            const agentId = pick(rng, liveAgents);
            const info = agentInfo[agentId];
            const payee = pick(rng, payees);
            if (payee.address === info.signerWallet.address) { steps++; continue; }
            const key = `${agentId}:${payee.address}`;
            const nonce = BigInt(nonceCursor[key] || 0);
            const deadline = (await nowTs()) + 3600n;
            const resourceId = ethers.id(`tmp:${ctx}`);
            // Sign for `amount`, then submit with a mutated amount -> digest
            // mismatch -> recovered signer != expected -> InvalidSignature.
            const signedAmount = ONE;
            const submittedAmount = ONE + 1n;
            const sig = await signAuth(info.signerWallet, stack.facAddr, chainId, {
              agentId, token: stack.tokenAddr, payee: payee.address, amount: signedAmount, resourceId, nonce, deadline,
            });
            await expect(
              stack.fac.connect(payee).settle(agentId, stack.tokenAddr, payee.address, submittedAmount, resourceId, nonce, deadline, sig),
              `[402 TAMPER] tampered field accepted ${ctx}`
            ).to.be.revertedWithCustomError(stack.fac, "InvalidSignature");
            tampered++;

          } else if (action === "topup") {
            const agentId = pick(rng, agentIds);
            const op = agentInfo[agentId].operator;
            const amt = ONE * BigInt(ri(rng, 10, 2000));
            state.minted += amt;
            state.funded += amt;
            await topUp(stack, op, agentId, amt);
            topups++;

          } else if (action === "register") {
            if (agentIds.length >= 6) { steps++; continue; }
            const op = pick(rng, operators);
            const amt = ONE * BigInt(ri(rng, 50, 3000));
            state.minted += amt;
            state.funded += amt;
            const info = await registerAndFund(stack, op, amt);
            agentIds.push(info.agentId);
            agentInfo[info.agentId] = { ...info, paused: false };
            regs++;

          } else if (action === "rotate") {
            // Rotating the signer invalidates any previously-signed-but-unsubmitted
            // auth. We assert the OLD signer can no longer authorize a settlement.
            const agentId = pick(rng, agentIds.filter((id) => !agentInfo[id].paused));
            if (agentId === undefined) { steps++; continue; }
            const info = agentInfo[agentId];
            const oldSigner = info.signerWallet;
            const newSigner = ethers.Wallet.createRandom();
            await (await stack.agent.connect(info.operator).setSigner(agentId, newSigner.address)).wait();
            agentInfo[agentId].signerWallet = newSigner;
            // Old signer's fresh, correctly-formed auth must now be rejected.
            const payee = pick(rng, payees);
            if (payee.address !== newSigner.address && payee.address !== oldSigner.address) {
              const key = `${agentId}:${payee.address}`;
              const nonce = BigInt(nonceCursor[key] || 0);
              const deadline = (await nowTs()) + 3600n;
              const resourceId = ethers.id(`rot:${ctx}`);
              const sig = await signAuth(oldSigner, stack.facAddr, chainId, {
                agentId, token: stack.tokenAddr, payee: payee.address, amount: ONE, resourceId, nonce, deadline,
              });
              await expect(
                stack.fac.connect(payee).settle(agentId, stack.tokenAddr, payee.address, ONE, resourceId, nonce, deadline, sig),
                `[402 ROTATE] rotated-out signer still authorizes ${ctx}`
              ).to.be.revertedWithCustomError(stack.fac, "InvalidSignature");
            }
            rotations++;

          } else if (action === "pause") {
            const agentId = pick(rng, agentIds);
            const info = agentInfo[agentId];
            const newPaused = !info.paused;
            await (await stack.agent.connect(info.operator).setPaused(agentId, newPaused)).wait();
            agentInfo[agentId].paused = newPaused;
            if (newPaused) {
              // A settle against a paused agent must revert (registry gate); no funds move.
              const payee = pick(rng, payees);
              if (payee.address !== info.signerWallet.address) {
                const key = `${agentId}:${payee.address}`;
                const nonce = BigInt(nonceCursor[key] || 0);
                const deadline = (await nowTs()) + 3600n;
                const resourceId = ethers.id(`pz:${ctx}`);
                const sig = await signAuth(info.signerWallet, stack.facAddr, chainId, {
                  agentId, token: stack.tokenAddr, payee: payee.address, amount: ONE, resourceId, nonce, deadline,
                });
                await expect(
                  stack.fac.connect(payee).settle(agentId, stack.tokenAddr, payee.address, ONE, resourceId, nonce, deadline, sig),
                  `[402 PAUSED] paused agent still settled ${ctx}`
                ).to.be.revertedWithCustomError(stack.agent, "AgentPausedErr");
              }
            }
            pauses++;

          } else if (action === "advance") {
            await time.increase(ri(rng, 1, 2 * 24 * 60 * 60));
            await network.provider.send("evm_mine");
            advances++;
          }
        } catch (e) {
          // A genuine invariant break must always propagate. Registry-side
          // reverts on edge amounts (InsufficientBalance etc.) are legal and
          // leave state unchanged, so the global battery below still holds.
          if (/402 [A-Z]|INVARIANT/.test(String(e.message))) throw e;
          if (!/InsufficientBalance|reverted/i.test(String(e.message))) throw e;
        }

        await assertGlobal(ctx, stack, agentIds, state);
        steps++;
      }
    }

    console.log(
      `      [402 main] steps=${steps} settled=${settled} (dust=${dustSettles}) replay=${replays} ` +
        `expired=${expired} wrongPayee=${wrongPayee} badSig=${badSig} tamper=${tampered} ` +
        `topup=${topups} reg=${regs} rotate=${rotations} pause=${pauses} adv=${advances}`
    );
    expect(steps).to.be.greaterThan(200);
    expect(settled, "not enough successful settlements to be meaningful").to.be.greaterThan(40);
    expect(dustSettles, "never hit the fee-floors-to-0 dust edge").to.be.greaterThan(0);
    expect(replays + expired + wrongPayee + badSig + tampered, "not enough rejection coverage").to.be.greaterThan(20);
  });

  // -------------------------------------------------------------------------
  // NO SELF-DEAL: a leaked signer key cannot pay ITSELF to drain the prepaid
  // balance. payee == expected signer must always revert SelfDealForbidden and
  // move zero funds, across randomized amounts.
  it("self-deal: payee == signer always reverts SelfDealForbidden and moves nothing", async function () {
    const deployer = signers[0];
    const operator = signers[1];
    const stack = await deployStack(deployer);

    let trials = 0;
    const rng = rngFor("selfdeal", 0);

    for (let i = 0; i < N.SELFDEAL_TRIALS; i++) {
      // The signer key must be able to SEND the settle tx (msg.sender == payee ==
      // signer), so fund the random wallet with ETH and connect it as a sender.
      const signerWallet = ethers.Wallet.createRandom().connect(ethers.provider);
      await setBal(signerWallet.address, ONE * 10n);

      const topAmt = ONE * BigInt(ri(rng, 10, 1000));
      const tx = await stack.agent
        .connect(operator)
        .registerAgent(signerWallet.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, "");
      const agentId = (await tx.wait()).logs.find((l) => l.fragment?.name === "AgentRegistered").args.agentId;
      await topUp(stack, operator, agentId, topAmt);

      const amount = rndAmount(rng, topAmt);
      const deadline = (await nowTs()) + 3600n;
      const resourceId = ethers.id(`selfdeal:${i}`);
      const nonce = 0n;
      const sig = await signAuth(signerWallet, stack.facAddr, chainId, {
        agentId, token: stack.tokenAddr, payee: signerWallet.address, amount, resourceId, nonce, deadline,
      });

      const regIntBefore = await stack.agent.balances(agentId, stack.tokenAddr);
      const signerBalBefore = await stack.token.balanceOf(signerWallet.address);

      await expect(
        stack.fac
          .connect(signerWallet)
          .settle(agentId, stack.tokenAddr, signerWallet.address, amount, resourceId, nonce, deadline, sig),
        `[402 SELFDEAL] i${i} signer paid itself amt=${amount}`
      ).to.be.revertedWithCustomError(stack.fac, "SelfDealForbidden");

      // Nothing moved: prepaid balance and signer token balance unchanged; nonce
      // NOT consumed (the revert rolled everything back).
      expect(await stack.agent.balances(agentId, stack.tokenAddr), `[402 SELFDEAL-BAL] i${i}`).to.equal(regIntBefore);
      expect(await stack.token.balanceOf(signerWallet.address), `[402 SELFDEAL-TOK] i${i}`).to.equal(signerBalBefore);
      expect(await stack.fac.consumed(agentId, signerWallet.address, nonce), `[402 SELFDEAL-NONCE] i${i}`).to.equal(false);
      trials++;
    }
    console.log(`      [402 self-deal] trials=${trials} all reverted SelfDealForbidden`);
    expect(trials).to.equal(N.SELFDEAL_TRIALS);
  });

  // -------------------------------------------------------------------------
  // NO CROSS-CONTRACT / CROSS-CHAIN REPLAY: the EIP-712 domain binds each auth
  // to a specific verifyingContract AND chainId, so a signature minted for a
  // different facilitator or a different chain cannot be replayed here.
  it("domain separation: cross-contract and cross-chain signatures both revert InvalidSignature", async function () {
    const deployer = signers[0];
    const operator = signers[1];
    const payee = signers[10];

    // Two independent, identically-configured facilitators, each with its OWN
    // registry, sharing the SAME agent signer key so ONLY the verifyingContract
    // differs between the two domains.
    const stackA = await deployStack(deployer);
    const stackB = await deployStack(deployer);

    const signerWallet = ethers.Wallet.createRandom();
    const amt = ONE * 10n;

    // Register the same signer on both registries and fund A.
    const txA = await stackA.agent.connect(operator).registerAgent(signerWallet.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, "");
    const agentIdA = (await txA.wait()).logs.find((l) => l.fragment?.name === "AgentRegistered").args.agentId;
    await topUp(stackA, operator, agentIdA, ONE * 100n);
    const txB = await stackB.agent.connect(operator).registerAgent(signerWallet.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, "");
    const agentIdB = (await txB.wait()).logs.find((l) => l.fragment?.name === "AgentRegistered").args.agentId;
    // agentIds happen to match (both 0), which makes the cross-contract replay
    // maximally dangerous if the domain did NOT bind the verifyingContract.
    expect(agentIdA).to.equal(agentIdB);

    const deadline = (await nowTs()) + 3600n;
    const resourceId = ethers.id("cross:res");
    const value = { agentId: agentIdA, token: stackA.tokenAddr, payee: payee.address, amount: amt, resourceId, nonce: 7n, deadline };

    // (1) CROSS-CONTRACT: signed for facilitator B, submitted to facilitator A.
    const sigForB = await signAuth(signerWallet, stackB.facAddr, chainId, value);
    await expect(
      stackA.fac.connect(payee).settle(agentIdA, stackA.tokenAddr, payee.address, amt, resourceId, 7n, deadline, sigForB),
      "[402 XCONTRACT] a signature for facilitator B settled on facilitator A"
    ).to.be.revertedWithCustomError(stackA.fac, "InvalidSignature");

    // (2) CROSS-CHAIN: signed with a foreign chainId, submitted here.
    const sigForOtherChain = await signAuth(signerWallet, stackA.facAddr, chainId + 1n, value);
    await expect(
      stackA.fac.connect(payee).settle(agentIdA, stackA.tokenAddr, payee.address, amt, resourceId, 7n, deadline, sigForOtherChain),
      "[402 XCHAIN] a foreign-chain signature settled here"
    ).to.be.revertedWithCustomError(stackA.fac, "InvalidSignature");

    // (3) CONTROL: the correctly-domained signature DOES settle, proving only the
    //     domain (not some other field) was the reason (1)+(2) failed.
    const sigForA = await signAuth(signerWallet, stackA.facAddr, chainId, value);
    const payeeBefore = await stackA.token.balanceOf(payee.address);
    await (await stackA.fac.connect(payee).settle(agentIdA, stackA.tokenAddr, payee.address, amt, resourceId, 7n, deadline, sigForA)).wait();
    expect(await stackA.token.balanceOf(payee.address) - payeeBefore).to.equal(amt - feeOf(amt));

    console.log("      [402 domain] cross-contract + cross-chain rejected; same-domain control settled");
  });

  // -------------------------------------------------------------------------
  // LIVENESS + RESIDUAL CONSERVATION: a sink whose flush() reverts must NEVER
  // block a user settlement. The payee is still paid amount-fee, and the fee is
  // retained in the facilitator as a sweepable residual, so debit == payee +
  // residual (no wei lost). Randomized amounts.
  it("sink-failure liveness: non-accepting sink never blocks settlement; fee retained as exact residual", async function () {
    const deployer = signers[0];
    const operator = signers[1];
    const payees = signers.slice(10, 16);
    // A NON-ACCEPTING sink: a plain token contract with no flush(address,uint256)
    // selector, so the facilitator's low-level SINK.call(flush(...)) returns false
    // (identical outcome to a sink whose flush() reverts). The best-effort call is
    // swallowed and the fee is left as a facilitator residual.
    const deadSink = await TokenF.connect(deployer).deploy("DeadSink", "DEAD", 18);
    await deadSink.waitForDeployment();
    const stack = await deployStack(deployer, await deadSink.getAddress());

    const signerWallet = ethers.Wallet.createRandom();
    const tx = await stack.agent.connect(operator).registerAgent(signerWallet.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, "");
    const agentId = (await tx.wait()).logs.find((l) => l.fragment?.name === "AgentRegistered").args.agentId;
    await topUp(stack, operator, agentId, ONE * 100_000n);

    const rng = rngFor("sinkfail", 0);
    let trials = 0, feeAccrued = 0n, debited = 0n, paid = 0n, dust = 0;

    for (let i = 0; i < N.EDGE_TRIALS; i++) {
      const bal = await stack.agent.balances(agentId, stack.tokenAddr);
      if (bal === 0n) break;
      const amount = rndAmount(rng, bal > ONE * 1000n ? ONE * 1000n : bal);
      const payee = pick(rng, payees);
      if (payee.address === signerWallet.address) continue;
      const key = `sf:${payee.address}`;
      // simple per-payee monotonic nonce
      const nonce = BigInt(i);
      const deadline = (await nowTs()) + 3600n;
      const resourceId = ethers.id(`sinkfail:${i}`);
      const sig = await signAuth(signerWallet, stack.facAddr, chainId, {
        agentId, token: stack.tokenAddr, payee: payee.address, amount, resourceId, nonce, deadline,
      });

      const fee = feeOf(amount);
      const payeeBefore = await stack.token.balanceOf(payee.address);
      const facBefore = await stack.token.balanceOf(stack.facAddr);
      const regIntBefore = await stack.agent.balances(agentId, stack.tokenAddr);

      // Despite the sink reverting inside settle, the tx SUCCEEDS (best-effort
      // sink call is swallowed).
      await (await stack.fac.connect(payee).settle(agentId, stack.tokenAddr, payee.address, amount, resourceId, nonce, deadline, sig)).wait();

      const payeeAfter = await stack.token.balanceOf(payee.address);
      const facAfter = await stack.token.balanceOf(stack.facAddr);
      const regIntAfter = await stack.agent.balances(agentId, stack.tokenAddr);

      expect(regIntBefore - regIntAfter, `[402 SF-DEBIT] i${i}`).to.equal(amount);
      expect(payeeAfter - payeeBefore, `[402 SF-PAY] payee not paid amount-fee i${i}`).to.equal(amount - fee);
      // fee could not reach the sink, so it stays as facilitator residual.
      expect(facAfter - facBefore, `[402 SF-RESIDUAL] residual != fee i${i} fee=${fee}`).to.equal(fee);
      // CONSERVATION under sink failure: debit == payee credit + retained residual.
      expect((payeeAfter - payeeBefore) + (facAfter - facBefore), `[402 SF-CONSERVE] i${i}`).to.equal(amount);

      debited += amount;
      paid += amount - fee;
      feeAccrued += fee;
      if (fee === 0n) dust++;
      trials++;
    }

    // Total residual sitting in the facilitator equals every un-flushed fee.
    expect(await stack.token.balanceOf(stack.facAddr), "[402 SF-TOTAL] residual != sum of fees").to.equal(feeAccrued);
    expect(paid + feeAccrued, "[402 SF-SUM] paid+residual != debited").to.equal(debited);
    console.log(
      `      [402 sink-fail] trials=${trials} (dust=${dust}) debited=${ethers.formatEther(debited)} ` +
        `paid=${ethers.formatEther(paid)} residual=${ethers.formatEther(feeAccrued)} (all settlements succeeded)`
    );
    expect(trials).to.be.greaterThan(10);
  });
});
