// =============================================================================
// SEEDED RANDOMIZED PROPERTY / STATEFUL-INVARIANT fuzzing for the LIVE
// AereInsuranceFund (canonical mainnet 0x5Ab95C549c2A2b07913Df7edD4a16fd108B7CAAC
// per sdk-js/src/addresses.ts, "7-day cooldown"). Source under test:
//   contracts/lending/AereInsuranceFund.sol
// exercised against the REAL AereLendingMarket (contracts/lending/
// AereLendingMarket.sol), which is the production counterpart whose
// repayOnBehalfOf / debtBalance / DEBT_TOKEN the fund actually calls. Genuine
// BadDebt is manufactured via an insolvent liquidation, exactly the situation
// the fund exists to absorb. Market interest is frozen (setBorrowRate(0)) so
// debt accounting is exact and every invariant is crisp.
//
// TOOLCHAIN: hardhat-based randomized invariant fuzzing (NOT Foundry -- `forge`
// is not installed and 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. Existing repo mocks are reused (MockERC20Lending,
// MockAggregator); no new contract is added, nothing is deployed to any node.
//
// SCOPE: run this file in ISOLATION (the full suite OOM/segfaults on the
// unrelated ML-DSA PQC KAT tests):
//     npx hardhat test test/insurance-fund-invariant-property.test.js
//
// INVARIANTS FUZZED (exactly the AereInsuranceFund brief):
//   G1 CUSTODY-BALANCE: for every token, real ERC-20 balanceOf(fund) equals the
//      fund's tracked balances[token] plus any untracked stray transfers. The
//      tracked mapping is NEVER greater than the real balance, so the fund can
//      never be asked to pay out more than it truly holds.
//   G2 ACCOUNTING-CONSERVATION: balances[token] == totalDonated - totalCovered.
//   G3 CAP: for every registered market, covered <= lifetimeCap, ALWAYS. Cap can
//      never be exceeded; the cap check applies to the CLAMPED payout amount
//      (min(request, owed)), so a request over the raw cap that clamps under it
//      still succeeds, while a clamped amount over the cap reverts CapExceeded.
//   G4 NO-VALUE-CREATION: cumulative coverage paid in a token never exceeds
//      cumulative donations of that token (can't pay out value that never came in).
//   G5 COOLDOWN: two successful coverages on the same market are always at least
//      COOLDOWN_SECONDS apart; a second cover inside the window reverts.
//   G6 DEBT-ONLY-DECREASES: fund actions never mint or increase any borrower's
//      debt; repayOnBehalfOf can only REDUCE it, by exactly the tokens paid.
//   G7 COVER-NEEDS-REAL-DEBT: coverage is only ever paid against a borrower who
//      actually owes, and the paid amount is clamped to the outstanding debt;
//      covering a no-debt account reverts NoDebtToCover.
//   G8 NO-ESCAPE-HATCH: the ONLY state-mutating externals are
//      {proposeMarket, registerMarket, cancelPendingMarket, donate, coverBadDebt}
//      and coverBadDebt is the ONLY path that removes tokens -- there is no
//      withdraw / rescue / sweep / emergency / owner-drain function, and only
//      FOUNDATION can call the payout path or propose a market.
//
// 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) : 0x1F5EED;
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 ONE6 = 10n ** 6n;   // USDC.e style debt token
const ONE18 = 10n ** 18n; // collateral
const REG_DELAY = 7 * 86400 + 5;

const N = {
  CAMPAIGNS: Number(process.env.IF_CAMPAIGNS || 3),
  STEPS: Number(process.env.IF_STEPS || 60),
  MARKETS: Number(process.env.IF_MARKETS || 2),
  BORROWERS: Number(process.env.IF_BORROWERS || 2),
};

