// AerePQ-Screen: institution-grade post-quantum compliance passport.
//
// Exercises the three new compliance contracts end to end over Aere's LIVE PQC primitives:
//   - AereTrustRegistry        federated accredited-issuer trust list (eIDAS-2.0-in-spirit)
//   - AereVerifiableCredential W3C VC 2.0 anchor + verify, POST-QUANTUM (Falcon-512) issuer signature
//   - AereBitstringStatusList  W3C Bitstring Status List revocation + zk non-revocation stub
//
// Aere has no PQC precompile in the local Hardhat EVM, so we install the repo's MockPQCPrecompile at
// 0x0AE1 (Falcon-512) via hardhat_setCode, exactly as the erc8004-adapters / AereRecoveryRegistry
// tests do. The mock parses the input at the LIVE precompile's spec offsets and accepts iff
// commitment == keccak256(pk || message), so a positive result proves the Trust Registry routed the
// correct input to the correct precompile and enforces fail-closed semantics. The REAL precompile
// path (0x0AE1 accepting a genuine Falcon-512 signature) is proven separately against live mainnet
// 2800 via eth_call, as AerePQCKeyRegistry / AerePQCAttestation already are.
//
// Run: npx hardhat test test/aere-pq-screen.test.js

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

const FALCON = ethers.getAddress("0x0000000000000000000000000000000000000ae1"); // Falcon-512 precompile
const SCHEME_FALCON512 = 1;
const REG_STATUS_ACTIVE = 1;
const NO_KEY = (1n << 256n) - 1n; // type(uint256).max sentinel

const TYPE_KYC = ethers.id("KYCCredential");
const TYPE_ADDRESS = ethers.id("ProofOfAddressCredential");

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

function makeFalconPubKey(rng) {
  const raw = rng.bytes(897);
  raw[0] = 0x09; // Falcon-512 public-key header byte
  return ethers.hexlify(raw);
}

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

// A genuine Falcon-512 signature envelope under the mock's semantics: nonce(40) || esig(0x29 || commit).
function genuineFalcon(pubKey, message, rng) {
  const nonce = rng.bytes(40);
  const esig = ethers.concat([new Uint8Array([0x29]), commitmentFor(pubKey, message)]);
  return ethers.hexlify(ethers.concat([nonce, esig]));
}

// Flip one byte inside the commitment region (a genuine forgery the precompile must reject).
function tamperFalcon(sigHex) {
  const b = ethers.getBytes(sigHex);
  b[41] ^= 0x01; // Falcon commitment starts at offset 41 (40 nonce + 1 header)
  return ethers.hexlify(b);
}

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

