// Hardhat test suite for the Week-1 sAERE + AereSink stack.
//
// Covers:
//   1. WAERE — wrap/unwrap solvency, ERC-20 invariants
//   2. sAERE — dead-share seed, ERC-4626 invariants, INFLATION ATTACK
//      defense, donation-attack neutrality, rate appreciation on direct
//      WAERE transfer (the sink mechanism)
//   3. AereSink — bps validation, native-AERE path (no swap), non-AERE
//      path (mock router), dust accumulation + sweep, reentrancy guard,
//      "no admin" surface (no setters callable)
//
// Run: npx hardhat test test/saere-sink.test.js

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

const DEAD_ADDRESS = "0x000000000000000000000000000000000000dEaD";
const ONE        = 10n ** 18n;
const DEAD_SEED  = 1000n;

async function deployStack(opts = {}) {
  const [deployer, alice, bob, fakeRouter, feeder] = await ethers.getSigners();

  const WAERE = await (await ethers.getContractFactory("WAERE")).deploy();
  await WAERE.waitForDeployment();
  const waere = await WAERE.getAddress();

  // Compute the next contract address (sAERE) to pre-approve.
  const nonce = await ethers.provider.getTransactionCount(deployer.address);
  const futureSaere = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 2 });
  // Wrap 1000 AERE
  await WAERE.connect(deployer).deposit({ value: DEAD_SEED });
  await WAERE.connect(deployer).approve(futureSaere, DEAD_SEED);

  const sAERE_C = await ethers.getContractFactory("sAERE");
  const sAERE = await sAERE_C.deploy(waere);
  await sAERE.waitForDeployment();
  const saere = await sAERE.getAddress();

  // For Sink we need a router; tests that don't exercise the swap path use a
  // dummy address. Tests that do use a MockRouter (constructed inline below).
  const routerAddr = opts.router || fakeRouter.address;

  // Default bps 1500/4000/4500. Some tests override.
  const burnBps    = opts.burnBps    ?? 1500;
  const buybackBps = opts.buybackBps ?? 4000;
  const stakerBps  = opts.stakerBps  ?? 4500;

  // Use a fresh "burn vault" — for tests just a unique EOA so we can check balance.
  const burnVault = opts.burnVault || bob.address;

  const Sink = await ethers.getContractFactory("AereSink");
  const sink = await Sink.deploy(waere, burnVault, saere, routerAddr, burnBps, buybackBps, stakerBps, 200, ethers.ZeroAddress);
  await sink.waitForDeployment();
  const sinkAddr = await sink.getAddress();

  return { deployer, alice, bob, fakeRouter, feeder, WAERE, sAERE, sink, waere, saere, sinkAddr, burnVault };
}

describe("WAERE — wrap/unwrap solvency", function () {
  it("totalSupply equals contract native balance after wrap and unwrap", async function () {
    const { WAERE, alice } = await deployStack();
    await WAERE.connect(alice).deposit({ value: 5n * ONE });
    expect(await WAERE.totalSupply()).to.equal(5n * ONE + DEAD_SEED);
    expect(await ethers.provider.getBalance(await WAERE.getAddress())).to.equal(5n * ONE + DEAD_SEED);

    await WAERE.connect(alice).withdraw(3n * ONE);
    expect(await WAERE.totalSupply()).to.equal(2n * ONE + DEAD_SEED);
    expect(await ethers.provider.getBalance(await WAERE.getAddress())).to.equal(2n * ONE + DEAD_SEED);
  });

  it("rejects withdraw above balance", async function () {
    const { WAERE, alice } = await deployStack();
    await WAERE.connect(alice).deposit({ value: ONE });
    await expect(WAERE.connect(alice).withdraw(2n * ONE)).to.be.revertedWith("WAERE: insufficient balance");
  });

  it("ERC-20 transferFrom respects allowance", async function () {
    const { WAERE, alice, bob } = await deployStack();
    await WAERE.connect(alice).deposit({ value: 10n * ONE });
    await WAERE.connect(alice).approve(bob.address, 4n * ONE);
    await WAERE.connect(bob).transferFrom(alice.address, bob.address, 3n * ONE);
    expect(await WAERE.balanceOf(bob.address)).to.equal(3n * ONE);
    expect(await WAERE.allowance(alice.address, bob.address)).to.equal(ONE);
    await expect(
      WAERE.connect(bob).transferFrom(alice.address, bob.address, 2n * ONE)
    ).to.be.revertedWith("WAERE: insufficient allowance");
  });
});

