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

// ---------------------------------------------------------------------------
// AerePQCKeyRegistry — on-chain proof-of-possession, rotation, revocation,
// owner-only mutation, and seeded invariant fuzz.
//
// Hardhat has no PQC precompile, so we install MockPQCPrecompile at 0x0AE1..0x0AE4
// (hardhat_setCode), exactly as the AerePQCAttestation / AereCryptoRegistry suites do.
// The mock parses the input at the precompiles' SPEC offsets and accepts iff
// commitment == keccak256(pk || message). A "genuine signature" is one whose commitment
// equals keccak256(pk || challenge). Because the registry builds the SAME wire format as
// AerePQCAttestation (proven separately against the LIVE mainnet precompile via eth_call),
// a positive proof-of-possession here proves the registry encoded the input correctly.
// A registrant who does not hold the key cannot produce a matching commitment.
// ---------------------------------------------------------------------------

const PRECOMPILE = {
  1: ethers.getAddress("0x0000000000000000000000000000000000000ae1"), // Falcon-512
  2: ethers.getAddress("0x0000000000000000000000000000000000000ae2"), // Falcon-1024
  3: ethers.getAddress("0x0000000000000000000000000000000000000ae3"), // ML-DSA-44
  4: ethers.getAddress("0x0000000000000000000000000000000000000ae4"), // SLH-DSA-128s
};

const META = {
  1: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true },
  2: { pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, falcon: true },
  3: { pkLen: 1312, sigLen: 2420, falcon: false },
  4: { pkLen: 32, sigLen: 7856, falcon: false },
};

const Status = { NONE: 0, ACTIVE: 1, ROTATED: 2, REVOKED: 3 };
const NO_KEY = (1n << 256n) - 1n; // type(uint256).max sentinel

// Deterministic seeded RNG (keccak hash-chain), so the fuzz is fully reproducible.
function makeRng(seedText) {
  let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
  function 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 int(n) {
    const b = bytes(4);
    return (((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]) >>> 0) % n;
  }
  return { bytes, int };
}

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]));
}

function envelope(scheme, commitment, rng) {
  const m = META[scheme];
  if (m.falcon) {
    const nonce = rng.bytes(40);
    const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]); // 33 bytes
    return ethers.hexlify(ethers.concat([nonce, esig]));
  } else {
    const sig = new Uint8Array(m.sigLen);
    sig.set(ethers.getBytes(commitment), 0);
    return ethers.hexlify(sig);
  }
}

// A genuine signature envelope binding to `message` (bytes32) under `pubKey`.
function genuine(scheme, pubKey, message, rng) {
  return envelope(scheme, commitmentFor(pubKey, message), rng);
}

// Flip one byte inside the commitment region -> a genuine forgery (mock rejects it).
function tamperCommitment(scheme, sigHex) {
  const b = ethers.getBytes(sigHex);
  const idx = META[scheme].falcon ? 41 : 0;
  b[idx] ^= 0x01;
  return ethers.hexlify(b);
}

