// Hardhat tests for AereRetroPGFRound1.
// Covers: fund → propose → challenge → dismiss → finalize → claim path,
// per-category cap enforcement, leaf replay protection, and sweep window.
//
// Run: npx hardhat test test/retropgf-round1.test.js

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

const ONE = 10n ** 18n;
const TOTAL = 10_000_000n * ONE;

// Per immediate_build_plan default split (10M / 6 buckets):
// 1 Infrastructure, 2 Dev tooling, 3 Applications, 4 Education, 5 Security, 6 Buffer
const CATEGORY_CAPS = [
  2_500_000n * ONE,  // 0 Infrastructure
  2_000_000n * ONE,  // 1 Dev tooling
  2_500_000n * ONE,  // 2 Applications
  1_000_000n * ONE,  // 3 Education
  1_500_000n * ONE,  // 4 Security
    500_000n * ONE,  // 5 Buffer
];

async function deployWithMerkle(leaves) {
  const [deployer, reserve, alice, bob] = await ethers.getSigners();

  // WAERE (reuse existing).
  const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
  await WAERE.waitForDeployment();

  // Round1.
  const Round = await ethers.getContractFactory("AereRetroPGFRound1");
  const round = await Round.deploy(await WAERE.getAddress(), reserve.address, TOTAL, CATEGORY_CAPS);
  await round.waitForDeployment();

  // Fund the deployer with 10M WAERE.
  // We can wrap from hardhat funded ETH. Hardhat default is 10k ETH; mainnet
  // genesis allocation here is irrelevant. Wrap 10M ETH? hardhat default isn't
  // enough. Instead, monkey-patch by directly minting via .deposit() chunks
  // is impossible — we don't have 10M ETH. Use a smaller TOTAL for tests
  // by deploying with smaller caps.
  // Alternative: deploy a fresh round with smaller TOTAL for tests.
  return { deployer, reserve, alice, bob, WAERE, round };
}

// Smaller test fixture so we can fund within hardhat default balance.
async function deploySmall() {
  const [deployer, reserve, alice, bob, carol] = await ethers.getSigners();
  const TOTAL_SMALL = 600n * ONE;
  const CATEGORY_CAPS_SMALL = [100n * ONE, 100n * ONE, 100n * ONE, 100n * ONE, 100n * ONE, 100n * ONE];

  const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
  await WAERE.waitForDeployment();

  const Round = await ethers.getContractFactory("AereRetroPGFRound1");
  const round = await Round.deploy(await WAERE.getAddress(), reserve.address, TOTAL_SMALL, CATEGORY_CAPS_SMALL);
  await round.waitForDeployment();

  return { deployer, reserve, alice, bob, carol, WAERE, round, TOTAL: TOTAL_SMALL };
}

// We use a custom leaf-encoding that matches the contract's keccak256(abi.encode(recipient, category, amount)).
// OpenZeppelin's StandardMerkleTree double-hashes; the contract uses single-hash. Implement minimal Merkle manually.
function leafOf(recipient, category, amount) {
  return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["address", "uint8", "uint256"], [recipient, category, amount]));
}

function buildSimpleTree(leaves) {
  // For tests: small N. Sort + pair + hash.
  let layer = leaves.slice().map(l => l.toLowerCase());
  layer.sort();
  const layers = [layer];
  while (layer.length > 1) {
    const next = [];
    for (let i = 0; i < layer.length; i += 2) {
      const a = layer[i];
      const b = layer[i + 1] ?? layer[i];
      const [lo, hi] = a < b ? [a, b] : [b, a];
      next.push(ethers.keccak256(ethers.concat([lo, hi])));
    }
    layer = next;
    layers.push(layer);
  }
  return { root: layer[0], layers };
}

function proofFor(tree, leaf) {
  const proof = [];
  let target = leaf.toLowerCase();
  for (let i = 0; i < tree.layers.length - 1; i++) {
    const lay = tree.layers[i];
    const idx = lay.indexOf(target);
    if (idx < 0) throw new Error("leaf not in layer");
    const sib = idx ^ 1;
    if (sib < lay.length) proof.push(lay[sib]);
    // Move up.
    const a = lay[idx];
    const b = lay[sib] ?? lay[idx];
    const [lo, hi] = a < b ? [a, b] : [b, a];
    target = ethers.keccak256(ethers.concat([lo, hi]));
  }
  return proof;
}