describe("sAERE — ERC-4626 receipt vault", function () {
  it("seeds 1000 dead shares at deploy and totalSupply == seed", async function () {
    const { sAERE } = await deployStack();
    expect(await sAERE.balanceOf(DEAD_ADDRESS)).to.equal(DEAD_SEED);
    expect(await sAERE.totalSupply()).to.equal(DEAD_SEED);
  });

  it("inflation-attack: first depositor cannot manipulate price", async function () {
    // Classic attack: deploy fresh, attacker deposits 1 wei underlying to mint 1 share,
    // then directly transfers a large WAERE amount to vault, so totalAssets jumps.
    // The next depositor calling deposit(x) receives floor(x * 1 / totalAssets) shares.
    // With dead-share seed, totalShares starts at 1000 not 1, so the math denominator
    // is large enough that a victim depositing 1e18 underlying receives at least
    // 1e18 * 1000 / (1 + N) shares — never zero.
    const { sAERE, WAERE, alice, bob } = await deployStack();

    // Bob acts as the attacker.
    await WAERE.connect(bob).deposit({ value: 1n });
    await WAERE.connect(bob).approve(await sAERE.getAddress(), 1n);
    await sAERE.connect(bob).deposit(1n, bob.address);
    // R5 HIGH-4: with _decimalsOffset()=6 the share/asset ratio starts at ~1e6 so
    // a 1-wei deposit yields ~1000 shares (rounded down via OZ math), not 1.
    expect(await sAERE.balanceOf(bob.address)).to.be.gte(1n);

    // Attacker tries to spike totalAssets by direct transfer (donation attack).
    // Use 1000 WAERE — far above the dead seed but within hardhat default balance.
    await WAERE.connect(bob).deposit({ value: 1_000n * ONE });
    await WAERE.connect(bob).transfer(await sAERE.getAddress(), 1_000n * ONE);

    // Alice now deposits 1 AERE (1e18). She should receive a non-zero share count.
    await WAERE.connect(alice).deposit({ value: ONE });
    await WAERE.connect(alice).approve(await sAERE.getAddress(), ONE);
    await sAERE.connect(alice).deposit(ONE, alice.address);
    const aliceShares = await sAERE.balanceOf(alice.address);
    expect(aliceShares).to.be.gt(0n);
  });

  it("rate appreciates on direct WAERE transfer to vault (the sink mechanism)", async function () {
    const { sAERE, WAERE, alice, bob } = await deployStack();

    // Alice deposits 100 AERE.
    await WAERE.connect(alice).deposit({ value: 100n * ONE });
    await WAERE.connect(alice).approve(await sAERE.getAddress(), 100n * ONE);
    await sAERE.connect(alice).deposit(100n * ONE, alice.address);

    const pricePerShareBefore = await sAERE.pricePerShare();

    // Bob (acting as the sink) transfers 10 WAERE directly to the vault — no shares minted.
    await WAERE.connect(bob).deposit({ value: 10n * ONE });
    await WAERE.connect(bob).transfer(await sAERE.getAddress(), 10n * ONE);

    const pricePerShareAfter = await sAERE.pricePerShare();
    expect(pricePerShareAfter).to.be.gt(pricePerShareBefore);

    // Alice now redeems her full share balance and receives ~110 WAERE
    // (minus dust from dead-share seed proportional bleed).
    const aliceShares = await sAERE.balanceOf(alice.address);
    await sAERE.connect(alice).redeem(aliceShares, alice.address, alice.address);
    const aliceWaere = await WAERE.balanceOf(alice.address);
    expect(aliceWaere).to.be.gte(109n * ONE);
    expect(aliceWaere).to.be.lte(110n * ONE);
  });

  it("has no admin surface (sAERE exposes only ERC-4626 functions)", async function () {
    const { sAERE } = await deployStack();
    // Reading interfaces — assert each admin-ish selector either returns empty
    // data or reverts; never returns a real value.
    const sigs = [
      ethers.id("owner()").slice(0, 10),
      ethers.id("pause()").slice(0, 10),
      ethers.id("setOwner(address)").slice(0, 10),
      ethers.id("upgradeTo(address)").slice(0, 10),
      ethers.id("setRouter(address)").slice(0, 10),
    ];
    for (const sig of sigs) {
      try {
        const res = await ethers.provider.call({ to: await sAERE.getAddress(), data: sig });
        // If it returned, must be empty (no value), otherwise it would be a leak.
        expect(res === "0x" || res.length <= 2).to.equal(true);
      } catch (e) {
        // Reverting is also acceptable — means the selector is unknown.
      }
    }
  });
});

