const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const kat = require("./falcon512_kat0.json");

// ---------------------------------------------------------------------------
// AerePQCMessageVerifier: on-chain PQC message verification for cross-chain
// bridging / oracle attestations, on the LIVE mainnet precompiles.
//
// Two precompile backends are installed at the precompile addresses via
// hardhat_setCode (the Hardhat EVM has no PQC precompile):
//
//   * Falcon512PrecompileShim at 0x0AE1 - delegates to AereFalcon512Verifier, the
//     KAT-proven pure-Solidity Falcon-512 verifier. Used for the GENUINE accept +
//     tamper-reject over the OFFICIAL NIST Falcon-512 KAT vector, so a positive here
//     is a real Falcon-512 signature verified end to end through the contract's real
//     precompile-calling path (input = pk || sm, raw 32-byte return).
//
//   * MockPQCPrecompile at 0x0AE1 / 0x0AE3 - a faithful mock (parses the precompiles'
//     SPEC offsets, accepts iff commitment == keccak256(pk || message)). Used for the
//     signer-bound / domain-separation / rotation / consumer tests, where genuine
//     signatures must be minted for arbitrary domain-separated digests that no offline
//     signer is available to sign. The real wire encoding is proven by the KAT path
//     above and (separately) against the live mainnet precompile via eth_call.
// ---------------------------------------------------------------------------

const AE1 = ethers.getAddress("0x0000000000000000000000000000000000000ae1"); // Falcon-512
const AE3 = ethers.getAddress("0x0000000000000000000000000000000000000ae3"); // ML-DSA-44

const SCHEME_FALCON512 = 1;
const SCHEME_MLDSA44 = 3;

const MESSAGE_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes("AerePQCMessageVerifier.v1.message"));
const ACTION_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes("AerePQCGatedAction.v1.action"));

// Deterministic seeded RNG (keccak hash-chain) so the tests are 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 falconPubKey(rng) {
  const raw = rng.bytes(897);
  raw[0] = 0x09; // Falcon-512 header
  return ethers.hexlify(raw);
}

function mldsaPubKey(rng) {
  return ethers.hexlify(rng.bytes(1312));
}

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

// Genuine Falcon-512 envelope for the mock: nonce(40) || esig ; esig = 0x29 || commitment(32).
function falconMockEnvelope(pubKey, message, rng) {
  const commitment = commitmentFor(pubKey, message);
  const nonce = rng.bytes(40);
  const esig = ethers.concat([new Uint8Array([0x29]), commitment]);
  return ethers.hexlify(ethers.concat([nonce, esig]));
}

// Genuine ML-DSA-44 envelope for the mock: 2420-byte sig with commitment in the first 32 bytes.
function mldsaMockEnvelope(pubKey, message) {
  const sig = new Uint8Array(2420);
  sig.set(ethers.getBytes(commitmentFor(pubKey, message)), 0);
  return ethers.hexlify(sig);
}

// Reproduce signerMessageDigest for an ARBITRARY chainId (to test cross-chain replay).
function foreignSignerDigest(chainId, mvAddr, signerId, epoch, domainTag, payload) {
  const enc = ethers.AbiCoder.defaultAbiCoder().encode(
    ["bytes32", "uint256", "address", "uint256", "uint32", "bytes32", "bytes"],
    [MESSAGE_DOMAIN, chainId, mvAddr, signerId, epoch, domainTag, payload]
  );
  return ethers.keccak256(enc);
}

async function setCodeFrom(factoryName, addr, ...ctorArgs) {
  const F = await ethers.getContractFactory(factoryName);
  const c = await F.deploy(...ctorArgs);
  await c.waitForDeployment();
  const code = await ethers.provider.getCode(await c.getAddress());
  await network.provider.send("hardhat_setCode", [addr, code]);
}

async function deployVerifier() {
  const F = await ethers.getContractFactory("AerePQCMessageVerifier");
  const mv = await F.deploy();
  await mv.waitForDeployment();
  return mv;
}

async function registerSigner(mv, scheme, pubKey) {
  const tx = await mv.registerSigner(scheme, pubKey);
  const rc = await tx.wait();
  const log = rc.logs.map((l) => mv.interface.parseLog(l)).find((p) => p && p.name === "SignerRegistered");
  return Number(log.args.signerId);
}