describe("AereRetroPGFRound1", function () {
  it("constructor requires category caps sum == total", async function () {
    const [deployer, reserve] = await ethers.getSigners();
    const WAERE = await (await ethers.getContractFactory("contracts/WAERE.sol:WAERE")).deploy();
    const Round = await ethers.getContractFactory("AereRetroPGFRound1");
    const badCaps = [50n * ONE, 50n * ONE, 50n * ONE, 50n * ONE, 50n * ONE, 50n * ONE]; // 300, not 600
    await expect(Round.deploy(await WAERE.getAddress(), reserve.address, 600n * ONE, badCaps)).to.be.revertedWithCustomError(Round, "CapsMustSumToTotal");
  });

  it("fund + propose + finalize + claim flow", async function () {
    const { deployer, reserve, alice, bob, carol, WAERE, round, TOTAL } = await deploySmall();

    // Fund WAERE.
    await WAERE.connect(deployer).deposit({ value: TOTAL });
    await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
    await round.connect(deployer).fund();

    // Build a small payout tree: alice gets 50e18 in cat 0; bob gets 30e18 in cat 1.
    const a_amt = 50n * ONE;
    const b_amt = 30n * ONE;
    const lAlice = leafOf(alice.address, 0, a_amt);
    const lBob = leafOf(bob.address, 1, b_amt);
    const tree = buildSimpleTree([lAlice, lBob]);

    await round.connect(deployer).proposePayoutRoot(tree.root);

    // Try to claim before finalize: should revert WrongState.
    await expect(round.connect(alice).claim(alice.address, 0, a_amt, proofFor(tree, lAlice))).to.be.revertedWithCustomError(round, "WrongState");

    // Try to finalize early.
    await expect(round.finalize()).to.be.revertedWithCustomError(round, "TimelockNotElapsed");

    // Advance 14 days.
    await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await round.finalize();

    // Alice claims.
    const aliceBefore = await WAERE.balanceOf(alice.address);
    await round.connect(alice).claim(alice.address, 0, a_amt, proofFor(tree, lAlice));
    expect(await WAERE.balanceOf(alice.address)).to.equal(aliceBefore + a_amt);

    // Double-claim reverts.
    await expect(round.connect(alice).claim(alice.address, 0, a_amt, proofFor(tree, lAlice))).to.be.revertedWithCustomError(round, "AlreadyClaimed");

    // Bob claims.
    await round.connect(bob).claim(bob.address, 1, b_amt, proofFor(tree, lBob));
    expect(await WAERE.balanceOf(bob.address)).to.equal(b_amt);
  });

  it("rejects claim with wrong proof", async function () {
    const { deployer, alice, WAERE, round, TOTAL } = await deploySmall();
    await WAERE.connect(deployer).deposit({ value: TOTAL });
    await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
    await round.connect(deployer).fund();

    const l = leafOf(alice.address, 0, 50n * ONE);
    const tree = buildSimpleTree([l, leafOf("0x" + "00".repeat(20), 1, 10n * ONE)]);
    await round.connect(deployer).proposePayoutRoot(tree.root);
    await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await round.finalize();

    // Use a junk proof.
    await expect(
      round.connect(alice).claim(alice.address, 0, 50n * ONE, [ethers.ZeroHash])
    ).to.be.revertedWithCustomError(round, "InvalidProof");
  });

  it("category cap enforcement: overflow reverts", async function () {
    const { deployer, alice, WAERE, round, TOTAL } = await deploySmall();
    await WAERE.connect(deployer).deposit({ value: TOTAL });
    await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
    await round.connect(deployer).fund();

    // Each category cap is 100 ONE; this leaf claims 101 from cat 2.
    const l = leafOf(alice.address, 2, 101n * ONE);
    const tree = buildSimpleTree([l]);
    await round.connect(deployer).proposePayoutRoot(tree.root);
    await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await round.finalize();

    await expect(
      round.connect(alice).claim(alice.address, 2, 101n * ONE, proofFor(tree, l))
    ).to.be.revertedWithCustomError(round, "CategoryOverflow");
  });

  it("challenge flow: anyone can challenge; owner can dismiss; window doesn't reset", async function () {
    const { deployer, alice, bob, WAERE, round, TOTAL } = await deploySmall();
    await WAERE.connect(deployer).deposit({ value: TOTAL });
    await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
    await round.connect(deployer).fund();

    const l = leafOf(alice.address, 0, 50n * ONE);
    const tree = buildSimpleTree([l]);
    await round.connect(deployer).proposePayoutRoot(tree.root);

    // Bob challenges.
    await round.connect(bob).challenge("alice doesnt deserve cat 0");
    // State is Challenged → cannot finalize even after window.
    await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await expect(round.finalize()).to.be.revertedWithCustomError(round, "WrongState");

    // Owner dismisses; state returns to Proposed; CAN finalize now (window already passed).
    await round.connect(deployer).dismissChallenge(0, "evidence below shows alice DOES deserve");
    await round.finalize();

    // Alice claims.
    await round.connect(alice).claim(alice.address, 0, 50n * ONE, proofFor(tree, l));
  });

  it("sweep unclaimed after 365 days returns funds to ecosystem reserve", async function () {
    const { deployer, reserve, alice, WAERE, round, TOTAL } = await deploySmall();
    await WAERE.connect(deployer).deposit({ value: TOTAL });
    await WAERE.connect(deployer).approve(await round.getAddress(), TOTAL);
    await round.connect(deployer).fund();

    const l = leafOf(alice.address, 0, 50n * ONE);
    const tree = buildSimpleTree([l]);
    await round.connect(deployer).proposePayoutRoot(tree.root);
    await ethers.provider.send("evm_increaseTime", [14 * 24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await round.finalize();

    // Alice claims her 50.
    await round.connect(alice).claim(alice.address, 0, 50n * ONE, proofFor(tree, l));
    const remaining = TOTAL - 50n * ONE;

    // Try to sweep early.
    await expect(round.sweepUnclaimed()).to.be.revertedWithCustomError(round, "SweepWindowNotElapsed");

    await ethers.provider.send("evm_increaseTime", [365 * 24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await round.sweepUnclaimed();
    expect(await WAERE.balanceOf(reserve.address)).to.equal(remaining);
  });
});
