// AUDIT FIX VERIFICATION — F12 + F13 — 2026-07-10
//
// Closes the campaign at 13/13. For each finding the live V1 REPRODUCES the bug
// and the V2 fix CORRECTS it. Runs fully local against the REAL sources that
// get deployed to chain 2800 (no mainnet txs here).
//
//   F12  AereLendingOracle NavOracle feed is a DEAD PATH: configureFeed
//        (NavOracle -> AereNavOracle) reverts because the deployed NAV oracle
//        (Merkle reserve snapshots) has no latestForAsset(bytes32). FIX: a
//        purpose-built owner-attested AereNavOracleAdapter implements
//        latestForAsset, so a NavOracle-type feed configures + reads cleanly.
//
//   F13  AERE402Facilitator replay key consumed[agentId][nonce] is GLOBAL per
//        agent, so two providers with colliding per-provider nonces collide and
//        a valid settlement reverts. FIX: AERE402FacilitatorV2 keys replay per
//        (agentId, payee, nonce); AereAgentV2 is cross-wired to it.

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

const ONE = 10n ** 18n;

// ─────────────────────────────────────────────────────────────────────────────
// F12
// ─────────────────────────────────────────────────────────────────────────────
describe("F12 AereLendingOracle NavOracle dead path — V1 reproduces / V2 corrects", function () {
  this.timeout(120000);

  it("V1 REPRODUCES: configureFeed(NavOracle -> raw AereNavOracle) reverts (no latestForAsset)", async () => {
    const [owner] = await ethers.getSigners();
    const lo = await (await ethers.getContractFactory("AereLendingOracle")).connect(owner).deploy();
    await lo.waitForDeployment();
    const nav = await (await ethers.getContractFactory("AereNavOracle")).deploy();
    await nav.waitForDeployment();

    // The deployed AereNavOracle exposes NO latestForAsset(bytes32).
    const navNames = nav.interface.fragments.filter(f => f.type === "function").map(f => f.name);
    expect(navNames).to.not.include("latestForAsset");

    const asset = ethers.Wallet.createRandom().address;
    // FeedType.NavOracle = 1. Auto-poke calls latestForAsset -> revert.
    await expect(
      lo.connect(owner).configureFeed(asset, 1, await nav.getAddress(), ethers.id("BUIDL"), 86400, 0, 18)
    ).to.be.reverted;
  });

  it("V2 CORRECTS: a NavOracle feed pointed at AereNavOracleAdapter configures + reads without reverting", async () => {
    const [owner] = await ethers.getSigners();
    const lo = await (await ethers.getContractFactory("AereLendingOracle")).connect(owner).deploy();
    await lo.waitForDeployment();
    const nav = await (await ethers.getContractFactory("AereNavOracle")).deploy();
    await nav.waitForDeployment();

    // Adapter references the (real) NavOracle for provenance but attests prices itself.
    const adapter = await (await ethers.getContractFactory("AereNavOracleAdapter")).connect(owner).deploy(await nav.getAddress());
    await adapter.waitForDeployment();
    expect(await adapter.NAV_ORACLE()).to.equal(await nav.getAddress());

    // The adapter implements the exact interface AereLendingOracle expects.
    const adNames = adapter.interface.fragments.filter(f => f.type === "function").map(f => f.name);
    expect(adNames).to.include("latestForAsset");

    const asset = ethers.Wallet.createRandom().address;
    const assetKey = ethers.id("BUIDL");

    // Un-attested asset -> (0,0) -> LendingOracle treats as InvalidFeedResponse
    // (the auto-poke's ts==0 check fires before the zero-price check).
    const empty = await adapter.latestForAsset(assetKey);
    expect(empty[0]).to.equal(0n);
    expect(empty[1]).to.equal(0n);
    await expect(
      lo.connect(owner).configureFeed(asset, 1, await adapter.getAddress(), assetKey, 86400, 0, 18)
    ).to.be.revertedWithCustomError(lo, "InvalidFeedResponse");

    // Foundation attests a $1.02 NAV (18-dec convention).
    const navValue = 102n * ONE / 100n; // 1.02e18
    await adapter.connect(owner).setNav(assetKey, navValue);
    const reading = await adapter.latestForAsset(assetKey);
    expect(reading[0]).to.equal(navValue);
    expect(reading[1]).to.be.gt(0n);

    // NavOracle feed now configures WITHOUT reverting (F12 fixed).
    await expect(
      lo.connect(owner).configureFeed(asset, 1, await adapter.getAddress(), assetKey, 86400, 500, 18)
    ).to.not.be.reverted;

    // And getPrice returns the normalised NAV.
    const price = await lo.getPrice(asset);
    expect(price).to.equal(navValue);

    // Sanity: the feed really is FeedType.NavOracle (1) pointed at the adapter.
    const feed = await lo.feeds(asset);
    expect(feed.feedType).to.equal(1n);
    expect(feed.source).to.equal(await adapter.getAddress());
  });

  it("V2 guards: setNav is owner-only and rejects a zero NAV; staleness still enforced by the LendingOracle", async () => {
    const [owner, stranger] = await ethers.getSigners();
    const nav = await (await ethers.getContractFactory("AereNavOracle")).deploy();
    await nav.waitForDeployment();
    const adapter = await (await ethers.getContractFactory("AereNavOracleAdapter")).connect(owner).deploy(await nav.getAddress());
    await adapter.waitForDeployment();

    const key = ethers.id("USDY");
    await expect(adapter.connect(stranger).setNav(key, ONE)).to.be.revertedWith("Ownable: caller is not the owner");
    await expect(adapter.connect(owner).setNav(key, 0)).to.be.revertedWithCustomError(adapter, "ZeroNav");

    // Configure a NavOracle feed with a 1s staleness, then let it go stale.
    const lo = await (await ethers.getContractFactory("AereLendingOracle")).connect(owner).deploy();
    await lo.waitForDeployment();
    await adapter.connect(owner).setNav(key, ONE);
    const asset = ethers.Wallet.createRandom().address;
    await lo.connect(owner).configureFeed(asset, 1, await adapter.getAddress(), key, 1, 0, 18);
    // fresh read works
    expect(await lo.getPrice(asset)).to.equal(ONE);
    // age it past staleness
    await ethers.provider.send("evm_increaseTime", [5]);
    await ethers.provider.send("evm_mine", []);
    await expect(lo.getPrice(asset)).to.be.revertedWithCustomError(lo, "FeedStale");
  });
});

