// AereAccountMigrator tests.
//
// Proves the account-layer migration path is real, no-custody, fail-closed, and
// atomic:
//   - a user moves ERC-20 balances from an EOA to a destination in one tx
//     (both the plain `migrate` and the Falcon-derived `migrateToPqcAccount`);
//   - native AERE forwards to the destination;
//   - a zero destination reverts;
//   - a failed token transfer reverts the WHOLE migration (atomicity);
//   - the migrator can only send to the specified destination (nothing leaks to
//     a third party);
//   - there is no admin function that can drain, and the migrator holds no custody.
//
// The destination in the strong path is a REAL AerePQCAccount deployed through the
// REAL AerePQCAccountFactory (Falcon verify mocked, since a genuine Falcon-512
// signature cannot be produced inside the hardhat process; the CREATE2 derivation
// and asset movement are fully real).

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

const ONE18 = 10n ** 18n;

// A valid-shape 897-byte NIST Falcon-512 public key: header 0x09 then zeros.
// predictAddress / createAccount only check length + header, so this exercises
// the real CREATE2 path without a real key.
function falconPubKey() {
  const b = new Uint8Array(897);
  b[0] = 0x09;
  return "0x" + Buffer.from(b).toString("hex");
}

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

  const ERC20 = await ethers.getContractFactory("MockERC20Lending");
  const tokenA = await ERC20.deploy("Token A", "TKA", 18);
  await tokenA.waitForDeployment();
  const tokenB = await ERC20.deploy("Token B", "TKB", 18);
  await tokenB.waitForDeployment();

  // Real PQC account factory (Falcon verify + entryPoint mocked / placeholder).
  const Verifier = await ethers.getContractFactory("MockFalcon512Verifier");
  const verifier = await Verifier.deploy();
  await verifier.waitForDeployment();
  await verifier.setDefault(true);

  const entryPoint = deployer.address; // any non-zero address; unused by migration

  const Factory = await ethers.getContractFactory("AerePQCAccountFactory");
  const factory = await Factory.deploy(entryPoint, await verifier.getAddress());
  await factory.waitForDeployment();

  const Migrator = await ethers.getContractFactory("AereAccountMigrator");
  const migrator = await Migrator.deploy();
  await migrator.waitForDeployment();

  return { deployer, alice, bob, carol, tokenA, tokenB, factory, migrator };
}

