// End-to-end test of the USDC.e Hyperlane Warp Route:
//   Ethereum (chain 1) → Lock native USDC in AereHypERC20Collateral
//   → MockMailbox dispatch → MockMailbox sister handle
//   → AereHypERC20Synthetic mints USDC.e to recipient on AERE (chain 2800).
//
//   Round-trip: User on AERE burns USDC.e via transferRemote
//   → reverse-direction message → AereHypERC20Collateral releases native USDC.
//
// Mocks the Mailbox via MockHypMailbox (in-tx delivery, no relayer needed).
//
// Run: npx hardhat test test/usdce-warp-route.test.js

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

const ETH_DOMAIN  = 1;     // Hyperlane mainnet domain id
const AERE_DOMAIN = 2800;  // we use chainId as domain id (matches actual Hyperlane convention for AERE deploy)
const SIX_DECIMALS = 10n ** 6n;

function addrToBytes32(addr) {
  return ethers.zeroPadValue(addr, 32);
}

describe("USDC.e Hyperlane Warp Route — full round trip", function () {
  let deployer, alice, bob;
  let ethMailbox, aereMailbox;
  let usdcNative;        // native USDC on Ethereum (mock)
  let collateral;        // AereHypERC20Collateral on Ethereum
  let usdce;             // USDC.e ERC-20 on AERE
  let synthetic;         // AereHypERC20Synthetic on AERE

  beforeEach(async function () {
    [deployer, alice, bob] = await ethers.getSigners();

    // 1) Two mock Mailboxes (one per chain).
    const Mailbox = await ethers.getContractFactory("MockHypMailbox");
    ethMailbox = await Mailbox.deploy(ETH_DOMAIN);
    await ethMailbox.waitForDeployment();
    aereMailbox = await Mailbox.deploy(AERE_DOMAIN);
    await aereMailbox.waitForDeployment();

    // 2) Native USDC mock on Ethereum, with seed balance for Alice.
    usdcNative = await (await ethers.getContractFactory("MockUSDC")).deploy();
    await usdcNative.waitForDeployment();
    await usdcNative.mint(alice.address, 1_000_000n * SIX_DECIMALS);
    // Liquidity for the Ethereum-side collateral lockbox to release on inbound.
    // (In production, the collateral starts empty and accrues with deposits.)

    // 3) Collateral lockbox on Ethereum.
    collateral = await (await ethers.getContractFactory("AereHypERC20Collateral")).deploy(
      await ethMailbox.getAddress(),
      await usdcNative.getAddress()
    );
    await collateral.waitForDeployment();

    // 4) USDC.e on AERE — Synthetic deploys AFTER it (synthetic is the BRIDGE).
    //    Compute the synthetic's future address so we can pass it into USDC.e's constructor.
    const nonce = await ethers.provider.getTransactionCount(deployer.address);
    const futureSyntheticAddr = ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 });

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

    synthetic = await (await ethers.getContractFactory("AereHypERC20Synthetic")).deploy(
      await aereMailbox.getAddress(),
      await usdce.getAddress(),
      6 // AUDIT FIX (HIGH #17): expectedDecimals (USDC.e has 6)
    );
    await synthetic.waitForDeployment();

    expect((await synthetic.getAddress()).toLowerCase()).to.equal(futureSyntheticAddr.toLowerCase());
    expect(await usdce.BRIDGE()).to.equal(await synthetic.getAddress());

    // 5) Enroll trusted routers (each side knows the other's contract address).
    await collateral.enrollRouter(AERE_DOMAIN, addrToBytes32(await synthetic.getAddress()));
    // AUDIT FIX (HIGH #16): synthetic uses 7-day timelock now.
    await synthetic.proposeEnrollRouter(ETH_DOMAIN, addrToBytes32(await collateral.getAddress()));
    await ethers.provider.send("evm_increaseTime", [7 * 24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await synthetic.finalizeEnrollRouter(ETH_DOMAIN);

    // 6) Pair the mock Mailboxes. Each Mailbox knows its sister AND the
    //    local-side application contract that receives inbound .handle()
    //    calls. The sister Mailbox is the one that performs the delivery
    //    so the recipient sees msg.sender == local Mailbox (matching
    //    production Hyperlane semantics).
    await ethMailbox.setSister(await aereMailbox.getAddress(), await collateral.getAddress());
    await aereMailbox.setSister(await ethMailbox.getAddress(), await synthetic.getAddress());
  });

  it("deposit (Ethereum → AERE): lock USDC, mint USDC.e", async function () {
    const amount = 100n * SIX_DECIMALS; // 100 USDC

    // Alice approves the collateral and bridges 100 USDC to bob's address on AERE.
    await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
    const fee = await ethMailbox.fee();

    const tx = await collateral.connect(alice).transferRemote(
      AERE_DOMAIN,
      addrToBytes32(bob.address),
      amount,
      { value: fee }
    );
    const receipt = await tx.wait();

    // Collateral locked.
    expect(await usdcNative.balanceOf(await collateral.getAddress())).to.equal(amount);
    expect(await usdcNative.balanceOf(alice.address)).to.equal(1_000_000n * SIX_DECIMALS - amount);

    // USDC.e minted to bob on AERE.
    expect(await usdce.balanceOf(bob.address)).to.equal(amount);
    expect(await usdce.totalSupply()).to.equal(amount);

    // ReceivedTransferRemote emitted on AERE side.
    const aereLogs = await aereMailbox.queryFilter(aereMailbox.filters.Handled());
    expect(aereLogs.length).to.equal(1);
  });

  it("withdrawal (AERE → Ethereum): burn USDC.e, release USDC", async function () {
    // First bridge 200 USDC over so Bob has USDC.e to send back.
    const amount = 200n * SIX_DECIMALS;
    await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
    const fee = await ethMailbox.fee();
    await collateral.connect(alice).transferRemote(AERE_DOMAIN, addrToBytes32(bob.address), amount, { value: fee });

    expect(await usdce.balanceOf(bob.address)).to.equal(amount);
    expect(await usdcNative.balanceOf(await collateral.getAddress())).to.equal(amount);

    // Now Bob bridges 80 USDC.e back to Alice on Ethereum.
    const back = 80n * SIX_DECIMALS;
    const feeBack = await aereMailbox.fee();
    await synthetic.connect(bob).transferRemote(
      ETH_DOMAIN,
      addrToBytes32(alice.address),
      back,
      { value: feeBack }
    );

    // Bob's USDC.e burned.
    expect(await usdce.balanceOf(bob.address)).to.equal(amount - back);

    // Alice received 80 USDC on Ethereum.
    expect(await usdcNative.balanceOf(alice.address)).to.equal(1_000_000n * SIX_DECIMALS - amount + back);

    // Collateral lockbox decreased by 80 USDC.
    expect(await usdcNative.balanceOf(await collateral.getAddress())).to.equal(amount - back);
  });

  it("reserve invariant: collateral.lockedSupply() == usdce.totalSupply()", async function () {
    const amount = 500n * SIX_DECIMALS;
    await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
    const fee = await ethMailbox.fee();
    await collateral.connect(alice).transferRemote(AERE_DOMAIN, addrToBytes32(bob.address), amount, { value: fee });
    expect(await collateral.lockedSupply()).to.equal(await usdce.totalSupply());

    // Half round-trip:
    const back = 200n * SIX_DECIMALS;
    const feeBack = await aereMailbox.fee();
    await synthetic.connect(bob).transferRemote(ETH_DOMAIN, addrToBytes32(alice.address), back, { value: feeBack });
    expect(await collateral.lockedSupply()).to.equal(await usdce.totalSupply());
  });

  it("rejects mint from non-bridge caller", async function () {
    await expect(usdce.connect(alice).mint(alice.address, 1n)).to.be.revertedWithCustomError(usdce, "OnlyBridge");
    await expect(usdce.connect(bob).burnFrom(bob.address, 1n)).to.be.revertedWithCustomError(usdce, "OnlyBridge");
  });

  it("rejects handle() from non-mailbox caller", async function () {
    await expect(
      synthetic.connect(alice).handle(
        ETH_DOMAIN,
        addrToBytes32(await collateral.getAddress()),
        ethers.zeroPadValue("0x00", 64)
      )
    ).to.be.revertedWithCustomError(synthetic, "NotMailbox");
  });

  it("rejects message from unknown router (replay-protection across domains)", async function () {
    // De-enroll the AERE synthetic on collateral, then simulate inbound to collateral
    // from AereMailbox.
    await collateral.unenrollRouter(AERE_DOMAIN);
    // Re-pair the mocks so dispatching from AereMailbox reaches collateral.
    // (Already set in beforeEach; we just call the path.)
    const feeBack = await aereMailbox.fee();

    // Try a transfer Aere → Eth — collateral has no router enrolled for AERE_DOMAIN, so
    // the inbound handle() on collateral reverts UnknownRouter.
    await usdcNative.connect(alice).approve(await collateral.getAddress(), 100n * SIX_DECIMALS);
    await collateral.enrollRouter(AERE_DOMAIN, addrToBytes32(await synthetic.getAddress())); // re-enroll for first leg
    await collateral.connect(alice).transferRemote(AERE_DOMAIN, addrToBytes32(bob.address), 100n * SIX_DECIMALS, { value: feeBack });

    // De-enroll AGAIN to simulate a router rotation that breaks reverse leg.
    await collateral.unenrollRouter(AERE_DOMAIN);

    await expect(
      synthetic.connect(bob).transferRemote(ETH_DOMAIN, addrToBytes32(alice.address), 50n * SIX_DECIMALS, { value: feeBack })
    ).to.be.revertedWithCustomError(collateral, "UnknownRouter");
  });

  it("requires sufficient IGP fee on outbound", async function () {
    const amount = 1n * SIX_DECIMALS;
    await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
    const fee = await ethMailbox.fee();
    await expect(
      collateral.connect(alice).transferRemote(
        AERE_DOMAIN,
        addrToBytes32(bob.address),
        amount,
        { value: fee - 1n }
      )
    ).to.be.revertedWithCustomError(collateral, "InsufficientIgpPayment");
  });

  it("refunds overpayment of IGP fee", async function () {
    const amount = 50n * SIX_DECIMALS;
    await usdcNative.connect(alice).approve(await collateral.getAddress(), amount);
    const fee = await ethMailbox.fee();
    const overpay = fee + 5n * 10n ** 15n; // fee + 0.005 ETH

    const balBefore = await ethers.provider.getBalance(alice.address);
    const tx = await collateral.connect(alice).transferRemote(
      AERE_DOMAIN,
      addrToBytes32(bob.address),
      amount,
      { value: overpay }
    );
    const receipt = await tx.wait();
    const gasCost = receipt.gasUsed * receipt.gasPrice;
    const balAfter = await ethers.provider.getBalance(alice.address);

    // Alice paid only fee + gas, refund of (overpay - fee) returned.
    expect(balBefore - balAfter - gasCost).to.equal(fee);
  });
});
