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

// ---------------------------------------------------------------------------
// AereHybridAuthorizer — a crypto-agility CONSUMER over AereCryptoRegistry.
//
// The authorizer never pins a scheme: every check calls registry.resolveActive(id) to
// follow the successor chain to the live ACTIVE row, then verifies through the registry's
// routed verifier. So a Foundation-governed scheme swap on the REGISTRY (deprecate old ->
// setSuccessor -> new ACTIVE) migrates this consumer with NO redeploy.
//
// Like the registry test, Hardhat has no PQC precompile, so we install MockPQCPrecompile
// at 0x0AE1..0x0AE4 (hardhat_setCode). The mock parses the input at the precompiles' SPEC
// offsets and accepts iff commitment == keccak256(pk || message); the registry builds the
// SAME wire format, so a positive authorize through the registry proves correct encoding.
// The REAL encoding is separately proven against the LIVE mainnet precompiles via eth_call
// (AerePQCAttestation suite / conformance runner). Mock-vs-live caveat: these tests prove
// the ROUTING/POLICY/AGILITY logic, not the cryptography of the native precompiles.
// ---------------------------------------------------------------------------

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: { name: "Falcon-512", pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, sigLen: 0, falcon: true, gas: 120000 },
  2: { name: "Falcon-1024", pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, sigLen: 0, falcon: true, gas: 145000 },
  3: { name: "ML-DSA-44", pkLen: 1312, esigHeader: 0x00, sigLen: 2420, falcon: false, gas: 351000 },
  4: { name: "SLH-DSA-128s", pkLen: 32, esigHeader: 0x00, sigLen: 7856, falcon: false, gas: 400000 },
};

const Status = { UNKNOWN: 0, ACTIVE: 1, DEPRECATED: 2, REVOKED: 3 };

// Deterministic seeded RNG (keccak hash-chain) — identical to AereCryptoRegistry.test.js.
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;
  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);
  }
}

function genuine(scheme, pubKey, message, rng) {
  return envelope(scheme, commitmentFor(pubKey, message), rng);
}

function tamperCommitment(scheme, sigHex) {
  const b = ethers.getBytes(sigHex);
  const idx = META[scheme].falcon ? 41 : 0;
  b[idx] ^= 0x01;
  return ethers.hexlify(b);
}

// Convenience: registry.addAlgorithm args for a scheme's live shape, pointing at its mock.
function algoArgs(scheme, name) {
  const m = META[scheme];
  return [name, scheme, PRECOMPILE[scheme], m.pkLen, m.sigLen, m.esigHeader, m.gas, true];
}