// =============================================================================
describe("INVARIANT: AereInsuranceFund (bad-debt coverage, custody + bounded payout)", function () {
  this.timeout(0);

  let signers;
  let ERC20F, AggF, OracleF, WAEREF, BurnF, SinkF, MarketF, FundF;

  before(async function () {
    signers = await ethers.getSigners();
    ERC20F = await ethers.getContractFactory("MockERC20Lending");
    AggF = await ethers.getContractFactory("MockAggregator");
    OracleF = await ethers.getContractFactory("AereLendingOracle");
    WAEREF = await ethers.getContractFactory("contracts/WAERE.sol:WAERE");
    BurnF = await ethers.getContractFactory("AereFeeBurnVault");
    SinkF = await ethers.getContractFactory("AereSink");
    MarketF = await ethers.getContractFactory("AereLendingMarket");
    FundF = await ethers.getContractFactory("AereInsuranceFund");
    console.log(
      `      [ifund] base seed 0x${BASE_SEED.toString(16)} | ` +
        `${N.CAMPAIGNS} campaigns x ${N.STEPS} steps, ${N.MARKETS} markets/${N.BORROWERS} borrowers each`
    );
  });

  // Shared infra (oracle + sink) for a campaign. Debt token is shared across
  // all markets in a campaign so the balances/donation/coverage accounting is a
  // single, aggregated ledger (the hardest case to keep consistent).
  async function deployInfra(deployer) {
    const debt = await ERC20F.deploy("USDC.e", "USDC.e", 6);
    await debt.waitForDeployment();
    const aggD = await AggF.deploy(1_00000000); // $1.00, 8 decimals
    await aggD.waitForDeployment();
    const oracle = await OracleF.deploy();
    await oracle.waitForDeployment();
    await oracle.configureFeed(await debt.getAddress(), 0, await aggD.getAddress(), ethers.ZeroHash, 30 * 86400, 0, 8);

    const waere = await WAEREF.deploy();
    await waere.waitForDeployment();
    const burn = await BurnF.deploy();
    await burn.waitForDeployment();
    const sink = await SinkF.deploy(
      await waere.getAddress(), await burn.getAddress(),
      await waere.getAddress(), await waere.getAddress(),
      1500, 4000, 4500, 200, ethers.ZeroAddress
    );
    await sink.waitForDeployment();
    return { debt, aggD, oracle, sink };
  }

  // Build one market on shared infra + manufacture real BadDebt on `borrowers`
  // via an insolvent liquidation. Interest is frozen so residual debt is static.
  async function buildMarketWithBadDebt(infra, deployer, supplier, liquidator, borrowers) {
    const collateral = await ERC20F.deploy("BUIDL.e", "BUIDL.e", 18);
    await collateral.waitForDeployment();
    const aggC = await AggF.deploy(1_00000000); // $1.00 to start
    await aggC.waitForDeployment();
    await infra.oracle.configureFeed(await collateral.getAddress(), 0, await aggC.getAddress(), ethers.ZeroHash, 30 * 86400, 0, 8);

    const cfg = {
      collateral: await collateral.getAddress(),
      debtToken: await infra.debt.getAddress(),
      sink: await infra.sink.getAddress(),
      oracle: await infra.oracle.getAddress(),
      ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500, borrowFeeBps: 100,
      marketDebtCap: 1_000_000_000n * ONE6,
      collateralDecimals: 18, debtDecimals: 6,
    };
    const market = await MarketF.deploy(cfg);
    await market.waitForDeployment();
    const marketAddr = await market.getAddress();

    // Freeze interest so debt accounting stays exact across time warps.
    await (await market.connect(deployer).setBorrowRate(0)).wait();

    // Supplier provides borrowable liquidity.
    await (await infra.debt.mint(supplier.address, 50_000_000n * ONE6)).wait();
    await (await infra.debt.connect(supplier).approve(marketAddr, ethers.MaxUint256)).wait();
    await (await market.connect(supplier).supply(20_000_000n * ONE6)).wait();

    // Each borrower posts 1000 collateral @ $1, borrows 690 (< 70% LTV).
    for (const b of borrowers) {
      await (await collateral.mint(b.address, 1000n * ONE18)).wait();
      await (await collateral.connect(b).approve(marketAddr, ethers.MaxUint256)).wait();
      await (await market.connect(b).depositCollateral(1000n * ONE18)).wait();
      await (await market.connect(b).borrow(690n * ONE6)).wait();
    }

    // Crash collateral to $0.05 -> deeply insolvent, then liquidate fully.
    await (await aggC.set(5_000_000)).wait();
    await (await market.accrueInterest()).wait();
    await (await infra.debt.mint(liquidator.address, BigInt(borrowers.length) * 1000n * ONE6)).wait();
    await (await infra.debt.connect(liquidator).approve(marketAddr, ethers.MaxUint256)).wait();
    for (const b of borrowers) {
      await (await market.connect(liquidator).liquidate(b.address, 690n * ONE6)).wait();
      const resid = await market.debtBalance(b.address);
      expect(resid, `bad-debt setup produced no residual for ${b.address}`).to.be.gt(0n);
    }
    return { market, marketAddr, collateral, aggC };
  }

  // Structural proof: the fund has no escape hatch and only Foundation controls
  // the privileged paths. Run once per campaign against the deployed instance.
  function assertNoEscapeHatch(fund) {
    const allowedMutators = new Set([
      "proposeMarket", "registerMarket", "cancelPendingMarket", "donate", "coverBadDebt",
    ]);
    const banned = /withdraw|rescue|sweep|emergency|drain|transferOwnership|renounce|setOwner|setFoundation|migrate|skim|recover/i;
    for (const f of fund.interface.fragments) {
      if (f.type !== "function") continue;
      if (f.stateMutability === "view" || f.stateMutability === "pure") continue;
      expect(banned.test(f.name), `[ifund G8] fund exposes suspicious mutator "${f.name}"`).to.equal(false);
      expect(allowedMutators.has(f.name), `[ifund G8] unexpected state-mutating external "${f.name}"`).to.equal(true);
    }
  }

  // The global invariant battery -- asserted after EVERY step.
  async function assertInvariants(ctx, fund, debt, ledger) {
    const debtAddr = await debt.getAddress();
    const tracked = await fund.balances(debtAddr);
    const real = await debt.balanceOf(await fund.getAddress());

    // G1 custody-balance: real == tracked + stray, and tracked <= real always.
    expect(real, `[ifund G1] real balance != tracked+stray ${ctx} tracked=${tracked} stray=${ledger.stray} real=${real}`)
      .to.equal(tracked + ledger.stray);
    expect(tracked, `[ifund G1b] tracked mapping exceeds real balance ${ctx}`).to.be.lte(real);

    // G2 accounting-conservation.
    expect(tracked, `[ifund G2] balances != donated-covered ${ctx} donated=${ledger.donated} covered=${ledger.covered}`)
      .to.equal(ledger.donated - ledger.covered);

    // G4 no-value-creation.
    expect(ledger.covered, `[ifund G4] covered ${ledger.covered} > donated ${ledger.donated} ${ctx}`)
      .to.be.lte(ledger.donated);

    // G3 cap per market + cooldown-spacing bookkeeping consistency.
    for (const m of ledger.markets) {
      const info = await fund.markets(m.addr);
      // info = [registered, lifetimeCap, covered, lastCoverageAt]
      expect(info[2], `[ifund G3] market covered ${info[2]} > cap ${info[1]} ${ctx} market=${m.addr.slice(0, 8)}`)
        .to.be.lte(info[1]);
      // on-chain covered must match our independently-tracked per-market total.
      expect(info[2], `[ifund G3b] per-market covered drift ${ctx} market=${m.addr.slice(0, 8)}`).to.equal(m.covered);
    }

    // G6 debt-only-decreases: no borrower debt ever exceeds its recorded high-water.
    for (const b of ledger.borrowers) {
      const d = await b.market.debtBalance(b.addr);
      expect(d, `[ifund G6] borrower debt rose ${ctx} borrower=${b.addr.slice(0, 8)} was<=${b.debt} now=${d}`)
        .to.be.lte(b.debt);
      b.debt = d; // ratchet down
    }
  }

  // -------------------------------------------------------------------------
  it("multi-market / multi-actor coverage: custody, bounded payout, cap+cooldown, conservation", async function () {
    const deployer = signers[0];
    let totalSteps = 0, covers = 0, capReverts = 0, cooldownReverts = 0,
        noDebtReverts = 0, foundationReverts = 0, donations = 0, strays = 0, insufReverts = 0;

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

      const foundation = signers[1];
      const supplier = signers[2];
      const liquidator = signers[3];
      const donors = [signers[4], signers[5], signers[6]];
      const attackers = [signers[7], signers[8]];
      // borrower pool: MARKETS * BORROWERS distinct addresses from a stable base.
      const borrowerSigners = signers.slice(9, 9 + N.MARKETS * N.BORROWERS);

      const infra = await deployInfra(deployer);
      const debt = infra.debt;
      const debtAddr = await debt.getAddress();

      const fund = await FundF.deploy(foundation.address, ri(rng, 3600, 2 * 86400)); // 1h..2d cooldown
      await fund.waitForDeployment();
      const fundAddr = await fund.getAddress();
      const cooldown = await fund.COOLDOWN_SECONDS();

      assertNoEscapeHatch(fund);

      // Access-control probes: attackers can neither propose nor cover.
      await expect(
        fund.connect(attackers[0]).proposeMarket(borrowerSigners[0].address, 1n),
        "[ifund G8] attacker proposeMarket not rejected"
      ).to.be.revertedWithCustomError(fund, "NotFoundation");

      const ledger = {
        donated: 0n, covered: 0n, stray: 0n,
        markets: [], borrowers: [],
      };

      // Build markets + bad debt, register each with a random lifetime cap.
      let bi = 0;
      for (let mi = 0; mi < N.MARKETS; mi++) {
        const brs = borrowerSigners.slice(bi, bi + N.BORROWERS);
        bi += N.BORROWERS;
        const mk = await buildMarketWithBadDebt(infra, deployer, supplier, liquidator, brs);
        // Cap chosen to sometimes be tight (forces CapExceeded) and sometimes loose.
        const cap = BigInt(ri(rng, 200, 4000)) * ONE6;
        await (await fund.connect(foundation).proposeMarket(mk.marketAddr, cap)).wait();
        ledger.markets.push({ addr: mk.marketAddr, market: mk.market, cap, covered: 0n });
        for (const b of brs) {
          ledger.borrowers.push({ addr: b.address, market: mk.market, debt: await mk.market.debtBalance(b.address) });
        }
      }
      // Elapse the 7-day registration timelock once, then execute all pending.
      await time.increase(REG_DELAY);
      await network.provider.send("evm_mine");
      for (const m of ledger.markets) {
        await (await fund.connect(pick(rng, [foundation, ...attackers])).registerMarket(m.addr)).wait();
      }

      // Seed the fund with an initial donation.
      const seed = BigInt(ri(rng, 3000, 12000)) * ONE6;
      await (await debt.mint(donors[0].address, seed)).wait();
      await (await debt.connect(donors[0]).approve(fundAddr, ethers.MaxUint256)).wait();
      await (await fund.connect(donors[0]).donate(debtAddr, seed)).wait();
      ledger.donated += seed;
      donations++;

      await assertInvariants(`c${c} init`, fund, debt, ledger);

      // Non-foundation cover must be rejected (state-independent guard).
      await expect(
        fund.connect(attackers[1]).coverBadDebt(ledger.markets[0].addr, ledger.borrowers[0].addr, 1n),
        "[ifund G8] attacker coverBadDebt not rejected"
      ).to.be.revertedWithCustomError(fund, "NotFoundation");
      foundationReverts++;

      for (let s = 0; s < N.STEPS; s++) {
        const action = pick(rng, [
          "cover", "cover", "cover", "cover",
          "donate", "donate", "stray",
          "advance", "advance",
          "coverAttacker", "coverNoDebt", "coverExceedCap", "advance",
        ]);
        const ctx = `c${c} s${s} action=${action}`;

        try {
          if (action === "donate") {
            const donor = pick(rng, donors);
            const amt = BigInt(ri(rng, 1, 5000)) * ONE6;
            await (await debt.mint(donor.address, amt)).wait();
            await (await debt.connect(donor).approve(fundAddr, amt)).wait();
            await (await fund.connect(donor).donate(debtAddr, amt)).wait();
            ledger.donated += amt;
            donations++;

          } else if (action === "stray") {
            // Untracked direct transfer: must inflate real balance ONLY, never
            // the tracked mapping, and never enable extra payout (G1/G1b).
            const donor = pick(rng, donors);
            const amt = BigInt(ri(rng, 1, 2000)) * ONE6;
            await (await debt.mint(donor.address, amt)).wait();
            await (await debt.connect(donor).transfer(fundAddr, amt)).wait();
            ledger.stray += amt;
            strays++;

          } else if (action === "advance") {
            await time.increase(ri(rng, 1, 3 * 86400));
            await network.provider.send("evm_mine");

          } else if (action === "cover") {
            const m = pick(rng, ledger.markets);
            const brs = ledger.borrowers.filter((b) => b.market === m.market);
            const b = pick(rng, brs);
            const owed = await m.market.debtBalance(b.addr);
            const remaining = await fund.coverageRemaining(m.addr);
            const ready = await fund.cooldownReady(m.addr);
            const tracked = await fund.balances(debtAddr);

            // Random request: sometimes partial, sometimes >= owed (forces clamp),
            // sometimes tiny.
            let amount;
            const k = rng();
            if (k < 0.3) amount = BigInt(ri(rng, 1, 50)) * ONE6;
            else if (k < 0.7) amount = owed === 0n ? 1n : (owed * BigInt(ri(rng, 1, 90))) / 100n;
            else amount = owed + BigInt(ri(rng, 0, 500)) * ONE6;
            if (amount === 0n) amount = 1n;

            const clamped = amount > owed ? owed : amount;
            const willCapRevert = clamped > remaining;                // cap applies to the CLAMPED payout
            const willSucceed =
              ready && !willCapRevert && owed > 0n && tracked >= clamped;

            const beforeFund = await debt.balanceOf(fundAddr);
            const beforeMarket = await debt.balanceOf(m.addr);
            const beforeDebt = owed;
            const info = await fund.markets(m.addr);
            const lastAt = info[3];

            if (willSucceed) {
              const tx = await fund.connect(foundation).coverBadDebt(m.addr, b.addr, amount);
              const rc = await tx.wait();
              const blk = await ethers.provider.getBlock(rc.blockNumber);

              const afterFund = await debt.balanceOf(fundAddr);
              const afterMarket = await debt.balanceOf(m.addr);
              const afterDebt = await m.market.debtBalance(b.addr);
              const delta = beforeFund - afterFund;

              // bounded payout == exactly the clamped amount, nothing more.
              expect(delta, `[ifund PAYOUT] paid ${delta} != clamped ${clamped} ${ctx}`).to.equal(clamped);
              // market received exactly what the fund paid (no leakage / mint).
              expect(afterMarket - beforeMarket, `[ifund PAYOUT-MKT] market delta != paid ${ctx}`).to.equal(delta);
              // borrower debt fell by exactly the paid amount (G6 / G7 clamp).
              expect(beforeDebt - afterDebt, `[ifund PAYOUT-DEBT] debt drop != paid ${ctx} before=${beforeDebt} after=${afterDebt}`).to.equal(delta);
              // tracked mapping fell by the same amount (G2).
              const afterTracked = await fund.balances(debtAddr);
              expect(tracked - afterTracked, `[ifund PAYOUT-TRACK] tracked delta != paid ${ctx}`).to.equal(delta);
              // cap accounting.
              const info2 = await fund.markets(m.addr);
              expect(info2[2] - info[2], `[ifund PAYOUT-COVERED] covered delta != paid ${ctx}`).to.equal(delta);
              // G5 cooldown spacing: timestamp advanced to now, and if a prior
              // cover existed the gap is >= cooldown.
              expect(info2[3], `[ifund COOLDOWN-STAMP] lastCoverageAt != block time ${ctx}`).to.equal(BigInt(blk.timestamp));
              if (lastAt !== 0n) {
                expect(BigInt(blk.timestamp) - lastAt, `[ifund G5] cover inside cooldown window ${ctx}`).to.be.gte(cooldown);
              }
              // borrower value did NOT increase (G6). Ratchet the high-water down.
              const bl = ledger.borrowers.find((x) => x.addr === b.addr && x.market === b.market);
              expect(afterDebt, `[ifund G6] debt increased on cover ${ctx}`).to.be.lte(bl.debt);
              bl.debt = afterDebt;

              ledger.covered += delta;
              m.covered += delta;
              covers++;
            } else {
              // Expected to revert. Confirm it does AND that no state moved.
              let reverted = false;
              try {
                await (await fund.connect(foundation).coverBadDebt(m.addr, b.addr, amount)).wait();
              } catch (e) {
                reverted = true;
                if (/CapExceeded/.test(e.message)) capReverts++;
                else if (/CooldownActive/.test(e.message)) cooldownReverts++;
                else if (/NoDebtToCover/.test(e.message)) noDebtReverts++;
                else if (/InsufficientBalance/.test(e.message)) insufReverts++;
              }
              expect(reverted, `[ifund PREDICT] expected cover revert but it succeeded ${ctx} amount=${amount} owed=${owed} remaining=${remaining} ready=${ready} tracked=${tracked}`).to.equal(true);
              // state unchanged.
              expect(await debt.balanceOf(fundAddr), `[ifund NOMOVE-fund] ${ctx}`).to.equal(beforeFund);
              expect((await fund.markets(m.addr))[2], `[ifund NOMOVE-covered] ${ctx}`).to.equal(info[2]);
            }

          } else if (action === "coverAttacker") {
            // Payout path is Foundation-only regardless of state.
            const m = pick(rng, ledger.markets);
            const b = pick(rng, ledger.borrowers.filter((x) => x.market === m.market));
            await expect(
              fund.connect(pick(rng, attackers)).coverBadDebt(m.addr, b.addr, 1n * ONE6),
              `[ifund G8] non-foundation cover succeeded ${ctx}`
            ).to.be.revertedWithCustomError(fund, "NotFoundation");
            foundationReverts++;

          } else if (action === "coverNoDebt") {
            // Foundation covering an address with zero debt must revert -- but the
            // contract checks cooldown BEFORE the debt check, so only assert
            // NoDebtToCover when the cooldown is actually clear.
            const m = pick(rng, ledger.markets);
            if (await fund.cooldownReady(m.addr)) {
              const remaining = await fund.coverageRemaining(m.addr);
              if (remaining > 0n) {
                await expect(
                  fund.connect(foundation).coverBadDebt(m.addr, attackers[0].address, 1n),
                  `[ifund G7] no-debt cover not rejected ${ctx}`
                ).to.be.revertedWithCustomError(fund, "NoDebtToCover");
                noDebtReverts++;
              }
            }

          } else if (action === "coverExceedCap") {
            // Post-clamp cap semantics: CapExceeded fires only when the amount
            // that would ACTUALLY be paid (min(request, owed)) exceeds the
            // remaining cap. Send more than owed so the clamp binds, and only
            // assert the revert when owed itself is over the remaining cap; if
            // owed <= remaining the correct behavior is a clamped SUCCESS, not a
            // revert, so we do not force one here.
            const m = pick(rng, ledger.markets);
            if (await fund.cooldownReady(m.addr)) {
              const remaining = await fund.coverageRemaining(m.addr);
              const b = pick(rng, ledger.borrowers.filter((x) => x.market === m.market));
              const owed = await m.market.debtBalance(b.addr);
              if (owed > remaining) {
                const amount = owed + 1n; // clamps to owed, which is over remaining
                const beforeFund = await debt.balanceOf(fundAddr);
                await expect(
                  fund.connect(foundation).coverBadDebt(m.addr, b.addr, amount),
                  `[ifund G3] over-cap cover not rejected ${ctx} remaining=${remaining} owed=${owed}`
                ).to.be.revertedWithCustomError(fund, "CapExceeded");
                expect(await debt.balanceOf(fundAddr), `[ifund G3 NOMOVE] ${ctx}`).to.equal(beforeFund);
                capReverts++;
              }
            }
          }
        } catch (e) {
          if (/\[ifund /.test(String(e.message))) throw e; // real invariant break
          // Otherwise an incidental revert on an edge amount is legal; fall through.
        }

        await assertInvariants(ctx, fund, debt, ledger);
        totalSteps++;
      }

      // Finale: verify the ledger closes out consistently against ground truth.
      await assertInvariants(`c${c} finale`, fund, debt, ledger);
    }

    console.log(
      `      [ifund main] steps=${totalSteps} covers=${covers} donations=${donations} strays=${strays} ` +
        `capRev=${capReverts} cooldownRev=${cooldownReverts} noDebtRev=${noDebtReverts} insufRev=${insufReverts} foundationRev=${foundationReverts}`
    );
    expect(totalSteps).to.be.greaterThan(N.CAMPAIGNS * N.STEPS - 1);
    expect(covers, "no successful coverages exercised").to.be.greaterThan(10);
    expect(capReverts + cooldownReverts + noDebtReverts, "guard reverts never exercised").to.be.greaterThan(3);
    expect(foundationReverts, "access-control path never exercised").to.be.greaterThan(3);
  });

  // -------------------------------------------------------------------------
  // Production-parameter campaign: the LIVE fund runs a 7-day cooldown. Prove the
  // exact "cannot exceed cap within a cooldown window" bound holds at that value:
  // after one cover, a second is impossible for 7 days, and cap is a hard lifetime
  // ceiling even across many windows.
  it("production 7-day cooldown: one cover per window, cap is a hard lifetime ceiling", async function () {
    const rng = rngFor("prod", 0);
    const deployer = signers[0];
    const foundation = signers[1];
    const supplier = signers[2];
    const liquidator = signers[3];
    const donor = signers[4];
    const borrowers = [signers[9], signers[10]];

    const infra = await deployInfra(deployer);
    const debt = infra.debt;
    const debtAddr = await debt.getAddress();

    const SEVEN_DAYS = 7n * 86400n;
    const fund = await FundF.deploy(foundation.address, SEVEN_DAYS);
    await fund.waitForDeployment();
    const fundAddr = await fund.getAddress();
    assertNoEscapeHatch(fund);

    const mk = await buildMarketWithBadDebt(infra, deployer, supplier, liquidator, borrowers);
    const cap = 300n * ONE6; // small lifetime cap
    await (await fund.connect(foundation).proposeMarket(mk.marketAddr, cap)).wait();
    await time.increase(REG_DELAY);
    await network.provider.send("evm_mine");
    await (await fund.connect(foundation).registerMarket(mk.marketAddr)).wait();

    await (await debt.mint(donor.address, 100_000n * ONE6)).wait();
    await (await debt.connect(donor).approve(fundAddr, ethers.MaxUint256)).wait();
    await (await fund.connect(donor).donate(debtAddr, 100_000n * ONE6)).wait();

    let covered = 0n, windows = 0;
    // Cover 50 USDC.e per window; cap is 300 so exactly 6 windows then hard stop.
    for (let w = 0; w < 10; w++) {
      const remaining = await fund.coverageRemaining(mk.marketAddr);
      const b = borrowers[w % borrowers.length].address;
      const owed = await mk.market.debtBalance(b);
      const amount = 50n * ONE6;

      if (remaining === 0n) {
        await expect(
          fund.connect(foundation).coverBadDebt(mk.marketAddr, b, amount),
          `[ifund PROD-CAP] cover after cap exhausted (w=${w})`
        ).to.be.revertedWithCustomError(fund, "CapExceeded");
        continue;
      }

      expect(owed, `[ifund PROD] borrower unexpectedly debt-free w=${w}`).to.be.gt(0n);
      const before = await debt.balanceOf(fundAddr);
      await (await fund.connect(foundation).coverBadDebt(mk.marketAddr, b, amount)).wait();
      const delta = before - (await debt.balanceOf(fundAddr));
      expect(delta, `[ifund PROD] paid != 50 w=${w}`).to.equal(amount);
      covered += delta;
      windows++;

      // Immediately trying again (same block-ish, within 7 days) must revert.
      await expect(
        fund.connect(foundation).coverBadDebt(mk.marketAddr, b, amount),
        `[ifund G5 PROD] second cover inside 7-day window succeeded (w=${w})`
      ).to.be.revertedWithCustomError(fund, "CooldownActive");

      // Advance just under 7 days: still locked.
      await time.increase(Number(SEVEN_DAYS) - 60);
      await network.provider.send("evm_mine");
      if ((await fund.coverageRemaining(mk.marketAddr)) > 0n) {
        await expect(
          fund.connect(foundation).coverBadDebt(mk.marketAddr, b, amount),
          `[ifund G5 PROD] cover at T-60s succeeded (w=${w})`
        ).to.be.revertedWithCustomError(fund, "CooldownActive");
      }
      // Cross the boundary.
      await time.increase(120);
      await network.provider.send("evm_mine");
    }

    // Exactly cap/50 = 6 successful windows, covered == cap, and the mapping /
    // real balance agree.
    expect(windows, "[ifund PROD] wrong number of covered windows").to.equal(6);
    expect(covered, "[ifund PROD] total covered != cap").to.equal(cap);
    const info = await fund.markets(mk.marketAddr);
    expect(info[2], "[ifund PROD] on-chain covered != cap").to.equal(cap);
    expect(await fund.coverageRemaining(mk.marketAddr), "[ifund PROD] remaining not zero").to.equal(0n);
    expect(await debt.balanceOf(fundAddr), "[ifund PROD G1] balance mismatch")
      .to.equal(await fund.balances(debtAddr));
    expect(100_000n * ONE6 - (await fund.balances(debtAddr)), "[ifund PROD G2] conservation")
      .to.equal(cap);

    console.log(`      [ifund prod] 7-day windows covered=${windows} totalCovered=${ethers.formatUnits(covered, 6)} cap=${ethers.formatUnits(cap, 6)}`);
  });
});
