// AereModularAccount stack tests.
//
// Deploys a LOCAL copy of AERE's own AereEntryPointV2 (the same source that is
// live at 0x8D6f40598d552fF0Cb358b6012cF4227B86aF770 on chain 2800) and drives
// every userOp through ep.handleOps, exactly like the live bundler path.
//
// Routing convention under test (first signature byte):
//   0x00 || 65-byte ECDSA            -> root-owner fallback (EIP-191 over userOpHash)
//   0x01 || validator(20) || payload -> installed type-1 validator module
//
// Run: npx hardhat test test/modular-account.test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");
const { loadFixture, time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");

const coder = ethers.AbiCoder.defaultAbiCoder();

const VERIF_GAS = 500_000n;
const CALL_GAS = 1_000_000n;
const PRE_VERIF_GAS = 21_000n;
const MAX_FEE = 10n ** 9n; // 1 gwei
const NATIVE_SENTINEL = "0x00000000";
const MAGIC_1271 = "0x1626ba7e";
const FAIL_1271 = "0xffffffff";
const DAY = 24 * 3600;

function packUints(hi, lo) {
  return ethers.toBeHex((hi << 128n) | lo, 32);
}

async function buildUserOp(ep, accountAddr, callData) {
  return {
    sender: accountAddr,
    nonce: await ep.getNonce(accountAddr),
    initCode: "0x",
    callData,
    accountGasLimits: packUints(VERIF_GAS, CALL_GAS),
    preVerificationGas: PRE_VERIF_GAS,
    gasFees: packUints(MAX_FEE, MAX_FEE),
    paymasterAndData: "0x",
    signature: "0x",
  };
}

async function signRoot(ep, op, rootSigner) {
  const hash = await ep.getUserOpHash(op);
  const sig = await rootSigner.signMessage(ethers.getBytes(hash));
  op.signature = ethers.concat(["0x00", sig]);
  return op;
}

async function signSession(ep, op, validatorAddr, sessionWallet) {
  const hash = await ep.getUserOpHash(op);
  const sig = await sessionWallet.signMessage(ethers.getBytes(hash));
  op.signature = ethers.concat(["0x01", validatorAddr, sig]);
  return op;
}

function sessionInitData(key, validAfter, validUntil, maxValuePerOp, targets, selectors) {
  return coder.encode(
    ["address", "uint48", "uint48", "uint256", "address[]", "bytes4[]"],
    [key, validAfter, validUntil, maxValuePerOp, targets, selectors]
  );
}

describe("AereModularAccount + session keys + social recovery", function () {
  async function fixture() {
    const [deployer, root, guardian1, guardian2, guardian3, rando, newOwner, bundler] =
      await ethers.getSigners();

    const EP = await ethers.getContractFactory("AereEntryPointV2");
    const ep = await EP.deploy();
    await ep.waitForDeployment();
    const epAddr = await ep.getAddress();

    const Factory = await ethers.getContractFactory("AereModularAccountFactory");
    const factory = await Factory.deploy(epAddr);
    await factory.waitForDeployment();

    const Validator = await ethers.getContractFactory("AereSessionKeyValidator");
    const validator = await Validator.deploy();
    await validator.waitForDeployment();
    const validatorAddr = await validator.getAddress();

    const Recovery = await ethers.getContractFactory("AereSocialRecoveryModule");
    const recovery = await Recovery.deploy();
    await recovery.waitForDeployment();
    const recoveryAddr = await recovery.getAddress();

    const Ping = await ethers.getContractFactory("AerePingCounter");
    const ping = await Ping.deploy();
    await ping.waitForDeployment();
    const pingAddr = await ping.getAddress();
    const pingSel = ping.interface.getFunction("ping").selector;

    // Create the account (root owner = root signer).
    const salt = 1n;
    const predicted = await factory["getAddress(address,uint256)"](root.address, salt);
    await (await factory.createAccount(root.address, salt)).wait();
    const account = await ethers.getContractAt("AereModularAccount", predicted);
    const accountAddr = predicted;

    // Fund the account so it can prefund its EntryPoint deposit.
    await (await deployer.sendTransaction({ to: accountAddr, value: ethers.parseEther("1") })).wait();

    // Session key wallet (never funded, never a tx sender - signature only).
    const sessionKey = ethers.Wallet.createRandom();

    return {
      deployer, root, guardian1, guardian2, guardian3, rando, newOwner, bundler,
      ep, epAddr, factory, validator, validatorAddr, recovery, recoveryAddr,
      ping, pingAddr, pingSel, account, accountAddr, sessionKey, salt,
    };
  }

  // Fixture with the session validator installed and a session enabled:
  // scope = { (ping, ping()), (ping, native transfer) }, maxValue 0.01 ETH,
  // window = [now, now + 1h].
  async function sessionFixture() {
    const ctx = await loadFixture(fixture);
    const now = await time.latest();
    const validUntil = now + 3600;
    const initData = sessionInitData(
      ctx.sessionKey.address, 0, validUntil, ethers.parseEther("0.01"),
      [ctx.pingAddr, ctx.pingAddr], [ctx.pingSel, NATIVE_SENTINEL]
    );
    await (await ctx.account.connect(ctx.root).installModule(1, ctx.validatorAddr, initData)).wait();
    return { ...ctx, validUntil };
  }

  // ---- 1. Account creation ---------------------------------------------------

  describe("account creation (CREATE2 factory)", function () {
    it("deploys at the deterministic getAddress and sets root owner + entryPoint", async function () {
      const { factory, account, accountAddr, root, epAddr, salt } = await loadFixture(fixture);
      expect(await factory["getAddress(address,uint256)"](root.address, salt)).to.equal(accountAddr);
      expect(await account.rootOwner()).to.equal(root.address);
      expect(await account.entryPoint()).to.equal(epAddr);
      expect(await account.accountId()).to.equal("aere.modular-account.1.0.0");
    });

    it("createAccount is idempotent for the same (owner, salt)", async function () {
      const { factory, root, salt, accountAddr } = await loadFixture(fixture);
      await (await factory.createAccount(root.address, salt)).wait(); // second call, no revert
      expect(await factory["getAddress(address,uint256)"](root.address, salt)).to.equal(accountAddr);
    });

    it("different owner produces a different address (no front-running)", async function () {
      const { factory, root, rando, salt } = await loadFixture(fixture);
      const a = await factory["getAddress(address,uint256)"](root.address, salt);
      const b = await factory["getAddress(address,uint256)"](rando.address, salt);
      expect(a).to.not.equal(b);
    });

    it("initialize cannot be called twice", async function () {
      const { account, epAddr, rando } = await loadFixture(fixture);
      await expect(account.connect(rando).initialize(epAddr, rando.address))
        .to.be.revertedWithCustomError(account, "Unauthorized");
    });
  });

  // ---- 2. Root-owner fallback path via LIVE-shape EntryPoint -------------------

  describe("root-owner fallback validation (0x00 prefix)", function () {
    it("root-signed userOp executes through ep.handleOps", async function () {
      const { ep, account, accountAddr, root, bundler, ping, pingAddr } = await loadFixture(fixture);
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signRoot(ep, await buildUserOp(ep, accountAddr, callData), root);
      await (await ep.connect(bundler).handleOps([op], bundler.address)).wait();
      expect(await ping.pings()).to.equal(1n);
      expect(await ep.getNonce(accountAddr)).to.equal(1n);
    });

    it("non-root signature on 0x00 path REVERTS handleOps", async function () {
      const { ep, account, accountAddr, rando, bundler, ping, pingAddr } = await loadFixture(fixture);
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signRoot(ep, await buildUserOp(ep, accountAddr, callData), rando);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
    });

    it("root can rotate its own key via execute self-call, direct setRootOwner is blocked", async function () {
      const { account, accountAddr, root, newOwner, rando } = await loadFixture(fixture);
      await expect(account.connect(root).setRootOwner(newOwner.address))
        .to.be.revertedWithCustomError(account, "Unauthorized");
      await expect(account.connect(rando).execute(accountAddr, 0, "0x"))
        .to.be.revertedWithCustomError(account, "Unauthorized");
      const data = account.interface.encodeFunctionData("setRootOwner", [newOwner.address]);
      await (await account.connect(root).execute(accountAddr, 0, data)).wait();
      expect(await account.rootOwner()).to.equal(newOwner.address);
    });
  });

  // ---- 3. Module management -----------------------------------------------------

  describe("ERC-7579-style module management", function () {
    it("install validator module + isModuleInstalled + selector constants match", async function () {
      const { account, validator, validatorAddr, root } = await loadFixture(fixture);
      await (await account.connect(root).installModule(1, validatorAddr, "0x")).wait();
      expect(await account.isModuleInstalled(1, validatorAddr, "0x")).to.equal(true);
      expect(await account.isModuleInstalled(2, validatorAddr, "0x")).to.equal(false);
      expect(await validator.EXECUTE_SELECTOR()).to.equal(account.interface.getFunction("execute").selector);
      expect(await validator.EXECUTE_BATCH_SELECTOR()).to.equal(account.interface.getFunction("executeBatch").selector);
    });

    it("stranger cannot install or uninstall modules", async function () {
      const { account, validatorAddr, rando } = await loadFixture(fixture);
      await expect(account.connect(rando).installModule(1, validatorAddr, "0x"))
        .to.be.revertedWithCustomError(account, "Unauthorized");
      await expect(account.connect(rando).uninstallModule(1, validatorAddr, "0x"))
        .to.be.revertedWithCustomError(account, "Unauthorized");
    });

    it("wrong module type / non-contract module are rejected", async function () {
      const { account, validatorAddr, recoveryAddr, root, rando } = await loadFixture(fixture);
      await expect(account.connect(root).installModule(3, validatorAddr, "0x"))
        .to.be.revertedWithCustomError(account, "UnsupportedModuleType");
      await expect(account.connect(root).installModule(1, rando.address, "0x"))
        .to.be.revertedWithCustomError(account, "InvalidModule");
      // recovery module declares type 2, not type 1
      await expect(account.connect(root).installModule(1, recoveryAddr, "0x"))
        .to.be.revertedWithCustomError(account, "InvalidModule");
    });

    it("executeFromExecutor rejected for non-installed executor", async function () {
      const { account, rando } = await loadFixture(fixture);
      await expect(account.connect(rando).executeFromExecutor(rando.address, 0, "0x"))
        .to.be.revertedWithCustomError(account, "Unauthorized");
    });
  });

  // ---- 4. Session key happy path -------------------------------------------------

  describe("session key userOps (0x01 prefix, signed ONLY by the session key)", function () {
    it("in-scope call (ping) executes through ep.handleOps", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr } =
        await sessionFixture();
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      const rc = await (await ep.connect(bundler).handleOps([op], bundler.address)).wait();
      expect(await ping.pings()).to.equal(1n);
      // UserOperationEvent(success=true)
      const evt = rc.logs.map(l => { try { return ep.interface.parseLog(l); } catch { return null; } })
        .find(p => p && p.name === "UserOperationEvent");
      expect(evt.args.success).to.equal(true);
      expect(evt.args.sender).to.equal(accountAddr);
    });

    it("in-scope native transfer within maxValuePerOp executes", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, pingAddr } =
        await sessionFixture();
      const before = await ethers.provider.getBalance(pingAddr);
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, ethers.parseEther("0.005"), "0x"]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await (await ep.connect(bundler).handleOps([op], bundler.address)).wait();
      expect(await ethers.provider.getBalance(pingAddr)).to.equal(before + ethers.parseEther("0.005"));
    });

    it("in-scope executeBatch (two pings) executes", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr } =
        await sessionFixture();
      const pingData = ping.interface.encodeFunctionData("ping");
      const callData = account.interface.encodeFunctionData("executeBatch", [[
        { target: pingAddr, value: 0, data: pingData },
        { target: pingAddr, value: 0, data: pingData },
      ]]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await (await ep.connect(bundler).handleOps([op], bundler.address)).wait();
      expect(await ping.pings()).to.equal(2n);
    });
  });

  // ---- 5. Session key negative paths ---------------------------------------------

  describe("session key scope enforcement (all REVERT handleOps)", function () {
    it("out-of-scope target reverts", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, rando } =
        await sessionFixture();
      const callData = account.interface.encodeFunctionData("execute", [rando.address, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
    });

    it("wrong selector on allowed target reverts", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr } =
        await sessionFixture();
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("forbidden")]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
      expect(await ping.forbiddenCalls()).to.equal(0n);
    });

    it("value above maxValuePerOp reverts", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, pingAddr } =
        await sessionFixture();
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, ethers.parseEther("0.02"), "0x"]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
    });

    it("batch with one out-of-scope call reverts entirely", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr, rando } =
        await sessionFixture();
      const pingData = ping.interface.encodeFunctionData("ping");
      const callData = account.interface.encodeFunctionData("executeBatch", [[
        { target: pingAddr, value: 0, data: pingData },
        { target: rando.address, value: 0, data: "0x" },
      ]]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
      expect(await ping.pings()).to.equal(0n);
    });

    it("session key cannot call installModule (only execute/executeBatch enterable)", async function () {
      const { ep, account, accountAddr, validatorAddr, recoveryAddr, sessionKey, bundler } =
        await sessionFixture();
      const callData = account.interface.encodeFunctionData("installModule", [2, recoveryAddr, "0x"]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
    });

    it("expired session (past validUntil) reverts", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, bundler, ping, pingAddr } =
        await sessionFixture();
      await time.increase(2 * 3600); // window was 1h
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
    });

    it("not-yet-valid session (validAfter in the future) reverts, then works after time passes", async function () {
      const { ep, account, accountAddr, validator, validatorAddr, root, bundler, ping, pingAddr, pingSel } =
        await loadFixture(fixture);
      const futureKey = ethers.Wallet.createRandom();
      const now = await time.latest();
      const initData = sessionInitData(futureKey.address, now + 3600, now + 7200, 0, [pingAddr], [pingSel]);
      await (await account.connect(root).installModule(1, validatorAddr, initData)).wait();

      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      let op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, futureKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");

      await time.increase(3700);
      op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, futureKey);
      await (await ep.connect(bundler).handleOps([op], bundler.address)).wait();
      expect(await ping.pings()).to.equal(1n);
      (validator); // silence unused
    });

    it("revoked session reverts (revocation via root-driven account.execute)", async function () {
      const { ep, account, accountAddr, validator, validatorAddr, sessionKey, root, bundler, ping, pingAddr } =
        await sessionFixture();
      const revokeData = validator.interface.encodeFunctionData("revokeSession", [sessionKey.address]);
      await (await account.connect(root).execute(validatorAddr, 0, revokeData)).wait();
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
    });

    it("uninstalling the validator removes routing", async function () {
      const { ep, account, accountAddr, validatorAddr, sessionKey, root, bundler, ping, pingAddr } =
        await sessionFixture();
      await (await account.connect(root).uninstallModule(1, validatorAddr, "0x")).wait();
      expect(await account.isModuleInstalled(1, validatorAddr, "0x")).to.equal(false);
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signSession(ep, await buildUserOp(ep, accountAddr, callData), validatorAddr, sessionKey);
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "AccountValidationFailed");
    });

    it("stranger cannot register a session for someone else's account (state is per-msg.sender)", async function () {
      const { validator, accountAddr, rando, pingAddr, pingSel } = await sessionFixture();
      const evilKey = ethers.Wallet.createRandom();
      // rando CAN call enableSession, but it writes rando's own namespace,
      // not the account's - the account's session set is untouched.
      await (await validator.connect(rando).enableSession(evilKey.address, 0, 0, 0, [pingAddr], [pingSel])).wait();
      const s = await validator.sessions(accountAddr, evilKey.address);
      expect(s.enabled).to.equal(false);
    });
  });

  // ---- 6. Social recovery -----------------------------------------------------------

  async function recoveryFixture() {
    const ctx = await loadFixture(fixture);
    const initData = coder.encode(
      ["address[]", "uint256"],
      [[ctx.guardian1.address, ctx.guardian2.address, ctx.guardian3.address], 2]
    );
    await (await ctx.account.connect(ctx.root).installModule(2, ctx.recoveryAddr, initData)).wait();
    return ctx;
  }

  describe("social recovery (2-of-3 guardians, 48h timelock)", function () {
    it("install as executor sets guardians and threshold", async function () {
      const { account, recovery, recoveryAddr, accountAddr, guardian1, guardian2, guardian3, rando } =
        await recoveryFixture();
      expect(await account.isModuleInstalled(2, recoveryAddr, "0x")).to.equal(true);
      expect(await recovery.isGuardian(accountAddr, guardian1.address)).to.equal(true);
      expect(await recovery.isGuardian(accountAddr, guardian2.address)).to.equal(true);
      expect(await recovery.isGuardian(accountAddr, guardian3.address)).to.equal(true);
      expect(await recovery.isGuardian(accountAddr, rando.address)).to.equal(false);
      expect(await recovery.thresholdOf(accountAddr)).to.equal(2n);
      expect((await recovery.guardiansOf(accountAddr)).length).to.equal(3);
    });

    it("non-guardian cannot initiate or support", async function () {
      const { recovery, accountAddr, rando, newOwner, guardian1 } = await recoveryFixture();
      await expect(recovery.connect(rando).initiateRecovery(accountAddr, newOwner.address))
        .to.be.revertedWithCustomError(recovery, "NotGuardian");
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await expect(recovery.connect(rando).supportRecovery(accountAddr))
        .to.be.revertedWithCustomError(recovery, "NotGuardian");
    });

    it("premature executeRecovery reverts even with threshold met", async function () {
      const { recovery, accountAddr, guardian1, guardian2, newOwner } = await recoveryFixture();
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await (await recovery.connect(guardian2).supportRecovery(accountAddr)).wait();
      await expect(recovery.connect(guardian1).executeRecovery(accountAddr))
        .to.be.revertedWithCustomError(recovery, "TimelockNotElapsed");
    });

    it("below-threshold executeRecovery reverts even after 48h", async function () {
      const { recovery, accountAddr, guardian1, newOwner } = await recoveryFixture();
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await time.increase(2 * DAY + 60);
      await expect(recovery.connect(guardian1).executeRecovery(accountAddr))
        .to.be.revertedWithCustomError(recovery, "BelowThreshold");
    });

    it("non-guardian executeRecovery reverts even when otherwise ready", async function () {
      const { recovery, accountAddr, guardian1, guardian2, rando, newOwner } = await recoveryFixture();
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await (await recovery.connect(guardian2).supportRecovery(accountAddr)).wait();
      await time.increase(2 * DAY + 60);
      await expect(recovery.connect(rando).executeRecovery(accountAddr))
        .to.be.revertedWithCustomError(recovery, "NotGuardian");
    });

    it("happy path: initiate + support + 48h -> root owner replaced, control transfers", async function () {
      const { recovery, account, accountAddr, guardian1, guardian2, root, newOwner, ping, pingAddr } =
        await recoveryFixture();
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await (await recovery.connect(guardian2).supportRecovery(accountAddr)).wait();
      await time.increase(2 * DAY + 60);
      await (await recovery.connect(guardian1).executeRecovery(accountAddr)).wait();
      expect(await account.rootOwner()).to.equal(newOwner.address);

      // Old root lost direct admin; new owner has it.
      await expect(account.connect(root).execute(pingAddr, 0, ping.interface.encodeFunctionData("ping")))
        .to.be.revertedWithCustomError(account, "Unauthorized");
      await (await account.connect(newOwner).execute(pingAddr, 0, ping.interface.encodeFunctionData("ping"))).wait();
      expect(await ping.pings()).to.equal(1n);
    });

    it("current owner can cancelRecovery; execution then impossible", async function () {
      const { recovery, accountAddr, guardian1, guardian2, root, newOwner } = await recoveryFixture();
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await (await recovery.connect(guardian2).supportRecovery(accountAddr)).wait();
      await (await recovery.connect(root).cancelRecovery(accountAddr)).wait();
      await time.increase(2 * DAY + 60);
      await expect(recovery.connect(guardian1).executeRecovery(accountAddr))
        .to.be.revertedWithCustomError(recovery, "NoActiveRecovery");
    });

    it("stranger cannot cancelRecovery", async function () {
      const { recovery, accountAddr, guardian1, rando, newOwner } = await recoveryFixture();
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await expect(recovery.connect(rando).cancelRecovery(accountAddr))
        .to.be.revertedWithCustomError(recovery, "NotOwner");
    });

    it("duplicate support reverts; guardian add/remove is account-only", async function () {
      const { recovery, account, accountAddr, guardian1, root, rando, newOwner } = await recoveryFixture();
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await expect(recovery.connect(guardian1).supportRecovery(accountAddr))
        .to.be.revertedWithCustomError(recovery, "AlreadyApproved");

      // rando calling addGuardian writes rando's own namespace, not the account's.
      await (await recovery.connect(rando).addGuardian(newOwner.address)).wait();
      expect(await recovery.isGuardian(accountAddr, newOwner.address)).to.equal(false);

      // Account (driven by root) can add a guardian for itself.
      const addData = recovery.interface.encodeFunctionData("addGuardian", [rando.address]);
      await (await account.connect(root).execute(await recovery.getAddress(), 0, addData)).wait();
      expect(await recovery.isGuardian(accountAddr, rando.address)).to.equal(true);
    });

    it("uninstall cleans guardians and active recovery", async function () {
      const { recovery, recoveryAddr, account, accountAddr, guardian1, root, newOwner } =
        await recoveryFixture();
      await (await recovery.connect(guardian1).initiateRecovery(accountAddr, newOwner.address)).wait();
      await (await account.connect(root).uninstallModule(2, recoveryAddr, "0x")).wait();
      expect(await recovery.isGuardian(accountAddr, guardian1.address)).to.equal(false);
      expect(await recovery.thresholdOf(accountAddr)).to.equal(0n);
      const r = await recovery.activeRecovery(accountAddr);
      expect(r.active).to.equal(false);
      // And the module lost executor rights: even a ready recovery could not fire.
      expect(await account.isModuleInstalled(2, recoveryAddr, "0x")).to.equal(false);
    });
  });

  // ---- 7. EIP-1271 -------------------------------------------------------------------

  describe("EIP-1271 isValidSignature", function () {
    function wrap1271(accountAddr, chainId, hash) {
      return ethers.keccak256(coder.encode(
        ["bytes32", "address", "uint256", "bytes32"],
        [
          ethers.keccak256(ethers.toUtf8Bytes("AereAccount1271(address account,uint256 chainid,bytes32 hash)")),
          accountAddr, chainId, hash,
        ]
      ));
    }

    it("root path (0x00) returns magic for a valid root signature", async function () {
      const { account, accountAddr, root } = await loadFixture(fixture);
      const { chainId } = await ethers.provider.getNetwork();
      const hash = ethers.keccak256(ethers.toUtf8Bytes("Sign in to dapp.example"));
      const wrapped = wrap1271(accountAddr, chainId, hash);
      const sig = ethers.concat(["0x00", await root.signMessage(ethers.getBytes(wrapped))]);
      expect(await account.isValidSignature(hash, sig)).to.equal(MAGIC_1271);
    });

    it("root path rejects a stranger's signature", async function () {
      const { account, accountAddr, rando } = await loadFixture(fixture);
      const { chainId } = await ethers.provider.getNetwork();
      const hash = ethers.keccak256(ethers.toUtf8Bytes("Sign in to dapp.example"));
      const wrapped = wrap1271(accountAddr, chainId, hash);
      const sig = ethers.concat(["0x00", await rando.signMessage(ethers.getBytes(wrapped))]);
      expect(await account.isValidSignature(hash, sig)).to.equal(FAIL_1271);
    });

    it("validator path (0x01) returns magic for an active session key, fails after expiry", async function () {
      const { account, accountAddr, validatorAddr, sessionKey } = await sessionFixture();
      const { chainId } = await ethers.provider.getNetwork();
      const hash = ethers.keccak256(ethers.toUtf8Bytes("dapp login"));
      const wrapped = wrap1271(accountAddr, chainId, hash);
      const inner = await sessionKey.signMessage(ethers.getBytes(wrapped));
      const sig = ethers.concat(["0x01", validatorAddr, inner]);
      expect(await account.isValidSignature(hash, sig)).to.equal(MAGIC_1271);

      await time.increase(2 * 3600); // past validUntil
      expect(await account.isValidSignature(hash, sig)).to.equal(FAIL_1271);
    });

    it("validator path fails for a non-installed validator address", async function () {
      const { account, accountAddr, sessionKey, recoveryAddr } = await sessionFixture();
      const { chainId } = await ethers.provider.getNetwork();
      const hash = ethers.keccak256(ethers.toUtf8Bytes("dapp login"));
      const wrapped = wrap1271(accountAddr, chainId, hash);
      const inner = await sessionKey.signMessage(ethers.getBytes(wrapped));
      const sig = ethers.concat(["0x01", recoveryAddr, inner]);
      expect(await account.isValidSignature(hash, sig)).to.equal(FAIL_1271);
    });
  });

  // ---- 8. EntryPoint economics ---------------------------------------------------------

  describe("EntryPoint deposit handling (AereEntryPointV2 semantics)", function () {
    it("account self-funds its EP deposit during validation and the bundler is paid", async function () {
      const { ep, account, accountAddr, root, bundler, ping, pingAddr } = await loadFixture(fixture);
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signRoot(ep, await buildUserOp(ep, accountAddr, callData), root);
      const bundlerBefore = await ethers.provider.getBalance(bundler.address);
      await (await ep.connect(bundler).handleOps([op], bundler.address)).wait();
      const dep = await ep.balanceOf(accountAddr);
      // Deposit was topped to maxCost and charged actualGasUsed * maxFee.
      const maxCost = (VERIF_GAS + CALL_GAS + PRE_VERIF_GAS) * MAX_FEE;
      expect(dep).to.be.lt(maxCost);
      expect(dep).to.be.gt(0n);
      (bundlerBefore); // bundler pays tx gas but receives the op fee; net not asserted
    });

    it("nonce replay is rejected by the EntryPoint", async function () {
      const { ep, account, accountAddr, root, bundler, ping, pingAddr } = await loadFixture(fixture);
      const callData = account.interface.encodeFunctionData("execute", [pingAddr, 0, ping.interface.encodeFunctionData("ping")]);
      const op = await signRoot(ep, await buildUserOp(ep, accountAddr, callData), root);
      await (await ep.connect(bundler).handleOps([op], bundler.address)).wait();
      await expect(ep.connect(bundler).handleOps([op], bundler.address))
        .to.be.revertedWithCustomError(ep, "FailedOp");
    });
  });
});
