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

// ---------------------------------------------------------------------------
// AerePQFinalityCertificate: the ADDITIVE Post-Quantum Finality Certificate.
//
// WHAT IS UNDER TEST (real, on-chain): AerePQAttestationKeyRegistry (per-validator
// hash-based attestation keys + a deterministic validator-set root) and
// AereFinalityCertificateVerifier (binds ONE succinct SP1 proof to the registry root,
// enforces the ceil(2N/3) quorum, records PQ finality, exposes isFinalPQ).
//
// TEST DOUBLE: MockSp1Verifier is the deployed SP1 gateway stand-in. Its verifyProof
// REVERTS on an invalid proof (exact production gateway semantics), so it faithfully
// exercises the fail-closed path. It accepts only (vkey, publicValues, proof) triples
// pre-registered via setValid(), so a "valid proof" here means the gateway asserted the
// exact bound public values.
//
// HONEST BOUNDARY: the OFF-CHAIN SP1 aggregation guest circuit (verify N hash-based
// XMSS/leanSig signatures against the validator-set root inside the zkVM and emit these
// public values) is NOT implemented and is [MEASURE]. These tests prove the on-chain
// verifier + registry only, against a mock gateway, exactly like AerePQAggregate and the
// zk light clients. Aere consensus stays classical ECDSA QBFT; this is purely additive.
// ---------------------------------------------------------------------------

const SCHEME_XMSS = 1;
const SCHEME_LEANSIG = 3;

function abicoder() {
  return ethers.AbiCoder.defaultAbiCoder();
}

// The fixed 192-byte SP1 public-values layout the aggregation circuit must emit.
function encodePV({ blockHash, height, root, size, quorum, domain }) {
  return abicoder().encode(
    ["bytes32", "uint256", "bytes32", "uint256", "uint256", "bytes32"],
    [blockHash, height, root, size, quorum, domain]
  );
}

// Marker the MockSp1Verifier keys a valid proof on: keccak256(abi.encode(vkey, publicValues, proof)).
function proofMarker(vkey, publicValues, proof) {
  return ethers.keccak256(abicoder().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof]));
}

// A 48-byte hash-based (XMSS-style) attestation public key: root(32) || seed(16).
function xmssKey(label) {
  const root = ethers.keccak256(ethers.toUtf8Bytes("xmss-root:" + label));
  const seed = ethers.keccak256(ethers.toUtf8Bytes("xmss-seed:" + label));
  return ethers.concat([root, ethers.dataSlice(seed, 0, 16)]);
}

