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

// Unit tests for the post-quantum ERC-4337 smart account (roadmap #270).
//
// The Falcon-512 verification itself is exhaustively KAT-validated in
// AereFalcon512Verifier.test.js, and the REAL end-to-end Falcon path (genuine
// reference-generated keypair + signature, verified by the LIVE on-chain
// verifier) is proven on mainnet by scripts/deploy-pqc-account.js. Here we
// isolate the ACCOUNT + FACTORY logic with a settable MockFalcon512Verifier so
// both the accept and reject branches of every path are asserted precisely.

const PK = "0x09" + "ab".repeat(896);      // 897-byte, 0x09-headed Falcon-512 pubkey shape
const BAD_PK_LEN = "0x09" + "ab".repeat(10);
const BAD_PK_HDR = "0x08" + "ab".repeat(896);
const NONCE = "0x" + "11".repeat(40);      // 40-byte Falcon salt
const COMPSIG = "0x" + "22".repeat(80);    // compressed s2 encoding (dummy)

const coder = ethers.AbiCoder.defaultAbiCoder();
const envelope = (nonce, compSig) => coder.encode(["bytes", "bytes"], [nonce, compSig]);
// EIP-1271 (isValidSignature) verifies the raw 32 bytes of the hash.
const msgOf = (hash) => hash;
// The fund-moving paths (validateUserOp, executeWithFalcon) are DOMAIN-SEPARATED:
// they verify over abi.encodePacked(domain, hash) = 64 bytes. Constants must match
// the contract, so a raw-hash EIP-1271 login signature can never authorize a spend.
const USEROP_DOMAIN = ethers.id("AerePQCAccount.validateUserOp.v1");
const EXEC_DOMAIN = ethers.id("AerePQCAccount.executeWithFalcon.v1");
const msgDomained = (domain, hash) => ethers.concat([domain, hash]);

async function deployFixture() {
  const [deployer, entryPointEOA, other] = await ethers.getSigners();
  const Mock = await ethers.getContractFactory("MockFalcon512Verifier");
  const mock = await Mock.deploy();
  await mock.waitForDeployment();

  const Factory = await ethers.getContractFactory("AerePQCAccountFactory");
  const factory = await Factory.deploy(entryPointEOA.address, await mock.getAddress());
  await factory.waitForDeployment();

  return { deployer, entryPointEOA, other, mock, factory };
}

describe("AerePQCAccountFactory", function () {
  it("predicts deterministically and deploys at the predicted address", async function () {
    const { factory } = await deployFixture();
    const predicted = await factory.predictAddress(PK, 0);
    await (await factory.createAccount(PK, 0)).wait();
    expect(await ethers.provider.getCode(predicted)).to.not.equal("0x");
  });

  it("is idempotent: a second createAccount returns the same account", async function () {
    const { factory } = await deployFixture();
    const predicted = await factory.predictAddress(PK, 0);
    await (await factory.createAccount(PK, 0)).wait();
    await (await factory.createAccount(PK, 0)).wait(); // no revert, no redeploy
    const acct = await ethers.getContractAt("AerePQCAccount", predicted);
    expect((await acct.falconPubKey()).toLowerCase()).to.equal(PK.toLowerCase());
  });

  it("different salt or different pubkey => different address", async function () {
    const { factory } = await deployFixture();
    const a = await factory.predictAddress(PK, 0);
    const b = await factory.predictAddress(PK, 1);
    const c = await factory.predictAddress("0x09" + "cd".repeat(896), 0);
    expect(a).to.not.equal(b);
    expect(a).to.not.equal(c);
  });

  it("rejects a malformed Falcon public key", async function () {
    const { factory } = await deployFixture();
    await expect(factory.createAccount(BAD_PK_LEN, 0)).to.be.revertedWithCustomError(factory, "InvalidFalconPubKey");
    await expect(factory.createAccount(BAD_PK_HDR, 0)).to.be.revertedWithCustomError(factory, "InvalidFalconPubKey");
  });

  it("wires the account's owner key, entryPoint and verifier", async function () {
    const { factory, entryPointEOA, mock } = await deployFixture();
    const predicted = await factory.predictAddress(PK, 7);
    await (await factory.createAccount(PK, 7)).wait();
    const acct = await ethers.getContractAt("AerePQCAccount", predicted);
    expect(await acct.entryPoint()).to.equal(entryPointEOA.address);
    expect(await acct.falcon()).to.equal(await mock.getAddress());
    expect(await acct.factory()).to.equal(await factory.getAddress());
  });
});

