// AereInsuranceFund + repayOnBehalfOf tests.

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

const ONE6  = 10n ** 6n;
const ONE18 = 10n ** 18n;

async function deployStack(opts = {}) {
  const ERC20 = await ethers.getContractFactory("MockERC20Lending");
  const collateral = await ERC20.deploy("BUIDL.e", "BUIDL.e", 18);
  await collateral.waitForDeployment();
  const debt = await ERC20.deploy("USDC.e", "USDC.e", 6);
  await debt.waitForDeployment();

  const Agg = await ethers.getContractFactory("MockAggregator");
  const aggC = await Agg.deploy(1_00000000);
  await aggC.waitForDeployment();
  const aggD = await Agg.deploy(1_00000000);
  await aggD.waitForDeployment();

  const Oracle = await ethers.getContractFactory("AereLendingOracle");
  const oracle = await Oracle.deploy();
  await oracle.waitForDeployment();
  // 30d staleness so 7-day insurance-fund timelock doesn't trip FeedStale.
  await oracle.configureFeed(await collateral.getAddress(), 0, await aggC.getAddress(), ethers.ZeroHash, 30 * 86400, opts.devBps ?? 5000, 8);
  await oracle.configureFeed(await debt.getAddress(),       0, await aggD.getAddress(), ethers.ZeroHash, 30 * 86400, opts.devBps ?? 5000, 8);

  const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
  await WAERE.waitForDeployment();
  const burn = await (await ethers.getContractFactory("AereFeeBurnVault")).deploy();
  await burn.waitForDeployment();
  const sink = await (await ethers.getContractFactory("AereSink")).deploy(
    await WAERE.getAddress(), await burn.getAddress(),
    await WAERE.getAddress(), await WAERE.getAddress(),
    1500, 4000, 4500, 200, ethers.ZeroAddress
  );
  await sink.waitForDeployment();

  const cfg = {
    collateral: await collateral.getAddress(),
    debtToken: await debt.getAddress(),
    sink: await sink.getAddress(),
    oracle: await oracle.getAddress(),
    ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500, borrowFeeBps: 100,
    marketDebtCap: 1_000_000n * ONE6,
    collateralDecimals: 18, debtDecimals: 6,
  };
  const Market = await ethers.getContractFactory("AereLendingMarket");
  const market = await Market.deploy(cfg);
  await market.waitForDeployment();

  return { collateral, debt, oracle, market, aggC, sink };
}

describe("AereLendingMarket.repayOnBehalfOf", function () {
  it("permissionless: anyone can repay anyone else's debt", async function () {
    const { collateral, debt, market } = await deployStack();
    const [, alice, bob, samaritan] = await ethers.getSigners();
    await debt.mint(alice.address, 100_000n * ONE6);
    await debt.connect(alice).approve(await market.getAddress(), 100_000n * ONE6);
    await market.connect(alice).supply(100_000n * ONE6);
    await collateral.mint(bob.address, 100n * ONE18);
    await collateral.connect(bob).approve(await market.getAddress(), 100n * ONE18);
    await market.connect(bob).depositCollateral(100n * ONE18);
    await market.connect(bob).borrow(50n * ONE6);

    expect(await market.debtBalance(bob.address)).to.equal(50n * ONE6);

    await debt.mint(samaritan.address, 50n * ONE6);
    await debt.connect(samaritan).approve(await market.getAddress(), 50n * ONE6);
    await market.connect(samaritan).repayOnBehalfOf(bob.address, 50n * ONE6);
    expect(await market.debtBalance(bob.address)).to.equal(0n);
  });
});

