// =============================================================================
// TDD proof for the AereDelegate7702V2 SESSION-KEY SCOPE-NARROWING fix
// (MEDIUM, session-key scope escape).
//
// BUG: SessionKey.allowedSelectors / allowedTargets were only ever set to true
// (addSessionKey) and NEVER cleared. revokeSessionKey only set active=false and
// left every flag set. Re-adding the same key address therefore reactivated the
// UNION of all historical targets/selectors, silently regranting authority the
// owner did NOT re-grant.
//
// FIX: a per-key scopeEpoch increments on every addSessionKey and every
// revokeSessionKey; allowedSelectors/allowedTargets are keyed by
// (key, scopeEpoch, sel/target); executeWithSessionKey checks the flag at the
// key's CURRENT scopeEpoch. A revoke + re-grant starts a fresh, empty scope so
// only the newly-listed targets/selectors are live, and old scope can never be
// reactivated. spent resets per grant.
//
// Run in ISOLATION:
//   npx hardhat test test/delegate7702v2-scope-epoch-fix.test.js
//
// No em-dashes in this file.
// =============================================================================

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

const E = (n) => ethers.parseEther(String(n));

async function setBal(addr, amountWei) {
  await network.provider.send("hardhat_setBalance", [addr, "0x" + amountWei.toString(16)]);
}
async function latestTs() {
  return Number((await ethers.provider.getBlock("latest")).timestamp);
}
async function deployRaw(deployer, initCode) {
  const rc = await (await deployer.sendTransaction({ data: initCode })).wait();
  return rc.contractAddress;
}
const SINK_INIT = "0x60016000f3"; // runtime = STOP: any call/value succeeds, keeps value
const dataFor = (selector, padWords) => selector + "00".repeat(padWords * 32);