describe("AereAccountMigrator", function () {
  it("migrates ERC-20 balances from an EOA to a destination in one tx", async function () {
    const env = await deployStack();
    const pk = falconPubKey();
    const salt = 42n;

    // Deploy the real destination PQC account through the real factory.
    await env.factory.createAccount(pk, salt);
    const dest = await env.factory.predictAddress(pk, salt);
    expect(await ethers.provider.getCode(dest)).to.not.equal("0x");

    // Alice (old EOA) funds and approves the migrator for both tokens.
    await env.tokenA.mint(env.alice.address, 100n * ONE18);
    await env.tokenB.mint(env.alice.address, 250n * ONE18);
    await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 100n * ONE18);
    await env.tokenB.connect(env.alice).approve(await env.migrator.getAddress(), 250n * ONE18);

    const tokens = [await env.tokenA.getAddress(), await env.tokenB.getAddress()];
    const amounts = [100n * ONE18, 250n * ONE18];

    await expect(env.migrator.connect(env.alice).migrate(dest, tokens, amounts, false))
      .to.emit(env.migrator, "Migrated")
      .withArgs(env.alice.address, dest, tokens, amounts, 0n);

    // Everything landed on the destination; nothing left on the old EOA.
    expect(await env.tokenA.balanceOf(dest)).to.equal(100n * ONE18);
    expect(await env.tokenB.balanceOf(dest)).to.equal(250n * ONE18);
    expect(await env.tokenA.balanceOf(env.alice.address)).to.equal(0n);
    expect(await env.tokenB.balanceOf(env.alice.address)).to.equal(0n);
    // The migrator took no custody.
    expect(await env.tokenA.balanceOf(await env.migrator.getAddress())).to.equal(0n);
    expect(await env.tokenB.balanceOf(await env.migrator.getAddress())).to.equal(0n);
  });

  it("migrateToPqcAccount derives the destination from the Falcon key via the live factory", async function () {
    const env = await deployStack();
    const pk = falconPubKey();
    const salt = 7n;
    const dest = await env.factory.predictAddress(pk, salt);

    await env.tokenA.mint(env.alice.address, 500n * ONE18);
    await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 500n * ONE18);

    const tokens = [await env.tokenA.getAddress()];
    const amounts = [500n * ONE18];

    // expectedDestination guard matches -> ok. Note: destination need NOT be
    // deployed yet; it can hold assets at the counterfactual address.
    await expect(env.migrator.connect(env.alice).migrateToPqcAccount(
      await env.factory.getAddress(), pk, salt, dest, tokens, amounts, false
    )).to.emit(env.migrator, "Migrated").withArgs(env.alice.address, dest, tokens, amounts, 0n);

    expect(await env.tokenA.balanceOf(dest)).to.equal(500n * ONE18);

    // A wrong expectedDestination fails closed.
    await env.tokenA.mint(env.alice.address, 1n * ONE18);
    await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 1n * ONE18);
    await expect(env.migrator.connect(env.alice).migrateToPqcAccount(
      await env.factory.getAddress(), pk, salt, env.bob.address, tokens, [1n * ONE18], false
    )).to.be.revertedWithCustomError(env.migrator, "AddressMismatch");
  });

  it("forwards native AERE to the destination in the same migration", async function () {
    const env = await deployStack();
    const pk = falconPubKey();
    const salt = 9n;
    await env.factory.createAccount(pk, salt); // real account has receive()
    const dest = await env.factory.predictAddress(pk, salt);

    const before = await ethers.provider.getBalance(dest);
    await env.migrator.connect(env.alice).migrate(dest, [], [], true, { value: 3n * ONE18 });
    const after = await ethers.provider.getBalance(dest);
    expect(after - before).to.equal(3n * ONE18);
    // No native stuck in the migrator.
    expect(await ethers.provider.getBalance(await env.migrator.getAddress())).to.equal(0n);
  });

  it("reverts on a zero destination", async function () {
    const env = await deployStack();
    await env.tokenA.mint(env.alice.address, 1n * ONE18);
    await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 1n * ONE18);
    await expect(env.migrator.connect(env.alice).migrate(
      ethers.ZeroAddress, [await env.tokenA.getAddress()], [1n * ONE18], false
    )).to.be.revertedWithCustomError(env.migrator, "ZeroDestination");
  });

  it("is atomic: a failed token transfer reverts the whole migration", async function () {
    const env = await deployStack();
    const pk = falconPubKey();
    const salt = 11n;
    const dest = await env.factory.predictAddress(pk, salt);

    await env.tokenA.mint(env.alice.address, 100n * ONE18);
    await env.tokenB.mint(env.alice.address, 100n * ONE18);
    // Approve tokenA fully, but UNDER-approve tokenB so its transfer fails.
    await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 100n * ONE18);
    await env.tokenB.connect(env.alice).approve(await env.migrator.getAddress(), 10n * ONE18);

    const tokens = [await env.tokenA.getAddress(), await env.tokenB.getAddress()];
    const amounts = [100n * ONE18, 100n * ONE18];

    await expect(env.migrator.connect(env.alice).migrate(dest, tokens, amounts, false)).to.be.reverted;

    // tokenA (the first, "successful" leg) was NOT moved: no partial migration.
    expect(await env.tokenA.balanceOf(dest)).to.equal(0n);
    expect(await env.tokenA.balanceOf(env.alice.address)).to.equal(100n * ONE18);
    expect(await env.tokenB.balanceOf(dest)).to.equal(0n);
  });

  it("can only send to the specified destination; nothing leaks to a third party", async function () {
    const env = await deployStack();
    const dest = env.carol.address;

    await env.tokenA.mint(env.alice.address, 100n * ONE18);
    await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 100n * ONE18);

    const bobBefore = await env.tokenA.balanceOf(env.bob.address);
    await env.migrator.connect(env.alice).migrate(dest, [await env.tokenA.getAddress()], [100n * ONE18], false);

    // Only `dest` (carol) received; an unrelated address (bob) got nothing, and
    // the migrator retained nothing.
    expect(await env.tokenA.balanceOf(dest)).to.equal(100n * ONE18);
    expect(await env.tokenA.balanceOf(env.bob.address)).to.equal(bobBefore);
    expect(await env.tokenA.balanceOf(await env.migrator.getAddress())).to.equal(0n);
  });

  it("has no admin drain: no owner/withdraw/rescue/sweep, and stray tokens cannot be pulled out", async function () {
    const env = await deployStack();
    const iface = env.migrator.interface;
    // No admin / custody-exit surface exists in the ABI.
    for (const fn of ["owner", "withdraw", "rescue", "rescueERC20", "sweep", "sweepToken", "transferOwnership", "initialize"]) {
      expect(iface.getFunction(fn), `unexpected function ${fn}`).to.equal(null);
    }

    // Even if tokens are force-sent to the migrator, there is no function to move
    // them out: the balance is stuck-but-safe (no privileged drain).
    await env.tokenA.mint(await env.migrator.getAddress(), 5n * ONE18);
    expect(await env.tokenA.balanceOf(await env.migrator.getAddress())).to.equal(5n * ONE18);
    // The only asset-moving entrypoints require the CALLER to be the token source;
    // they cannot move the migrator's own balance to anyone.
    // (Confirmed by ABI absence above; nothing further to call.)
  });

  it("fails closed on native: stray value when not moving native, and moveNative with zero value", async function () {
    const env = await deployStack();
    const dest = env.carol.address;
    await env.tokenA.mint(env.alice.address, 1n * ONE18);
    await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 1n * ONE18);

    // Sending value while moveNative=false must revert (StrayNative).
    await expect(env.migrator.connect(env.alice).migrate(
      dest, [await env.tokenA.getAddress()], [1n * ONE18], false, { value: 1n }
    )).to.be.revertedWithCustomError(env.migrator, "StrayNative");

    // moveNative=true with zero value must revert (ZeroNative).
    await expect(env.migrator.connect(env.alice).migrate(dest, [], [], true, { value: 0n }))
      .to.be.revertedWithCustomError(env.migrator, "ZeroNative");

    // An empty migration (no tokens, no native) reverts (NothingToMigrate).
    await expect(env.migrator.connect(env.alice).migrate(dest, [], [], false))
      .to.be.revertedWithCustomError(env.migrator, "NothingToMigrate");
  });

  it("rejects malformed asset sets: length mismatch, zero token, zero amount", async function () {
    const env = await deployStack();
    const dest = env.carol.address;
    const a = await env.tokenA.getAddress();

    await expect(env.migrator.connect(env.alice).migrate(dest, [a], [1n, 2n], false))
      .to.be.revertedWithCustomError(env.migrator, "LengthMismatch");
    await expect(env.migrator.connect(env.alice).migrate(dest, [ethers.ZeroAddress], [1n], false))
      .to.be.revertedWithCustomError(env.migrator, "ZeroToken");

    await env.tokenA.mint(env.alice.address, 1n * ONE18);
    await env.tokenA.connect(env.alice).approve(await env.migrator.getAddress(), 1n * ONE18);
    await expect(env.migrator.connect(env.alice).migrate(dest, [a], [0n], false))
      .to.be.revertedWithCustomError(env.migrator, "ZeroAmount");
  });
});