describe("AereInsuranceFund", function () {
  async function deployFund(opts = {}) {
    const [, foundation] = await ethers.getSigners();
    const Fund = await ethers.getContractFactory("AereInsuranceFund");
    const fund = await Fund.deploy(foundation.address, opts.cooldown ?? 3600);
    await fund.waitForDeployment();
    return { fund, foundation };
  }

  /// Helper: propose + warp 7 days + execute registerMarket.
  async function registerWithTimelock(fund, foundation, marketAddr, cap) {
    await fund.connect(foundation).proposeMarket(marketAddr, cap);
    await ethers.provider.send("evm_increaseTime", [7 * 86400 + 1]);
    await ethers.provider.send("evm_mine", []);
    await fund.connect(foundation).registerMarket(marketAddr);
  }

  it("permissionless donate increases balance + emits Donated", async function () {
    const { debt } = await deployStack();
    const { fund } = await deployFund();
    const [, , donor] = await ethers.getSigners();
    await debt.mint(donor.address, 1000n * ONE6);
    await debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6);
    await expect(fund.connect(donor).donate(await debt.getAddress(), 1000n * ONE6))
      .to.emit(fund, "Donated");
    expect(await fund.balances(await debt.getAddress())).to.equal(1000n * ONE6);
  });

  it("only Foundation can proposeMarket + reverts on double-propose-after-register", async function () {
    const env = await deployStack();
    const { fund, foundation } = await deployFund();
    const [, , alice] = await ethers.getSigners();
    const marketAddr = await env.market.getAddress();
    // Non-foundation cannot propose.
    await expect(fund.connect(alice).proposeMarket(marketAddr, 10_000n * ONE6))
      .to.be.revertedWithCustomError(fund, "NotFoundation");
    // Foundation proposes.
    await fund.connect(foundation).proposeMarket(marketAddr, 10_000n * ONE6);
    // Cannot execute before timelock.
    await expect(fund.connect(foundation).registerMarket(marketAddr))
      .to.be.revertedWithCustomError(fund, "TimelockNotElapsed");
    // Advance 7 days + 1s.
    await ethers.provider.send("evm_increaseTime", [7 * 86400 + 1]);
    await ethers.provider.send("evm_mine", []);
    // Anyone may execute the pending registration.
    await fund.connect(alice).registerMarket(marketAddr);
    // Now AlreadyRegistered for any further proposal.
    await expect(fund.connect(foundation).proposeMarket(marketAddr, 1n))
      .to.be.revertedWithCustomError(fund, "AlreadyRegistered");
  });

  it("H4 fix: anyone can cancelPendingMarket during 7-day delay", async function () {
    const env = await deployStack();
    const { fund, foundation } = await deployFund();
    const [, , alice] = await ethers.getSigners();
    const marketAddr = await env.market.getAddress();
    await fund.connect(foundation).proposeMarket(marketAddr, 10_000n * ONE6);
    // Alice (community observer) detects this looks fishy + cancels.
    await fund.connect(alice).cancelPendingMarket(marketAddr, "suspicious admin powers");
    await ethers.provider.send("evm_increaseTime", [7 * 86400 + 1]);
    await ethers.provider.send("evm_mine", []);
    // Even after timelock, the cancelled registration cannot be executed.
    await expect(fund.connect(foundation).registerMarket(marketAddr))
      .to.be.revertedWithCustomError(fund, "NoPendingRegistration");
  });

  it("Foundation covers BadDebt via market.repayOnBehalfOf", async function () {
    const env = await deployStack({ devBps: 0 });
    const { fund, foundation } = await deployFund({ cooldown: 0 });
    const [, , alice, bob, keeper, donor] = await ethers.getSigners();

    // Set up bad debt: bob borrows, price crashes, keeper liquidates insolvent.
    await env.debt.mint(alice.address, 100_000n * ONE6);
    await env.debt.connect(alice).approve(await env.market.getAddress(), 100_000n * ONE6);
    await env.market.connect(alice).supply(100_000n * ONE6);
    await env.collateral.mint(bob.address, 100n * ONE18);
    await env.collateral.connect(bob).approve(await env.market.getAddress(), 100n * ONE18);
    await env.market.connect(bob).depositCollateral(100n * ONE18);
    await env.market.connect(bob).borrow(60n * ONE6);
    await env.aggC.set(30_000_000); // $0.30
    await env.market.accrueInterest();
    await env.debt.mint(keeper.address, 200n * ONE6);
    await env.debt.connect(keeper).approve(await env.market.getAddress(), 200n * ONE6);
    await env.market.connect(keeper).liquidate(bob.address, 60n * ONE6);
    const remaining = await env.market.debtBalance(bob.address);
    expect(remaining).to.be.gt(0n);

    // Fund the insurance pool + register the market.
    await env.debt.mint(donor.address, 1000n * ONE6);
    await env.debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6);
    await fund.connect(donor).donate(await env.debt.getAddress(), 1000n * ONE6);
    await registerWithTimelock(fund, foundation, await env.market.getAddress(), 1000n * ONE6);

    // Force an accrual + read post-accrual to lock in the current debt
    // for this same block. Then pass exactly that to coverBadDebt — same
    // block means no extra interest accrues between view and tx.
    await env.market.accrueInterest();
    const remainingNow = await env.market.debtBalance(bob.address);
    await expect(fund.connect(foundation).coverBadDebt(await env.market.getAddress(), bob.address, remainingNow))
      .to.emit(fund, "BadDebtCovered")
      .to.emit(env.market, "Repay");
    // Allow tiny dust (sub-cent) from per-second interest between accrue + cover.
    const final = await env.market.debtBalance(bob.address);
    expect(final).to.be.lt(1000n);
  });

  it("rejects non-Foundation caller on coverBadDebt", async function () {
    const env = await deployStack();
    const { fund } = await deployFund();
    const [, , alice] = await ethers.getSigners();
    await expect(fund.connect(alice).coverBadDebt(await env.market.getAddress(), alice.address, 1n))
      .to.be.revertedWithCustomError(fund, "NotFoundation");
  });

  it("enforces lifetime cap", async function () {
    const env = await deployStack({ devBps: 0 });
    const { fund, foundation } = await deployFund({ cooldown: 0 });
    const [, , alice, bob, keeper, donor] = await ethers.getSigners();
    // Bad debt setup (shortened).
    await env.debt.mint(alice.address, 100_000n * ONE6);
    await env.debt.connect(alice).approve(await env.market.getAddress(), 100_000n * ONE6);
    await env.market.connect(alice).supply(100_000n * ONE6);
    await env.collateral.mint(bob.address, 100n * ONE18);
    await env.collateral.connect(bob).approve(await env.market.getAddress(), 100n * ONE18);
    await env.market.connect(bob).depositCollateral(100n * ONE18);
    await env.market.connect(bob).borrow(60n * ONE6);
    await env.aggC.set(30_000_000);
    await env.market.accrueInterest();
    await env.debt.mint(keeper.address, 200n * ONE6);
    await env.debt.connect(keeper).approve(await env.market.getAddress(), 200n * ONE6);
    await env.market.connect(keeper).liquidate(bob.address, 60n * ONE6);

    await env.debt.mint(donor.address, 1000n * ONE6);
    await env.debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6);
    await fund.connect(donor).donate(await env.debt.getAddress(), 1000n * ONE6);
    // Cap = 5 USDC.e — tiny.
    await registerWithTimelock(fund, foundation, await env.market.getAddress(), 5n * ONE6);

    const remaining = await env.market.debtBalance(bob.address);
    await expect(fund.connect(foundation).coverBadDebt(await env.market.getAddress(), bob.address, remaining))
      .to.be.revertedWithCustomError(fund, "CapExceeded");
  });

  it("enforces cooldown between consecutive coverages", async function () {
    const env = await deployStack({ devBps: 0 });
    const { fund, foundation } = await deployFund({ cooldown: 3600 });
    const [, , alice, bob, keeper, donor] = await ethers.getSigners();
    await env.debt.mint(alice.address, 100_000n * ONE6);
    await env.debt.connect(alice).approve(await env.market.getAddress(), 100_000n * ONE6);
    await env.market.connect(alice).supply(100_000n * ONE6);
    await env.collateral.mint(bob.address, 100n * ONE18);
    await env.collateral.connect(bob).approve(await env.market.getAddress(), 100n * ONE18);
    await env.market.connect(bob).depositCollateral(100n * ONE18);
    await env.market.connect(bob).borrow(60n * ONE6);
    await env.aggC.set(30_000_000);
    await env.market.accrueInterest();
    await env.debt.mint(keeper.address, 200n * ONE6);
    await env.debt.connect(keeper).approve(await env.market.getAddress(), 200n * ONE6);
    await env.market.connect(keeper).liquidate(bob.address, 60n * ONE6);

    await env.debt.mint(donor.address, 1000n * ONE6);
    await env.debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6);
    await fund.connect(donor).donate(await env.debt.getAddress(), 1000n * ONE6);
    await registerWithTimelock(fund, foundation, await env.market.getAddress(), 1000n * ONE6);

    const remaining = await env.market.debtBalance(bob.address);
    const firstPay = remaining / 2n;
    // First cover bob's bad debt (real outstanding).
    await fund.connect(foundation).coverBadDebt(await env.market.getAddress(), bob.address, firstPay);
    // Setup another bad-debt borrower to attempt cover during cooldown.
    const [, , , , , , carol] = await ethers.getSigners();
    await env.collateral.mint(carol.address, 100n * ONE18);
    await env.collateral.connect(carol).approve(await env.market.getAddress(), 100n * ONE18);
    await env.market.connect(carol).depositCollateral(100n * ONE18);
    // Borrow before price recovers? We've already crashed price to $0.30.
    // Carol's borrow capacity at $0.30 = 100 * $0.30 * 70% = $21. Borrow $20.
    await env.market.connect(carol).borrow(20n * ONE6);
    await expect(
      fund.connect(foundation).coverBadDebt(await env.market.getAddress(), carol.address, 1n * ONE6)
    ).to.be.revertedWithCustomError(fund, "CooldownActive");
  });

  it("no admin withdrawal — funds are sticky", async function () {
    const { fund, foundation } = await deployFund();
    // No `withdraw` function exists on the contract — proven by the absence.
    // Just verify the Foundation has no escape hatch.
    expect(fund.interface.getFunction("withdraw")).to.equal(null);
  });

  it("rejects coverage of borrower with no debt", async function () {
    const env = await deployStack();
    const { fund, foundation } = await deployFund({ cooldown: 0 });
    const [, , alice, , , donor] = await ethers.getSigners();
    await env.debt.mint(donor.address, 1000n * ONE6);
    await env.debt.connect(donor).approve(await fund.getAddress(), 1000n * ONE6);
    await fund.connect(donor).donate(await env.debt.getAddress(), 1000n * ONE6);
    await registerWithTimelock(fund, foundation, await env.market.getAddress(), 1000n * ONE6);
    await expect(
      fund.connect(foundation).coverBadDebt(await env.market.getAddress(), alice.address, 1n)
    ).to.be.revertedWithCustomError(fund, "NoDebtToCover");
  });
});
