// ERC-8004 + x402/AP2 conformance adapters over Aere's LIVE, already-deployed agent stack.
//
// The three adapters wrap (never redeploy) the live primitives:
//   - AereIdentityRegistry8004   over AereAgentDID          (agent identity / DID)
//   - AereReputationRegistry8004 over AereAIReputation      (attest / read feedback)
//   - AereValidationRegistry8004 over AerePQCKeyRegistry    (POST-QUANTUM request/respond proof)
// plus AereAP2MandateVerifier: a Falcon-512-signed x402/AP2 spend mandate.
//
// Aere has no PQC precompile in the local Hardhat EVM, so we install the repo's MockPQCPrecompile
// at 0x0AE1 (Falcon-512) and 0x0AE3 (ML-DSA-44) via hardhat_setCode, exactly as the fault-harness
// AereRecoveryRegistry test does. The mock parses the input at the LIVE precompile's spec offsets
// and accepts iff commitment == keccak256(pk || message), so a positive result here proves the
// adapter routed the correct input to the correct precompile and enforces fail-closed semantics.
//
// [MEASURE] The REAL precompile path (that on-chain 0x0AE1 / 0x0AE3 accept a genuine Falcon-512 /
// ML-DSA-44 signature) is exercised separately against LIVE mainnet 2800 via eth_call (the same way
// AerePQCKeyRegistry / AerePQCAttestation are proven) and is NOT re-proven inside this unit test.
// The adapters stay hard-wired to the live registry / precompiles for production.
//
// Run: npx hardhat test test/erc8004-adapters.test.js

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

const PRECOMPILE = {
  1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), // Falcon-512
  3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), // ML-DSA-44
};

const META = {
  1: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true }, // Falcon-512
  3: { pkLen: 1312, sigLen: 2420, falcon: false }, // ML-DSA-44
};

const REG_STATUS_ACTIVE = 1;

// ---- deterministic seeded RNG (keccak hash-chain) so the test is fully reproducible ----
function makeRng(seedText) {
  let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
  return {
    bytes(n) {
      const out = new Uint8Array(n);
      let i = 0;
      while (i < n) {
        seed = ethers.keccak256(seed);
        const chunk = ethers.getBytes(seed);
        for (let j = 0; j < chunk.length && i < n; j++, i++) out[i] = chunk[j];
      }
      return out;
    },
  };
}

function makePubKey(scheme, rng) {
  const m = META[scheme];
  const raw = rng.bytes(m.pkLen);
  if (m.falcon) raw[0] = m.pkHeader; // Falcon requires the header byte
  return ethers.hexlify(raw);
}

function commitmentFor(pubKey, message) {
  return ethers.keccak256(ethers.concat([pubKey, message]));
}

// A genuine PQC signature envelope binding `message` to `pubKey` under the mock's semantics.
function genuine(scheme, pubKey, message, rng) {
  const m = META[scheme];
  const commitment = commitmentFor(pubKey, message);
  if (m.falcon) {
    // Falcon: envelope = nonce(40) || esig ; esig = esigHeader || commitment(32) => 73 bytes.
    const nonce = rng.bytes(40);
    const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]);
    return ethers.hexlify(ethers.concat([nonce, esig]));
  }
  // ML-DSA / fixed: sig = commitment(32) || filler ; total sigLen bytes.
  const filler = rng.bytes(m.sigLen - 32);
  return ethers.hexlify(ethers.concat([commitment, filler]));
}

// Flip one byte inside the commitment region (a genuine forgery the precompile must reject).
function tamper(scheme, sigHex) {
  const b = ethers.getBytes(sigHex);
  const off = META[scheme].falcon ? 41 : 0; // Falcon commitment starts at 41; ML-DSA at 0.
  b[off] ^= 0x01;
  return ethers.hexlify(b);
}

async function futureTs(secs) {
  const b = await ethers.provider.getBlock("latest");
  return b.timestamp + secs;
}

