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

// ---------------------------------------------------------------------------
// AereCryptoRegistry — access control, seeding, routed verification, lifecycle
// policy, successor resolution, and seeded invariant fuzz.
//
// Hardhat has no PQC precompile, so we install MockPQCPrecompile at 0x0AE1..0x0AE4
// (hardhat_setCode), exactly as the AerePQCAttestation test does. 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 verify() through the
// registry proves it encoded the input at the correct offsets. The REAL encoding is
// separately proven against the LIVE mainnet precompiles via eth_call (AerePQCAttestation
// suite / scripts). SHAKE256 (scheme 5) is a HASH entry the registry refuses to route.
// ---------------------------------------------------------------------------

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

// Scheme metadata identical to the live seeded rows; the seeded algorithmId equals the
// scheme number (id 1 = Falcon-512, ... id 4 = SLH-DSA-128s, id 5 = SHAKE256).
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), 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; // realistic Falcon header (not required by the mock)
  return ethers.hexlify(raw);
}

// commitment the mock expects for a genuine signature over `message` by `pubKey`.
function commitmentFor(pubKey, message) {
  return ethers.keccak256(ethers.concat([pubKey, message]));
}

// Build the signature envelope with a chosen commitment (genuine or forged).
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` (a 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; // Falcon: esig[1] at 40+1; fixed: sig[0]
  b[idx] ^= 0x01;
  return ethers.hexlify(b);
}

describe("AereCryptoRegistry", function () {
  let reg, owner, other, mockCode;

  before(async function () {
    [owner, other] = await ethers.getSigners();
    // Deploy one mock per scheme; capture its runtime bytecode (immutable scheme baked in).
    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("AereCryptoRegistry");
    reg = await R.deploy();
    await reg.waitForDeployment();
  });

  // Send addAlgorithm, parse the event, return the new id.
  async function addAlgo(args, signer = owner) {
    const tx = await reg.connect(signer).addAlgorithm(...args);
    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);
  }

  // Convenience args for a Falcon-512-style row pointing at the scheme-1 mock.
  function falcon512Args(name, gas = 120000) {
    return [name, 1, PRECOMPILE[1], 897, 0, 0x29, gas, true];
  }

  // -----------------------------------------------------------------------
  // Access control
  // -----------------------------------------------------------------------
  describe("access control (Ownable)", function () {
    const OWNABLE_REVERT = "Ownable: caller is not the owner";

    it("sets the deployer as owner", async function () {
      expect(await reg.owner()).to.equal(owner.address);
    });

    it("reverts for a non-owner on addAlgorithm / setStatus / setSuccessor / seedLiveSchemes", async function () {
      await expect(reg.connect(other).addAlgorithm(...falcon512Args("X"))).to.be.revertedWith(OWNABLE_REVERT);
      await expect(reg.connect(other).setStatus(1, Status.REVOKED)).to.be.revertedWith(OWNABLE_REVERT);
      await expect(reg.connect(other).setSuccessor(1, 2)).to.be.revertedWith(OWNABLE_REVERT);
      await expect(reg.connect(other).seedLiveSchemes()).to.be.revertedWith(OWNABLE_REVERT);
    });

    it("lets the owner add an algorithm", async function () {
      const id = await addAlgo(falcon512Args("Falcon-512"));
      expect(id).to.equal(1);
      expect(await reg.algorithmCount()).to.equal(1n);
    });
  });

  // -----------------------------------------------------------------------
  // addAlgorithm validation
  // -----------------------------------------------------------------------
  describe("addAlgorithm validation", function () {
    it("rejects an empty or over-long name", async function () {
      await expect(reg.addAlgorithm("", 1, PRECOMPILE[1], 897, 0, 0x29, 1, true)).to.be.revertedWithCustomError(
        reg,
        "InvalidAlgorithmParams"
      );
      const long = "n".repeat(65);
      await expect(reg.addAlgorithm(long, 1, PRECOMPILE[1], 897, 0, 0x29, 1, true)).to.be.revertedWithCustomError(
        reg,
        "InvalidAlgorithmParams"
      );
    });

    it("rejects a zero verifier", async function () {
      await expect(reg.addAlgorithm("X", 1, ethers.ZeroAddress, 897, 0, 0x29, 1, true)).to.be.revertedWithCustomError(
        reg,
        "InvalidAlgorithmParams"
      );
    });

    it("rejects a signature scheme with pubKeyLen 0", async function () {
      await expect(reg.addAlgorithm("X", 1, PRECOMPILE[1], 0, 0, 0x29, 1, true)).to.be.revertedWithCustomError(
        reg,
        "InvalidAlgorithmParams"
      );
    });

    it("rejects a fixed-concatenation signature scheme with sigLen 0", async function () {
      await expect(reg.addAlgorithm("X", 3, PRECOMPILE[3], 1312, 0, 0x00, 1, true)).to.be.revertedWithCustomError(
        reg,
        "InvalidAlgorithmParams"
      );
    });

    it("allows a hash-only entry with pubKeyLen 0 and sigLen 0", async function () {
      const id = await addAlgo(["SHAKE256", 5, PRECOMPILE[1], 0, 0, 0x00, 30000, false]);
      const a = await reg.getAlgorithm(id);
      expect(a.isSignature).to.equal(false);
    });
  });

  // -----------------------------------------------------------------------
  // Seeding
  // -----------------------------------------------------------------------
  describe("seedLiveSchemes", function () {
    it("registers exactly the five live schemes with correct fields", async function () {
      await reg.seedLiveSchemes();
      expect(await reg.algorithmCount()).to.equal(5n);
      expect(await reg.seeded()).to.equal(true);

      const expected = [
        { id: 1, name: "Falcon-512", scheme: 1, verifier: PRECOMPILE[1], pk: 897, sig: 0, esig: 0x29, sign: true, gas: 120000 },
        { id: 2, name: "Falcon-1024", scheme: 2, verifier: PRECOMPILE[2], pk: 1793, sig: 0, esig: 0x2a, sign: true, gas: 145000 },
        { id: 3, name: "ML-DSA-44", scheme: 3, verifier: PRECOMPILE[3], pk: 1312, sig: 2420, esig: 0x00, sign: true, gas: 351000 },
        { id: 4, name: "SLH-DSA-128s", scheme: 4, verifier: PRECOMPILE[4], pk: 32, sig: 7856, esig: 0x00, sign: true, gas: 400000 },
        { id: 5, name: "SHAKE256", scheme: 5, verifier: ethers.getAddress("0x0000000000000000000000000000000000000ae5"), pk: 0, sig: 0, esig: 0x00, sign: false, gas: 30000 },
      ];

      for (const e of expected) {
        const a = await reg.getAlgorithm(e.id);
        expect(a.name).to.equal(e.name);
        expect(Number(a.scheme)).to.equal(e.scheme);
        expect(a.verifier).to.equal(e.verifier);
        expect(Number(a.pubKeyLen)).to.equal(e.pk);
        expect(Number(a.sigLen)).to.equal(e.sig);
        expect(Number(a.esigHeader)).to.equal(e.esig);
        expect(Number(a.status)).to.equal(Status.ACTIVE);
        expect(Number(a.gasEstimate)).to.equal(e.gas);
        expect(Number(a.successorId)).to.equal(0);
        expect(a.isSignature).to.equal(e.sign);
        expect(Number(a.addedBlock)).to.be.greaterThan(0);
      }
    });

    it("is one-shot: a second seed reverts AlreadySeeded", async function () {
      await reg.seedLiveSchemes();
      await expect(reg.seedLiveSchemes()).to.be.revertedWithCustomError(reg, "AlreadySeeded");
    });

    it("precompileFor(scheme) returns the ACTIVE verifier and reverts for an unknown scheme", async function () {
      await reg.seedLiveSchemes();
      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, "UnknownScheme");
    });
  });

  // -----------------------------------------------------------------------
  // Routed verification (happy path per scheme)
  // -----------------------------------------------------------------------
  describe("verify (routed happy path)", function () {
    beforeEach(async function () {
      await reg.seedLiveSchemes();
    });

    for (const scheme of [1, 2, 3, 4]) {
      it(`routes ${META[scheme].name} and returns true for genuine, false for tampered`, async function () {
        const rng = makeRng("verify-" + scheme);
        const pk = makePubKey(scheme, rng);
        const messageHash = ethers.keccak256(ethers.toUtf8Bytes("msg-" + scheme));
        const good = genuine(scheme, pk, messageHash, rng);
        expect(await reg.verify(scheme, pk, messageHash, good)).to.equal(true);

        const bad = tamperCommitment(scheme, good);
        expect(await reg.verify(scheme, pk, messageHash, bad)).to.equal(false);
      });
    }

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

    it("returns false for a signature by a different key", async function () {
      const rng = makeRng("wrong-key");
      const scheme = 3;
      const pk = makePubKey(scheme, rng);
      const otherPk = makePubKey(scheme, rng);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const sigOther = genuine(scheme, otherPk, messageHash, rng);
      expect(await reg.verify(scheme, pk, messageHash, sigOther)).to.equal(false);
    });

    it("returns false for a wrong-length public key or signature (fail-closed, no revert)", async function () {
      const rng = makeRng("bad-len");
      const scheme = 3; // ML-DSA-44: pk 1312, sig 2420
      const pk = makePubKey(scheme, rng);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const good = genuine(scheme, pk, messageHash, rng);
      // short pk
      expect(await reg.verify(scheme, "0x" + "00".repeat(1311), messageHash, good)).to.equal(false);
      // short signature
      expect(await reg.verify(scheme, pk, messageHash, "0x" + "00".repeat(2419))).to.equal(false);
      // Falcon with only a nonce (no esig)
      const f = makePubKey(1, rng);
      expect(await reg.verify(1, f, messageHash, "0x" + "00".repeat(40))).to.equal(false);
    });

    it("returns false for a hash-only entry (SHAKE256, isSignature=false)", async function () {
      const rng = makeRng("hash");
      const anyPk = makePubKey(1, rng);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const anySig = genuine(1, anyPk, messageHash, rng);
      expect(await reg.verify(5, anyPk, messageHash, anySig)).to.equal(false);
    });

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

    it("treats an empty verifier return (pre-fork / wiped) as invalid", async function () {
      const rng = makeRng("empty");
      const scheme = 2;
      const pk = makePubKey(scheme, rng);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const sig = genuine(scheme, pk, messageHash, rng);
      expect(await reg.verify(scheme, pk, messageHash, sig)).to.equal(true);
      await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], "0x"]);
      expect(await reg.verify(scheme, pk, messageHash, sig)).to.equal(false);
    });
  });

  // -----------------------------------------------------------------------
  // Explicit encoding check: registry input == the precompile SPEC layout.
  // -----------------------------------------------------------------------
  describe("encoding matches the precompile spec", function () {
    beforeEach(async function () {
      await reg.seedLiveSchemes();
    });

    it("Falcon-1024: the same (pk,msg,sig) verified directly by the mock also verifies via the registry", async function () {
      const rng = makeRng("enc-falcon");
      const scheme = 2;
      const m = META[scheme];
      const pk = ethers.getBytes(makePubKey(scheme, rng));
      const message = ethers.getBytes(ethers.keccak256(ethers.toUtf8Bytes("enc")));
      const commitment = ethers.getBytes(commitmentFor(ethers.hexlify(pk), ethers.hexlify(message)));
      const nonce = rng.bytes(40);
      const esig = ethers.concat([new Uint8Array([m.esigHeader]), commitment]);
      const sigLen = ethers.getBytes(esig).length;
      const sm = ethers.concat([new Uint8Array([(sigLen >> 8) & 0xff, sigLen & 0xff]), nonce, message, esig]);
      const specInput = ethers.hexlify(ethers.concat([pk, sm]));

      // Direct precompile call with the hand-built spec input.
      const ret = await ethers.provider.call({ to: PRECOMPILE[scheme], data: specInput });
      expect(BigInt(ret)).to.equal(1n);

      // The registry, given the envelope (nonce || esig), must build the identical input.
      const env = ethers.hexlify(ethers.concat([nonce, esig]));
      expect(await reg.verify(scheme, ethers.hexlify(pk), ethers.hexlify(message), env)).to.equal(true);
    });

    it("ML-DSA-44: fixed concatenation pk||sig||msg matches", async function () {
      const rng = makeRng("enc-mldsa");
      const scheme = 3;
      const m = META[scheme];
      const pk = ethers.getBytes(makePubKey(scheme, rng));
      const message = ethers.getBytes(ethers.keccak256(ethers.toUtf8Bytes("enc2")));
      const commitment = ethers.getBytes(commitmentFor(ethers.hexlify(pk), ethers.hexlify(message)));
      const sig = new Uint8Array(m.sigLen);
      sig.set(commitment, 0);
      const specInput = ethers.hexlify(ethers.concat([pk, sig, message]));
      const ret = await ethers.provider.call({ to: PRECOMPILE[scheme], data: specInput });
      expect(BigInt(ret)).to.equal(1n);
      expect(await reg.verify(scheme, ethers.hexlify(pk), ethers.hexlify(message), ethers.hexlify(sig))).to.equal(true);
    });
  });

  // -----------------------------------------------------------------------
  // Lifecycle policy: UNKNOWN / REVOKED rejected; DEPRECATED still verifies
  // -----------------------------------------------------------------------
  describe("lifecycle policy", function () {
    beforeEach(async function () {
      await reg.seedLiveSchemes();
    });

    it("DEPRECATED still verifies, but isActive=false and isUsable=false", async function () {
      const rng = makeRng("dep");
      const scheme = 1;
      const pk = makePubKey(scheme, rng);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const sig = genuine(scheme, pk, messageHash, rng);

      await reg.setStatus(1, Status.DEPRECATED);
      expect(Number(await reg.statusOf(1))).to.equal(Status.DEPRECATED);
      expect(await reg.isActive(1)).to.equal(false);
      expect(await reg.isUsable(1)).to.equal(false);
      expect(await reg.verify(1, pk, messageHash, sig)).to.equal(true); // legacy verify still works
    });

    it("REVOKED never verifies", async function () {
      const rng = makeRng("rev");
      const scheme = 1;
      const pk = makePubKey(scheme, rng);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("m"));
      const sig = genuine(scheme, pk, messageHash, rng);
      expect(await reg.verify(1, pk, messageHash, sig)).to.equal(true);

      await reg.setStatus(1, Status.REVOKED);
      expect(await reg.verify(1, pk, messageHash, sig)).to.equal(false);
      expect(await reg.isActive(1)).to.equal(false);
      expect(await reg.isUsable(1)).to.equal(false);
    });

    it("an ACTIVE hash-only entry is isActive but not isUsable", async function () {
      expect(await reg.isActive(5)).to.equal(true); // SHAKE256 seeded ACTIVE
      expect(await reg.isUsable(5)).to.equal(false); // but not a usable signature verifier
    });

    it("setStatus cannot set UNKNOWN", async function () {
      await expect(reg.setStatus(1, Status.UNKNOWN)).to.be.revertedWithCustomError(reg, "InvalidStatusTarget");
    });

    it("REVOKED is terminal: no further status change", async function () {
      await reg.setStatus(1, Status.REVOKED);
      await expect(reg.setStatus(1, Status.ACTIVE)).to.be.revertedWithCustomError(reg, "AlgorithmRevoked");
      await expect(reg.setStatus(1, Status.DEPRECATED)).to.be.revertedWithCustomError(reg, "AlgorithmRevoked");
    });

    it("a DEPRECATED row can be reactivated to ACTIVE", async function () {
      await reg.setStatus(1, Status.DEPRECATED);
      await reg.setStatus(1, Status.ACTIVE);
      expect(await reg.isActive(1)).to.equal(true);
      expect(await reg.isUsable(1)).to.equal(true);
    });

    it("setStatus / setSuccessor revert on an unknown id", async function () {
      await expect(reg.setStatus(999, Status.ACTIVE)).to.be.revertedWithCustomError(reg, "UnknownAlgorithm");
      await expect(reg.setSuccessor(999, 1)).to.be.revertedWithCustomError(reg, "UnknownAlgorithm");
      await expect(reg.setSuccessor(1, 999)).to.be.revertedWithCustomError(reg, "UnknownAlgorithm");
    });

    it("statusOf/isActive/isUsable report UNKNOWN/false for a never-registered id", async function () {
      expect(Number(await reg.statusOf(999))).to.equal(Status.UNKNOWN);
      expect(await reg.isActive(999)).to.equal(false);
      expect(await reg.isUsable(999)).to.equal(false);
    });
  });

  // -----------------------------------------------------------------------
  // Successor resolution
  // -----------------------------------------------------------------------
  describe("resolveActive (successor chain)", function () {
    beforeEach(async function () {
      await reg.seedLiveSchemes();
    });

    it("returns the id itself when it is ACTIVE", async function () {
      expect(Number(await reg.resolveActive(1))).to.equal(1);
    });

    it("follows a single successor from a DEPRECATED row to its ACTIVE successor", async function () {
      const v2 = await addAlgo(falcon512Args("Falcon-512-v2")); // id 6, ACTIVE
      await reg.setStatus(1, Status.DEPRECATED);
      await reg.setSuccessor(1, v2);
      expect(Number(await reg.resolveActive(1))).to.equal(v2);
    });

    it("follows a multi-hop chain to the first ACTIVE row", async function () {
      const v2 = await addAlgo(falcon512Args("Falcon-512-v2")); // id 6
      const v3 = await addAlgo(falcon512Args("Falcon-512-v3")); // id 7
      await reg.setStatus(1, Status.DEPRECATED);
      await reg.setStatus(v2, Status.DEPRECATED);
      await reg.setSuccessor(1, v2);
      await reg.setSuccessor(v2, v3);
      expect(Number(await reg.resolveActive(1))).to.equal(v3);
    });

    it("reverts NoActiveSuccessor when the chain dead-ends with no ACTIVE row", async function () {
      await reg.setStatus(2, Status.DEPRECATED); // no successor set
      await expect(reg.resolveActive(2)).to.be.revertedWithCustomError(reg, "NoActiveSuccessor");
    });

    it("reverts SuccessorCycle on a cyclic chain with no ACTIVE row", async function () {
      const x = await addAlgo(falcon512Args("X")); // id 6
      const y = await addAlgo(falcon512Args("Y")); // id 7
      await reg.setStatus(x, Status.DEPRECATED);
      await reg.setStatus(y, Status.DEPRECATED);
      await reg.setSuccessor(x, y);
      await reg.setSuccessor(y, x);
      await expect(reg.resolveActive(x)).to.be.revertedWithCustomError(reg, "SuccessorCycle");
    });

    it("reverts UnknownAlgorithm for a nonexistent id", async function () {
      await expect(reg.resolveActive(999)).to.be.revertedWithCustomError(reg, "UnknownAlgorithm");
    });

    it("setSuccessor rejects a self-successor", async function () {
      await expect(reg.setSuccessor(1, 1)).to.be.revertedWithCustomError(reg, "InvalidSuccessor");
    });
  });

  // -----------------------------------------------------------------------
  // Seeded invariant fuzz
  // -----------------------------------------------------------------------
  describe("seeded invariant fuzz", function () {
    it("a REVOKED algorithm never verifies, across many rows and random statuses", async function () {
      const rng = makeRng("fuzz-revoked-v1");
      const N = 16;
      const rows = [];
      for (let i = 0; i < N; i++) {
        const id = await addAlgo(falcon512Args("F-" + i));
        const pk = makePubKey(1, rng);
        const messageHash = ethers.hexlify(rng.bytes(32));
        const sig = genuine(1, pk, messageHash, rng);
        // random status: 0 ACTIVE, 1 DEPRECATED, 2 REVOKED
        const pick = rng.int(3);
        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, pk, messageHash, sig, status });
      }

      for (const r of rows) {
        const expected = r.status !== Status.REVOKED; // ACTIVE or DEPRECATED verify; REVOKED never
        expect(await reg.verify(r.id, r.pk, r.messageHash, r.sig)).to.equal(expected);
        // A tampered signature is always false regardless of status.
        expect(await reg.verify(r.id, r.pk, r.messageHash, tamperCommitment(1, r.sig))).to.equal(false);
        // Revoked rows are never usable/active.
        if (r.status === Status.REVOKED) {
          expect(await reg.isUsable(r.id)).to.equal(false);
          expect(await reg.isActive(r.id)).to.equal(false);
        }
      }
      // Unknown ids are always false.
      expect(await reg.verify(N + 100, rows[0].pk, rows[0].messageHash, rows[0].sig)).to.equal(false);
    });

    it("adding N algorithms keeps ids monotonic and counts consistent", async function () {
      const rng = makeRng("fuzz-monotonic-v1");
      const N = 24;
      let prev = Number(await reg.algorithmCount());
      for (let i = 0; i < N; i++) {
        const sig = rng.int(2) === 0;
        const args = sig
          ? falcon512Args("A-" + i)
          : ["H-" + i, 9, PRECOMPILE[1], 0, 0, 0x00, rng.int(1000000), false];
        const id = await addAlgo(args);
        expect(id).to.equal(prev + 1); // monotonic, +1 each time
        expect(Number(await reg.algorithmCount())).to.equal(id); // count == last id
        prev = id;
      }
    });

    it("resolveActive always terminates (returns an ACTIVE id or reverts) over random graphs", async function () {
      const rng = makeRng("fuzz-resolve-v1");
      const M = 12;
      const ids = [];
      for (let i = 0; i < M; i++) ids.push(await addAlgo(falcon512Args("G-" + i)));

      // Randomly deprecate ~2/3 of them (leave the rest ACTIVE).
      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;
        }
      }
      // Random successor edges (may form cycles); skip self-edges.
      const succ = {};
      for (const id of ids) {
        if (rng.int(2) === 0) {
          let t = ids[rng.int(M)];
          if (t !== id) {
            await reg.setSuccessor(id, t);
            succ[id] = t;
          }
        }
      }

      // Off-chain reference walk: what resolveActive SHOULD do.
      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 ref = refResolve(id);
        if (typeof ref === "number") {
          const got = Number(await reg.resolveActive(id));
          expect(got).to.equal(ref);
          expect(await reg.isActive(got)).to.equal(true);
        } else {
          // dead-end or cycle -> must revert (and thus terminate, not hang).
          let reverted = false;
          try {
            await reg.resolveActive(id);
          } catch (e) {
            reverted = true;
          }
          expect(reverted).to.equal(true);
        }
      }
    });
  });

  // -----------------------------------------------------------------------
  // Gas measurement (informational)
  // -----------------------------------------------------------------------
  describe("gas", function () {
    it("reports deploy / seed / verify / admin gas", async function () {
      const R = await ethers.getContractFactory("AereCryptoRegistry");
      const fresh = await R.deploy();
      const deployRc = await fresh.deploymentTransaction().wait();

      const seedRc = await (await fresh.seedLiveSchemes()).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 gVerifyFalcon = await fresh.verify.estimateGas(2, pkF, msg, sigF);
      const gVerifyMldsa = await fresh.verify.estimateGas(3, pkM, msg, sigM);
      const gAdd = await fresh.addAlgorithm.estimateGas(...falcon512Args("Falcon-512-v2"));
      const addRc = await (await fresh.addAlgorithm(...falcon512Args("Falcon-512-v2"))).wait();
      const gStatus = await fresh.setStatus.estimateGas(6, Status.DEPRECATED);
      const gSucc = await fresh.setSuccessor.estimateGas(1, 6);

      console.log("        gas — deploy         :", deployRc.gasUsed.toString());
      console.log("        gas — seedLiveSchemes :", seedRc.gasUsed.toString());
      console.log("        gas — verify (Falcon-1024, mock):", gVerifyFalcon.toString());
      console.log("        gas — verify (ML-DSA-44, mock)  :", gVerifyMldsa.toString());
      console.log("        gas — addAlgorithm    :", gAdd.toString(), "(actual", addRc.gasUsed.toString() + ")");
      console.log("        gas — setStatus       :", gStatus.toString());
      console.log("        gas — setSuccessor    :", gSucc.toString());

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