// AereLendingMarket + AereLendingOracle tests.
// Updated after H1/H2/H3/H5 fixes: scaled-principal accounting + supplyIndex
// + insolvent-liquidation clamp + oracle auto-poke + owner-only poke.

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

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

async function deployStack(opts = {}) {
  const ltvBps = opts.ltvBps ?? 9000;
  const liqThresholdBps = opts.liqThresholdBps ?? 9200;
  const liqBonusBps = opts.liqBonusBps ?? 100;
  const borrowFeeBps = opts.borrowFeeBps ?? 1000; // 10% of interest by default — sharp for visible test math

  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); // $1
  await aggC.waitForDeployment();
  const aggD = await Agg.deploy(1_00000000); // $1
  await aggD.waitForDeployment();

  const Oracle = await ethers.getContractFactory("AereLendingOracle");
  const oracle = await Oracle.deploy();
  await oracle.waitForDeployment();
  // configureFeed AUTO-POKES now — lastGoodPrice locked at config time.
  // Deviation cap is configurable per test scenario (default 50% for liquidation tests
  // that simulate big crash; tests that target deviation behaviour override explicitly).
  const devBps = opts.devBps ?? 5000;
  await oracle.configureFeed(await collateral.getAddress(), 0, await aggC.getAddress(), ethers.ZeroHash, 86400, devBps, 8);
  await oracle.configureFeed(await debt.getAddress(),       0, await aggD.getAddress(), ethers.ZeroHash, 86400, devBps, 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,
    liqThresholdBps,
    liqBonusBps,
    borrowFeeBps,
    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, aggD, sink };
}

async function advance(seconds) {
  await ethers.provider.send("evm_increaseTime", [seconds]);
  await ethers.provider.send("evm_mine", []);
}

describe("AereLendingMarket — accounting (H1/H2 fixes)", function () {
  it("supplier earns yield via supplyIndex (H2 fix)", async function () {
    const { collateral, debt, market } = await deployStack();
    const [, alice, bob] = await ethers.getSigners();

    // Alice supplies 100,000 USDC.e.
    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);
    const aliceBefore = await market.supplierBalance(alice.address);
    expect(aliceBefore).to.equal(100_000n * ONE6);

    // Bob borrows 50,000 against 100,000 BUIDL.e collateral.
    await collateral.mint(bob.address, 100_000n * ONE18);
    await collateral.connect(bob).approve(await market.getAddress(), 100_000n * ONE18);
    await market.connect(bob).depositCollateral(100_000n * ONE18);
    await market.connect(bob).borrow(50_000n * ONE6);

    // Advance 1 year.
    await advance(365 * 24 * 3600);
    await market.accrueInterest();

    // Alice's real balance now > principal.
    const aliceAfter = await market.supplierBalance(alice.address);
    expect(aliceAfter).to.be.gt(aliceBefore);
    // ~5% APY * 50% utilisation * 90% net (10% to sink) ≈ 2.25% on full supply
    // Sanity floor: at least 1% growth.
    expect(aliceAfter - aliceBefore).to.be.gt(1000n * ONE6);
  });

  it("sink fee accumulates as pendingSinkFee (H2 fix) + flushable", async function () {
    const { collateral, debt, market } = await deployStack({ borrowFeeBps: 2000 });
    const [, alice, bob] = 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, 100_000n * ONE18);
    await collateral.connect(bob).approve(await market.getAddress(), 100_000n * ONE18);
    await market.connect(bob).depositCollateral(100_000n * ONE18);
    await market.connect(bob).borrow(50_000n * ONE6);
    await advance(365 * 24 * 3600);
    await market.accrueInterest();

    const pending = await market.pendingSinkFee();
    expect(pending).to.be.gt(0n);

    // flushSinkFee zeroes the pending.
    await market.flushSinkFee();
    expect(await market.pendingSinkFee()).to.equal(0n);
  });

  it("totalBorrowsCurrent grows with interest; cap still enforced (H1 fix)", async function () {
    const { collateral, debt, market } = await deployStack();
    const [, alice, bob] = await ethers.getSigners();
    await debt.mint(alice.address, 500_000n * ONE6);
    await debt.connect(alice).approve(await market.getAddress(), 500_000n * ONE6);
    await market.connect(alice).supply(500_000n * ONE6);
    await collateral.mint(bob.address, 100_000n * ONE18);
    await collateral.connect(bob).approve(await market.getAddress(), 100_000n * ONE18);
    await market.connect(bob).depositCollateral(100_000n * ONE18);
    await market.connect(bob).borrow(50_000n * ONE6);

    const t0 = await market.totalBorrowsCurrent();
    expect(t0).to.equal(50_000n * ONE6);
    await advance(180 * 24 * 3600);
    await market.accrueInterest();
    const t1 = await market.totalBorrowsCurrent();
    expect(t1).to.be.gt(t0);
  });

  it("repay correctly reduces scaled debt; no underflow on totalBorrows", async function () {
    const { collateral, debt, market } = await deployStack();
    const [, alice, bob] = 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, 100_000n * ONE18);
    await collateral.connect(bob).approve(await market.getAddress(), 100_000n * ONE18);
    await market.connect(bob).depositCollateral(100_000n * ONE18);
    await market.connect(bob).borrow(50_000n * ONE6);

    await advance(30 * 24 * 3600);
    await market.accrueInterest();

    const owed = await market.debtBalance(bob.address);
    expect(owed).to.be.gt(50_000n * ONE6);
    // Overpay slightly so that even if interest accrues between view + tx,
    // the contract clamps `pay` to the exact `debtReal` and fully zeros state.
    const overpay = owed + 1000n * ONE6;
    await debt.mint(bob.address, overpay);
    await debt.connect(bob).approve(await market.getAddress(), overpay);
    await market.connect(bob).repay(overpay);
    expect(await market.debtBalance(bob.address)).to.equal(0n);
    expect(await market.totalBorrowsScaled()).to.equal(0n);
  });
});