describe("ERC-8004 + x402/AP2 adapters over Aere's live agent stack", function () {
  let owner, other, foundation;
  let reg, did, identity, validation, mandate;
  let mockCode;

  before(async function () {
    [owner, other, foundation] = await ethers.getSigners();
    mockCode = {};
    const F = await ethers.getContractFactory("MockPQCPrecompile");
    for (const scheme of [1, 3]) {
      const mock = await F.deploy(scheme);
      await mock.waitForDeployment();
      mockCode[scheme] = await ethers.provider.getCode(await mock.getAddress());
    }
  });

  async function installMocks() {
    for (const scheme of [1, 3]) {
      await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]);
    }
  }

  // Register a PQC key under `signer` in the live registry (proof-of-possession via the precompile).
  async function registerKey(scheme, signer, rng) {
    const pk = makePubKey(scheme, rng);
    const nonce = await reg.identityNonce(signer.address);
    const ch = await reg.popChallenge(signer.address, scheme, pk, nonce);
    const tx = await reg.connect(signer).registerKey(scheme, pk, genuine(scheme, pk, ch, rng));
    const rc = await tx.wait();
    const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered");
    return { keyId: Number(log.args.keyId), pk, scheme };
  }

  // A Falcon root -> DID agent -> registered ERC-8004 identity, all over the live primitives.
  async function makeAgent(seed) {
    const rng = makeRng(seed);
    const root = await registerKey(1, owner, rng);
    await did.connect(owner).createAgent(root.keyId);
    const domain = `agent-${seed}.aere.network`;
    const card = `https://${domain}/.well-known/agent-card.json`;
    await identity.connect(owner).register(root.keyId, domain, card);
    return { rng, agentId: root.keyId, rootPk: root.pk, domain, card };
  }

  beforeEach(async function () {
    await installMocks();

    reg = await (await ethers.getContractFactory("AerePQCKeyRegistry")).deploy();
    await reg.waitForDeployment();

    did = await (await ethers.getContractFactory("AereAgentDID")).deploy(
      await reg.getAddress(),
      ethers.ZeroAddress, // agentBond cross-link (informational; unused by the adapters)
      ethers.ZeroAddress // aiReputation cross-link (informational)
    );
    await did.waitForDeployment();

    identity = await (await ethers.getContractFactory("AereIdentityRegistry8004")).deploy(await did.getAddress());
    await identity.waitForDeployment();

    validation = await (await ethers.getContractFactory("AereValidationRegistry8004")).deploy(
      await reg.getAddress(),
      await did.getAddress()
    );
    await validation.waitForDeployment();

    // Mandate verifier with an OPEN accounting path (SETTLEMENT_EXECUTOR = 0) for verification.
    mandate = await (await ethers.getContractFactory("AereAP2MandateVerifier")).deploy(
      await reg.getAddress(),
      ethers.ZeroAddress
    );
    await mandate.waitForDeployment();
  });

  // =========================================================================
  // 1. Identity adapter over AereAgentDID
  // =========================================================================
  describe("AereIdentityRegistry8004 (identity: register / resolve)", function () {
    it("wires the live DID and reads the registry from it", async function () {
      expect(await identity.DID()).to.equal(await did.getAddress());
      expect(await identity.KEY_REGISTRY()).to.equal(await reg.getAddress());
    });

    it("registers an existing DID agent with a DID string + AgentCard URI, and resolves it", async function () {
      const a = await makeAgent("id-1");

      const info = await identity.getAgent(a.agentId);
      expect(info.agentId).to.equal(BigInt(a.agentId));
      expect(info.did).to.equal(`did:aere:31337:${a.agentId}`); // chainId 31337 in Hardhat
      expect(info.agentDomain).to.equal(a.domain);
      expect(info.agentCardURI).to.equal(a.card);
      expect(info.agentAddress).to.equal(owner.address); // current controller, read live from the registry
      expect(info.registered).to.equal(true);

      // Resolve by domain and by controller address.
      const byDomain = await identity.resolveByDomain(a.domain);
      expect(byDomain.agentId).to.equal(BigInt(a.agentId));
      const byAddr = await identity.resolveByAddress(owner.address);
      expect(byAddr.agentId).to.equal(BigInt(a.agentId));

      expect(await identity.isRegistered(a.agentId)).to.equal(true);
      expect(await identity.agentCount()).to.equal(1n);
    });

    it("only the agent's controller (Falcon-root owner) can register / update", async function () {
      const rng = makeRng("id-ctrl");
      const root = await registerKey(1, owner, rng);
      await did.connect(owner).createAgent(root.keyId);

      // A stranger cannot bind metadata to someone else's agent.
      await expect(
        identity.connect(other).register(root.keyId, "evil.aere.network", "ipfs://x")
      ).to.be.revertedWithCustomError(identity, "NotController");

      // The controller can, then update.
      await identity.connect(owner).register(root.keyId, "ok.aere.network", "ipfs://card1");
      await expect(
        identity.connect(other).updateAgent(root.keyId, "ok2.aere.network", "ipfs://card2")
      ).to.be.revertedWithCustomError(identity, "NotController");
      await identity.connect(owner).updateAgent(root.keyId, "ok2.aere.network", "ipfs://card2");
      const info = await identity.getAgent(root.keyId);
      expect(info.agentDomain).to.equal("ok2.aere.network");
      expect(info.agentCardURI).to.equal("ipfs://card2");
    });

    it("reverts for an unknown DID agent and rejects duplicate domains", async function () {
      await expect(identity.connect(owner).register(999, "d.aere.network", "u")).to.be.revertedWithCustomError(
        identity,
        "UnknownAgent"
      );
      const a = await makeAgent("id-dup");
      const rng = makeRng("id-dup2");
      const root = await registerKey(1, owner, rng);
      await did.connect(owner).createAgent(root.keyId);
      await expect(identity.connect(owner).register(root.keyId, a.domain, "u")).to.be.revertedWithCustomError(
        identity,
        "DomainTaken"
      );
    });
  });

  // =========================================================================
  // 2. Reputation adapter over AereAIReputation (attest / read)
  // =========================================================================
  describe("AereReputationRegistry8004 (reputation: attest / read feedback)", function () {
    let token, sink, bond, rep, repAdapter;

    async function deployRep(attestorAdapter) {
      token = await (await ethers.getContractFactory("MockERC20Lending")).deploy("AERE", "AERE", 18);
      await token.waitForDeployment();
      sink = await (await ethers.getContractFactory("MockSinkSimple")).deploy();
      await sink.waitForDeployment();
      bond = await (await ethers.getContractFactory("AereAgentBond")).deploy(
        await token.getAddress(),
        await sink.getAddress(),
        [foundation.address]
      );
      await bond.waitForDeployment();

      if (attestorAdapter) {
        // The live AereAIReputation attestor set is fixed at deploy with no add path, so to let the
        // adapter WRITE feedback through it, the reputation instance must list the adapter as an
        // attestor. Predict the adapter address so it can be authorized at reputation deploy.
        const nonce = await owner.getNonce();
        const predicted = ethers.getCreateAddress({ from: owner.address, nonce: nonce + 1 });
        rep = await (await ethers.getContractFactory("AereAIReputation")).deploy(await bond.getAddress(), [predicted]);
        await rep.waitForDeployment();
        repAdapter = await (await ethers.getContractFactory("AereReputationRegistry8004")).deploy(
          await rep.getAddress(),
          await identity.getAddress()
        );
        await repAdapter.waitForDeployment();
        expect(await repAdapter.getAddress()).to.equal(predicted);
        // M1: giveFeedback is gated to an owner-curated author allowlist. Authorize `other` so the
        // existing feedback flows below (which submit as `other`) exercise the write path.
        await repAdapter.connect(owner).addFeedbackAuthor(other.address);
      } else {
        // Reputation that does NOT authorize the adapter (mirrors the immutable mainnet Foundation set).
        rep = await (await ethers.getContractFactory("AereAIReputation")).deploy(await bond.getAddress(), [
          foundation.address,
        ]);
        await rep.waitForDeployment();
        repAdapter = await (await ethers.getContractFactory("AereReputationRegistry8004")).deploy(
          await rep.getAddress(),
          await identity.getAddress()
        );
        await repAdapter.waitForDeployment();
        // M1: authorize `other` as a feedback author (owner-curated allowlist).
        await repAdapter.connect(owner).addFeedbackAuthor(other.address);
      }
    }

    it("forwards ERC-8004 feedback into the live reputation and reads the score back", async function () {
      const a = await makeAgent("rep-1");
      await deployRep(true);

      // Maps the ERC-8004 agentId to exactly one live (operator, agentKey) slot.
      const [operator, agentKey] = await repAdapter.liveKeyOf(a.agentId);
      expect(operator).to.equal(owner.address); // the agent's controller
      expect(agentKey).to.equal(ethers.zeroPadValue(ethers.toBeHex(a.agentId), 32));
      expect(await repAdapter.canWriteThrough()).to.equal(true);

      // Attest a positive interaction through the adapter; it forwards to the live attest.
      const ev1 = ethers.keccak256(ethers.toUtf8Bytes("evidence-1"));
      await expect(repAdapter.connect(other).giveFeedback(a.agentId, 1, ev1, "ipfs://ev1")).to.emit(
        repAdapter,
        "NewFeedback"
      );

      // Read the canonical live score straight from AereAIReputation via the adapter.
      // scoreOf = positiveCount*10 (no slash, no dispute) => 1 positive => 10.
      expect(await repAdapter.getScore(a.agentId)).to.equal(10n);
      const stats = await repAdapter.getStats(a.agentId);
      expect(stats.positiveCount).to.equal(1n);
      expect(stats.disputeCount).to.equal(0n);

      // A dispute (-1) lowers the score; two positives raise it.
      const ev2 = ethers.keccak256(ethers.toUtf8Bytes("evidence-2"));
      await repAdapter.connect(other).giveFeedback(a.agentId, 1, ev2, "");
      const ev3 = ethers.keccak256(ethers.toUtf8Bytes("evidence-3"));
      await repAdapter.connect(other).giveFeedback(a.agentId, -1, ev3, "");
      // positive=2 (=>20), dispute=1 (=>-20) => 0.
      expect(await repAdapter.getScore(a.agentId)).to.equal(0n);
      const stats2 = await repAdapter.getStats(a.agentId);
      expect(stats2.positiveCount).to.equal(2n);
      expect(stats2.disputeCount).to.equal(1n);
    });

    it("rejects feedback for an unregistered agent and an out-of-range delta", async function () {
      await deployRep(true); // `other` is authorized as a feedback author in deployRep
      await expect(repAdapter.connect(other).giveFeedback(12345, 1, ethers.id("x"), "")).to.be.revertedWithCustomError(
        repAdapter,
        "AgentNotRegistered"
      );
      const a = await makeAgent("rep-bad");
      await expect(repAdapter.connect(other).giveFeedback(a.agentId, 2, ethers.id("x"), "")).to.be.revertedWithCustomError(
        repAdapter,
        "InvalidDelta"
      );
    });

    // M1 (MEDIUM) fix: giveFeedback is gated to an owner-curated allowlist so the adapter cannot be an
    // open reputation firehose once it is a live attestor. Proves: an unauthorized caller reverts, an
    // authorized caller succeeds, revocation re-blocks, and only the owner may manage the allowlist.
    it("gates giveFeedback to owner-authorized feedback authors (M1)", async function () {
      const a = await makeAgent("rep-gate");
      await deployRep(true); // adapter is a live attestor; deployRep authorizes `other`, not `foundation`
      expect(await repAdapter.canWriteThrough()).to.equal(true);

      // An UNAUTHORIZED caller cannot write, even though the adapter is a live attestor.
      expect(await repAdapter.isAuthorizedFeedbackAuthor(foundation.address)).to.equal(false);
      const ev = ethers.keccak256(ethers.toUtf8Bytes("evidence-gate"));
      await expect(
        repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev, "")
      ).to.be.revertedWithCustomError(repAdapter, "NotAuthorizedFeedbackAuthor");
      expect(await repAdapter.getScore(a.agentId)).to.equal(0n); // nothing written

      // Owner authorizes `foundation`; the SAME call now succeeds and moves the live score.
      await expect(repAdapter.connect(owner).addFeedbackAuthor(foundation.address))
        .to.emit(repAdapter, "FeedbackAuthorSet")
        .withArgs(foundation.address, true);
      await expect(repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev, "")).to.emit(repAdapter, "NewFeedback");
      expect(await repAdapter.getScore(a.agentId)).to.equal(10n);

      // Owner revokes; the caller is fail-closed blocked again.
      await repAdapter.connect(owner).removeFeedbackAuthor(foundation.address);
      const ev2 = ethers.keccak256(ethers.toUtf8Bytes("evidence-gate-2"));
      await expect(
        repAdapter.connect(foundation).giveFeedback(a.agentId, 1, ev2, "")
      ).to.be.revertedWithCustomError(repAdapter, "NotAuthorizedFeedbackAuthor");

      // The author allowlist is owner-only (OZ v4 Ownable: "Ownable: caller is not the owner").
      await expect(repAdapter.connect(other).addFeedbackAuthor(other.address)).to.be.reverted;
    });

    it("is honest about the immutable attestor set: write-through reverts when the adapter is not authorized", async function () {
      const a = await makeAgent("rep-noauth");
      await deployRep(false); // reputation lists Foundation only, not the adapter (the mainnet case)

      expect(await repAdapter.canWriteThrough()).to.equal(false);
      // `other` is an authorized feedback author (deployRep), so it clears the caller gate and reaches
      // the fail-closed attestor check on the live reputation.
      await expect(
        repAdapter.connect(other).giveFeedback(a.agentId, 1, ethers.id("ev"), "")
      ).to.be.revertedWithCustomError(repAdapter, "AdapterNotAttestor");

      // The READ path still adapts the live contract with no authorization: score starts at 0.
      expect(await repAdapter.getScore(a.agentId)).to.equal(0n);
    });
  });

  // =========================================================================
  // 3. Validation adapter over the live PQC precompiles (the differentiator)
  // =========================================================================
  describe("AereValidationRegistry8004 (validation: request/respond with a PQC proof)", function () {
    it("accepts a genuine Falcon-512 (0x0AE1) validation response and records it", async function () {
      const a = await makeAgent("val-falcon");
      const rng = makeRng("val-falcon-key");
      const v = await registerKey(1, other, rng); // a Falcon-512 validator key
      expect(await reg.statusOf(v.keyId)).to.equal(REG_STATUS_ACTIVE);

      const dataHash = ethers.id("model-output-commitment-A");
      const tx = await validation.requestValidation(v.keyId, a.agentId, dataHash);
      const rc = await tx.wait();
      const reqLog = rc.logs.map((l) => validation.interface.parseLog(l)).find((p) => p && p.name === "ValidationRequest");
      const requestId = reqLog.args.requestId;

      const response = 92; // ERC-8004 score 0..100
      const challenge = await validation.responseChallenge(requestId, response);
      const proof = genuine(1, v.pk, challenge, rng);

      await expect(validation.respondValidation(requestId, response, proof))
        .to.emit(validation, "ValidationResponse")
        .withArgs(requestId, v.keyId, a.agentId, response, 1);

      expect(await validation.isValidated(requestId)).to.equal(true);
      const rec = await validation.getValidation(requestId);
      expect(rec.status).to.equal(2); // Completed
      expect(rec.response).to.equal(92n);
      expect(rec.pqcScheme).to.equal(1n);
      expect(rec.serverAgentId).to.equal(BigInt(a.agentId));
    });

    it("accepts a genuine ML-DSA-44 (0x0AE3) validation response too", async function () {
      const a = await makeAgent("val-mldsa");
      const rng = makeRng("val-mldsa-key");
      const v = await registerKey(3, other, rng); // an ML-DSA-44 validator key

      const dataHash = ethers.id("model-output-commitment-B");
      const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash);
      await validation.requestValidation(v.keyId, a.agentId, dataHash);

      const response = 77;
      const challenge = await validation.responseChallenge(requestId, response);
      const proof = genuine(3, v.pk, challenge, rng);
      await expect(validation.respondValidation(requestId, response, proof))
        .to.emit(validation, "ValidationResponse")
        .withArgs(requestId, v.keyId, a.agentId, response, 3);
      expect(await validation.isValidated(requestId)).to.equal(true);
    });

    it("REJECTS a tampered proof fail-closed (nothing recorded)", async function () {
      const a = await makeAgent("val-tamper");
      const rng = makeRng("val-tamper-key");
      const v = await registerKey(1, other, rng);

      const dataHash = ethers.id("model-output-commitment-C");
      const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash);
      await validation.requestValidation(v.keyId, a.agentId, dataHash);

      const response = 88;
      const challenge = await validation.responseChallenge(requestId, response);
      const good = genuine(1, v.pk, challenge, rng);
      const bad = tamper(1, good);

      await expect(validation.respondValidation(requestId, response, bad)).to.be.revertedWithCustomError(
        validation,
        "PQCValidationFailed"
      );
      // Fail-closed: request stays Pending, nothing recorded.
      expect(await validation.isValidated(requestId)).to.equal(false);
      const rec = await validation.getValidation(requestId);
      expect(rec.status).to.equal(1); // Pending
    });

    it("REJECTS a proof bound to a DIFFERENT response score (binding), and treats a stale precompile as invalid", async function () {
      const a = await makeAgent("val-bind");
      const rng = makeRng("val-bind-key");
      const v = await registerKey(1, other, rng);

      const dataHash = ethers.id("model-output-commitment-D");
      const requestId = await validation.requestValidation.staticCall(v.keyId, a.agentId, dataHash);
      await validation.requestValidation(v.keyId, a.agentId, dataHash);

      // Sign the challenge for response=50, then try to submit response=51 with it.
      const challenge50 = await validation.responseChallenge(requestId, 50);
      const proof50 = genuine(1, v.pk, challenge50, rng);
      await expect(validation.respondValidation(requestId, 51, proof50)).to.be.revertedWithCustomError(
        validation,
        "PQCValidationFailed"
      );

      // Wipe the precompile (pre-fork / stale chain): an empty account returns empty data => invalid.
      await network.provider.send("hardhat_setCode", [PRECOMPILE[1], "0x"]);
      await expect(validation.respondValidation(requestId, 50, proof50)).to.be.revertedWithCustomError(
        validation,
        "PQCValidationFailed"
      );
      expect(await validation.isValidated(requestId)).to.equal(false);
    });

    it("rejects requests with a non-PQC / inactive validator key or an unknown server agent", async function () {
      const a = await makeAgent("val-guard");
      const rng = makeRng("val-guard-key");
      const v = await registerKey(1, other, rng);

      // Unknown server agent.
      await expect(validation.requestValidation(v.keyId, 424242, ethers.id("d"))).to.be.revertedWithCustomError(
        validation,
        "ServerAgentUnknown"
      );
      // Revoked validator key is no longer ACTIVE.
      await reg.connect(other).revokeKey(v.keyId);
      await expect(validation.requestValidation(v.keyId, a.agentId, ethers.id("d"))).to.be.revertedWithCustomError(
        validation,
        "ValidatorKeyNotActive"
      );
    });
  });

  // =========================================================================
  // 4. x402 / AP2 post-quantum mandate verifier
  // =========================================================================
  describe("AereAP2MandateVerifier (Falcon-512-signed x402/AP2 spend mandate)", function () {
    async function makeMandate(overrides = {}) {
      const rng = makeRng(overrides.seed || "mandate-key");
      const agent = await registerKey(1, owner, rng); // the paying agent's Falcon-512 key
      const start = overrides.periodStart ?? (await futureTs(-10));
      const end = overrides.periodEnd ?? (await futureTs(3600));
      const m = {
        agentKeyId: agent.keyId,
        token: overrides.token ?? "0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8", // live WAERE address (label only)
        maxAmount: overrides.maxAmount ?? ethers.parseUnits("10", 18),
        periodStart: start,
        periodEnd: end,
        purpose: overrides.purpose ?? ethers.id("inference:anthropic"),
        mandateNonce: overrides.mandateNonce ?? 0,
      };
      return { rng, agent, m };
    }

    it("verifies a genuine Falcon mandate signature (pure view) and authorizes spend up to the cap", async function () {
      const { rng, agent, m } = await makeMandate();
      const mh = await mandate.mandateHash(m);
      const sig = genuine(1, agent.pk, mh, rng);

      expect(await mandate.verifyMandate(m, sig)).to.equal(true);

      // Authorize two spends within the 10-unit cap.
      await expect(mandate.authorizeSpend(m, ethers.parseUnits("4", 18), sig)).to.emit(mandate, "MandateAuthorized");
      await mandate.authorizeSpend(m, ethers.parseUnits("3", 18), sig);
      expect(await mandate.spentUnder(mh)).to.equal(ethers.parseUnits("7", 18));
      expect(await mandate.remainingUnder(m)).to.equal(ethers.parseUnits("3", 18));
    });

    it("rejects spend over the cumulative cap", async function () {
      const { rng, agent, m } = await makeMandate({ maxAmount: ethers.parseUnits("5", 18) });
      const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng);
      await mandate.authorizeSpend(m, ethers.parseUnits("4", 18), sig);
      await expect(mandate.authorizeSpend(m, ethers.parseUnits("2", 18), sig)).to.be.revertedWithCustomError(
        mandate,
        "MandateCapExceeded"
      );
    });

    it("rejects an expired mandate window", async function () {
      const { rng, agent, m } = await makeMandate({ periodStart: await futureTs(-100), periodEnd: await futureTs(-10) });
      const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng);
      await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError(
        mandate,
        "MandateExpired"
      );
    });

    it("REJECTS a tampered mandate signature fail-closed (verifyMandate false, authorize reverts)", async function () {
      const { rng, agent, m } = await makeMandate();
      const good = genuine(1, agent.pk, await mandate.mandateHash(m), rng);
      const bad = tamper(1, good);

      expect(await mandate.verifyMandate(m, bad)).to.equal(false);
      await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), bad)).to.be.revertedWithCustomError(
        mandate,
        "MandateSignatureInvalid"
      );
      expect(await mandate.spentUnder(await mandate.mandateHash(m))).to.equal(0n);
    });

    it("halts the mandate the instant the Falcon key is revoked (root lifecycle)", async function () {
      const { rng, agent, m } = await makeMandate({ seed: "mandate-revoke" });
      const sig = genuine(1, agent.pk, await mandate.mandateHash(m), rng);
      await mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig); // works before revoke

      await reg.connect(owner).revokeKey(agent.keyId);
      expect(await mandate.verifyMandate(m, sig)).to.equal(false);
      await expect(mandate.authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError(
        mandate,
        "AgentKeyNotActiveFalcon"
      );
    });

    it("enforces the settlement-executor gate when one is configured", async function () {
      // A verifier wired to a specific executor rejects authorizeSpend from anyone else.
      const gated = await (await ethers.getContractFactory("AereAP2MandateVerifier")).deploy(
        await reg.getAddress(),
        other.address // SETTLEMENT_EXECUTOR
      );
      await gated.waitForDeployment();
      const { rng, agent, m } = await makeMandate({ seed: "mandate-gated" });
      const sig = genuine(1, agent.pk, await gated.mandateHash(m), rng);
      await expect(gated.connect(owner).authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.be.revertedWithCustomError(
        gated,
        "NotSettlementExecutor"
      );
      // The configured executor can.
      await expect(gated.connect(other).authorizeSpend(m, ethers.parseUnits("1", 18), sig)).to.emit(
        gated,
        "MandateAuthorized"
      );
    });
  });
});