// ─────────────────────────────────────────────────────────────────────────────
// F13
// ─────────────────────────────────────────────────────────────────────────────
async function deployAgenticStack(Version) {
  // Version: "V1" (AereAgent + AERE402Facilitator) or "V2" (AereAgentV2 + AERE402FacilitatorV2)
  const [deployer] = await ethers.getSigners();

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

  const nonce = await ethers.provider.getTransactionCount(deployer.address);
  const futureSaere = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 2 });
  await WAERE.connect(deployer).deposit({ value: 1000n });
  await WAERE.connect(deployer).approve(futureSaere, 1000n);
  const sAERE = await (await ethers.getContractFactory("sAERE")).deploy(await WAERE.getAddress());
  await sAERE.waitForDeployment();

  const burnVault = await (await ethers.getContractFactory("AereFeeBurnVault")).deploy();
  await burnVault.waitForDeployment();
  const fakeRouter = await (await ethers.getContractFactory("WAERE")).deploy();
  await fakeRouter.waitForDeployment();

  const sink = await (await ethers.getContractFactory("AereSink")).deploy(
    await WAERE.getAddress(),
    await burnVault.getAddress(),
    await sAERE.getAddress(),
    await fakeRouter.getAddress(),
    1500, 4000, 4500, 200, ethers.ZeroAddress
  );
  await sink.waitForDeployment();

  const agentName = Version === "V2" ? "AereAgentV2" : "AereAgent";
  const facName = Version === "V2" ? "AERE402FacilitatorV2" : "AERE402Facilitator";

  const nonce2 = await ethers.provider.getTransactionCount(deployer.address);
  const futureFacilitator = ethers.getCreateAddress({ from: deployer.address, nonce: nonce2 + 1 });
  const agent = await (await ethers.getContractFactory(agentName)).deploy(futureFacilitator);
  await agent.waitForDeployment();
  const fac = await (await ethers.getContractFactory(facName)).deploy(await agent.getAddress(), await sink.getAddress());
  await fac.waitForDeployment();
  expect((await fac.getAddress()).toLowerCase()).to.equal(futureFacilitator.toLowerCase());
  // cross-wiring
  expect(await agent.AERE402_FACILITATOR()).to.equal(await fac.getAddress());
  expect(await fac.AGENT_REGISTRY()).to.equal(await agent.getAddress());

  return { WAERE, sink, agent, fac };
}