describe("AereLendingMarket — liquidation (H3 fix)", function () {
  it("partial liquidation routes correctly", async function () {
    const { collateral, debt, market, aggC } = await deployStack({
      ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500,
    });
    const [, alice, bob, keeper] = 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, 1000n * ONE18);
    await collateral.connect(bob).approve(await market.getAddress(), 1000n * ONE18);
    await market.connect(bob).depositCollateral(1000n * ONE18);
    await market.connect(bob).borrow(700n * ONE6);

    await aggC.set(80000000); // $0.80 — liquidatable
    await market.accrueInterest();

    await debt.mint(keeper.address, 500n * ONE6);
    await debt.connect(keeper).approve(await market.getAddress(), 500n * ONE6);
    await expect(market.connect(keeper).liquidate(bob.address, 300n * ONE6))
      .to.emit(market, "Liquidated");
  });

  it("insolvent position: clamps pay AND emits BadDebt (H3 fix)", async function () {
    const { collateral, debt, market, aggC } = await deployStack({
      ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500,
      devBps: 0, // disable deviation cap so the simulated crash works
    });
    const [, alice, bob, keeper] = 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(60n * ONE6); // 60% LTV at $1/$1 — safe

    // Collateral crashes to $0.30 — collateral worth $30, debt $60 → deeply insolvent.
    await aggC.set(30000000);
    await market.accrueInterest();

    await debt.mint(keeper.address, 200n * ONE6);
    await debt.connect(keeper).approve(await market.getAddress(), 200n * ONE6);

    const keeperBefore = await debt.balanceOf(keeper.address);
    const colBefore = await collateral.balanceOf(keeper.address);
    await expect(market.connect(keeper).liquidate(bob.address, 60n * ONE6))
      .to.emit(market, "BadDebt");

    const keeperAfter = await debt.balanceOf(keeper.address);
    const colAfter = await collateral.balanceOf(keeper.address);
    const paid = keeperBefore - keeperAfter;
    const received = colAfter - colBefore;
    // Liquidator received some collateral and paid LESS than requested 60.
    expect(received).to.be.gt(0n);
    expect(paid).to.be.lt(60n * ONE6);
    // Bob's collateral is now zero; debt residual remains.
    expect(await market.collateralOf(bob.address)).to.equal(0n);
    expect(await market.debtBalance(bob.address)).to.be.gt(0n);
  });
});