describe("AereSink — bps & immutables", function () {
  it("constructor reverts if bps do not sum to 10000", async function () {
    const [d] = await ethers.getSigners();
    const Sink = await ethers.getContractFactory("AereSink");
    await expect(
      Sink.deploy(d.address, d.address, d.address, d.address, 1000, 1000, 1000, 200, ethers.ZeroAddress)
    ).to.be.revertedWithCustomError(Sink, "BpsMustSumTo10000");
  });

  it("constructor reverts on zero address inputs", async function () {
    const [d] = await ethers.getSigners();
    const Sink = await ethers.getContractFactory("AereSink");
    await expect(
      Sink.deploy(ethers.ZeroAddress, d.address, d.address, d.address, 1500, 4000, 4500, 200, ethers.ZeroAddress)
    ).to.be.revertedWithCustomError(Sink, "ZeroAddress");
  });

  it("exposes only immutable getters — no setters reachable", async function () {
    const { sink } = await deployStack();
    const sigs = [
      ethers.id("setSink(address)").slice(0, 10),
      ethers.id("setRouter(address)").slice(0, 10),
      ethers.id("setBps(uint16,uint16,uint16)").slice(0, 10),
      ethers.id("pause()").slice(0, 10),
      ethers.id("owner()").slice(0, 10),
      ethers.id("transferOwnership(address)").slice(0, 10),
    ];
    for (const sig of sigs) {
      try {
        const res = await ethers.provider.call({ to: await sink.getAddress(), data: sig });
        expect(res === "0x" || res.length <= 2).to.equal(true);
      } catch (e) {
        // Reverting is also acceptable.
      }
    }
  });
});

describe("AereSink — native-AERE flush path (no swap)", function () {
  it("routes AERE input to burn vault and sAERE pro-rata", async function () {
    const { WAERE, sAERE, sink, alice, burnVault } = await deployStack();
    const sinkAddr = await sink.getAddress();
    const saereAddr = await sAERE.getAddress();

    // Alice gets 100 WAERE and approves the sink.
    await WAERE.connect(alice).deposit({ value: 100n * ONE });
    await WAERE.connect(alice).approve(sinkAddr, 100n * ONE);

    const burnBefore  = await WAERE.balanceOf(burnVault);
    const saereBefore = await WAERE.balanceOf(saereAddr);

    await sink.connect(alice).flush(await WAERE.getAddress(), 100n * ONE);

    const burnAfter  = await WAERE.balanceOf(burnVault);
    const saereAfter = await WAERE.balanceOf(saereAddr);

    // 1500 + 4000 = 5500 bps to burn (combined since input is AERE).
    expect(burnAfter - burnBefore).to.equal(55n * ONE);
    // 4500 bps to staker yield = 45 WAERE.
    expect(saereAfter - saereBefore).to.equal(45n * ONE);
  });

  it("emits Flushed with correct buckets", async function () {
    const { WAERE, sink, alice } = await deployStack();
    const sinkAddr = await sink.getAddress();
    await WAERE.connect(alice).deposit({ value: 100n * ONE });
    await WAERE.connect(alice).approve(sinkAddr, 100n * ONE);

    await expect(sink.connect(alice).flush(await WAERE.getAddress(), 100n * ONE))
      .to.emit(sink, "Flushed")
      .withArgs(alice.address, await WAERE.getAddress(), 100n * ONE, 15n * ONE, 40n * ONE, 45n * ONE);
  });

  it("reverts on zero amount", async function () {
    const { WAERE, sink, alice } = await deployStack();
    await WAERE.connect(alice).approve(await sink.getAddress(), 0);
    await expect(sink.connect(alice).flush(await WAERE.getAddress(), 0))
      .to.be.revertedWithCustomError(sink, "ZeroAmount");
  });
});

describe("AereSink — dust sweep", function () {
  it("sweepDust re-routes residual balance through the same buckets", async function () {
    const { WAERE, sAERE, sink, alice, burnVault } = await deployStack();
    const sinkAddr = await sink.getAddress();
    const saereAddr = await sAERE.getAddress();

    // Someone (anyone) sends 200 WAERE directly to the sink, bypassing flush.
    await WAERE.connect(alice).deposit({ value: 200n * ONE });
    await WAERE.connect(alice).transfer(sinkAddr, 200n * ONE);

    const burnBefore  = await WAERE.balanceOf(burnVault);
    const saereBefore = await WAERE.balanceOf(saereAddr);

    await sink.connect(alice).sweepDust(await WAERE.getAddress());

    expect(await WAERE.balanceOf(burnVault) - burnBefore).to.equal(110n * ONE);
    expect(await WAERE.balanceOf(saereAddr) - saereBefore).to.equal(90n * ONE);
  });

  it("sweepDust reverts when balance is zero", async function () {
    const { WAERE, sink } = await deployStack();
    await expect(sink.sweepDust(await WAERE.getAddress()))
      .to.be.revertedWithCustomError(sink, "ZeroAmount");
  });
});