async function signAuth(signerWallet, fac, agentId, token, payee, amount, resourceId, nonce, deadline) {
  const domain = {
    name: "AERE402Facilitator",
    version: "1",
    chainId: (await ethers.provider.getNetwork()).chainId,
    verifyingContract: await fac.getAddress(),
  };
  const types = {
    PaymentAuth: [
      { name: "agentId", type: "uint256" },
      { name: "token", type: "address" },
      { name: "payee", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "resourceId", type: "bytes32" },
      { name: "nonce", type: "uint256" },
      { name: "deadline", type: "uint256" },
    ],
  };
  return signerWallet.signTypedData(domain, types, { agentId, token, payee, amount, resourceId, nonce, deadline });
}

async function setupAgent(agent, WAERE, operator) {
  const signer = ethers.Wallet.createRandom();
  const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 10n * ONE, 0, 0, "");
  const agentId = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId;
  await WAERE.connect(operator).deposit({ value: 100n * ONE });
  await WAERE.connect(operator).approve(await agent.getAddress(), 100n * ONE);
  await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 100n * ONE);
  return { signer, agentId };
}

describe("F13 AERE402Facilitator per-agent nonce collision — V1 reproduces / V2 corrects", function () {
  this.timeout(120000);

  it("V1 REPRODUCES: two payees, same nonce -> provider B's valid settlement reverts NonceAlreadyConsumed", async () => {
    const [, operator, , payeeA, payeeB] = await ethers.getSigners();
    const { WAERE, agent, fac } = await deployAgenticStack("V1");
    const { signer, agentId } = await setupAgent(agent, WAERE, operator);

    const amount = 2n * ONE;
    const token = await WAERE.getAddress();
    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;

    const sigA = await signAuth(signer, fac, agentId, token, payeeA.address, amount, ethers.id("A:/gpu"), 1n, deadline);
    await fac.connect(payeeA).settle(agentId, token, payeeA.address, amount, ethers.id("A:/gpu"), 1n, deadline, sigA);
    expect(await fac.consumed(agentId, 1n)).to.equal(true);

    // Provider B, own monotonic nonce=1, DIFFERENT payee, fully valid -> collides.
    const sigB = await signAuth(signer, fac, agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline);
    await expect(
      fac.connect(payeeB).settle(agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline, sigB)
    ).to.be.revertedWithCustomError(fac, "NonceAlreadyConsumed");
  });

  it("V2 CORRECTS: two payees, same nonce BOTH settle; true replay (same agentId+payee+nonce) still reverts", async () => {
    const [, operator, , payeeA, payeeB] = await ethers.getSigners();
    const { WAERE, agent, fac } = await deployAgenticStack("V2");
    const { signer, agentId } = await setupAgent(agent, WAERE, operator);

    const amount = 2n * ONE;
    const token = await WAERE.getAddress();
    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;

    // Both providers use nonce=1 (per-provider monotonic convention).
    const sigA = await signAuth(signer, fac, agentId, token, payeeA.address, amount, ethers.id("A:/gpu"), 1n, deadline);
    await fac.connect(payeeA).settle(agentId, token, payeeA.address, amount, ethers.id("A:/gpu"), 1n, deadline, sigA);

    const balBBefore = await WAERE.balanceOf(payeeB.address);
    const sigB = await signAuth(signer, fac, agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline);
    await fac.connect(payeeB).settle(agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline, sigB);

    // Per-payee namespace: both consumed independently.
    expect(await fac.consumed(agentId, payeeA.address, 1n)).to.equal(true);
    expect(await fac.consumed(agentId, payeeB.address, 1n)).to.equal(true);

    // Provider B actually got paid (amount - 25bps fee).
    const fee = amount * 25n / 10_000n;
    expect(await WAERE.balanceOf(payeeB.address)).to.equal(balBBefore + (amount - fee));

    // TRUE replay still blocked: same (agentId, payeeB, nonce=1).
    const sigReplay = await signAuth(signer, fac, agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline);
    await expect(
      fac.connect(payeeB).settle(agentId, token, payeeB.address, amount, ethers.id("B:/api"), 1n, deadline, sigReplay)
    ).to.be.revertedWithCustomError(fac, "NonceAlreadyConsumed");
  });
});