describe("AereLendingMarket — LIQUIDATIONS NEVER PAUSE (oracle-freeze fix, 2026-07-12)", function () {
  // Adversarial-review HIGH: getPrice()'s pause/deviation/staleness reverts froze
  // liquidate(), contradicting the "LIQUIDATIONS NEVER PAUSE" invariant. Fix: the
  // liquidation path reads getPriceForLiquidation() which never freezes.
  async function liquidatablePosition() {
    // 10% deviation cap so a 30% crash trips getPrice's deviation revert.
    const s = await deployStack({ ltvBps: 7000, liqThresholdBps: 8000, liqBonusBps: 500, devBps: 1000 });
    const [, alice, bob, keeper] = await ethers.getSigners();
    await s.debt.mint(alice.address, 100_000n * ONE6);
    await s.debt.connect(alice).approve(await s.market.getAddress(), 100_000n * ONE6);
    await s.market.connect(alice).supply(100_000n * ONE6);
    await s.collateral.mint(bob.address, 1000n * ONE18);
    await s.collateral.connect(bob).approve(await s.market.getAddress(), 1000n * ONE18);
    await s.market.connect(bob).depositCollateral(1000n * ONE18);
    await s.market.connect(bob).borrow(700n * ONE6);
    await s.debt.mint(keeper.address, 500n * ONE6);
    await s.debt.connect(keeper).approve(await s.market.getAddress(), 500n * ONE6);
    return { ...s, alice, bob, keeper };
  }

  it("deviation-cap breach: getPrice() reverts, but liquidate() still succeeds", async function () {
    const { market, oracle, collateral, aggC, bob, keeper } = await liquidatablePosition();
    await aggC.set(70000000); // $0.70, a 30% crash > the 10% deviation cap
    await market.accrueInterest();
    await expect(oracle.getPrice(await collateral.getAddress()))
      .to.be.revertedWithCustomError(oracle, "FeedDeviationExceeded");
    await expect(market.connect(keeper).liquidate(bob.address, 300n * ONE6))
      .to.emit(market, "Liquidated");
  });

  it("paused feed: getPrice() reverts, but liquidate() still succeeds", async function () {
    const { market, oracle, collateral, aggC, bob, keeper } = await liquidatablePosition();
    await aggC.set(70000000);
    await market.accrueInterest();
    await oracle.setFeedPaused(await collateral.getAddress(), true);
    await expect(oracle.getPrice(await collateral.getAddress()))
      .to.be.revertedWithCustomError(oracle, "FeedPausedErr");
    await expect(market.connect(keeper).liquidate(bob.address, 300n * ONE6))
      .to.emit(market, "Liquidated");
  });

  it("stale feed: getPrice() reverts, but liquidate() still succeeds", async function () {
    const { market, oracle, collateral, aggC, bob, keeper } = await liquidatablePosition();
    await aggC.setStale(70000000, 1); // crash reading, ancient timestamp
    await market.accrueInterest();
    await expect(oracle.getPrice(await collateral.getAddress()))
      .to.be.revertedWithCustomError(oracle, "FeedStale");
    await expect(market.connect(keeper).liquidate(bob.address, 300n * ONE6))
      .to.emit(market, "Liquidated");
  });
});

describe("AereLendingOracle — H5 fix", function () {
  it("configureFeed auto-pokes lastGoodPrice (closes deviation-skip exploit)", async function () {
    const [deployer] = await ethers.getSigners();
    const Oracle = await ethers.getContractFactory("AereLendingOracle");
    const oracle = await Oracle.deploy();
    await oracle.waitForDeployment();
    const Agg = await ethers.getContractFactory("MockAggregator");
    const agg = await Agg.deploy(1_00000000);
    await agg.waitForDeployment();
    const ERC20 = await ethers.getContractFactory("MockERC20Lending");
    const t = await ERC20.deploy("X", "X", 18);
    await t.waitForDeployment();

    await oracle.configureFeed(await t.getAddress(), 0, await agg.getAddress(), ethers.ZeroHash, 86400, 1000, 8);
    const feed = await oracle.feeds(await t.getAddress());
    expect(feed.lastGoodPrice).to.equal(10n ** 18n);
  });

  it("poke is owner-only", async function () {
    const { oracle, collateral } = await deployStack();
    const [, alice] = await ethers.getSigners();
    await expect(oracle.connect(alice).poke(await collateral.getAddress()))
      .to.be.revertedWith("Ownable: caller is not the owner");
  });

  it("pokeBounded permissionless but rejects out-of-band moves", async function () {
    const { oracle, collateral, aggC } = await deployStack({ devBps: 1000 }); // 10% cap
    const [, alice] = await ethers.getSigners();
    await aggC.set(105_000_000); // $1.05 — within 10% deviation
    await oracle.connect(alice).pokeBounded(await collateral.getAddress());
    const feed = await oracle.feeds(await collateral.getAddress());
    expect(feed.lastGoodPrice).to.equal(105n * 10n ** 16n);

    // Now $1.50 — outside 10% cap from $1.05 reference.
    await aggC.set(150_000_000);
    await expect(oracle.connect(alice).pokeBounded(await collateral.getAddress()))
      .to.be.revertedWithCustomError(oracle, "FeedDeviationExceeded");
  });
});
