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

describe("AereCompliancePool", () => {
  let foundation, casp, alice, bob, relayer;
  let token, verifier, pool;
  const DENOM = ethers.parseUnits("1000", 18);

  beforeEach(async () => {
    [foundation, casp, alice, bob, relayer] = await ethers.getSigners();

    const T = await ethers.getContractFactory("MockERC20Lending");
    token = await T.deploy("USDC.e", "USDCe", 18);
    await token.waitForDeployment();

    const V = await ethers.getContractFactory("MockPrivacyPoolVerifier");
    verifier = await V.deploy();
    await verifier.waitForDeployment();

    const P = await ethers.getContractFactory("AereCompliancePool");
    pool = await P.deploy(
      await token.getAddress(), DENOM, foundation.address, await verifier.getAddress()
    );
    await pool.waitForDeployment();

    await token.mint(alice.address, DENOM * 10n);
    await token.connect(alice).approve(await pool.getAddress(), ethers.MaxUint256);
  });

  function rh(s) { return ethers.keccak256(ethers.toUtf8Bytes(s)); }

  async function depositOnce(commitTag = "commit-1") {
    await pool.connect(foundation).setComplianceProvider(casp.address, true);
    const commit = rh(commitTag);
    const trh = rh("travel-rule");
    await pool.connect(alice).deposit(commit, trh, casp.address, "ipfs://tr");
    return await pool.latestDepositRoot();
  }

  async function publishAssoc(tag = "assoc") {
    const ar = rh(tag);
    await pool.connect(foundation).proposeAssociationRoot(ar);
    await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await pool.publishAssociationRoot(ar);
    return ar;
  }

  it("only Foundation registers compliance providers", async () => {
    await expect(
      pool.connect(alice).setComplianceProvider(casp.address, true)
    ).to.be.revertedWithCustomError(pool, "NotFoundation");
    await pool.connect(foundation).setComplianceProvider(casp.address, true);
    expect(await pool.isComplianceProvider(casp.address)).to.equal(true);
    expect((await pool.complianceProviders()).length).to.equal(1);
  });

  it("deposit pulls DENOM + emits Deposited + advances on-chain Merkle root (audit fix HIGH #28)", async () => {
    const commit = rh("commit-1");
    const trh = rh("travel-rule-hash-1");
    await expect(
      pool.connect(alice).deposit(commit, trh, casp.address, "ipfs://tr")
    ).to.be.revertedWithCustomError(pool, "NotAComplianceProvider");
    await pool.connect(foundation).setComplianceProvider(casp.address, true);

    const balBefore = await token.balanceOf(await pool.getAddress());
    const rootBefore = await pool.latestDepositRoot();
    await pool.connect(alice).deposit(commit, trh, casp.address, "ipfs://tr");
    expect(await token.balanceOf(await pool.getAddress())).to.equal(balBefore + DENOM);
    expect(await pool.depositCount()).to.equal(1n);
    const rootAfter = await pool.latestDepositRoot();
    expect(rootAfter).to.not.equal(rootBefore);
    expect(await pool.isKnownRoot(rootAfter)).to.equal(true);
  });

  it("association root 24h challenge window (audit fix MED #28)", async () => {
    const a1 = rh("assoc-root-1");
    await pool.connect(foundation).proposeAssociationRoot(a1);
    await expect(pool.publishAssociationRoot(a1)).to.be.revertedWithCustomError(pool, "UnknownAssociationRoot");
    await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await expect(pool.publishAssociationRoot(a1))
      .to.emit(pool, "AssociationRootPublished").withArgs(a1);
    expect(await pool.isAssociationRootValid(a1)).to.equal(true);
  });

  it("association root challenge blocks finalisation", async () => {
    const a1 = rh("assoc-root-challenged");
    await pool.connect(foundation).proposeAssociationRoot(a1);
    await pool.connect(alice).challengeAssociationRoot(a1, "this would let me bypass OFAC");
    await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await expect(pool.publishAssociationRoot(a1)).to.be.revertedWithCustomError(pool, "UnknownAssociationRoot");
    await pool.connect(foundation).dismissAssociationRootChallenge(a1);
    await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await pool.publishAssociationRoot(a1);
    expect(await pool.isAssociationRootValid(a1)).to.equal(true);
  });

  it("withdraw happy path: valid proof, known on-chain deposit root, fresh nullifier", async () => {
    const dr = await depositOnce("c1");
    const ar = await publishAssoc();
    const nh = rh("nullifier-1");
    const balBefore = await token.balanceOf(bob.address);
    const relayerFee = ethers.parseUnits("5", 18);
    await expect(
      pool.connect(relayer).withdraw("0x1234", dr, ar, nh, bob.address, relayer.address, relayerFee, 0)
    ).to.emit(pool, "Withdrawn");
    expect(await token.balanceOf(bob.address)).to.equal(balBefore + DENOM - relayerFee);
    expect(await token.balanceOf(relayer.address)).to.equal(relayerFee);
    expect(await pool.spentNullifiers(nh)).to.equal(true);
  });

  it("withdraw double-spend rejected (same nullifier)", async () => {
    const dr = await depositOnce("c2");
    const ar = await publishAssoc("ar2");
    const nh = rh("nullifier-2");
    await pool.connect(relayer).withdraw("0x", dr, ar, nh, bob.address, relayer.address, 0, 0);
    await expect(
      pool.connect(relayer).withdraw("0x", dr, ar, nh, bob.address, relayer.address, 0, 0)
    ).to.be.revertedWithCustomError(pool, "AlreadySpent");
  });

  it("withdraw unknown association root rejected", async () => {
    const dr = await depositOnce("c3");
    await expect(
      pool.connect(relayer).withdraw("0x", dr, rh("never-published"), rh("n"), bob.address, relayer.address, 0, 0)
    ).to.be.revertedWithCustomError(pool, "UnknownAssociationRoot");
  });

  it("withdraw unknown deposit root rejected (audit fix HIGH #28: only on-chain roots accepted)", async () => {
    const ar = await publishAssoc("ar4");
    // depositRoot fabricated; not in on-chain ring buffer → reverts.
    await expect(
      pool.connect(relayer).withdraw("0x", rh("fake-dr"), ar, rh("n"), bob.address, relayer.address, 0, 0)
    ).to.be.revertedWithCustomError(pool, "UnknownDepositRoot");
  });

  it("withdraw invalid proof rejected by verifier", async () => {
    const dr = await depositOnce("c5");
    const ar = await publishAssoc("ar5");
    await verifier.setAccept(false);
    await expect(
      pool.connect(relayer).withdraw("0x", dr, ar, rh("n"), bob.address, relayer.address, 0, 0)
    ).to.be.revertedWithCustomError(pool, "InvalidProof");
  });

  it("fee + refund > DENOMINATION rejected", async () => {
    const dr = await depositOnce("c6");
    const ar = await publishAssoc("ar6");
    await expect(
      pool.connect(relayer).withdraw("0x", dr, ar, rh("n"), bob.address, relayer.address, DENOM, 1n)
    ).to.be.revertedWithCustomError(pool, "FeeTooLarge");
  });

  it("older on-chain root remains valid (root history)", async () => {
    await pool.connect(foundation).setComplianceProvider(casp.address, true);
    await pool.connect(alice).deposit(rh("a1"), rh("t"), casp.address, "");
    const r1 = await pool.latestDepositRoot();
    await pool.connect(alice).deposit(rh("a2"), rh("t"), casp.address, "");
    const r2 = await pool.latestDepositRoot();
    expect(r1).to.not.equal(r2);
    expect(await pool.isKnownRoot(r1)).to.equal(true);
    expect(await pool.isKnownRoot(r2)).to.equal(true);

    const ar = await publishAssoc("ar7");
    // Withdraw against the OLDER root still works.
    await pool.connect(relayer).withdraw("0x", r1, ar, rh("n"), bob.address, relayer.address, 0, 0);
  });

  it("duplicate commitment rejected", async () => {
    await pool.connect(foundation).setComplianceProvider(casp.address, true);
    const c = rh("same");
    await pool.connect(alice).deposit(c, rh("t"), casp.address, "");
    await expect(
      pool.connect(alice).deposit(c, rh("t"), casp.address, "")
    ).to.be.revertedWithCustomError(pool, "CommitmentReused");
  });
});