describe("AereDelegate7702V2 - scope narrowing on revoke + re-grant (scopeEpoch fix)", function () {
  this.timeout(0);

  let signers, F;

  before(async () => {
    signers = await ethers.getSigners();
    F = await ethers.getContractFactory("AereDelegate7702V2");
  });

  async function stand() {
    const deployer = signers[0];
    const delegate = await F.deploy();
    await delegate.waitForDeployment();
    const delegateAddr = await delegate.getAddress();
    const owner = await ethers.getImpersonatedSigner(delegateAddr);
    await setBal(delegateAddr, E(1_000_000));
    const sinkA = await deployRaw(deployer, SINK_INIT);
    const sinkB = await deployRaw(deployer, SINK_INIT);
    return { delegate, delegateAddr, owner, sinkA, sinkB };
  }

  it("re-granting a NARROWER scope drops the old target + selector (no UNION reactivation)", async () => {
    const { delegate, owner, sinkA, sinkB } = await stand();
    const key = signers[1];
    const selKeep = "0x11111111";
    const selDrop = "0x22222222";
    const farExpiry = (await latestTs()) + 365 * 24 * 3600;

    // GRANT 1 (broad): selectors {keep, drop}, targets {sinkA, sinkB}, cap 10.
    await (await delegate.connect(owner).addSessionKey(
      key.address, farExpiry, E(10), [selKeep, selDrop], [sinkA, sinkB]
    )).wait();

    // Use the broad scope: call the soon-to-be-dropped target+selector -> works.
    await (await delegate.connect(key).executeWithSessionKey(
      { target: sinkB, value: E(1), data: dataFor(selDrop, 1) }
    )).wait();
    expect(await ethers.provider.getBalance(sinkB)).to.equal(E(1));

    // REVOKE.
    await (await delegate.connect(owner).revokeSessionKey(key.address)).wait();

    // RE-GRANT (narrower): only selector {keep}, only target {sinkA}, fresh cap 5.
    await (await delegate.connect(owner).addSessionKey(
      key.address, farExpiry, E(5), [selKeep], [sinkA]
    )).wait();

    // The old target (sinkB) is NOT re-granted -> rejected.
    await expect(
      delegate.connect(key).executeWithSessionKey({ target: sinkB, value: 0n, data: dataFor(selKeep, 1) })
    ).to.be.revertedWithCustomError(delegate, "TargetNotAllowed");

    // The old selector (drop) at the still-allowed target -> rejected.
    await expect(
      delegate.connect(key).executeWithSessionKey({ target: sinkA, value: 0n, data: dataFor(selDrop, 1) })
    ).to.be.revertedWithCustomError(delegate, "SelectorNotAllowed");

    // ONLY the newly-granted (sinkA, selKeep) pair is live.
    const before = await ethers.provider.getBalance(sinkA);
    await (await delegate.connect(key).executeWithSessionKey(
      { target: sinkA, value: E(2), data: dataFor(selKeep, 1) }
    )).wait();
    expect((await ethers.provider.getBalance(sinkA)) - before).to.equal(E(2));

    // Views reflect the current (narrow) scope only.
    const scopeDropTarget = await delegate.sessionKeyScope(key.address, selKeep, sinkB);
    expect(scopeDropTarget.targetAllowed).to.equal(false);
    const scopeDropSel = await delegate.sessionKeyScope(key.address, selDrop, sinkA);
    expect(scopeDropSel.selectorAllowed).to.equal(false);
    const scopeLive = await delegate.sessionKeyScope(key.address, selKeep, sinkA);
    expect(scopeLive.selectorAllowed).to.equal(true);
    expect(scopeLive.targetAllowed).to.equal(true);
  });

  it("spend cap applies to the NEW grant (fresh budget, old spend does not carry, cap still binds)", async () => {
    const { delegate, owner, sinkA } = await stand();
    const key = signers[2];
    const sel = "0x33333333";
    const farExpiry = (await latestTs()) + 365 * 24 * 3600;

    // GRANT 1: cap 10, spend 8.
    await (await delegate.connect(owner).addSessionKey(key.address, farExpiry, E(10), [sel], [sinkA])).wait();
    await (await delegate.connect(key).executeWithSessionKey({ target: sinkA, value: E(8), data: dataFor(sel, 1) })).wait();
    let scope = await delegate.sessionKeyScope(key.address, sel, sinkA);
    expect(scope.spent).to.equal(E(8));

    // REVOKE + RE-GRANT with a fresh cap of 5. spent must reset to 0.
    await (await delegate.connect(owner).revokeSessionKey(key.address)).wait();
    await (await delegate.connect(owner).addSessionKey(key.address, farExpiry, E(5), [sel], [sinkA])).wait();
    scope = await delegate.sessionKeyScope(key.address, sel, sinkA);
    expect(scope.spent).to.equal(0n);
    expect(scope.spendCap).to.equal(E(5));

    // New cap binds: 5 works, one wei more reverts.
    await (await delegate.connect(key).executeWithSessionKey({ target: sinkA, value: E(5), data: dataFor(sel, 1) })).wait();
    await expect(
      delegate.connect(key).executeWithSessionKey({ target: sinkA, value: 1n, data: dataFor(sel, 1) })
    ).to.be.revertedWithCustomError(delegate, "SpendCapExceeded");
  });

  it("the revoked scope alone (no re-grant) is fully dead", async () => {
    const { delegate, owner, sinkA } = await stand();
    const key = signers[3];
    const sel = "0x44444444";
    const farExpiry = (await latestTs()) + 365 * 24 * 3600;
    await (await delegate.connect(owner).addSessionKey(key.address, farExpiry, E(5), [sel], [sinkA])).wait();
    await (await delegate.connect(owner).revokeSessionKey(key.address)).wait();
    await expect(
      delegate.connect(key).executeWithSessionKey({ target: sinkA, value: 0n, data: dataFor(sel, 1) })
    ).to.be.revertedWithCustomError(delegate, "SessionKeyExpired");
  });
});
