// AereRaaSFactory + AereRollupSettlement tests.

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

async function deployStack(opts = {}) {
  const [deployer, foundation, alice, bob, keeper] = await ethers.getSigners();
  // BOND_TOKEN + DEBT_TOKEN — reuse MockERC20Lending
  const ERC20 = await ethers.getContractFactory("MockERC20Lending");
  const bond = await ERC20.deploy("WAERE", "WAERE", 18);
  await bond.waitForDeployment();
  const debt = await ERC20.deploy("USDC.e", "USDC.e", 6);
  await debt.waitForDeployment();

  // Sink — reuse the real AereSink.
  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();

  // Factory.
  const Fac = await ethers.getContractFactory("AereRaaSFactory");
  const fac = await Fac.deploy(
    await bond.getAddress(),
    await sink.getAddress(),
    foundation.address,
    1000n * 10n ** 18n,    // registrationBond = 1000 WAERE
    1000,                  // minSinkBps = 10%
    6 * 3600,              // defaultChallengeWindow = 6h
  );
  await fac.waitForDeployment();

  return { deployer, foundation, alice, bob, keeper, bond, debt, sink, fac };
}

async function registerRollup(env, opts = {}) {
  const id = opts.id ?? ethers.id("rollup-" + Math.floor(Math.random() * 1e9));
  await env.bond.mint(env.alice.address, 1000n * 10n ** 18n);
  await env.bond.connect(env.alice).approve(await env.fac.getAddress(), 1000n * 10n ** 18n);
  const tx = await env.fac.connect(env.alice).registerRollup(
    id,
    env.alice.address,        // rollupOwner
    env.bob.address,          // sequencer
    await env.debt.getAddress(),
    opts.sinkBps ?? 1500,
    opts.rollupBps ?? 7500,
    opts.sequencerBps ?? 1000,
    opts.minChallengeBond ?? 100n * 10n ** 6n,
    opts.challengeWindow ?? 0,
    "ipfs://QmMeta"
  );
  await tx.wait();
  const r = await env.fac.rollups(id);
  const settlement = await ethers.getContractAt("AereRollupSettlement", r.settlement);
  return { id, settlement };
}