describe("AerePQ-Screen (post-quantum compliance passport)", function () {
  let owner, issuerA, issuerB, holder, relayer;
  let mockCode;
  let reg, trust, statusList, vc, sp1;

  before(async function () {
    [owner, issuerA, issuerB, holder, relayer] = await ethers.getSigners();
    const F = await ethers.getContractFactory("MockPQCPrecompile");
    const mock = await F.deploy(SCHEME_FALCON512);
    await mock.waitForDeployment();
    mockCode = await ethers.provider.getCode(await mock.getAddress());
  });

  async function installFalconMock() {
    await network.provider.send("hardhat_setCode", [FALCON, mockCode]);
  }

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

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

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

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

    trust = await (await ethers.getContractFactory("AereTrustRegistry")).deploy(await reg.getAddress());
    await trust.waitForDeployment();

    statusList = await (await ethers.getContractFactory("AereBitstringStatusList")).deploy(await sp1.getAddress());
    await statusList.waitForDeployment();

    vc = await (await ethers.getContractFactory("AereVerifiableCredential")).deploy(
      await trust.getAddress(),
      await statusList.getAddress()
    );
    await vc.waitForDeployment();
  });

  // Accredit issuerA for KYC with a Falcon key; create a Revocation list it controls.
  async function accreditAndList(rngSeed = "issuerA") {
    const rng = makeRng(rngSeed);
    const key = await registerFalconKey(issuerA, rng);
    const tx = await trust.accreditIssuer(issuerA.address, "did:aere:2800:issuerA", [TYPE_KYC], key.keyId);
    const rc = await tx.wait();
    const issuerId = Number(
      rc.logs.map((l) => trust.interface.parseLog(l)).find((p) => p && p.name === "IssuerAccredited").args.issuerId
    );
    // issuerA creates its revocation status list (capacity 1024).
    const lt = await statusList.connect(issuerA).createList(0 /* Revocation */, 1024);
    const lrc = await lt.wait();
    const listId = Number(
      lrc.logs.map((l) => statusList.interface.parseLog(l)).find((p) => p && p.name === "ListCreated").args.listId
    );
    return { rng, issuerId, listId, pk: key.pk, keyId: key.keyId };
  }

  async function makeCredential(ctx, overrides = {}) {
    const c = {
      issuerId: overrides.issuerId ?? ctx.issuerId,
      subjectDidHash: overrides.subjectDidHash ?? ethers.keccak256(ethers.toUtf8Bytes("did:aere:2800:holder-1")),
      credentialType: overrides.credentialType ?? TYPE_KYC,
      validFrom: overrides.validFrom ?? (await futureTs(-60)),
      validUntil: overrides.validUntil ?? (await futureTs(365 * 24 * 3600)),
      statusListId: overrides.statusListId ?? ctx.listId,
      statusIndex: overrides.statusIndex ?? 42,
      claimsHash: overrides.claimsHash ?? ethers.id("kyc-claims-blob"),
    };
    const digest = await vc.credentialDigest(c);
    const sig = genuineFalcon(ctx.pk, digest, ctx.rng);
    return { c, digest, sig };
  }

  // =========================================================================
  // 1. Wiring + accreditation lifecycle (append-only, fail-closed)
  // =========================================================================
  describe("AereTrustRegistry (federated accredited issuers)", function () {
    it("accredits an issuer with a scope + Falcon key, and reports accreditation fail-closed", async function () {
      const ctx = await accreditAndList();
      expect(await trust.issuerCount()).to.equal(1n);
      expect(await trust.issuerStatus(ctx.issuerId)).to.equal(1); // Active
      expect(await trust.isAccredited(ctx.issuerId, TYPE_KYC)).to.equal(true);
      // Fail-closed: out-of-scope type, and unknown issuer id.
      expect(await trust.isAccredited(ctx.issuerId, TYPE_ADDRESS)).to.equal(false);
      expect(await trust.isAccredited(9999, TYPE_KYC)).to.equal(false);
      expect(await trust.pqcKeyOf(ctx.issuerId)).to.equal(BigInt(ctx.keyId));
    });

    it("only the owner may accredit / suspend / revoke", async function () {
      const rng = makeRng("noauth");
      const key = await registerFalconKey(issuerA, rng);
      // OZ v4 Ownable reverts with the string "Ownable: caller is not the owner".
      await expect(
        trust.connect(issuerB).accreditIssuer(issuerA.address, "did:aere:2800:x", [TYPE_KYC], key.keyId)
      ).to.be.reverted;
    });

    it("keeps an append-only status history across suspend / reinstate / revoke", async function () {
      const ctx = await accreditAndList();
      expect(await trust.statusHistoryLength(ctx.issuerId)).to.equal(1n); // Active at accreditation
      await trust.suspendIssuer(ctx.issuerId, ethers.id("audit-lapsed"));
      await trust.reinstateIssuer(ctx.issuerId, ethers.id("audit-restored"));
      await trust.revokeIssuer(ctx.issuerId, ethers.id("accreditation-withdrawn"));
      expect(await trust.statusHistoryLength(ctx.issuerId)).to.equal(4n);
      expect((await trust.statusHistoryAt(ctx.issuerId, 3)).status).to.equal(3); // Revoked
      // Terminal: revoked cannot be reinstated or re-revoked.
      await expect(trust.reinstateIssuer(ctx.issuerId, ethers.ZeroHash)).to.be.revertedWithCustomError(
        trust,
        "NotSuspended"
      );
      await expect(trust.revokeIssuer(ctx.issuerId, ethers.ZeroHash)).to.be.revertedWithCustomError(
        trust,
        "AlreadyRevoked"
      );
    });
  });

  // =========================================================================
  // 2. Issue + verify a credential with a POST-QUANTUM issuer signature
  // =========================================================================
  describe("AereVerifiableCredential (issue / verify / revoke)", function () {
    it("verifies and anchors a valid PQC-signed credential (0x0AE1), relayable by anyone", async function () {
      const ctx = await accreditAndList();
      const { c, digest, sig } = await makeCredential(ctx);

      // Full W3C verification of the presented credential passes.
      expect(await vc.verifyPresented(c, sig)).to.equal(true);
      expect(await vc.verifyCredentialSignature(c, sig)).to.equal(true);

      // Anchor it (relayed by a third party; authorship is the Falcon signature).
      await expect(vc.connect(relayer).anchorCredential(c, sig))
        .to.emit(vc, "CredentialAnchored")
        .withArgs(digest, ctx.issuerId, TYPE_KYC, c.subjectDidHash, ctx.listId, 42);

      expect(await vc.isAnchored(digest)).to.equal(true);
      expect(await vc.isValid(digest)).to.equal(true);
      // Cannot double-anchor.
      await expect(vc.anchorCredential(c, sig)).to.be.revertedWithCustomError(vc, "AlreadyAnchored");
    });

    it("REVOKES via the Bitstring Status List: verification flips to false", async function () {
      const ctx = await accreditAndList();
      const { c, digest, sig } = await makeCredential(ctx);
      await vc.anchorCredential(c, sig);
      expect(await vc.isValid(digest)).to.equal(true);
      expect(await vc.isRevoked(c)).to.equal(false);

      // The issuer flips the credential's status bit (revocation is terminal).
      await statusList.connect(issuerA).revoke(ctx.listId, 42);

      expect(await statusList.statusOf(ctx.listId, 42)).to.equal(true);
      expect(await vc.isRevoked(c)).to.equal(true);
      expect(await vc.verifyPresented(c, sig)).to.equal(false);
      expect(await vc.isValid(digest)).to.equal(false);

      // Revocation cannot be undone on a Revocation-purpose list.
      await expect(statusList.connect(issuerA).setStatus(ctx.listId, 42, false)).to.be.revertedWithCustomError(
        statusList,
        "RevocationIsTerminal"
      );
    });

    it("a non-accredited issuer (wrong scope) cannot issue that credential type", async function () {
      const ctx = await accreditAndList();
      // issuerB is accredited only for ProofOfAddress, not KYC.
      const rngB = makeRng("issuerB");
      const keyB = await registerFalconKey(issuerB, rngB);
      const txB = await trust.accreditIssuer(issuerB.address, "did:aere:2800:issuerB", [TYPE_ADDRESS], keyB.keyId);
      const rcB = await txB.wait();
      const issuerBId = Number(
        rcB.logs.map((l) => trust.interface.parseLog(l)).find((p) => p && p.name === "IssuerAccredited").args.issuerId
      );

      // A KYC credential from issuerB fails accreditation, even with a genuine signature.
      const ctxB = { rng: rngB, issuerId: issuerBId, listId: ctx.listId, pk: keyB.pk, keyId: keyB.keyId };
      const { c, sig } = await makeCredential(ctxB, { statusIndex: 7 });
      expect(await vc.verifyPresented(c, sig)).to.equal(false);
      await expect(vc.anchorCredential(c, sig)).to.be.revertedWithCustomError(vc, "NotAccredited");
    });

    it("REJECTS a tampered PQC signature fail-closed (nothing anchored)", async function () {
      const ctx = await accreditAndList();
      const { c, sig } = await makeCredential(ctx, { statusIndex: 11 });
      const bad = tamperFalcon(sig);

      expect(await vc.verifyCredentialSignature(c, bad)).to.equal(false);
      expect(await vc.verifyPresented(c, bad)).to.equal(false);
      await expect(vc.anchorCredential(c, bad)).to.be.revertedWithCustomError(vc, "SignatureInvalid");
      expect(await vc.isAnchored(await vc.credentialDigest(c))).to.equal(false);
    });

    it("fails closed when the issuer is SUSPENDED after anchoring, and recovers on reinstatement", async function () {
      const ctx = await accreditAndList();
      const { c, digest, sig } = await makeCredential(ctx, { statusIndex: 21 });
      await vc.anchorCredential(c, sig);
      expect(await vc.isValid(digest)).to.equal(true);

      await trust.suspendIssuer(ctx.issuerId, ethers.id("under-review"));
      expect(await vc.isValid(digest)).to.equal(false);
      expect(await vc.verifyPresented(c, sig)).to.equal(false);

      await trust.reinstateIssuer(ctx.issuerId, ethers.id("cleared"));
      expect(await vc.isValid(digest)).to.equal(true);
    });

    it("rejects an expired credential and binds the did:aere subject DID on anchor", async function () {
      const ctx = await accreditAndList();
      // Expired window.
      const { c: expiredC, sig: expiredSig } = await makeCredential(ctx, {
        validFrom: await futureTs(-100),
        validUntil: await futureTs(-10),
        statusIndex: 31,
      });
      expect(await vc.verifyPresented(expiredC, expiredSig)).to.equal(false);
      await expect(vc.anchorCredential(expiredC, expiredSig)).to.be.revertedWithCustomError(vc, "Expired");

      // did:aere binding on anchor.
      const subjectDid = "did:aere:2800:holder-42";
      const { c, digest, sig } = await makeCredential(ctx, {
        subjectDidHash: ethers.keccak256(ethers.toUtf8Bytes(subjectDid)),
        statusIndex: 33,
      });
      await expect(vc.anchorCredentialWithDid(c, subjectDid, sig))
        .to.emit(vc, "CredentialAnchoredWithDid")
        .withArgs(digest, subjectDid);
      // A DID that does not hash to the bound subject is rejected.
      const { c: c2, sig: sig2 } = await makeCredential(ctx, {
        subjectDidHash: ethers.keccak256(ethers.toUtf8Bytes("did:aere:2800:holder-99")),
        statusIndex: 34,
      });
      await expect(vc.anchorCredentialWithDid(c2, "did:aere:2800:someone-else", sig2)).to.be.revertedWithCustomError(
        vc,
        "DidMismatch"
      );
      // A non-did:aere subject is rejected even when the hash matches.
      const nonAere = "did:web:example.com";
      const { c: c3, sig: sig3 } = await makeCredential(ctx, {
        subjectDidHash: ethers.keccak256(ethers.toUtf8Bytes(nonAere)),
        statusIndex: 35,
      });
      await expect(vc.anchorCredentialWithDid(c3, nonAere, sig3)).to.be.revertedWithCustomError(vc, "NotAereDid");
    });
  });

  // =========================================================================
  // 3. Bitstring Status List + zk non-revocation stub (shape + freshness binding)
  // =========================================================================
  describe("AereBitstringStatusList (bitset + zk non-revocation stub)", function () {
    // L1 (LOW) fix: verifyProof returns void, so a NON-ZERO but code-less verifier makes the
    // try/verifyProof in verifyNonRevocation succeed silently (a revoked credential proves "not
    // revoked" = fail-OPEN). The constructor must reject a code-less verifier, while still allowing
    // the zero address (which keeps the zk path fail-closed OFF until configured).
    it("rejects a non-zero code-less non-revocation verifier at construction, but allows zero (L1 guard)", async function () {
      const F = await ethers.getContractFactory("AereBitstringStatusList");
      // A non-zero EOA verifier has no code.
      await expect(F.deploy(holder.address)).to.be.revertedWithCustomError(statusList, "VerifierHasNoCode");
      // The zero address is allowed: the zk non-revocation path stays fail-closed OFF.
      const off = await F.deploy(ethers.ZeroAddress);
      await off.waitForDeployment();
      expect(await off.nonRevocationConfigured()).to.equal(false);
      // Control: the real (code-bearing) mock SP1 gateway deploys fine.
      const ok = await F.deploy(await sp1.getAddress());
      await ok.waitForDeployment();
      expect(await ok.NON_REVOCATION_VERIFIER()).to.equal(await sp1.getAddress());
    });

    it("packs status bits and enforces controller-only mutation", async function () {
      await statusList.connect(issuerA).createList(0, 1024); // listId 0
      expect(await statusList.statusOf(0, 5)).to.equal(false);
      await statusList.connect(issuerA).setStatus(0, 5, true);
      expect(await statusList.statusOf(0, 5)).to.equal(true);
      // A non-controller cannot mutate.
      await expect(statusList.connect(issuerB).setStatus(0, 6, true)).to.be.revertedWithCustomError(
        statusList,
        "NotController"
      );
      // No silent no-op: re-setting the same value reverts.
      await expect(statusList.connect(issuerA).setStatus(0, 5, true)).to.be.revertedWithCustomError(
        statusList,
        "NoStatusChange"
      );
    });

    it("verifies a zk non-revocation proof against a FRESH committed root (mock SP1), fail-closed on staleness", async function () {
      // Configure the network non-revocation program (vkey). Verifier is the MockSp1Verifier.
      const vkey = ethers.id("aere-nonrevocation-v1-PLACEHOLDER"); // [MEASURE] real vkey once circuit is built
      await statusList.setNonRevocationVKey(vkey);
      expect(await statusList.nonRevocationConfigured()).to.equal(true);

      await statusList.connect(issuerA).createList(0, 1024); // listId 0
      // Controller publishes a commitment to the current bitstring at the current epoch.
      const root = ethers.id("bitstring-root-epoch-0");
      await statusList.connect(issuerA).publishStatusRoot(0, root);

      // Build public values in the exact ABI shape the contract decodes.
      const publicValues = ethers.AbiCoder.defaultAbiCoder().encode(
        ["uint256", "address", "uint256", "bytes32", "bytes32", "uint8"],
        [
          (await ethers.provider.getNetwork()).chainId,
          await statusList.getAddress(),
          0, // listId
          root, // statusRoot must equal the published root
          ethers.id("holder-credential-commitment"), // credentialCommitment (bound in-circuit)
          0, // purpose = Revocation
        ]
      );
      const proof = "0x1234"; // opaque; the mock accepts by (vkey, publicValues, proof) marker
      const marker = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
        ["bytes32", "bytes", "bytes"],
        [vkey, publicValues, proof]
      ));
      await sp1.setValid(marker, true);

      expect(await statusList.verifyNonRevocation(0, publicValues, proof)).to.equal(true);

      // Mutate a bit after publishing the root: now rootEpoch != epoch, so the proof is STALE -> false.
      await statusList.connect(issuerA).setStatus(0, 3, true);
      expect(await statusList.verifyNonRevocation(0, publicValues, proof)).to.equal(false);
    });

    it("reverts NonRevocationNotConfigured before a vkey is set (never mistaken for not-revoked)", async function () {
      await statusList.connect(issuerA).createList(0, 1024);
      const pv = ethers.AbiCoder.defaultAbiCoder().encode(
        ["uint256", "address", "uint256", "bytes32", "bytes32", "uint8"],
        [(await ethers.provider.getNetwork()).chainId, await statusList.getAddress(), 0, ethers.ZeroHash, ethers.ZeroHash, 0]
      );
      await expect(statusList.verifyNonRevocation(0, pv, "0x")).to.be.revertedWithCustomError(
        statusList,
        "NonRevocationNotConfigured"
      );
    });
  });
});
