// Round 3 fix: AereSink oracle-based amountOutMin computation.
//
// Sandwich-resistance: amountOutMin must be derived from an EXTERNAL oracle
// (price feed independent of DEX pool state), not from DEX reserves.

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

const ONE = 10n ** 18n;

describe("AereSink — oracle floor (Round 3 H1 fix)", function () {

  async function deployWithOracle(oracleAddr) {
    const [deployer] = await ethers.getSigners();
    const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
    await WAERE.waitForDeployment();
    const FeeToken = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
    await FeeToken.waitForDeployment();

    const burnVault = await (await ethers.getContractFactory("AereFeeBurnVault")).deploy();
    await burnVault.waitForDeployment();

    const router = await (await ethers.getContractFactory("MockSinkRouter")).deploy();
    await router.waitForDeployment();
    // Router will return AERE (the first WAERE).
    await router.setOutToken(await WAERE.getAddress());

    const sink = await (await ethers.getContractFactory("AereSink")).deploy(
      await WAERE.getAddress(),          // AERE
      await burnVault.getAddress(),      // BURN_VAULT
      await WAERE.getAddress(),          // sAERE stub (just receives)
      await router.getAddress(),         // DEX_ROUTER
      1500, 4000, 4500,                  // burn/buyback/staker bps
      500,                               // 5% slippage
      oracleAddr,                        // ORACLE
    );
    await sink.waitForDeployment();

    return { deployer, WAERE, FeeToken, burnVault, router, sink };
  }

  it("ORACLE = address(0) → amountOutMin = 1 (legacy behavior, sandwich-vulnerable)", async function () {
    const { router, sink, FeeToken, deployer, WAERE } = await deployWithOracle(ethers.ZeroAddress);
    // Fund router with AERE so it can pay the swap output.
    await deployer.sendTransaction({ to: await WAERE.getAddress(), value: 100n * ONE });
    await WAERE.transfer(await router.getAddress(), 100n * ONE);

    // Get fee tokens via WAERE wrap, then approve sink.
    await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE });
    await FeeToken.approve(await sink.getAddress(), 10n * ONE);

    await sink.flush(await FeeToken.getAddress(), 10n * ONE);
    expect(await router.lastAmountOutMin()).to.equal(1n);
  });

  it("ORACLE set with equal prices → amountOutMin reflects 5% slippage tolerance", async function () {
    const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy();
    await oracle.waitForDeployment();
    const { router, sink, FeeToken, deployer, WAERE } = await deployWithOracle(await oracle.getAddress());

    // Set both at $1 (1e18).
    await oracle.setPrice(await FeeToken.getAddress(), ONE);
    await oracle.setPrice(await WAERE.getAddress(), ONE);

    // Fund router with AERE.
    await deployer.sendTransaction({ to: await WAERE.getAddress(), value: 100n * ONE });
    await WAERE.transfer(await router.getAddress(), 100n * ONE);

    await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE });
    await FeeToken.approve(await sink.getAddress(), 10n * ONE);

    await sink.flush(await FeeToken.getAddress(), 10n * ONE);
    // Two swaps inside flush: burn+buyback bundle (55% of 10 = 5.5 AERE)
    // and staker (45% = 4.5 AERE). Last call captured by mock:
    // Expected on staker swap (4.5 AERE): floor = 4.5 * 95% = 4.275 AERE.
    const lastFloor = await router.lastAmountOutMin();
    const expectedStaker = (45n * 10n * ONE) / 100n;     // 4.5e18
    const minStaker = expectedStaker * 9500n / 10_000n;  // 4.275e18
    expect(lastFloor).to.equal(minStaker);
  });

  it("ORACLE returns zero for one asset → fallback amountOutMin = 1", async function () {
    const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy();
    await oracle.waitForDeployment();
    const { router, sink, FeeToken, deployer, WAERE } = await deployWithOracle(await oracle.getAddress());

    // FeeToken priced; AERE not (returns 0).
    await oracle.setPrice(await FeeToken.getAddress(), ONE);
    // AERE intentionally not set.

    await deployer.sendTransaction({ to: await WAERE.getAddress(), value: 100n * ONE });
    await WAERE.transfer(await router.getAddress(), 100n * ONE);
    await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE });
    await FeeToken.approve(await sink.getAddress(), 10n * ONE);

    await sink.flush(await FeeToken.getAddress(), 10n * ONE);
    expect(await router.lastAmountOutMin()).to.equal(1n);
  });

  it("ORACLE reverts → fallback amountOutMin = 1", async function () {
    const oracle = await (await ethers.getContractFactory("MockPriceOracle")).deploy();
    await oracle.waitForDeployment();
    const { router, sink, FeeToken, deployer, WAERE } = await deployWithOracle(await oracle.getAddress());
    await oracle.setRevert(true);

    await deployer.sendTransaction({ to: await WAERE.getAddress(), value: 100n * ONE });
    await WAERE.transfer(await router.getAddress(), 100n * ONE);
    await deployer.sendTransaction({ to: await FeeToken.getAddress(), value: 10n * ONE });
    await FeeToken.approve(await sink.getAddress(), 10n * ONE);

    await sink.flush(await FeeToken.getAddress(), 10n * ONE);
    expect(await router.lastAmountOutMin()).to.equal(1n);
  });
});