describe("AereRaaSFactory + AereRollupSettlement", function () {
  it("registers a rollup, deploys settlement contract", async function () {
    const env = await deployStack();
    const { id, settlement } = await registerRollup(env);
    const r = await env.fac.rollups(id);
    expect(r.registered).to.equal(true);
    expect(r.bond).to.equal(1000n * 10n ** 18n);
    expect(await settlement.ROLLUP_ID()).to.equal(id);
    expect(await settlement.SINK_BPS()).to.equal(1500);
  });

  it("rejects splits that don't sum to 10000", async function () {
    const env = await deployStack();
    await env.bond.mint(env.alice.address, 1000n * 10n ** 18n);
    await env.bond.connect(env.alice).approve(await env.fac.getAddress(), 1000n * 10n ** 18n);
    await expect(env.fac.connect(env.alice).registerRollup(
      ethers.id("badsplit"), env.alice.address, env.bob.address, await env.debt.getAddress(),
      1000, 7500, 1000, 0, 0, ""
    )).to.be.revertedWithCustomError(env.fac, "InvalidSplits");
  });

  it("rejects sinkBps below floor", async function () {
    const env = await deployStack();
    await env.bond.mint(env.alice.address, 1000n * 10n ** 18n);
    await env.bond.connect(env.alice).approve(await env.fac.getAddress(), 1000n * 10n ** 18n);
    await expect(env.fac.connect(env.alice).registerRollup(
      ethers.id("lowsink"), env.alice.address, env.bob.address, await env.debt.getAddress(),
      500, 8500, 1000, 0, 0, ""    // 5% < 10% floor
    )).to.be.revertedWithCustomError(env.fac, "SinkBpsTooLow");
  });

  it("sequencer proposes + auto-finalises after window", async function () {
    const env = await deployStack();
    const { settlement } = await registerRollup(env);
    await settlement.connect(env.bob).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1"));
    expect(await settlement.isFinalised(1)).to.equal(false);
    // Advance past 6h window.
    await ethers.provider.send("evm_increaseTime", [6 * 3600 + 60]);
    await ethers.provider.send("evm_mine", []);
    expect(await settlement.isFinalised(1)).to.equal(true);
    await expect(settlement.advanceFinalisation()).to.emit(settlement, "StateRootFinalised");
  });

  it("non-sequencer rejected on propose", async function () {
    const env = await deployStack();
    const { settlement } = await registerRollup(env);
    await expect(
      settlement.connect(env.alice).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1"))
    ).to.be.revertedWithCustomError(settlement, "NotSequencer");
  });

  it("challenge blocks finalisation; sequencer resolves IN WINDOW; bond routes", async function () {
    const env = await deployStack();
    const { settlement } = await registerRollup(env);
    await settlement.connect(env.bob).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1"));
    // Mint bond and challenge.
    await env.debt.mint(env.keeper.address, 200n * 10n ** 6n);
    await env.debt.connect(env.keeper).approve(await settlement.getAddress(), 200n * 10n ** 6n);
    await settlement.connect(env.keeper).challenge(1, 200n * 10n ** 6n, ethers.id("oh no"));
    // While window is still open, sequencer can resolve.
    // (Sequencer rejects challenge → keeps bond.)
    const seqBefore = await env.debt.balanceOf(env.bob.address);
    await settlement.connect(env.bob).resolveChallenge(1, false);
    expect(await env.debt.balanceOf(env.bob.address) - seqBefore).to.equal(200n * 10n ** 6n);
    // After window passes the root finalises (was never rejected).
    await ethers.provider.send("evm_increaseTime", [6 * 3600 + 60]);
    await ethers.provider.send("evm_mine", []);
    expect(await settlement.isFinalised(1)).to.equal(true);
  });

  it("revenue settlement splits sink/rollup/sequencer", async function () {
    const env = await deployStack();
    const { settlement } = await registerRollup(env, { sinkBps: 2000, rollupBps: 6000, sequencerBps: 2000 });
    // rollupOwner = alice settles 10,000 USDC.e.
    const amount = 10_000n * 10n ** 6n;
    await env.debt.mint(env.alice.address, amount);
    await env.debt.connect(env.alice).approve(await settlement.getAddress(), amount);
    const seqBefore = await env.debt.balanceOf(env.bob.address);
    const rolBefore = await env.debt.balanceOf(env.alice.address);
    await settlement.connect(env.alice).settleRevenue(amount);
    // sequencer (bob) +20%, rollupOwner (alice) +60% net (paid in 100% then receives 60%) — so net = -40%.
    expect(await env.debt.balanceOf(env.bob.address) - seqBefore).to.equal(amount * 2000n / 10000n);
    expect(rolBefore - (await env.debt.balanceOf(env.alice.address))).to.equal(amount * 4000n / 10000n);
  });

  it("exit returns bond on valid Foundation attestation", async function () {
    const env = await deployStack();
    const { id } = await registerRollup(env);
    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;

    const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
      ["string", "uint256", "address", "bytes32", "address", "uint64"],
      ["AereRaaSFactory:exit", (await ethers.provider.getNetwork()).chainId, await env.fac.getAddress(), id, env.alice.address, deadline]
    ));
    const sig = await env.foundation.signMessage(ethers.getBytes(hash));

    const before = await env.bond.balanceOf(env.alice.address);
    await env.fac.connect(env.alice).exitRollup(id, env.alice.address, deadline, sig);
    expect(await env.bond.balanceOf(env.alice.address) - before).to.equal(1000n * 10n ** 18n);
  });

  it("exit rejects bad signature", async function () {
    const env = await deployStack();
    const { id } = await registerRollup(env);
    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
    // signed by alice (NOT foundation)
    const hash = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
      ["string", "uint256", "address", "bytes32", "address", "uint64"],
      ["AereRaaSFactory:exit", (await ethers.provider.getNetwork()).chainId, await env.fac.getAddress(), id, env.alice.address, deadline]
    ));
    const sig = await env.alice.signMessage(ethers.getBytes(hash));
    await expect(
      env.fac.connect(env.alice).exitRollup(id, env.alice.address, deadline, sig)
    ).to.be.revertedWithCustomError(env.fac, "InvalidExitSig");
  });

  it("H6 fix: resolveChallenge after window closed reverts", async function () {
    const env = await deployStack();
    const { settlement } = await registerRollup(env);
    await settlement.connect(env.bob).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1"));
    await env.debt.mint(env.keeper.address, 200n * 10n ** 6n);
    await env.debt.connect(env.keeper).approve(await settlement.getAddress(), 200n * 10n ** 6n);
    await settlement.connect(env.keeper).challenge(1, 200n * 10n ** 6n, ethers.id("fraud"));

    // Wait past window — sequencer is now TOO LATE.
    await ethers.provider.send("evm_increaseTime", [6 * 3600 + 60]);
    await ethers.provider.send("evm_mine", []);
    await expect(
      settlement.connect(env.bob).resolveChallenge(1, false)
    ).to.be.revertedWithCustomError(settlement, "WindowClosed");
  });

  it("H6 fix: resolveChallengeExpired returns bond + marks root rejected", async function () {
    const env = await deployStack();
    const { settlement } = await registerRollup(env);
    await settlement.connect(env.bob).proposeStateRoot(1, ethers.id("root1"), ethers.id("blk1"));
    await env.debt.mint(env.keeper.address, 200n * 10n ** 6n);
    await env.debt.connect(env.keeper).approve(await settlement.getAddress(), 200n * 10n ** 6n);
    await settlement.connect(env.keeper).challenge(1, 200n * 10n ** 6n, ethers.id("fraud"));

    // Cannot expire-resolve while window still open.
    await expect(
      settlement.connect(env.alice).resolveChallengeExpired(1)
    ).to.be.revertedWithCustomError(settlement, "WindowOpen");

    await ethers.provider.send("evm_increaseTime", [6 * 3600 + 60]);
    await ethers.provider.send("evm_mine", []);

    const before = await env.debt.balanceOf(env.keeper.address);
    await settlement.connect(env.alice).resolveChallengeExpired(1);
    expect(await env.debt.balanceOf(env.keeper.address) - before).to.equal(200n * 10n ** 6n);
    expect(await settlement.isFinalised(1)).to.equal(false); // rejected
  });

  it("sequencer rotation by rollup owner", async function () {
    const env = await deployStack();
    const { settlement } = await registerRollup(env);
    await settlement.connect(env.alice).setSequencer(env.keeper.address);
    expect(await settlement.sequencer()).to.equal(env.keeper.address);
  });
});