describe("AerePQCAccount", function () {
  async function accountFixture() {
    const f = await deployFixture();
    const predicted = await f.factory.predictAddress(PK, 0);
    await (await f.factory.createAccount(PK, 0)).wait();
    const account = await ethers.getContractAt("AerePQCAccount", predicted);
    // Fund the account so it can move value in execute tests.
    await (await f.deployer.sendTransaction({ to: predicted, value: ethers.parseEther("1") })).wait();
    return { ...f, account, predicted };
  }

  it("initialize cannot be called twice, and only by the factory", async function () {
    const { account, entryPointEOA, mock, deployer } = await accountFixture();
    await expect(
      account.connect(deployer).initialize(entryPointEOA.address, await mock.getAddress(), PK)
    ).to.be.revertedWithCustomError(account, "Unauthorized");
  });

  it("EIP-1271 returns the magic value for a valid Falcon signature", async function () {
    const { account, mock } = await accountFixture();
    const hash = ethers.id("post-quantum login to dapp.example");
    await (await mock.setResult(PK, msgOf(hash), NONCE, COMPSIG, true)).wait();
    expect(await account.isValidSignature(hash, envelope(NONCE, COMPSIG))).to.equal("0x1626ba7e");
  });

  it("EIP-1271 returns failure (0x00000000) for a tampered signature", async function () {
    const { account, mock } = await accountFixture();
    const hash = ethers.id("post-quantum login to dapp.example");
    await (await mock.setResult(PK, msgOf(hash), NONCE, COMPSIG, true)).wait();
    // Flip one byte of the compressed signature -> the verifier rejects it.
    const tampered = "0x" + "22".repeat(79) + "23";
    expect(await account.isValidSignature(hash, envelope(NONCE, tampered))).to.equal("0x00000000");
  });

  it("validateUserOp returns 0 for a valid signature (only from the EntryPoint)", async function () {
    const { account, entryPointEOA, mock } = await accountFixture();
    const userOpHash = ethers.id("userop-1");
    await (await mock.setResult(PK, msgDomained(USEROP_DOMAIN, userOpHash), NONCE, COMPSIG, true)).wait();
    const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)];
    const val = await account.connect(entryPointEOA).validateUserOp.staticCall(op, userOpHash, 0);
    expect(val).to.equal(0n);
  });

  it("validateUserOp returns 1 (SIG_VALIDATION_FAILED) for a bad signature", async function () {
    const { account, entryPointEOA, mock } = await accountFixture();
    const userOpHash = ethers.id("userop-2");
    await (await mock.setResult(PK, msgDomained(USEROP_DOMAIN, userOpHash), NONCE, COMPSIG, false)).wait();
    const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)];
    const val = await account.connect(entryPointEOA).validateUserOp.staticCall(op, userOpHash, 0);
    expect(val).to.equal(1n);
  });

  it("validateUserOp reverts if not called by the EntryPoint", async function () {
    const { account, other } = await accountFixture();
    const op = [account.target, 0, "0x", "0x", ethers.ZeroHash, 0, ethers.ZeroHash, "0x", envelope(NONCE, COMPSIG)];
    await expect(
      account.connect(other).validateUserOp(op, ethers.id("x"), 0)
    ).to.be.revertedWithCustomError(account, "Unauthorized");
  });

  it("executeWithFalcon moves value when the Falcon signature is valid, and bumps the nonce", async function () {
    const { account, mock, other } = await accountFixture();
    const target = other.address;
    const value = ethers.parseEther("0.25");
    const data = "0x";
    const n = await account.execNonce();
    const challenge = await account.getExecuteChallenge(target, value, data, n);
    await (await mock.setResult(PK, msgDomained(EXEC_DOMAIN, challenge), NONCE, COMPSIG, true)).wait();

    const before = await ethers.provider.getBalance(target);
    await (await account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, data)).wait();
    const after = await ethers.provider.getBalance(target);
    expect(after - before).to.equal(value);
    expect(await account.execNonce()).to.equal(n + 1n);
  });

  it("executeWithFalcon reverts on an invalid Falcon signature", async function () {
    const { account, mock, other } = await accountFixture();
    const target = other.address;
    const value = ethers.parseEther("0.1");
    const n = await account.execNonce();
    const challenge = await account.getExecuteChallenge(target, value, "0x", n);
    await (await mock.setResult(PK, msgDomained(EXEC_DOMAIN, challenge), NONCE, COMPSIG, false)).wait();
    await expect(
      account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, "0x")
    ).to.be.revertedWithCustomError(account, "InvalidSignature");
  });

  // DOMAIN SEPARATION (adversarial-review HIGH, 2026-07-12): a Falcon signature
  // phished through the raw-hash EIP-1271 login surface must NOT authorize a spend.
  it("domain separation: an EIP-1271 login signature cannot be replayed as an executeWithFalcon spend", async function () {
    const { account, mock, other } = await accountFixture();
    const target = other.address;
    const value = ethers.parseEther("0.5");
    const data = "0x";
    const n = await account.execNonce();
    // Attacker computes the exact execute challenge (all inputs are public; execNonce is public).
    const challenge = await account.getExecuteChallenge(target, value, data, n);
    // The user is phished into signing `challenge` as a raw "login" hash: the mock
    // records a VALID Falcon signature for the RAW 32-byte message (the EIP-1271 surface).
    await (await mock.setResult(PK, msgOf(challenge), NONCE, COMPSIG, true)).wait();
    // That login signature IS accepted by the raw-hash isValidSignature surface...
    expect(await account.isValidSignature(challenge, envelope(NONCE, COMPSIG))).to.equal("0x1626ba7e");
    // ...but it must NOT authorize a spend: executeWithFalcon verifies over
    // (EXEC_DOMAIN || challenge), which the phished raw-hash signature does not cover.
    await expect(
      account.executeWithFalcon(envelope(NONCE, COMPSIG), target, value, data)
    ).to.be.revertedWithCustomError(account, "InvalidSignature");
    // No value moved, nonce unchanged.
    expect(await account.execNonce()).to.equal(n);
  });

  it("executeFromEntryPoint is EntryPoint-gated", async function () {
    const { account, other } = await accountFixture();
    await expect(
      account.connect(other).executeFromEntryPoint(other.address, 0, "0x")
    ).to.be.revertedWithCustomError(account, "Unauthorized");
  });

  it("executeFromEntryPoint dispatches the call when invoked by the EntryPoint", async function () {
    const { account, entryPointEOA, other } = await accountFixture();
    const value = ethers.parseEther("0.3");
    const before = await ethers.provider.getBalance(other.address);
    await (await account.connect(entryPointEOA).executeFromEntryPoint(other.address, value, "0x")).wait();
    const after = await ethers.provider.getBalance(other.address);
    expect(after - before).to.equal(value);
  });
});
