// Hardhat tests for AereNavOracle.
//
// Covers:
//   - propose snapshot + 24h challenge window
//   - attest after window with no challenges (permissionless)
//   - challenge + dismiss path; attest after dismissals
//   - verifyReserve against attested root with Merkle proof
//   - latestAttestedEpoch view
//   - monotonic epoch enforcement (cannot overwrite an existing epoch)
//
// Run: npx hardhat test test/nav-oracle.test.js

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

function leafOf(asset, chainId, reserveAmount, decimals) {
  return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
    ["address", "uint256", "uint256", "uint8"],
    [asset, chainId, reserveAmount, decimals]
  ));
}

function buildSimpleTree(leaves) {
  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]);
    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("AereNavOracle", function () {
  let oracle, deployer, alice, bob;

  beforeEach(async function () {
    [deployer, alice, bob] = await ethers.getSigners();
    oracle = await (await ethers.getContractFactory("AereNavOracle")).deploy();
    await oracle.waitForDeployment();
  });

  it("proposes a snapshot and attests after 24h with no challenge", async function () {
    const usdce = "0x" + "11".repeat(20);
    const usdte = "0x" + "22".repeat(20);
    const leaves = [
      leafOf(usdce, 2800, 1_000_000n * 10n ** 6n, 6),
      leafOf(usdte, 2800, 5_000_000n * 10n ** 6n, 6),
    ];
    const tree = buildSimpleTree(leaves);

    await oracle.proposeSnapshot(1, tree.root, "ipfs://QmExampleCid");

    // Cannot attest early.
    await expect(oracle.attest(1)).to.be.revertedWithCustomError(oracle, "TimelockNotElapsed");

    // Advance 24h.
    await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await oracle.attest(1);

    // Verify a reserve via Merkle proof.
    const leaf = leaves[0];
    const ok = await oracle.verifyReserve(1, usdce, 2800, 1_000_000n * 10n ** 6n, 6, proofFor(tree, leaf));
    expect(ok).to.equal(true);

    // Wrong amount → false.
    const bad = await oracle.verifyReserve(1, usdce, 2800, 999_999n * 10n ** 6n, 6, proofFor(tree, leaf));
    expect(bad).to.equal(false);
  });

  it("monotonic epoch — cannot overwrite an existing one", async function () {
    await oracle.proposeSnapshot(1, ethers.ZeroHash, "");
    await expect(oracle.proposeSnapshot(1, ethers.id("other"), "")).to.be.revertedWithCustomError(oracle, "EpochAlreadyExists");
  });

  it("challenge → dismiss → attest path; window does not reset", async function () {
    await oracle.proposeSnapshot(2, ethers.id("root2"), "ipfs://r2");

    await oracle.connect(bob).challenge(2, "double counting USDC.e");

    // Cannot attest while challenged even after window elapsed.
    await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await expect(oracle.attest(2)).to.be.revertedWithCustomError(oracle, "WrongState");

    // Owner dismisses.
    await oracle.dismissChallenge(2, 0, "double-count claim is false; see audit log");
    // Now attestable.
    await oracle.attest(2);

    const status = await oracle.snapshotStatus(2);
    expect(status.state).to.equal(2); // Attested
  });

  it("challenge after window elapses reverts", async function () {
    await oracle.proposeSnapshot(3, ethers.id("root3"), "");
    await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await expect(oracle.connect(bob).challenge(3, "too late")).to.be.revertedWithCustomError(oracle, "TimelockNotElapsed");
  });

  it("latestAttestedEpoch returns the highest attested epoch", async function () {
    await oracle.proposeSnapshot(5, ethers.id("a"), "");
    await oracle.proposeSnapshot(7, ethers.id("b"), "");
    await ethers.provider.send("evm_increaseTime", [24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await oracle.attest(5);
    await oracle.attest(7);
    const [epoch, root] = await oracle.latestAttestedEpoch();
    expect(epoch).to.equal(7n);
    expect(root).to.equal(ethers.id("b"));
  });

  it("verifyReserve returns false for non-attested snapshot", async function () {
    const usdce = "0x" + "11".repeat(20);
    const leaves = [leafOf(usdce, 2800, 1n, 6)];
    const tree = buildSimpleTree(leaves);
    await oracle.proposeSnapshot(9, tree.root, "");
    // Not yet attested.
    const ok = await oracle.verifyReserve(9, usdce, 2800, 1n, 6, proofFor(tree, leaves[0]));
    expect(ok).to.equal(false);
  });
});