describe("AerePQCKeyRegistry (proof-of-possession + lifecycle + seeded fuzz)", function () {
  let reg, owner, other, mockCode;

  before(async function () {
    [owner, other] = await ethers.getSigners();
    mockCode = {};
    const F = await ethers.getContractFactory("MockPQCPrecompile");
    for (const scheme of [1, 2, 3, 4]) {
      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, 2, 3, 4]) {
      await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], mockCode[scheme]]);
    }
  }

  beforeEach(async function () {
    await installMocks();
    const R = await ethers.getContractFactory("AerePQCKeyRegistry");
    reg = await R.deploy();
    await reg.waitForDeployment();
  });

  // Build a genuine PoP signature for `signer` registering `pk` under `scheme` at the
  // signer's current identity nonce, then send registerKey and return the new keyId.
  async function registerGenuine(scheme, pk, signer = owner, rng = makeRng("pop-" + scheme)) {
    const nonce = await reg.identityNonce(signer.address);
    const challenge = await reg.popChallenge(signer.address, scheme, pk, nonce);
    const sig = genuine(scheme, pk, challenge, rng);
    const tx = await reg.connect(signer).registerKey(scheme, pk, sig);
    const rc = await tx.wait();
    const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered");
    return Number(log.args.keyId);
  }

  // -----------------------------------------------------------------------
  // Registration: proof-of-possession is required and per-scheme validated
  // -----------------------------------------------------------------------
  describe("registerKey (proof-of-possession)", function () {
    it("registers a well-formed key for every scheme with a genuine PoP, assigning ids", async function () {
      const rng = makeRng("reg-all");
      let expectedId = 0;
      for (const scheme of [1, 2, 3, 4]) {
        const pk = makePubKey(scheme, rng);
        const id = await registerGenuine(scheme, pk, owner, rng);
        expect(id).to.equal(expectedId++);
        const k = await reg.getKey(id);
        expect(k.owner).to.equal(owner.address);
        expect(Number(k.scheme)).to.equal(scheme);
        expect(Number(k.status)).to.equal(Status.ACTIVE);
        expect(k.pubKey).to.equal(pk);
        expect(k.rotatedFromId).to.equal(NO_KEY);
        expect(k.rotatedToId).to.equal(NO_KEY);
        expect(Number(k.registeredBlock)).to.be.greaterThan(0);
      }
      expect(await reg.keyCount()).to.equal(4n);
      expect(Number(await reg.identityNonce(owner.address))).to.equal(4);
    });

    it("REVERTS when the PoP signature is a forgery (tampered commitment) — key not stored", async function () {
      const rng = makeRng("forge");
      const scheme = 2;
      const pk = makePubKey(scheme, rng);
      const nonce = await reg.identityNonce(owner.address);
      const challenge = await reg.popChallenge(owner.address, scheme, pk, nonce);
      const forged = tamperCommitment(scheme, genuine(scheme, pk, challenge, rng));
      await expect(reg.registerKey(scheme, pk, forged)).to.be.revertedWithCustomError(reg, "ProofOfPossessionFailed");
      expect(await reg.keyCount()).to.equal(0n);
      expect(Number(await reg.identityNonce(owner.address))).to.equal(0);
    });

    it("REVERTS when the PoP proves possession of a DIFFERENT key", async function () {
      const rng = makeRng("wrongkey");
      const scheme = 3;
      const pk = makePubKey(scheme, rng);
      const otherPk = makePubKey(scheme, rng);
      const nonce = await reg.identityNonce(owner.address);
      const challenge = await reg.popChallenge(owner.address, scheme, pk, nonce);
      // Signature is genuine for otherPk over the challenge, but we register pk -> reject.
      const sigForOther = genuine(scheme, otherPk, challenge, rng);
      await expect(reg.registerKey(scheme, pk, sigForOther)).to.be.revertedWithCustomError(
        reg,
        "ProofOfPossessionFailed"
      );
    });

    it("PoP is non-transferable: a proof made for owner cannot be replayed by `other`", async function () {
      const rng = makeRng("nontransfer");
      const scheme = 1;
      const pk = makePubKey(scheme, rng);
      // Build a genuine proof for OWNER's challenge (owner address + owner nonce).
      const ownerNonce = await reg.identityNonce(owner.address);
      const ownerChallenge = await reg.popChallenge(owner.address, scheme, pk, ownerNonce);
      const proofForOwner = genuine(scheme, pk, ownerChallenge, rng);
      // owner can use it.
      // ...but `other` cannot: their challenge binds `other`'s address, so it fails.
      await expect(reg.connect(other).registerKey(scheme, pk, proofForOwner)).to.be.revertedWithCustomError(
        reg,
        "ProofOfPossessionFailed"
      );
      // owner still can (proof is valid for owner).
      await expect(reg.connect(owner).registerKey(scheme, pk, proofForOwner)).to.emit(reg, "KeyRegistered");
    });

    it("PoP cannot be replayed by the same identity after the nonce advances", async function () {
      const rng = makeRng("replay");
      const scheme = 2;
      const pk = makePubKey(scheme, rng);
      const nonce0 = await reg.identityNonce(owner.address);
      const challenge0 = await reg.popChallenge(owner.address, scheme, pk, nonce0);
      const proof0 = genuine(scheme, pk, challenge0, rng);

      await reg.registerKey(scheme, pk, proof0); // consumes nonce 0
      expect(Number(await reg.identityNonce(owner.address))).to.equal(1);
      // Replaying the exact same (pk, proof) now fails: the challenge uses nonce 1.
      await expect(reg.registerKey(scheme, pk, proof0)).to.be.revertedWithCustomError(reg, "ProofOfPossessionFailed");
      expect(await reg.keyCount()).to.equal(1n);
    });

    it("rejects an unknown scheme or malformed public key BEFORE touching the precompile", async function () {
      await expect(reg.registerKey(5, "0x" + "00".repeat(897), "0x")).to.be.revertedWithCustomError(reg, "InvalidScheme");
      // Falcon-1024 correct length, wrong header.
      await expect(reg.registerKey(2, "0x09" + "00".repeat(1792), "0x")).to.be.revertedWithCustomError(
        reg,
        "InvalidPubKey"
      );
      // ML-DSA-44 wrong length.
      await expect(reg.registerKey(3, "0x" + "00".repeat(1311), "0x")).to.be.revertedWithCustomError(
        reg,
        "InvalidPubKey"
      );
    });

    it("records keysOf and allows multiple keys per identity", async function () {
      const rng = makeRng("multi");
      const a = await registerGenuine(1, makePubKey(1, rng), owner, rng);
      const b = await registerGenuine(3, makePubKey(3, rng), owner, rng);
      const list = (await reg.keysOf(owner.address)).map((x) => Number(x));
      expect(list).to.deep.equal([a, b]);
    });
  });

  // -----------------------------------------------------------------------
  // Rotation
  // -----------------------------------------------------------------------
  describe("rotateKey", function () {
    it("rotates an ACTIVE key to a fresh successor, retiring the old one and linking both", async function () {
      const rng = makeRng("rotate");
      const oldId = await registerGenuine(1, makePubKey(1, rng), owner, rng);

      const newPk = makePubKey(2, rng); // rotate Falcon-512 -> Falcon-1024
      const nonce = await reg.identityNonce(owner.address);
      const challenge = await reg.popChallenge(owner.address, 2, newPk, nonce);
      const sig = genuine(2, newPk, challenge, rng);

      await expect(reg.rotateKey(oldId, 2, newPk, sig)).to.emit(reg, "KeyRotated");
      const newId = oldId + 1;

      const oldK = await reg.getKey(oldId);
      const newK = await reg.getKey(newId);
      expect(Number(oldK.status)).to.equal(Status.ROTATED);
      expect(oldK.rotatedToId).to.equal(BigInt(newId));
      expect(Number(newK.status)).to.equal(Status.ACTIVE);
      expect(newK.rotatedFromId).to.equal(BigInt(oldId));
      expect(await reg.isActiveKey(oldId)).to.equal(false);
      expect(await reg.isActiveKey(newId)).to.equal(true);
    });

    it("only the key owner may rotate", async function () {
      const rng = makeRng("rot-owner");
      const oldId = await registerGenuine(1, makePubKey(1, rng), owner, rng);
      const newPk = makePubKey(1, rng);
      const nonce = await reg.identityNonce(other.address);
      const challenge = await reg.popChallenge(other.address, 1, newPk, nonce);
      const sig = genuine(1, newPk, challenge, rng);
      await expect(reg.connect(other).rotateKey(oldId, 1, newPk, sig)).to.be.revertedWithCustomError(
        reg,
        "NotKeyOwner"
      );
    });

    it("rotation requires a genuine PoP of the NEW key", async function () {
      const rng = makeRng("rot-pop");
      const oldId = await registerGenuine(2, makePubKey(2, rng), owner, rng);
      const newPk = makePubKey(2, rng);
      const nonce = await reg.identityNonce(owner.address);
      const challenge = await reg.popChallenge(owner.address, 2, newPk, nonce);
      const forged = tamperCommitment(2, genuine(2, newPk, challenge, rng));
      await expect(reg.rotateKey(oldId, 2, newPk, forged)).to.be.revertedWithCustomError(
        reg,
        "ProofOfPossessionFailed"
      );
      // old key untouched.
      expect(Number((await reg.getKey(oldId)).status)).to.equal(Status.ACTIVE);
    });

    it("cannot rotate a non-ACTIVE (already rotated or revoked) key", async function () {
      const rng = makeRng("rot-twice");
      const oldId = await registerGenuine(1, makePubKey(1, rng), owner, rng);
      // First rotation.
      const p1 = makePubKey(1, rng);
      let nonce = await reg.identityNonce(owner.address);
      let ch = await reg.popChallenge(owner.address, 1, p1, nonce);
      await reg.rotateKey(oldId, 1, p1, genuine(1, p1, ch, rng));
      // Second rotation of the SAME (now ROTATED) old key must fail.
      const p2 = makePubKey(1, rng);
      nonce = await reg.identityNonce(owner.address);
      ch = await reg.popChallenge(owner.address, 1, p2, nonce);
      await expect(reg.rotateKey(oldId, 1, p2, genuine(1, p2, ch, rng))).to.be.revertedWithCustomError(
        reg,
        "KeyNotActive"
      );
    });
  });

  // -----------------------------------------------------------------------
  // Revocation
  // -----------------------------------------------------------------------
  describe("revokeKey", function () {
    it("owner revokes an ACTIVE key; it becomes terminal and stops verifying", async function () {
      const rng = makeRng("rev");
      const scheme = 2;
      const pk = makePubKey(scheme, rng);
      const id = await registerGenuine(scheme, pk, owner, rng);

      // verifyWithKey works before revocation.
      const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const sig = genuine(scheme, pk, msg, rng);
      expect(await reg.verifyWithKey(id, msg, sig)).to.equal(true);

      await expect(reg.revokeKey(id)).to.emit(reg, "KeyRevoked");
      expect(Number(await reg.statusOf(id))).to.equal(Status.REVOKED);
      expect(await reg.isActiveKey(id)).to.equal(false);
      // verifyWithKey now fail-closes to false even for a genuine signature.
      expect(await reg.verifyWithKey(id, msg, sig)).to.equal(false);
    });

    it("only the owner may revoke, and revoke is idempotent-terminal", async function () {
      const rng = makeRng("rev-owner");
      const id = await registerGenuine(1, makePubKey(1, rng), owner, rng);
      await expect(reg.connect(other).revokeKey(id)).to.be.revertedWithCustomError(reg, "NotKeyOwner");
      await reg.revokeKey(id);
      await expect(reg.revokeKey(id)).to.be.revertedWithCustomError(reg, "KeyAlreadyRevoked");
    });

    it("reverts on unknown keyId for revoke / rotate / getKey", async function () {
      await expect(reg.revokeKey(999)).to.be.revertedWithCustomError(reg, "UnknownKey");
      await expect(reg.rotateKey(999, 1, "0x", "0x")).to.be.revertedWithCustomError(reg, "UnknownKey");
      await expect(reg.getKey(999)).to.be.revertedWithCustomError(reg, "UnknownKey");
    });
  });

  // -----------------------------------------------------------------------
  // verifyWithKey / verify semantics
  // -----------------------------------------------------------------------
  describe("verifyWithKey & stateless verify", function () {
    it("verifyWithKey returns false (never reverts) for unknown/malformed inputs", async function () {
      const rng = makeRng("vwk");
      const scheme = 1;
      const pk = makePubKey(scheme, rng);
      const id = await registerGenuine(scheme, pk, owner, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("x"));
      // unknown key id.
      expect(await reg.verifyWithKey(999, msg, "0x" + "00".repeat(73))).to.equal(false);
      // malformed (too-short Falcon) signature -> fail-closed false, no revert.
      expect(await reg.verifyWithKey(id, msg, "0x" + "00".repeat(40))).to.equal(false);
      // genuine works.
      expect(await reg.verifyWithKey(id, msg, genuine(scheme, pk, msg, rng))).to.equal(true);
    });

    it("a ROTATED key still verifies historically via verifyWithKey", async function () {
      const rng = makeRng("vwk-rot");
      const oldId = await registerGenuine(1, makePubKey(1, rng), owner, rng);
      const oldPk = (await reg.getKey(oldId)).pubKey;
      const newPk = makePubKey(1, rng);
      const nonce = await reg.identityNonce(owner.address);
      const ch = await reg.popChallenge(owner.address, 1, newPk, nonce);
      await reg.rotateKey(oldId, 1, newPk, genuine(1, newPk, ch, rng));

      const msg = ethers.keccak256(ethers.toUtf8Bytes("hist"));
      const sig = genuine(1, oldPk, msg, rng);
      expect(Number(await reg.statusOf(oldId))).to.equal(Status.ROTATED);
      expect(await reg.verifyWithKey(oldId, msg, sig)).to.equal(true); // historical still valid
    });

    it("stateless verify() checks any scheme over a raw message; false when precompile wiped", async function () {
      const rng = makeRng("stateless");
      const scheme = 3;
      const pk = makePubKey(scheme, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("s"));
      const sig = genuine(scheme, pk, msg, rng);
      expect(await reg.verify(scheme, pk, msg, sig)).to.equal(true);
      await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], "0x"]);
      expect(await reg.verify(scheme, pk, msg, sig)).to.equal(false);
    });

    it("precompileFor returns the canonical address and reverts on an unknown scheme", async function () {
      expect(await reg.precompileFor(1)).to.equal(PRECOMPILE[1]);
      expect(await reg.precompileFor(4)).to.equal(PRECOMPILE[4]);
      await expect(reg.precompileFor(9)).to.be.revertedWithCustomError(reg, "InvalidScheme");
    });
  });

  // -----------------------------------------------------------------------
  // Seeded invariant fuzz
  // -----------------------------------------------------------------------
  describe("seeded invariant fuzz", function () {
    it("proof-of-possession can never be forged/replayed across many random rounds", async function () {
      const rng = makeRng("fuzz-pop-v1");
      const ROUNDS = 40;
      let expectedKeys = 0n;

      for (let r = 0; r < ROUNDS; r++) {
        const scheme = [1, 2, 3, 4][rng.int(4)];
        const pk = makePubKey(scheme, rng);
        const signer = rng.int(2) === 0 ? owner : other;

        const nonce = await reg.identityNonce(signer.address);
        const challenge = await reg.popChallenge(signer.address, scheme, pk, nonce);

        // --- a battery of BAD proofs: all revert, nothing stored, nonce frozen ---
        const bad = [];
        bad.push(tamperCommitment(scheme, genuine(scheme, pk, challenge, rng))); // forgery
        {
          // genuine proof but for a different key
          const otherPk = makePubKey(scheme, rng);
          bad.push(genuine(scheme, otherPk, challenge, rng));
        }
        {
          // genuine proof but for a different (wrong) challenge/nonce
          const wrongCh = await reg.popChallenge(signer.address, scheme, pk, nonce + 7n);
          bad.push(genuine(scheme, pk, wrongCh, rng));
        }
        for (const f of bad) {
          await expect(reg.connect(signer).registerKey(scheme, pk, f)).to.be.revertedWithCustomError(
            reg,
            "ProofOfPossessionFailed"
          );
        }
        expect(await reg.keyCount()).to.equal(expectedKeys);
        expect(await reg.identityNonce(signer.address)).to.equal(nonce);

        // --- the genuine proof succeeds exactly once; state advances by one ---
        const good = genuine(scheme, pk, challenge, rng);
        await reg.connect(signer).registerKey(scheme, pk, good);
        expectedKeys += 1n;
        expect(await reg.keyCount()).to.equal(expectedKeys);
        expect(await reg.identityNonce(signer.address)).to.equal(nonce + 1n);

        // --- replay of that exact proof now fails (nonce moved on) ---
        await expect(reg.connect(signer).registerKey(scheme, pk, good)).to.be.revertedWithCustomError(
          reg,
          "ProofOfPossessionFailed"
        );
        expect(await reg.keyCount()).to.equal(expectedKeys);
      }
    });

    it("revoked keys never verify and are never active, across random lifecycles", async function () {
      const rng = makeRng("fuzz-life-v1");
      const N = 18;
      const rows = [];
      for (let i = 0; i < N; i++) {
        const scheme = [1, 2, 3, 4][rng.int(4)];
        const pk = makePubKey(scheme, rng);
        const id = await registerGenuine(scheme, pk, owner, rng);
        const msg = ethers.hexlify(rng.bytes(32));
        const sig = genuine(scheme, pk, msg, rng);
        // random action: 0 leave ACTIVE, 1 rotate away, 2 revoke
        const pick = rng.int(3);
        let status = Status.ACTIVE;
        if (pick === 1) {
          const np = makePubKey(scheme, rng);
          const n = await reg.identityNonce(owner.address);
          const ch = await reg.popChallenge(owner.address, scheme, np, n);
          await reg.rotateKey(id, scheme, np, genuine(scheme, np, ch, rng));
          status = Status.ROTATED;
        } else if (pick === 2) {
          await reg.revokeKey(id);
          status = Status.REVOKED;
        }
        rows.push({ id, scheme, pk, msg, sig, status });
      }

      for (const row of rows) {
        const expected = row.status !== Status.REVOKED; // ACTIVE or ROTATED verify; REVOKED never
        expect(await reg.verifyWithKey(row.id, row.msg, row.sig)).to.equal(expected);
        expect(await reg.isActiveKey(row.id)).to.equal(row.status === Status.ACTIVE);
        if (row.status === Status.REVOKED) {
          expect(Number(await reg.statusOf(row.id))).to.equal(Status.REVOKED);
        }
      }
    });
  });

  // -----------------------------------------------------------------------
  // Gas (informational)
  // -----------------------------------------------------------------------
  describe("gas", function () {
    it("reports deploy / register / rotate / revoke / verify gas (mock precompile)", async function () {
      const R = await ethers.getContractFactory("AerePQCKeyRegistry");
      const fresh = await R.deploy();
      const deployRc = await fresh.deploymentTransaction().wait();

      const rng = makeRng("gas");
      // register Falcon-512
      const pk1 = makePubKey(1, rng);
      let n = await fresh.identityNonce(owner.address);
      let ch = await fresh.popChallenge(owner.address, 1, pk1, n);
      const regRc = await (await fresh.registerKey(1, pk1, genuine(1, pk1, ch, rng))).wait();

      // rotate to Falcon-512
      const pk2 = makePubKey(1, rng);
      n = await fresh.identityNonce(owner.address);
      ch = await fresh.popChallenge(owner.address, 1, pk2, n);
      const rotRc = await (await fresh.rotateKey(0, 1, pk2, genuine(1, pk2, ch, rng))).wait();

      // register ML-DSA-44
      const pk3 = makePubKey(3, rng);
      n = await fresh.identityNonce(owner.address);
      ch = await fresh.popChallenge(owner.address, 3, pk3, n);
      const regMldsaRc = await (await fresh.registerKey(3, pk3, genuine(3, pk3, ch, rng))).wait();

      const revRc = await (await fresh.revokeKey(1)).wait();

      console.log("        gas — deploy                :", deployRc.gasUsed.toString());
      console.log("        gas — registerKey Falcon-512:", regRc.gasUsed.toString());
      console.log("        gas — rotateKey  Falcon-512 :", rotRc.gasUsed.toString());
      console.log("        gas — registerKey ML-DSA-44 :", regMldsaRc.gasUsed.toString());
      console.log("        gas — revokeKey             :", revRc.gasUsed.toString());
      expect(deployRc.gasUsed).to.be.greaterThan(0n);
    });
  });
});
