// AereWarpRouteV3 — USDC.e Hyperlane v3 route on the AERE side (chain 2800),
// bound to a faithful AereMailboxV3-like mock. Covers both token modes:
//   MintBurn (synthetic USDC.e, the canonical bridged representation) and
//   Lock (release an existing token the router does not mint).
//
// Run: npx hardhat test test/aere-warp-route-v3.test.js

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

const ETH_DOMAIN = 1;
const AERE_DOMAIN = 2800;
const SIX = 10n ** 6n;

function addrToBytes32(a) {
  return ethers.zeroPadValue(a, 32);
}
function packBody(recipientB32, amount) {
  return ethers.solidityPacked(["bytes32", "uint256"], [recipientB32, amount]);
}

describe("AereWarpRouteV3 — USDC.e Hyperlane v3 route (AERE side)", function () {
  let deployer, alice, bob, carol;
  let mailbox, igp;

  beforeEach(async function () {
    [deployer, alice, bob, carol] = await ethers.getSigners();
    mailbox = await (await ethers.getContractFactory("MockWarpMailboxV3")).deploy(AERE_DOMAIN);
    await mailbox.waitForDeployment();
    igp = await (await ethers.getContractFactory("MockWarpIGPV3")).deploy();
    await igp.waitForDeployment();
  });

  // ─────────────────────── MintBurn (synthetic USDC.e) ───────────────────────
  describe("MintBurn mode (synthetic USDC.e)", function () {
    let usdce, router;

    beforeEach(async function () {
      // USDC.e's BRIDGE is immutable, so predict the router address (deployer
      // nonce + 1) and deploy USDC.e pointing at it first — same pattern the
      // multi-stable-warp deploy script uses.
      const nonce = await ethers.provider.getTransactionCount(deployer.address);
      const futureRouter = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 });

      usdce = await (await ethers.getContractFactory("USDCe")).deploy(futureRouter);
      await usdce.waitForDeployment();

      router = await (await ethers.getContractFactory("AereWarpRouteV3")).deploy(
        await mailbox.getAddress(),
        await igp.getAddress(),
        await usdce.getAddress(),
        1 // Mode.MintBurn
      );
      await router.waitForDeployment();

      expect((await router.getAddress()).toLowerCase()).to.equal(futureRouter.toLowerCase());
      expect(await usdce.BRIDGE()).to.equal(await router.getAddress());
    });

    it("wires immutables (mailbox / igp / token / mode)", async function () {
      expect(await router.MAILBOX()).to.equal(await mailbox.getAddress());
      expect(await router.IGP()).to.equal(await igp.getAddress());
      expect(await router.TOKEN()).to.equal(await usdce.getAddress());
      expect(await router.MODE()).to.equal(1n);
    });

    it("enrollRemoteRouter is owner-only", async function () {
      const r = addrToBytes32(carol.address);
      await expect(router.connect(alice).enrollRemoteRouter(ETH_DOMAIN, r))
        .to.be.revertedWith("Ownable: caller is not the owner");
      await expect(router.enrollRemoteRouter(ETH_DOMAIN, r))
        .to.emit(router, "RemoteRouterEnrolled").withArgs(ETH_DOMAIN, r);
      expect(await router.routers(ETH_DOMAIN)).to.equal(r);
    });

    it("handle from a non-mailbox caller reverts", async function () {
      await expect(
        router.connect(alice).handle(ETH_DOMAIN, addrToBytes32(carol.address), "0x" + "00".repeat(64))
      ).to.be.revertedWithCustomError(router, "NotMailbox");
    });

    it("handle from an unenrolled remote router reverts", async function () {
      const body = packBody(addrToBytes32(bob.address), 5n * SIX);
      await expect(
        mailbox.mockDeliver(await router.getAddress(), ETH_DOMAIN, addrToBytes32(carol.address), body)
      ).to.be.revertedWithCustomError(router, "UnknownRouter");
    });

    it("enrolled inbound mints exactly the amount to the recipient", async function () {
      const remote = addrToBytes32(carol.address);
      await router.enrollRemoteRouter(ETH_DOMAIN, remote);
      const amount = 123n * SIX;
      const body = packBody(addrToBytes32(bob.address), amount);

      await expect(mailbox.mockDeliver(await router.getAddress(), ETH_DOMAIN, remote, body))
        .to.emit(router, "ReceivedTransferRemote").withArgs(ETH_DOMAIN, bob.address, amount);

      expect(await usdce.balanceOf(bob.address)).to.equal(amount);
      expect(await usdce.totalSupply()).to.equal(amount);
      expect(await router.totalReceivedIn()).to.equal(amount);
    });

    it("transferRemote burns caller tokens, quotes, dispatches with msg.value==quote, emits", async function () {
      const remote = addrToBytes32(carol.address);
      await router.enrollRemoteRouter(ETH_DOMAIN, remote);

      // Seed bob with USDC.e via a verified inbound.
      const seed = 100n * SIX;
      await mailbox.mockDeliver(
        await router.getAddress(), ETH_DOMAIN, remote, packBody(addrToBytes32(bob.address), seed)
      );

      const amount = 40n * SIX;
      const recipient = addrToBytes32(alice.address);
      const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipient, amount);
      expect(fee).to.equal(await mailbox.feeAmount());

      await expect(router.connect(bob).transferRemote(ETH_DOMAIN, recipient, amount, { value: fee }))
        .to.emit(router, "SentTransferRemote");

      // Caller's tokens burned.
      expect(await usdce.balanceOf(bob.address)).to.equal(seed - amount);
      // Dispatch received exactly the quoted fee, with the right payload.
      expect(await mailbox.lastFeePaid()).to.equal(fee);
      expect(await mailbox.lastDestination()).to.equal(ETH_DOMAIN);
      expect(await mailbox.lastRecipient()).to.equal(remote);
      expect(await mailbox.lastBody()).to.equal(packBody(recipient, amount));
      // No dust left in the router; mailbox holds exactly the fee.
      expect(await ethers.provider.getBalance(await router.getAddress())).to.equal(0n);
      expect(await ethers.provider.getBalance(await mailbox.getAddress())).to.equal(fee);
      expect(await router.totalSentOut()).to.equal(amount);
    });

    it("transferRemote refunds overpayment above the quote", async function () {
      const remote = addrToBytes32(carol.address);
      await router.enrollRemoteRouter(ETH_DOMAIN, remote);
      await mailbox.mockDeliver(
        await router.getAddress(), ETH_DOMAIN, remote, packBody(addrToBytes32(bob.address), 10n * SIX)
      );

      const amount = 3n * SIX;
      const recipient = addrToBytes32(alice.address);
      const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipient, amount);
      const overpay = fee + ethers.parseEther("0.05");

      const balBefore = await ethers.provider.getBalance(bob.address);
      const tx = await router.connect(bob).transferRemote(ETH_DOMAIN, recipient, amount, { value: overpay });
      const rc = await tx.wait();
      const gas = rc.gasUsed * rc.gasPrice;
      const balAfter = await ethers.provider.getBalance(bob.address);

      expect(balBefore - balAfter - gas).to.equal(fee);
      expect(await ethers.provider.getBalance(await router.getAddress())).to.equal(0n);
    });

    it("transferRemote reverts when msg.value < quote", async function () {
      const remote = addrToBytes32(carol.address);
      await router.enrollRemoteRouter(ETH_DOMAIN, remote);
      await mailbox.mockDeliver(
        await router.getAddress(), ETH_DOMAIN, remote, packBody(addrToBytes32(bob.address), 5n * SIX)
      );
      const recipient = addrToBytes32(alice.address);
      const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipient, 1n * SIX);
      await expect(
        router.connect(bob).transferRemote(ETH_DOMAIN, recipient, 1n * SIX, { value: fee - 1n })
      ).to.be.revertedWithCustomError(router, "InsufficientGasPayment");
    });

    it("transferRemote input guards: no router / zero amount / zero recipient", async function () {
      const recipient = addrToBytes32(alice.address);
      const fee = ethers.parseEther("0.001");
      await expect(router.connect(bob).transferRemote(999, recipient, 1n, { value: fee }))
        .to.be.revertedWithCustomError(router, "NoRouterEnrolled");
      await router.enrollRemoteRouter(ETH_DOMAIN, addrToBytes32(carol.address));
      await expect(router.connect(bob).transferRemote(ETH_DOMAIN, recipient, 0n, { value: fee }))
        .to.be.revertedWithCustomError(router, "ZeroAmount");
      await expect(router.connect(bob).transferRemote(ETH_DOMAIN, ethers.ZeroHash, 1n, { value: fee }))
        .to.be.revertedWithCustomError(router, "ZeroRecipient");
    });

    it("quoteGasPayment view reads the IGP", async function () {
      expect(await router.quoteGasPayment(ETH_DOMAIN, 50000n)).to.equal(50000n * 10n ** 9n);
    });
  });

  // ─────────────────────── Lock (release existing token) ───────────────────────
  describe("Lock mode (release existing token)", function () {
    let token, router;

    beforeEach(async function () {
      token = await (await ethers.getContractFactory("MockUSDC")).deploy();
      await token.waitForDeployment();
      router = await (await ethers.getContractFactory("AereWarpRouteV3")).deploy(
        await mailbox.getAddress(),
        await igp.getAddress(),
        await token.getAddress(),
        0 // Mode.Lock
      );
      await router.waitForDeployment();
    });

    it("transferRemote locks tokens into the router and dispatches", async function () {
      const remote = addrToBytes32(carol.address);
      await router.enrollRemoteRouter(ETH_DOMAIN, remote);
      const amount = 250n * SIX;
      await token.mint(bob.address, amount);
      await token.connect(bob).approve(await router.getAddress(), amount);
      const recipient = addrToBytes32(alice.address);
      const fee = await router.quoteTransferRemote(ETH_DOMAIN, recipient, amount);

      await expect(router.connect(bob).transferRemote(ETH_DOMAIN, recipient, amount, { value: fee }))
        .to.emit(router, "SentTransferRemote");

      expect(await token.balanceOf(await router.getAddress())).to.equal(amount);
      expect(await token.balanceOf(bob.address)).to.equal(0n);
      expect(await router.lockedSupply()).to.equal(amount);
    });

    it("enrolled inbound releases exactly the amount to the recipient", async function () {
      const remote = addrToBytes32(carol.address);
      await router.enrollRemoteRouter(ETH_DOMAIN, remote);
      await token.mint(await router.getAddress(), 500n * SIX);
      const amount = 175n * SIX;
      const body = packBody(addrToBytes32(bob.address), amount);

      await expect(mailbox.mockDeliver(await router.getAddress(), ETH_DOMAIN, remote, body))
        .to.emit(router, "ReceivedTransferRemote").withArgs(ETH_DOMAIN, bob.address, amount);

      expect(await token.balanceOf(bob.address)).to.equal(amount);
      expect(await router.lockedSupply()).to.equal(500n * SIX - amount);
    });

    it("handle from an unenrolled remote router reverts", async function () {
      await token.mint(await router.getAddress(), 100n * SIX);
      const body = packBody(addrToBytes32(bob.address), 1n * SIX);
      await expect(
        mailbox.mockDeliver(await router.getAddress(), ETH_DOMAIN, addrToBytes32(carol.address), body)
      ).to.be.revertedWithCustomError(router, "UnknownRouter");
    });
  });
});