describe("AerePQFinalityCertificate (attestation-key registry + certificate verifier)", function () {
  let sp1, registry, verifier;
  let deployer, relayer, other;
  let validators; // 7 signers
  const VKEY = ethers.keccak256(ethers.toUtf8Bytes("sp1-program:aere-pq-finality-cert-v1"));

  const BLOCK_HASH = ethers.keccak256(ethers.toUtf8Bytes("aere-block:12345678"));
  const BLOCK_HEIGHT = 12345678n;

  beforeEach(async function () {
    const signers = await ethers.getSigners();
    deployer = signers[0];
    relayer = signers[8];
    other = signers[9];
    validators = signers.slice(1, 8); // 7 validators

    sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
    await sp1.waitForDeployment();

    registry = await (await ethers.getContractFactory("AerePQAttestationKeyRegistry")).deploy();
    await registry.waitForDeployment();

    verifier = await (await ethers.getContractFactory("AereFinalityCertificateVerifier")).deploy(
      await sp1.getAddress(),
      VKEY,
      await registry.getAddress()
    );
    await verifier.waitForDeployment();

    // Enroll the 7-validator set (owner), then each validator self-registers its PQ key.
    for (const v of validators) {
      await registry.connect(deployer).enrollValidator(v.address);
    }
    for (const v of validators) {
      await registry.connect(v).registerAttestationKey(SCHEME_XMSS, xmssKey(v.address));
    }
  });

  // Build public values bound to the CURRENT registry state (unless overridden), and
  // (optionally) register the proof valid in the mock gateway.
  async function makeCert({
    blockHash = BLOCK_HASH,
    height = BLOCK_HEIGHT,
    quorum,
    proof = "0xabcd01",
    overrideRoot = null,
    overrideSize = null,
    overrideDomain = null,
    register = true,
  }) {
    const root = overrideRoot !== null ? overrideRoot : await registry.validatorSetRoot();
    const size = overrideSize !== null ? overrideSize : await registry.validatorCount();
    const domain = overrideDomain !== null ? overrideDomain : await verifier.attestationDomain();
    const pv = encodePV({ blockHash, height, root, size, quorum, domain });
    if (register) {
      await sp1.setValid(proofMarker(VKEY, pv, proof), true);
    }
    return { pv, proof };
  }

  // =========================================================================
  // 1. Registry: 7-validator set with hash-based attestation keys
  // =========================================================================
  describe("attestation-key registry", function () {
    it("registers a 7-validator set with hash-based keys, quorum threshold is 5", async function () {
      expect(await registry.validatorCount()).to.equal(7n);
      expect(await registry.isSetComplete()).to.equal(true);
      expect(await registry.activeKeyCount()).to.equal(7n);
      expect(await verifier.currentQuorumThreshold()).to.equal(5n); // ceil(2*7/3) = 5

      for (const v of validators) {
        expect(await registry.isEnrolled(v.address)).to.equal(true);
        expect(await registry.keyEpochOf(v.address)).to.equal(1n);
        const [keyId, scheme, epoch] = await registry.currentKey(v.address);
        expect(scheme).to.equal(SCHEME_XMSS);
        expect(epoch).to.equal(1n);
        expect(keyId).to.not.equal(ethers.MaxUint256);
      }
      expect(await registry.validatorSetRoot()).to.not.equal(ethers.ZeroHash);
    });

    it("is append-only and fail-closed: only enrolled validators register; enroll is idempotent-guarded", async function () {
      // A non-enrolled address cannot register a key.
      await expect(
        registry.connect(other).registerAttestationKey(SCHEME_XMSS, xmssKey("intruder"))
      ).to.be.revertedWithCustomError(registry, "NotEnrolled");

      // Re-enrolling an existing validator reverts (no silent re-ordering).
      await expect(
        registry.connect(deployer).enrollValidator(validators[0].address)
      ).to.be.revertedWithCustomError(registry, "AlreadyEnrolled");

      // Only the owner may enroll.
      await expect(
        registry.connect(other).enrollValidator(other.address)
      ).to.be.revertedWith("Ownable: caller is not the owner");

      // Bad scheme / bad key length are rejected.
      await expect(
        registry.connect(validators[0]).registerAttestationKey(99, xmssKey("x"))
      ).to.be.revertedWithCustomError(registry, "InvalidScheme");
      await expect(
        registry.connect(validators[0]).registerAttestationKey(SCHEME_XMSS, "0x1234")
      ).to.be.revertedWithCustomError(registry, "InvalidPubKey");
    });
  });

  // =========================================================================
  // 2. A valid certificate with quorum 5 records and isFinalPQ returns true
  // =========================================================================
  describe("valid certificate records PQ finality", function () {
    it("quorum 5 verifies, records, and isFinalPQ(blockHash) is true", async function () {
      const { pv, proof } = await makeCert({ quorum: 5 });

      // Strict view path returns the finalized block.
      const [bh, height] = await verifier.verifyCertificate(pv, proof);
      expect(bh).to.equal(BLOCK_HASH);
      expect(height).to.equal(BLOCK_HEIGHT);

      // Non-reverting path is true.
      expect(await verifier.tryVerifyCertificate(pv, proof)).to.equal(true);

      // Consumer confirm-a-specific-block path.
      expect(await verifier.assertFinalizes(BLOCK_HASH, BLOCK_HEIGHT, pv, proof)).to.equal(true);

      // isFinalPQ is false BEFORE recording.
      expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(false);

      // Recording emits and stores provenance.
      await expect(verifier.connect(relayer).recordCertificate(pv, proof))
        .to.emit(verifier, "CertificateRecorded")
        .withArgs(BLOCK_HASH, BLOCK_HEIGHT, 5, 7, await registry.validatorSetRoot(), relayer.address);

      expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(true);
      expect(await verifier.isFinalPQAtHeight(BLOCK_HASH, BLOCK_HEIGHT)).to.equal(true);
      expect(await verifier.highestFinalizedHeight()).to.equal(BLOCK_HEIGHT);
      expect(await verifier.highestFinalizedBlockHash()).to.equal(BLOCK_HASH);
      expect(await verifier.certificateCount()).to.equal(1n);

      const cert = await verifier.certificateOf(BLOCK_HASH);
      expect(cert.exists).to.equal(true);
      expect(cert.quorumCount).to.equal(5);
      expect(cert.validatorSetSize).to.equal(7);
      expect(cert.prover).to.equal(relayer.address);
    });

    it("full quorum (7) also records", async function () {
      const { pv, proof } = await makeCert({ quorum: 7, proof: "0xf011" });
      await verifier.recordCertificate(pv, proof);
      expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(true);
    });
  });

  // =========================================================================
  // 3. A certificate below quorum rejects
  // =========================================================================
  describe("below-quorum rejects fail-closed", function () {
    it("quorum 4 (< 5) is rejected even with a gateway-valid proof", async function () {
      const { pv, proof } = await makeCert({ quorum: 4 });

      await expect(verifier.verifyCertificate(pv, proof)).to.be.revertedWithCustomError(verifier, "BelowQuorum");
      expect(await verifier.tryVerifyCertificate(pv, proof)).to.equal(false);
      await expect(verifier.recordCertificate(pv, proof)).to.be.revertedWithCustomError(verifier, "BelowQuorum");
      expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(false);
    });

    it("an invalid proof (not registered in the gateway) is rejected fail-closed", async function () {
      const { pv, proof } = await makeCert({ quorum: 5, register: false });
      await expect(verifier.verifyCertificate(pv, proof)).to.be.revertedWithCustomError(sp1, "MockInvalidProof");
      expect(await verifier.tryVerifyCertificate(pv, proof)).to.equal(false);
      expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(false);
    });
  });

  // =========================================================================
  // 4. A wrong validator-set root rejects
  // =========================================================================
  describe("wrong validator-set root rejects", function () {
    it("a proof carrying a forged root is rejected before the gateway call", async function () {
      const forgedRoot = ethers.keccak256(ethers.toUtf8Bytes("attacker-validator-set"));
      const { pv, proof } = await makeCert({ quorum: 5, overrideRoot: forgedRoot });

      await expect(verifier.verifyCertificate(pv, proof)).to.be.revertedWithCustomError(
        verifier,
        "ValidatorSetRootMismatch"
      );
      expect(await verifier.tryVerifyCertificate(pv, proof)).to.equal(false);
    });

    it("a proof carrying the wrong set size or domain is rejected", async function () {
      const wrongSize = await makeCert({ quorum: 5, overrideSize: 9, proof: "0x51" });
      await expect(verifier.verifyCertificate(wrongSize.pv, wrongSize.proof)).to.be.revertedWithCustomError(
        verifier,
        "SizeMismatch"
      );

      const wrongDomain = await makeCert({
        quorum: 5,
        overrideDomain: ethers.keccak256(ethers.toUtf8Bytes("wrong-domain")),
        proof: "0x52",
      });
      await expect(verifier.verifyCertificate(wrongDomain.pv, wrongDomain.proof)).to.be.revertedWithCustomError(
        verifier,
        "DomainMismatch"
      );

      // Malformed (not 192 bytes) public values reject.
      await expect(verifier.verifyCertificate("0x1234", "0x53")).to.be.revertedWithCustomError(
        verifier,
        "BadPublicValues"
      );
    });
  });

  // =========================================================================
  // 5. A wrong block rejects
  // =========================================================================
  describe("wrong block rejects", function () {
    it("assertFinalizes rejects a certificate substituted for a different block", async function () {
      const { pv, proof } = await makeCert({ quorum: 5 });
      const otherBlock = ethers.keccak256(ethers.toUtf8Bytes("aere-block:99999999"));

      // The proof genuinely finalizes BLOCK_HASH...
      expect(await verifier.assertFinalizes(BLOCK_HASH, BLOCK_HEIGHT, pv, proof)).to.equal(true);
      // ...but cannot be claimed for a different block.
      await expect(
        verifier.assertFinalizes(otherBlock, BLOCK_HEIGHT, pv, proof)
      ).to.be.revertedWithCustomError(verifier, "BlockHashMismatch");
      // ...nor a different height.
      await expect(
        verifier.assertFinalizes(BLOCK_HASH, BLOCK_HEIGHT + 1n, pv, proof)
      ).to.be.revertedWithCustomError(verifier, "BlockHeightMismatch");
      expect(await verifier.tryAssertFinalizes(otherBlock, BLOCK_HEIGHT, pv, proof)).to.equal(false);
    });

    it("a zero block hash is rejected", async function () {
      const { pv, proof } = await makeCert({ blockHash: ethers.ZeroHash, quorum: 5, proof: "0x2000" });
      await expect(verifier.verifyCertificate(pv, proof)).to.be.revertedWithCustomError(verifier, "ZeroBlockHash");
    });
  });

  // =========================================================================
  // 6. Key rotation updates the set root; an old-root certificate no longer matches
  // =========================================================================
  describe("key rotation invalidates old-root certificates", function () {
    it("rotating one validator's key changes the root and rejects the pre-rotation certificate", async function () {
      const oldRoot = await registry.validatorSetRoot();

      // A certificate valid against the CURRENT (pre-rotation) root.
      const { pv: oldPV, proof: oldProof } = await makeCert({ quorum: 5, proof: "0x01d0" });
      // It verifies now.
      expect(await verifier.tryVerifyCertificate(oldPV, oldProof)).to.equal(true);

      // Validator 0 rotates its one-time/few-time key (epoch 1 -> 2), scheme change allowed.
      await registry.connect(validators[0]).registerAttestationKey(SCHEME_LEANSIG, xmssKey("rotated-key-0"));
      expect(await registry.keyEpochOf(validators[0].address)).to.equal(2n);

      const newRoot = await registry.validatorSetRoot();
      expect(newRoot).to.not.equal(oldRoot);

      // The pre-rotation certificate (bound to oldRoot) no longer matches the live registry.
      await expect(verifier.verifyCertificate(oldPV, oldProof)).to.be.revertedWithCustomError(
        verifier,
        "ValidatorSetRootMismatch"
      );
      expect(await verifier.tryVerifyCertificate(oldPV, oldProof)).to.equal(false);

      // A fresh certificate bound to the NEW root verifies and records.
      const { pv: newPV, proof: newProof } = await makeCert({ quorum: 5, proof: "0x0ec0" });
      await verifier.recordCertificate(newPV, newProof);
      expect(await verifier.isFinalPQ(BLOCK_HASH)).to.equal(true);

      // History is append-only: validator 0 has two keys on record.
      const history = await registry.keyHistoryOf(validators[0].address);
      expect(history.length).to.equal(2);
      const first = await registry.getKey(history[0]);
      expect(first.superseded).to.equal(true);
      const second = await registry.getKey(history[1]);
      expect(second.superseded).to.equal(false);
      expect(second.scheme).to.equal(SCHEME_LEANSIG);
    });
  });

  // =========================================================================
  // 7. Empty validator set fails closed
  // =========================================================================
  describe("empty validator set fails closed", function () {
    it("a verifier over an empty registry rejects any certificate", async function () {
      const emptyRegistry = await (await ethers.getContractFactory("AerePQAttestationKeyRegistry")).deploy();
      await emptyRegistry.waitForDeployment();
      const emptyVerifier = await (await ethers.getContractFactory("AereFinalityCertificateVerifier")).deploy(
        await sp1.getAddress(),
        VKEY,
        await emptyRegistry.getAddress()
      );
      await emptyVerifier.waitForDeployment();

      const domain = await emptyVerifier.attestationDomain();
      const pv = encodePV({
        blockHash: BLOCK_HASH,
        height: BLOCK_HEIGHT,
        root: await emptyRegistry.validatorSetRoot(),
        size: 0,
        quorum: 0,
        domain,
      });
      await sp1.setValid(proofMarker(VKEY, pv, "0xee"), true);
      await expect(emptyVerifier.verifyCertificate(pv, "0xee")).to.be.revertedWithCustomError(
        emptyVerifier,
        "EmptyValidatorSet"
      );
    });
  });
});
