// Hardhat tests for AereAgent + AERE402Facilitator.
//
// Covers:
//   - Agent registration with signer + passkey + rate limits
//   - Operator rotation, signer rotation, pause/unpause
//   - topUp / withdraw lifecycle
//   - Direct debit() rejected from non-facilitator
//   - EIP-712 settle happy path: split payee/sink, nonce consumed, event
//   - Nonce replay rejection (NonceAlreadyConsumed)
//   - Deadline rejection
//   - Wrong-payee rejection (PayeeMismatch)
//   - Bad signature rejection (InvalidSignature)
//   - Per-call cap enforcement
//   - Per-hour cap enforcement (windowed)
//   - Pause blocks settlement
//
// Run: npx hardhat test test/agentic.test.js

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

const ONE = 10n ** 18n;

async function deployStack() {
  const [deployer, operator, signer, payee, alice] = await ethers.getSigners();

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

  // sAERE seed
  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();

  // Burn vault + dummy router for AereSink
  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();

  // Pre-compute the facilitator address.
  const nonce2 = await ethers.provider.getTransactionCount(deployer.address);
  const futureFacilitator = ethers.getCreateAddress({ from: deployer.address, nonce: nonce2 + 1 });

  // AereAgent registry (immutable facilitator address).
  const agent = await (await ethers.getContractFactory("AereAgent")).deploy(futureFacilitator);
  await agent.waitForDeployment();

  // Facilitator.
  const fac = await (await ethers.getContractFactory("AERE402Facilitator")).deploy(
    await agent.getAddress(),
    await sink.getAddress()
  );
  await fac.waitForDeployment();
  expect((await fac.getAddress()).toLowerCase()).to.equal(futureFacilitator.toLowerCase());

  return { deployer, operator, signer, payee, alice, 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' },
    ],
  };
  const value = { agentId, token, payee, amount, resourceId, nonce, deadline };
  return signerWallet.signTypedData(domain, types, value);
}