describe("AerePQCMessageVerifier", function () {
  // =========================================================================
  // 1. Signer registry: register / rotate / revoke / transfer
  // =========================================================================
  describe("signer registry", function () {
    let mv;
    beforeEach(async function () {
      mv = await deployVerifier();
    });

    it("registers well-formed keys with sequential ids, epoch 0, ACTIVE", async function () {
      const rng = makeRng("reg");
      const [a] = await ethers.getSigners();

      const id0 = await registerSigner(mv, SCHEME_FALCON512, falconPubKey(rng));
      const id1 = await registerSigner(mv, SCHEME_MLDSA44, mldsaPubKey(rng));
      expect(id0).to.equal(0);
      expect(id1).to.equal(1);
      expect(await mv.signerCount()).to.equal(2n);

      const s0 = await mv.getSigner(0);
      expect(s0.owner).to.equal(a.address);
      expect(Number(s0.scheme)).to.equal(SCHEME_FALCON512);
      expect(Number(s0.status)).to.equal(1); // ACTIVE
      expect(Number(s0.epoch)).to.equal(0);
      expect(await mv.isActiveSigner(0)).to.equal(true);
      expect(Number(await mv.epochOf(1))).to.equal(0);
    });

    it("rejects an unknown scheme and malformed keys", async function () {
      await expect(mv.registerSigner(5, "0x" + "00".repeat(897))).to.be.revertedWithCustomError(mv, "InvalidScheme");
      // Falcon-512 wrong length
      await expect(mv.registerSigner(1, "0x09" + "00".repeat(895))).to.be.revertedWithCustomError(mv, "InvalidPubKey");
      // Falcon-512 correct length, wrong header
      await expect(mv.registerSigner(1, "0x0a" + "00".repeat(896))).to.be.revertedWithCustomError(mv, "InvalidPubKey");
      // ML-DSA-44 wrong length
      await expect(mv.registerSigner(3, "0x" + "00".repeat(1311))).to.be.revertedWithCustomError(mv, "InvalidPubKey");
    });

    it("rotates in place: owner-only, epoch advances, key/scheme update", async function () {
      const rng = makeRng("rot");
      const [owner, other] = await ethers.getSigners();
      const id = await registerSigner(mv, SCHEME_FALCON512, falconPubKey(rng));

      // A non-owner cannot rotate.
      await expect(
        mv.connect(other).rotateSigner(id, SCHEME_FALCON512, falconPubKey(rng))
      ).to.be.revertedWithCustomError(mv, "NotSignerOwner");

      const newKey = mldsaPubKey(rng); // rotate to a different scheme too
      await expect(mv.connect(owner).rotateSigner(id, SCHEME_MLDSA44, newKey)).to.emit(mv, "SignerRotated");
      const s = await mv.getSigner(id);
      expect(Number(s.epoch)).to.equal(1);
      expect(Number(s.scheme)).to.equal(SCHEME_MLDSA44);
      expect(s.pubKey).to.equal(newKey);
    });

    it("revokes (owner-only, terminal) and blocks rotation afterwards", async function () {
      const rng = makeRng("rev");
      const [owner, other] = await ethers.getSigners();
      const id = await registerSigner(mv, SCHEME_FALCON512, falconPubKey(rng));

      await expect(mv.connect(other).revokeSigner(id)).to.be.revertedWithCustomError(mv, "NotSignerOwner");
      await mv.connect(owner).revokeSigner(id);
      expect(await mv.isActiveSigner(id)).to.equal(false);
      expect(Number(await mv.statusOf(id))).to.equal(2); // REVOKED

      await expect(mv.connect(owner).rotateSigner(id, SCHEME_FALCON512, falconPubKey(rng)))
        .to.be.revertedWithCustomError(mv, "SignerNotActive");
    });

    it("transfers signer ownership", async function () {
      const rng = makeRng("xfer");
      const [owner, next] = await ethers.getSigners();
      const id = await registerSigner(mv, SCHEME_FALCON512, falconPubKey(rng));

      await expect(mv.connect(owner).transferSignerOwner(id, ethers.ZeroAddress))
        .to.be.revertedWithCustomError(mv, "ZeroAddress");
      await mv.connect(owner).transferSignerOwner(id, next.address);
      expect((await mv.getSigner(id)).owner).to.equal(next.address);
      // Old owner can no longer rotate; new owner can.
      await expect(mv.connect(owner).rotateSigner(id, SCHEME_FALCON512, falconPubKey(rng)))
        .to.be.revertedWithCustomError(mv, "NotSignerOwner");
      await expect(mv.connect(next).rotateSigner(id, SCHEME_FALCON512, falconPubKey(rng))).to.emit(mv, "SignerRotated");
    });

    it("reverts on unknown signerId views", async function () {
      await expect(mv.getSigner(0)).to.be.revertedWithCustomError(mv, "UnknownSigner");
      expect(Number(await mv.statusOf(0))).to.equal(0); // STATUS_NONE, does not revert
      expect(await mv.isActiveSigner(0)).to.equal(false);
    });
  });

  // =========================================================================
  // 2. GENUINE Falcon-512 accept via the KAT-proven verifier path (real crypto)
  // =========================================================================
  describe("genuine Falcon-512 accept (KAT-proven verifier path)", function () {
    let mv;

    before(async function () {
      // Deploy the real KAT-proven verifier, then install a shim over it at 0x0AE1.
      const V = await ethers.getContractFactory("AereFalcon512Verifier");
      const verifier = await V.deploy();
      await verifier.waitForDeployment();
      await setCodeFrom("Falcon512PrecompileShim", AE1, await verifier.getAddress());
    });

    beforeEach(async function () {
      mv = await deployVerifier();
    });

    // Envelope handed to verifyMessage: nonce(40) || esig ; esig = 0x29 || compsig.
    function katEnvelope() {
      return ethers.hexlify(ethers.concat([kat.nonce, new Uint8Array([0x29]), kat.compsig]));
    }

    it("verifyMessage ACCEPTS the official NIST Falcon-512 KAT signature", async function () {
      const ok = await mv.verifyMessage(SCHEME_FALCON512, kat.pk, kat.msg, katEnvelope());
      expect(ok).to.equal(true);
    });

    it("REJECTS a tampered signature (flip one byte of the compressed sig)", async function () {
      const env = ethers.getBytes(katEnvelope());
      env[100] ^= 0x01; // deep inside the compressed signature
      const ok = await mv.verifyMessage(SCHEME_FALCON512, kat.pk, kat.msg, ethers.hexlify(env));
      expect(ok).to.equal(false);
    });

    it("REJECTS a tampered message (flip one byte of the message)", async function () {
      const msg = ethers.getBytes(kat.msg);
      msg[0] ^= 0x01;
      const ok = await mv.verifyMessage(SCHEME_FALCON512, kat.pk, ethers.hexlify(msg), katEnvelope());
      expect(ok).to.equal(false);
    });

    it("REJECTS the genuine signature under the WRONG public key", async function () {
      const ok = await mv.verifyMessage(SCHEME_FALCON512, kat.pk1, kat.msg, katEnvelope());
      expect(ok).to.equal(false);
    });
  });

  // =========================================================================
  // 3. Domain separation, signer-bound verify, rotation, consumer gate (mock)
  // =========================================================================
  describe("signer-bound verification, domain separation, rotation, consumer", function () {
    let mv, falconMockCode, mldsaMockCode;

    before(async function () {
      const M = await ethers.getContractFactory("MockPQCPrecompile");
      const f = await M.deploy(1);
      await f.waitForDeployment();
      falconMockCode = await ethers.provider.getCode(await f.getAddress());
      const d = await M.deploy(3);
      await d.waitForDeployment();
      mldsaMockCode = await ethers.provider.getCode(await d.getAddress());
    });

    beforeEach(async function () {
      await network.provider.send("hardhat_setCode", [AE1, falconMockCode]);
      await network.provider.send("hardhat_setCode", [AE3, mldsaMockCode]);
      mv = await deployVerifier();
    });

    it("verifyBySigner ACCEPTS a genuine signer signature (Falcon-512 and ML-DSA-44)", async function () {
      const rng = makeRng("bysigner");
      const domain = ethers.keccak256(ethers.toUtf8Bytes("bridge-route-7"));
      const payload = ethers.toUtf8Bytes("cross-chain-packet-A");

      // Falcon-512 signer
      const fpk = falconPubKey(rng);
      const fid = await registerSigner(mv, SCHEME_FALCON512, fpk);
      const fdigest = await mv.signerMessageDigest(fid, domain, payload);
      const fsig = falconMockEnvelope(fpk, fdigest, rng);
      expect(await mv.verifyBySigner(fid, domain, payload, fsig)).to.equal(true);

      // ML-DSA-44 signer
      const dpk = mldsaPubKey(rng);
      const did = await registerSigner(mv, SCHEME_MLDSA44, dpk);
      const ddigest = await mv.signerMessageDigest(did, domain, payload);
      const dsig = mldsaMockEnvelope(dpk, ddigest);
      expect(await mv.verifyBySigner(did, domain, payload, dsig)).to.equal(true);
    });

    it("verifyBySigner returns false for unknown / revoked signer", async function () {
      const rng = makeRng("revoked");
      const domain = ethers.keccak256(ethers.toUtf8Bytes("d"));
      const payload = ethers.toUtf8Bytes("p");
      expect(await mv.verifyBySigner(99, domain, payload, "0x" + "00".repeat(73))).to.equal(false);

      const pk = falconPubKey(rng);
      const id = await registerSigner(mv, SCHEME_FALCON512, pk);
      const digest = await mv.signerMessageDigest(id, domain, payload);
      const sig = falconMockEnvelope(pk, digest, rng);
      expect(await mv.verifyBySigner(id, domain, payload, sig)).to.equal(true);
      await mv.revokeSigner(id);
      expect(await mv.verifyBySigner(id, domain, payload, sig)).to.equal(false);
    });

    it("DOMAIN SEPARATION: a signature for one domain tag is invalid under another", async function () {
      const rng = makeRng("domainsep");
      const domainA = ethers.keccak256(ethers.toUtf8Bytes("oracle-feed-ETHUSD"));
      const domainB = ethers.keccak256(ethers.toUtf8Bytes("oracle-feed-BTCUSD"));
      const payload = ethers.toUtf8Bytes("price=123456");

      const pk = falconPubKey(rng);
      const id = await registerSigner(mv, SCHEME_FALCON512, pk);
      const digestA = await mv.signerMessageDigest(id, domainA, payload);
      const sigA = falconMockEnvelope(pk, digestA, rng);

      expect(await mv.verifyBySigner(id, domainA, payload, sigA)).to.equal(true);
      // Same key, same payload, DIFFERENT domain -> different digest -> rejected.
      expect(await mv.verifyBySigner(id, domainB, payload, sigA)).to.equal(false);
    });

    it("CROSS-CHAIN SEPARATION: a signature bound to a foreign chainId is invalid here", async function () {
      const rng = makeRng("crosschain");
      const domain = ethers.keccak256(ethers.toUtf8Bytes("bridge"));
      const payload = ethers.toUtf8Bytes("withdraw-1000");
      const pk = falconPubKey(rng);
      const id = await registerSigner(mv, SCHEME_FALCON512, pk);
      const mvAddr = await mv.getAddress();

      const localChainId = (await ethers.provider.getNetwork()).chainId; // 31337
      const foreignChainId = 1n; // Ethereum mainnet

      // A signature genuine for the SAME (domain, payload) but bound to chainId=1.
      const foreignDigest = foreignSignerDigest(foreignChainId, mvAddr, id, 0, domain, payload);
      const foreignSig = falconMockEnvelope(pk, foreignDigest, rng);
      // On THIS chain (31337) the digest differs -> the foreign-bound signature is rejected.
      expect(await mv.verifyBySigner(id, domain, payload, foreignSig)).to.equal(false);

      // Sanity: the same envelope built for the local chainId verifies.
      const localDigest = foreignSignerDigest(localChainId, mvAddr, id, 0, domain, payload);
      expect(localDigest).to.equal(await mv.signerMessageDigest(id, domain, payload));
      const localSig = falconMockEnvelope(pk, localDigest, rng);
      expect(await mv.verifyBySigner(id, domain, payload, localSig)).to.equal(true);
    });

    it("KEY ROTATION: a pre-rotation signature is rejected; a post-rotation one is accepted", async function () {
      const rng = makeRng("rotate-verify");
      const domain = ethers.keccak256(ethers.toUtf8Bytes("route"));
      const payload = ethers.toUtf8Bytes("msg");

      const pk0 = falconPubKey(rng);
      const id = await registerSigner(mv, SCHEME_FALCON512, pk0);

      // Genuine under epoch 0 / key0.
      const digest0 = await mv.signerMessageDigest(id, domain, payload);
      const sig0 = falconMockEnvelope(pk0, digest0, rng);
      expect(await mv.verifyBySigner(id, domain, payload, sig0)).to.equal(true);

      // Rotate to key1 (epoch 1). The old signature must no longer verify: the digest now binds
      // epoch 1 AND the current key is key1, so both the message and the key have changed.
      const pk1 = falconPubKey(rng);
      await mv.rotateSigner(id, SCHEME_FALCON512, pk1);
      expect(Number(await mv.epochOf(id))).to.equal(1);
      expect(await mv.verifyBySigner(id, domain, payload, sig0)).to.equal(false);

      // A fresh signature under key1 / epoch 1 verifies.
      const digest1 = await mv.signerMessageDigest(id, domain, payload);
      expect(digest1).to.not.equal(digest0);
      const sig1 = falconMockEnvelope(pk1, digest1, rng);
      expect(await mv.verifyBySigner(id, domain, payload, sig1)).to.equal(true);
    });

    it("CONSUMER GATE: executes once on a valid PQC signature, rejects tampered and replays", async function () {
      const rng = makeRng("consumer");
      const G = await ethers.getContractFactory("AerePQCGatedAction");
      const gate = await G.deploy(await mv.getAddress());
      await gate.waitForDeployment();

      const pk = falconPubKey(rng);
      const id = await registerSigner(mv, SCHEME_FALCON512, pk);
      const payload = ethers.toUtf8Bytes("bridge-deliver:0xdeadbeef");

      // Sign the consumer's ACTION_DOMAIN + payload digest.
      const digest = await mv.signerMessageDigest(id, ACTION_DOMAIN, payload);
      const sig = falconMockEnvelope(pk, digest, rng);

      // Tampered signature -> gate reverts, nothing recorded.
      const bad = ethers.getBytes(sig);
      bad[41] ^= 0x01; // flip inside the commitment
      await expect(gate.executeIfAttested(id, payload, ethers.hexlify(bad)))
        .to.be.revertedWithCustomError(gate, "NotAttested");
      expect(await gate.actionCount()).to.equal(0n);

      // Genuine signature -> action executes exactly once.
      await expect(gate.executeIfAttested(id, payload, sig)).to.emit(gate, "ActionExecuted");
      expect(await gate.actionCount()).to.equal(1n);
      const actionId = await gate.actionId(id, payload);
      expect(await gate.consumed(actionId)).to.equal(true);

      // Replay of the same (signer, payload) -> rejected.
      await expect(gate.executeIfAttested(id, payload, sig))
        .to.be.revertedWithCustomError(gate, "AlreadyConsumed");
      expect(await gate.actionCount()).to.equal(1n);
    });
  });

  // =========================================================================
  // 4. Fail-closed: empty/stale precompile return is INVALID
  // =========================================================================
  describe("fail-closed", function () {
    it("treats an empty precompile return (pre-fork / stale) as invalid", async function () {
      const rng = makeRng("failclosed");
      // Install the faithful Falcon mock, register + build a genuine sig, then wipe the precompile.
      const M = await ethers.getContractFactory("MockPQCPrecompile");
      const f = await M.deploy(1);
      await f.waitForDeployment();
      await network.provider.send("hardhat_setCode", [AE1, await ethers.provider.getCode(await f.getAddress())]);

      const mv = await deployVerifier();
      const domain = ethers.keccak256(ethers.toUtf8Bytes("d"));
      const payload = ethers.toUtf8Bytes("p");
      const pk = falconPubKey(rng);
      const id = await registerSigner(mv, SCHEME_FALCON512, pk);
      const digest = await mv.signerMessageDigest(id, domain, payload);
      const sig = falconMockEnvelope(pk, digest, rng);
      expect(await mv.verifyBySigner(id, domain, payload, sig)).to.equal(true);

      // An address with no precompile returns success + empty returndata -> must be INVALID.
      await network.provider.send("hardhat_setCode", [AE1, "0x"]);
      expect(await mv.verifyBySigner(id, domain, payload, sig)).to.equal(false);
      expect(await mv.verifyMessage(SCHEME_FALCON512, pk, digest, sig)).to.equal(false);
    });
  });
});