describe("AereHybridAuthorizer", function () {
  let reg, auth, 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]]);
    }
  }

  // Add an algorithm row to the registry, return its id.
  async function addAlgo(scheme, name, signer = owner) {
    const tx = await reg.connect(signer).addAlgorithm(...algoArgs(scheme, name));
    const rc = await tx.wait();
    const log = rc.logs.map((l) => reg.interface.parseLog(l)).find((p) => p && p.name === "AlgorithmAdded");
    return Number(log.args.id);
  }

  async function deployAuthorizer(defaultId) {
    const A = await ethers.getContractFactory("AereHybridAuthorizer");
    const a = await A.deploy(await reg.getAddress(), defaultId);
    await a.waitForDeployment();
    return a;
  }

  beforeEach(async function () {
    await installMocks();
    const R = await ethers.getContractFactory("AereCryptoRegistry");
    reg = await R.deploy();
    await reg.waitForDeployment();
    await reg.seedLiveSchemes(); // ids 1..5 = Falcon-512/1024, ML-DSA-44, SLH-DSA-128s, SHAKE256
    auth = await deployAuthorizer(1); // default = Falcon-512
  });

  // -----------------------------------------------------------------------
  // Construction / wiring
  // -----------------------------------------------------------------------
  describe("construction", function () {
    it("wires the registry, sets the deployer as owner and a usable default", async function () {
      expect(await auth.registry()).to.equal(await reg.getAddress());
      expect(await auth.owner()).to.equal(owner.address);
      expect(Number(await auth.defaultAlgorithmId())).to.equal(1);
    });

    it("reverts ZeroRegistry for a zero registry", async function () {
      const A = await ethers.getContractFactory("AereHybridAuthorizer");
      await expect(A.deploy(ethers.ZeroAddress, 1)).to.be.revertedWithCustomError(auth, "ZeroRegistry");
    });

    it("reverts UnusableAlgorithm for a hash-only default (SHAKE256 id 5)", async function () {
      const A = await ethers.getContractFactory("AereHybridAuthorizer");
      await expect(A.deploy(await reg.getAddress(), 5)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
    });

    it("reverts UnusableAlgorithm for a nonexistent default id", async function () {
      const A = await ethers.getContractFactory("AereHybridAuthorizer");
      await expect(A.deploy(await reg.getAddress(), 999)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
    });

    it("reverts UnusableAlgorithm when the default id is REVOKED with no successor", async function () {
      await reg.setStatus(2, Status.REVOKED); // Falcon-1024 revoked, no successor
      const A = await ethers.getContractFactory("AereHybridAuthorizer");
      await expect(A.deploy(await reg.getAddress(), 2)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
    });
  });

  // -----------------------------------------------------------------------
  // checkAuthorized (fail-closed routed happy path)
  // -----------------------------------------------------------------------
  describe("checkAuthorized (routed, fail-closed)", function () {
    for (const scheme of [1, 2, 3, 4]) {
      it(`authorizes a genuine ${META[scheme].name} signature and rejects a tampered one`, async function () {
        const rng = makeRng("auth-" + scheme);
        const pk = makePubKey(scheme, rng);
        const msg = ethers.keccak256(ethers.toUtf8Bytes("msg-" + scheme));
        const good = genuine(scheme, pk, msg, rng);
        // requested id == scheme id (seeded 1:1)
        expect(await auth.checkAuthorized(scheme, pk, msg, good)).to.equal(true);
        expect(await auth.checkAuthorized(scheme, pk, msg, tamperCommitment(scheme, good))).to.equal(false);
      });
    }

    it("returns false (no revert) for an unknown requested id", async function () {
      const rng = makeRng("unknown");
      const pk = makePubKey(1, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const sig = genuine(1, pk, msg, rng);
      expect(await auth.checkAuthorized(0, pk, msg, sig)).to.equal(false);
      expect(await auth.checkAuthorized(999, pk, msg, sig)).to.equal(false);
    });

    it("returns false when the message differs from the signed one", async function () {
      const rng = makeRng("wrong-msg");
      const pk = makePubKey(1, rng);
      const signed = ethers.keccak256(ethers.toUtf8Bytes("signed"));
      const otherMsg = ethers.keccak256(ethers.toUtf8Bytes("other"));
      const sig = genuine(1, pk, signed, rng);
      expect(await auth.checkAuthorized(1, pk, otherMsg, sig)).to.equal(false);
    });

    it("routes to a hash-only entry as false (SHAKE256 id 5 is not usable)", async function () {
      const rng = makeRng("hash");
      const pk = makePubKey(1, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const sig = genuine(1, pk, msg, rng);
      // resolveActive(5) returns 5 (ACTIVE) but registry.verify refuses a hash-only row.
      expect(await auth.checkAuthorized(5, pk, msg, sig)).to.equal(false);
    });
  });

  // -----------------------------------------------------------------------
  // Per-account preference with a safe default
  // -----------------------------------------------------------------------
  describe("per-account preference", function () {
    it("uses the app default for an account with no preference", async function () {
      expect(Number(await auth.algorithmFor(other.address))).to.equal(1);
      expect(Number(await auth.preferredAlgorithmOf(other.address))).to.equal(0);
      expect(Number(await auth.resolvedAlgorithmFor(other.address))).to.equal(1);
    });

    it("lets an account pick a usable scheme and route checks under it", async function () {
      await auth.connect(other).setPreferredAlgorithm(3); // ML-DSA-44
      expect(Number(await auth.algorithmFor(other.address))).to.equal(3);

      const rng = makeRng("pref-mldsa");
      const pk = makePubKey(3, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("p"));
      const sig = genuine(3, pk, msg, rng);
      expect(await auth.checkAuthorizedFor(other.address, pk, msg, sig)).to.equal(true);

      // A Falcon-512 signature (the default scheme) is NOT accepted for this account.
      const fpk = makePubKey(1, rng);
      const fsig = genuine(1, fpk, msg, rng);
      expect(await auth.checkAuthorizedFor(other.address, fpk, msg, fsig)).to.equal(false);
    });

    it("clears a preference with id 0 and falls back to the safe default", async function () {
      await auth.connect(other).setPreferredAlgorithm(3);
      await auth.connect(other).setPreferredAlgorithm(0);
      expect(Number(await auth.preferredAlgorithmOf(other.address))).to.equal(0);
      expect(Number(await auth.algorithmFor(other.address))).to.equal(1);
    });

    it("rejects a preference that is unusable (hash-only / unknown / revoked-no-successor)", async function () {
      await expect(auth.connect(other).setPreferredAlgorithm(5)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
      await expect(auth.connect(other).setPreferredAlgorithm(999)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
      await reg.setStatus(4, Status.REVOKED);
      await expect(auth.connect(other).setPreferredAlgorithm(4)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
    });

    it("setPreferredAlgorithm is permissionless (any account, not owner-gated)", async function () {
      await expect(auth.connect(other).setPreferredAlgorithm(2)).to.not.be.reverted;
      expect(Number(await auth.algorithmFor(other.address))).to.equal(2);
    });
  });

  // -----------------------------------------------------------------------
  // THE HEADLINE: a successor swap transparently changes the accepted scheme,
  // with NO redeploy of the authorizer and the SAME requested id.
  // -----------------------------------------------------------------------
  describe("zero-redeploy scheme swap (deprecate -> successor -> new)", function () {
    it("the same requested id starts accepting the NEW scheme and rejecting the OLD one", async function () {
      const rng = makeRng("swap");
      const msg = ethers.keccak256(ethers.toUtf8Bytes("swap-msg"));

      // Old world: id 1 == Falcon-512.
      const fpk = makePubKey(1, rng);
      const fsig = genuine(1, fpk, msg, rng);
      // New world signature: an ML-DSA-44 key/sig.
      const mpk = makePubKey(3, rng);
      const msig = genuine(3, mpk, msg, rng);

      // BEFORE the swap: id 1 accepts Falcon-512, rejects the ML-DSA-44 signature.
      expect(await auth.checkAuthorized(1, fpk, msg, fsig)).to.equal(true);
      expect(await auth.checkAuthorized(1, mpk, msg, msig)).to.equal(false);

      // Foundation-governed swap on the REGISTRY (no authorizer redeploy):
      //   1. register the successor (an ML-DSA-44-shaped row), ACTIVE
      //   2. point id 1 -> successor
      //   3. deprecate id 1
      const newId = await addAlgo(3, "ML-DSA-44-succ"); // id 6, routes to the scheme-3 mock
      await reg.setSuccessor(1, newId);
      await reg.setStatus(1, Status.DEPRECATED);

      // AFTER the swap: SAME requested id 1 now routes to the successor.
      expect(Number(await auth.resolvedAlgorithmFor(owner.address))).to.equal(newId);
      // The old Falcon-512 signature no longer authorizes; the new ML-DSA-44 one does.
      expect(await auth.checkAuthorized(1, fpk, msg, fsig)).to.equal(false);
      expect(await auth.checkAuthorized(1, mpk, msg, msig)).to.equal(true);
    });

    it("a multi-hop successor chain resolves to the final ACTIVE scheme", async function () {
      const rng = makeRng("multi-hop");
      const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));

      const id6 = await addAlgo(2, "F1024-v2"); // ACTIVE
      const id7 = await addAlgo(4, "SLH-v3"); // ACTIVE
      await reg.setSuccessor(1, id6);
      await reg.setSuccessor(id6, id7);
      await reg.setStatus(1, Status.DEPRECATED);
      await reg.setStatus(id6, Status.DEPRECATED);

      expect(Number(await auth.resolvedAlgorithmFor(owner.address))).to.equal(id7);
      // A genuine SLH-DSA-128s (scheme 4) signature authorizes under requested id 1.
      const pk = makePubKey(4, rng);
      const sig = genuine(4, pk, msg, rng);
      expect(await auth.checkAuthorized(1, pk, msg, sig)).to.equal(true);
    });
  });

  // -----------------------------------------------------------------------
  // A REVOKED scheme is NEVER authorized
  // -----------------------------------------------------------------------
  describe("REVOKED is never authorized", function () {
    it("a revoked id with no successor authorizes nothing (fail-closed view, revert on authorize)", async function () {
      const rng = makeRng("revoked");
      const pk = makePubKey(1, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const sig = genuine(1, pk, msg, rng);

      expect(await auth.checkAuthorized(1, pk, msg, sig)).to.equal(true);

      await reg.setStatus(1, Status.REVOKED); // no successor
      // View fail-closes to false (resolveActive reverts NoActiveSuccessor, caught).
      expect(await auth.checkAuthorized(1, pk, msg, sig)).to.equal(false);
      // State-changing authorize surfaces the registry revert (NoActiveSuccessor).
      await expect(auth.authorize(1, pk, msg, sig)).to.be.revertedWithCustomError(reg, "NoActiveSuccessor");
    });

    it("a revoked id is skipped for its ACTIVE successor; a signature under the revoked scheme still fails", async function () {
      const rng = makeRng("revoked-succ");
      const msg = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const fpk = makePubKey(1, rng); // revoked-scheme (Falcon-512) key
      const fsig = genuine(1, fpk, msg, rng);
      const mpk = makePubKey(3, rng); // successor-scheme (ML-DSA-44) key
      const msig = genuine(3, mpk, msg, rng);

      const succ = await addAlgo(3, "ML-DSA-succ"); // ACTIVE
      await reg.setSuccessor(1, succ);
      await reg.setStatus(1, Status.REVOKED);

      // The revoked scheme's own signature can NEVER authorize (routes to successor).
      expect(await auth.checkAuthorized(1, fpk, msg, fsig)).to.equal(false);
      // Only a signature valid under the ACTIVE successor authorizes.
      expect(await auth.checkAuthorized(1, mpk, msg, msig)).to.equal(true);
      expect(Number(await auth.resolvedAlgorithmFor(owner.address))).to.equal(succ);
    });
  });

  // -----------------------------------------------------------------------
  // Owner governance: setDefaultAlgorithm / migrate — only owner
  // -----------------------------------------------------------------------
  describe("owner governance (only-owner)", function () {
    const OWNABLE_REVERT = "Ownable: caller is not the owner";

    it("reverts for a non-owner on setDefaultAlgorithm / migrate", async function () {
      await expect(auth.connect(other).setDefaultAlgorithm(2)).to.be.revertedWith(OWNABLE_REVERT);
      await expect(auth.connect(other).migrate(1)).to.be.revertedWith(OWNABLE_REVERT);
    });

    it("owner can retarget the default to another usable scheme", async function () {
      await expect(auth.setDefaultAlgorithm(3)).to.emit(auth, "DefaultAlgorithmSet").withArgs(3);
      expect(Number(await auth.defaultAlgorithmId())).to.equal(3);
    });

    it("setDefaultAlgorithm rejects an unusable target (hash-only / unknown)", async function () {
      await expect(auth.setDefaultAlgorithm(5)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
      await expect(auth.setDefaultAlgorithm(999)).to.be.revertedWithCustomError(auth, "UnusableAlgorithm");
    });

    it("migrate crystallizes the default onto the ACTIVE successor after a swap", async function () {
      const newId = await addAlgo(2, "F1024-succ");
      await reg.setSuccessor(1, newId);
      await reg.setStatus(1, Status.DEPRECATED);

      await expect(auth.migrate(1)).to.emit(auth, "DefaultMigrated").withArgs(1, newId);
      expect(Number(await auth.defaultAlgorithmId())).to.equal(newId);
    });

    it("migrate is a no-op (no event, default unchanged) when the id is still ACTIVE", async function () {
      await expect(auth.migrate(1)).to.not.emit(auth, "DefaultMigrated");
      expect(Number(await auth.defaultAlgorithmId())).to.equal(1);
    });

    it("migrate reverts NotCurrentDefault when oldId != the current default", async function () {
      await expect(auth.migrate(2)).to.be.revertedWithCustomError(auth, "NotCurrentDefault").withArgs(2, 1);
    });

    it("migrate reverts (from resolveActive) if the default dead-ends with no ACTIVE successor", async function () {
      // Make the default (1) DEPRECATED with no successor -> resolveActive reverts.
      await reg.setStatus(1, Status.DEPRECATED);
      await expect(auth.migrate(1)).to.be.revertedWithCustomError(reg, "NoActiveSuccessor");
    });
  });

  // -----------------------------------------------------------------------
  // State-changing authorize: ledger + events
  // -----------------------------------------------------------------------
  describe("authorize (state-changing, recorded)", function () {
    it("records a successful authorization and emits Authorized", async function () {
      const rng = makeRng("authorize");
      const pk = makePubKey(1, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("do-it"));
      const sig = genuine(1, pk, msg, rng);

      expect(await auth.isProofAuthorized(pk, msg)).to.equal(false);
      await expect(auth.authorize(1, pk, msg, sig))
        .to.emit(auth, "Authorized")
        .withArgs(owner.address, 1, 1, msg);

      expect(Number(await auth.authorizationCount())).to.equal(1);
      expect(await auth.isProofAuthorized(pk, msg)).to.equal(true);
      expect(Number(await auth.lastResolvedId())).to.equal(1);
      expect(await auth.lastMessageHash()).to.equal(msg);
    });

    it("reverts AuthorizationFailed on a tampered signature (verifier returns false)", async function () {
      const rng = makeRng("authorize-bad");
      const pk = makePubKey(1, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("nope"));
      const bad = tamperCommitment(1, genuine(1, pk, msg, rng));
      await expect(auth.authorize(1, pk, msg, bad))
        .to.be.revertedWithCustomError(auth, "AuthorizationFailed")
        .withArgs(1, 1);
      expect(Number(await auth.authorizationCount())).to.equal(0);
    });

    it("authorizeWithPreferred uses the caller's effective scheme and records the resolved id", async function () {
      await auth.connect(other).setPreferredAlgorithm(4); // SLH-DSA-128s
      const rng = makeRng("authorize-pref");
      const pk = makePubKey(4, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("pref-do"));
      const sig = genuine(4, pk, msg, rng);

      await expect(auth.connect(other).authorizeWithPreferred(pk, msg, sig))
        .to.emit(auth, "Authorized")
        .withArgs(other.address, 4, 4, msg);
      expect(Number(await auth.lastResolvedId())).to.equal(4);
    });

    it("authorize routes through a live successor swap without a redeploy", async function () {
      const rng = makeRng("authorize-swap");
      const msg = ethers.keccak256(ethers.toUtf8Bytes("swap-do"));
      const newId = await addAlgo(3, "ML-DSA-do");
      await reg.setSuccessor(1, newId);
      await reg.setStatus(1, Status.DEPRECATED);

      const pk = makePubKey(3, rng);
      const sig = genuine(3, pk, msg, rng);
      await expect(auth.authorize(1, pk, msg, sig))
        .to.emit(auth, "Authorized")
        .withArgs(owner.address, 1, newId, msg); // requestedId 1, resolvedId = successor
      expect(Number(await auth.lastResolvedId())).to.equal(newId);
    });
  });

  // -----------------------------------------------------------------------
  // Seeded invariant fuzz
  // -----------------------------------------------------------------------
  describe("seeded invariant fuzz", function () {
    it("a REVOKED scheme is NEVER authorized, across many rows and random statuses", async function () {
      const rng = makeRng("fuzz-authz-revoked-v1");
      const N = 16;
      const rows = [];
      for (let i = 0; i < N; i++) {
        const scheme = 1 + rng.int(4); // 1..4
        const id = await addAlgo(scheme, "R-" + i);
        const pk = makePubKey(scheme, rng);
        const msg = ethers.hexlify(rng.bytes(32));
        const sig = genuine(scheme, pk, msg, rng);
        const pick = rng.int(3); // 0 ACTIVE, 1 DEPRECATED, 2 REVOKED
        let status = Status.ACTIVE;
        if (pick === 1) {
          await reg.setStatus(id, Status.DEPRECATED);
          status = Status.DEPRECATED;
        } else if (pick === 2) {
          await reg.setStatus(id, Status.REVOKED);
          status = Status.REVOKED;
        }
        rows.push({ id, scheme, pk, msg, sig, status });
      }

      for (const r of rows) {
        // No successor is ever set on these rows. So:
        //   ACTIVE   -> authorizes a genuine sig (true)
        //   DEPRECATED / REVOKED (no ACTIVE successor) -> resolveActive reverts -> false
        const expected = r.status === Status.ACTIVE;
        expect(await auth.checkAuthorized(r.id, r.pk, r.msg, r.sig)).to.equal(expected);
        // A tampered signature is always false.
        expect(await auth.checkAuthorized(r.id, r.pk, r.msg, tamperCommitment(r.scheme, r.sig))).to.equal(false);
        // A REVOKED row can never be authorized, and authorize() must revert.
        if (r.status === Status.REVOKED) {
          let reverted = false;
          try {
            await auth.authorize.staticCall(r.id, r.pk, r.msg, r.sig);
          } catch (e) {
            reverted = true;
          }
          expect(reverted).to.equal(true);
        }
      }
    });

    it("checkAuthorized always terminates (never hangs) over random successor graphs", async function () {
      const rng = makeRng("fuzz-authz-resolve-v1");
      const M = 10;
      const ids = [];
      for (let i = 0; i < M; i++) ids.push(await addAlgo(1, "G-" + i));

      const status = {};
      for (const id of ids) {
        if (rng.int(3) !== 0) {
          await reg.setStatus(id, Status.DEPRECATED);
          status[id] = Status.DEPRECATED;
        } else {
          status[id] = Status.ACTIVE;
        }
      }
      const succ = {};
      for (const id of ids) {
        if (rng.int(2) === 0) {
          const t = ids[rng.int(M)];
          if (t !== id) {
            await reg.setSuccessor(id, t);
            succ[id] = t;
          }
        }
      }

      // Off-chain reference walk mirroring resolveActive.
      function refResolve(start) {
        let cur = start;
        const seen = new Set();
        while (true) {
          if (status[cur] === Status.ACTIVE) return cur;
          if (seen.has(cur)) return "cycle";
          seen.add(cur);
          const n = succ[cur];
          if (n === undefined) return "dead";
          cur = n;
        }
      }

      for (const id of ids) {
        const rng2 = makeRng("g-" + id);
        const pk = makePubKey(1, rng2);
        const msg = ethers.hexlify(rng2.bytes(32));
        const sig = genuine(1, pk, msg, rng2);
        const ref = refResolve(id);
        const got = await auth.checkAuthorized(id, pk, msg, sig);
        if (typeof ref === "number") {
          // Resolves to an ACTIVE Falcon-512 row -> the genuine Falcon-512 sig authorizes.
          expect(got).to.equal(true);
        } else {
          // Dead-end or cycle -> fail-closed false (terminated, did not hang).
          expect(got).to.equal(false);
        }
      }
    });
  });

  // -----------------------------------------------------------------------
  // Gas (informational)
  // -----------------------------------------------------------------------
  describe("gas", function () {
    it("reports deploy / authorize / admin gas", async function () {
      const A = await ethers.getContractFactory("AereHybridAuthorizer");
      const fresh = await A.deploy(await reg.getAddress(), 1);
      const deployRc = await fresh.deploymentTransaction().wait();

      const rng = makeRng("gas");
      const pkF = makePubKey(2, rng);
      const msg = ethers.keccak256(ethers.toUtf8Bytes("gas"));
      const sigF = genuine(2, pkF, msg, rng);
      const pkM = makePubKey(3, rng);
      const sigM = genuine(3, pkM, msg, rng);

      const gCheckFalcon = await fresh.checkAuthorized.estimateGas(2, pkF, msg, sigF);
      const gCheckMldsa = await fresh.checkAuthorized.estimateGas(3, pkM, msg, sigM);
      const gAuth = await fresh.authorize.estimateGas(2, pkF, msg, sigF);
      const authRc = await (await fresh.authorize(2, pkF, msg, sigF)).wait();
      const gSetDefault = await fresh.setDefaultAlgorithm.estimateGas(3);
      const gSetPref = await fresh.connect(other).setPreferredAlgorithm.estimateGas(3);

      console.log("        gas — deploy                :", deployRc.gasUsed.toString());
      console.log("        gas — checkAuthorized(F1024):", gCheckFalcon.toString());
      console.log("        gas — checkAuthorized(MLDSA):", gCheckMldsa.toString());
      console.log("        gas — authorize (F1024)     :", gAuth.toString(), "(actual", authRc.gasUsed.toString() + ")");
      console.log("        gas — setDefaultAlgorithm   :", gSetDefault.toString());
      console.log("        gas — setPreferredAlgorithm :", gSetPref.toString());

      expect(deployRc.gasUsed).to.be.greaterThan(0n);
    });
  });
});