describe("AereAgent + AERE402Facilitator", function () {
  it("registers an agent with rate limits + signer + passkey", async function () {
    const { agent, operator, signer } = await deployStack();
    const tx = await agent.connect(operator).registerAgent(
      signer.address,
      ethers.ZeroHash,
      ethers.ZeroHash,
      1n * ONE,        // perCall: 1 AERE max
      100n * ONE,      // perHour: 100 AERE max
      1000n * ONE,     // perDay:  1000 AERE max
      "ipfs://QmAgentMeta"
    );
    const id = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId;

    const a = await agent.agents(id);
    expect(a.registered).to.equal(true);
    expect(a.operator).to.equal(operator.address);
    expect(a.signer).to.equal(signer.address);
    expect(a.perCallCap).to.equal(1n * ONE);
    expect(a.metadataUri).to.equal("ipfs://QmAgentMeta");
  });

  it("debit rejected from non-facilitator", async function () {
    const { agent, operator, signer, payee, alice } = await deployStack();
    const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, "");
    const id = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId;
    await expect(
      agent.connect(alice).debit(id, ethers.ZeroAddress, payee.address, 1n)
    ).to.be.revertedWithCustomError(agent, "NotFacilitator");
  });

  it("EIP-712 settle: split payee/sink, nonce consumed", async function () {
    const { deployer, operator, signer, payee, WAERE, agent, fac, sink } = await deployStack();

    // Register agent.
    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;

    // Top up 100 WAERE.
    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);
    expect(await agent.balances(agentId, await WAERE.getAddress())).to.equal(100n * ONE);

    const amount = 5n * ONE;
    const resourceId = ethers.id("GET /v1/inference/claude");
    const nonce = 1n;
    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;

    const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, amount, resourceId, nonce, deadline);

    const payeeBefore = await WAERE.balanceOf(payee.address);
    await fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, amount, resourceId, nonce, deadline, sig);

    const fee = (amount * 25n) / 10_000n;
    const toPayee = amount - fee;
    expect(await WAERE.balanceOf(payee.address) - payeeBefore).to.equal(toPayee);
    expect(await fac.consumed(agentId, nonce)).to.equal(true);

    // Nonce replay rejected.
    await expect(
      fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, amount, resourceId, nonce, deadline, sig)
    ).to.be.revertedWithCustomError(fac, "NonceAlreadyConsumed");
  });

  it("rejects wrong-payee submission", async function () {
    const { operator, signer, payee, alice, WAERE, agent, fac } = await deployStack();
    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: 10n * ONE });
    await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE);
    await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE);

    const amount = 1n * ONE;
    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
    const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, amount, ethers.ZeroHash, 1n, deadline);

    // alice tries to settle a payment intended for payee.
    await expect(
      fac.connect(alice).settle(agentId, await WAERE.getAddress(), payee.address, amount, ethers.ZeroHash, 1n, deadline, sig)
    ).to.be.revertedWithCustomError(fac, "PayeeMismatch");
  });

  it("rejects expired deadline", async function () {
    const { operator, signer, payee, WAERE, agent, fac } = await deployStack();
    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: 10n * ONE });
    await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE);
    await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE);

    const deadline = 1; // expired
    const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline);

    await expect(
      fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline, sig)
    ).to.be.revertedWithCustomError(fac, "DeadlinePassed");
  });

  it("rejects bad signature (different signer)", async function () {
    const { operator, signer, payee, alice, WAERE, agent, fac } = await deployStack();
    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: 10n * ONE });
    await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE);
    await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE);

    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
    // alice signs instead of the registered signer.
    const sig = await signAuth(alice, fac, agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline);

    await expect(
      fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline, sig)
    ).to.be.revertedWithCustomError(fac, "InvalidSignature");
  });

  it("per-call cap enforced", async function () {
    const { operator, signer, payee, WAERE, agent, fac } = await deployStack();
    // Cap at 1 WAERE per call.
    const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 1n * ONE, 0, 0, "");
    const agentId = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId;

    await WAERE.connect(operator).deposit({ value: 10n * ONE });
    await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE);
    await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE);

    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
    const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, 2n * ONE, ethers.ZeroHash, 1n, deadline);

    await expect(
      fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, 2n * ONE, ethers.ZeroHash, 1n, deadline, sig)
    ).to.be.revertedWithCustomError(agent, "PerCallCapExceeded");
  });

  it("paused agent rejected at settle time", async function () {
    const { operator, signer, payee, WAERE, agent, fac } = await deployStack();
    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: 10n * ONE });
    await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE);
    await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE);

    await agent.connect(operator).setPaused(agentId, true);

    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
    const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline);

    await expect(
      fac.connect(payee).settle(agentId, await WAERE.getAddress(), payee.address, 1n * ONE, ethers.ZeroHash, 1n, deadline, sig)
    ).to.be.revertedWithCustomError(agent, "AgentPausedErr");
  });

  it("operator can rotate signer", async function () {
    const { operator, signer, payee, alice, agent } = await deployStack();
    const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, "");
    const agentId = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId;

    await agent.connect(operator).setSigner(agentId, alice.address);
    expect(await agent.signerOf(agentId)).to.equal(alice.address);
  });

  it("M3 fix: payee == signer rejected (self-deal forbidden)", async function () {
    const { operator, signer, WAERE, agent, fac } = await deployStack();
    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: 10n * ONE });
    await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE);
    await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE);

    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
    // Signer is also the payee — compromised key drain attempt.
    const sig = await signAuth(signer, fac, agentId, await WAERE.getAddress(), signer.address, 1n * ONE, ethers.ZeroHash, 1n, deadline);
    await expect(
      fac.connect(signer).settle(agentId, await WAERE.getAddress(), signer.address, 1n * ONE, ethers.ZeroHash, 1n, deadline, sig)
    ).to.be.revertedWithCustomError(fac, "SelfDealForbidden");
  });

  it("withdraw returns idle balance to operator-chosen recipient", async function () {
    const { operator, signer, alice, WAERE, agent } = await deployStack();
    const tx = await agent.connect(operator).registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 0, 0, 0, "");
    const agentId = (await tx.wait()).logs.find(l => l.fragment?.name === "AgentRegistered").args.agentId;

    await WAERE.connect(operator).deposit({ value: 10n * ONE });
    await WAERE.connect(operator).approve(await agent.getAddress(), 10n * ONE);
    await agent.connect(operator).topUp(agentId, await WAERE.getAddress(), 10n * ONE);

    const before = await WAERE.balanceOf(alice.address);
    await agent.connect(operator).withdraw(agentId, await WAERE.getAddress(), 4n * ONE, alice.address);
    expect(await WAERE.balanceOf(alice.address) - before).to.equal(4n * ONE);
    expect(await agent.balances(agentId, await WAERE.getAddress())).to.equal(6n * ONE);
  });
});
