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

// ---------------------------------------------------------------------------
// AerePQCAttestation adversarial test + seeded invariant fuzz.
//
// 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). A "genuine signature" is one
// whose commitment equals keccak256(pk || challenge); a forgery/replay/wrong-key/
// wrong-message signature has a mismatching commitment and MUST be rejected.
//
// The REAL precompile encoding is separately proven against the LIVE mainnet
// precompile via eth_call (scripts/pqc/verify-live-precompile.js).
// ---------------------------------------------------------------------------

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: { pkLen: 897, pkHeader: 0x09, esigHeader: 0x29, falcon: true },
  2: { pkLen: 1793, pkHeader: 0x0a, esigHeader: 0x2a, falcon: true },
  3: { pkLen: 1312, sigLen: 2420, falcon: false },
  4: { pkLen: 32, sigLen: 7856, falcon: false },
};

// 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; // Falcon requires the header byte
  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 attester 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` under `pubKey`.
function genuine(scheme, pubKey, message, rng) {
  return envelope(scheme, commitmentFor(pubKey, message), rng);
}

// Flip one byte inside the commitment region (Falcon: esig[1] at offset 41;
// fixed schemes: sig[0] at offset 0). This is the region the precompile binds
// the (key,message) pair to, so a flip here is a genuine forgery.
function tamperCommitment(scheme, sigHex) {
  const b = ethers.getBytes(sigHex);
  const idx = META[scheme].falcon ? 41 : 0;
  b[idx] ^= 0x01;
  return ethers.hexlify(b);
}

describe("AerePQCAttestation (adversarial + seeded fuzz)", function () {
  let att, mockCode;

  before(async function () {
    // 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 A = await ethers.getContractFactory("AerePQCAttestation");
    att = await A.deploy();
    await att.waitForDeployment();
  });

  // Helper: register a key, return its keyId (from the event).
  async function register(scheme, pubKey) {
    const tx = await att.registerKey(scheme, pubKey);
    const rc = await tx.wait();
    const log = rc.logs.map((l) => att.interface.parseLog(l)).find((p) => p && p.name === "KeyRegistered");
    return Number(log.args.keyId);
  }

  // -----------------------------------------------------------------------
  // Registration validation
  // -----------------------------------------------------------------------
  describe("registerKey", function () {
    it("accepts a well-formed key for every scheme and assigns sequential ids", async function () {
      const rng = makeRng("reg");
      let expectedId = 0;
      for (const scheme of [1, 2, 3, 4]) {
        const pk = makePubKey(scheme, rng);
        const id = await register(scheme, pk);
        expect(id).to.equal(expectedId++);
        const k = await att.getKey(id);
        expect(Number(k.scheme)).to.equal(scheme);
        expect(k.pubKey).to.equal(pk);
        expect(Number(k.nonce)).to.equal(0);
      }
      expect(await att.keyCount()).to.equal(4n);
    });

    it("rejects an unknown scheme", async function () {
      await expect(att.registerKey(5, "0x" + "00".repeat(897))).to.be.revertedWithCustomError(att, "InvalidScheme");
      await expect(att.registerKey(0, "0x" + "00".repeat(897))).to.be.revertedWithCustomError(att, "InvalidScheme");
    });

    it("rejects a wrong-length or wrong-header public key", async function () {
      // Falcon-1024 wrong length
      await expect(att.registerKey(2, "0x" + "0a" + "00".repeat(1791))).to.be.revertedWithCustomError(att, "InvalidPubKey");
      // Falcon-1024 correct length but wrong header (0x09 instead of 0x0A)
      await expect(att.registerKey(2, "0x" + "09" + "00".repeat(1792))).to.be.revertedWithCustomError(att, "InvalidPubKey");
      // Falcon-512 correct length but wrong header
      await expect(att.registerKey(1, "0x" + "0a" + "00".repeat(896))).to.be.revertedWithCustomError(att, "InvalidPubKey");
      // ML-DSA-44 wrong length
      await expect(att.registerKey(3, "0x" + "00".repeat(1311))).to.be.revertedWithCustomError(att, "InvalidPubKey");
      // SLH-DSA wrong length
      await expect(att.registerKey(4, "0x" + "00".repeat(31))).to.be.revertedWithCustomError(att, "InvalidPubKey");
    });
  });

  // -----------------------------------------------------------------------
  // Happy path per scheme
  // -----------------------------------------------------------------------
  describe("attest happy path", function () {
    for (const scheme of [1, 2, 3, 4]) {
      it(`records a genuine attestation for scheme ${scheme}`, async function () {
        const rng = makeRng("happy-" + scheme);
        const pk = makePubKey(scheme, rng);
        const keyId = await register(scheme, pk);
        const messageHash = ethers.keccak256(ethers.toUtf8Bytes("payload-" + scheme));

        const challenge = await att.attestChallenge(keyId, 0, messageHash);
        const sig = genuine(scheme, pk, challenge, rng);

        const before = await att.attestationCount();
        await expect(att.attest(keyId, messageHash, sig)).to.emit(att, "Attested");
        expect(await att.attestationCount()).to.equal(before + 1n);
        expect(Number(await att.nonceOf(keyId))).to.equal(1);

        const a = await att.getAttestation(keyId, 0);
        expect(a.exists).to.equal(true);
        expect(a.messageHash).to.equal(messageHash);
        expect(a.challenge).to.equal(challenge);
        expect(await att.isValidAttestation(keyId, 0, messageHash)).to.equal(true);
        expect(await att.isValidAttestation(keyId, 0, ethers.ZeroHash)).to.equal(false);
      });
    }

    it("verifySignature view returns true for genuine, false for tampered (raw messageHash binding)", async function () {
      const rng = makeRng("view");
      const scheme = 2;
      const pk = makePubKey(scheme, rng);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("direct-verify"));
      const good = genuine(scheme, pk, messageHash, rng);
      expect(await att.verifySignature(scheme, pk, messageHash, good)).to.equal(true);

      const bad = ethers.getBytes(good);
      bad[bad.length - 1] ^= 0xff;
      expect(await att.verifySignature(scheme, pk, messageHash, ethers.hexlify(bad))).to.equal(false);
    });
  });

  // -----------------------------------------------------------------------
  // Adversarial: forgery, wrong key, wrong message, replay
  // -----------------------------------------------------------------------
  describe("adversarial rejection (nothing recorded)", function () {
    async function setup(scheme) {
      const rng = makeRng("adv-" + scheme);
      const pk = makePubKey(scheme, rng);
      const keyId = await register(scheme, pk);
      const messageHash = ethers.keccak256(ethers.toUtf8Bytes("adv-msg"));
      const challenge = await att.attestChallenge(keyId, 0, messageHash);
      return { rng, pk, keyId, messageHash, challenge };
    }

    async function expectRejected(promise) {
      await expect(promise).to.be.revertedWithCustomError(att, "PQCVerificationFailed");
    }

    for (const scheme of [1, 2, 3, 4]) {
      it(`rejects a tampered signature for scheme ${scheme}`, async function () {
        const { rng, pk, keyId, messageHash, challenge } = await setup(scheme);
        const good = tamperCommitment(scheme, genuine(scheme, pk, challenge, rng));
        await expectRejected(att.attest(keyId, messageHash, good));
        expect(await att.attestationCount()).to.equal(0n);
        expect(Number(await att.nonceOf(keyId))).to.equal(0);
        expect((await att.getAttestation(keyId, 0)).exists).to.equal(false);
      });

      it(`rejects a signature over a DIFFERENT message for scheme ${scheme}`, async function () {
        const { rng, pk, keyId, messageHash } = await setup(scheme);
        const otherMessage = ethers.keccak256(ethers.toUtf8Bytes("other-msg"));
        const otherChallenge = await att.attestChallenge(keyId, 0, otherMessage);
        const sigForOther = genuine(scheme, pk, otherChallenge, rng);
        // signature is genuine for otherMessage, but we attest messageHash -> reject
        await expectRejected(att.attest(keyId, messageHash, sigForOther));
        expect(await att.attestationCount()).to.equal(0n);
      });

      it(`rejects a signature by a DIFFERENT key for scheme ${scheme}`, async function () {
        const { rng, keyId, messageHash, challenge } = await setup(scheme);
        const otherPk = makePubKey(scheme, rng);
        const sigOtherKey = genuine(scheme, otherPk, challenge, rng); // commitment uses other pk
        await expectRejected(att.attest(keyId, messageHash, sigOtherKey));
        expect(await att.attestationCount()).to.equal(0n);
      });
    }

    it("rejects replay of a consumed (message,signature) after the nonce advances", async function () {
      const scheme = 2;
      const { rng, pk, keyId, messageHash } = await setup(scheme);
      const challenge0 = await att.attestChallenge(keyId, 0, messageHash);
      const sig0 = genuine(scheme, pk, challenge0, rng);

      await att.attest(keyId, messageHash, sig0); // consumes nonce 0
      expect(Number(await att.nonceOf(keyId))).to.equal(1);

      // Exact replay of the same (message, signature) now fails: challenge uses nonce 1.
      await expect(att.attest(keyId, messageHash, sig0)).to.be.revertedWithCustomError(att, "PQCVerificationFailed");
      expect(await att.attestationCount()).to.equal(1n);
      expect(Number(await att.nonceOf(keyId))).to.equal(1);
    });

    it("rejects a stale-nonce signature (signed for an old nonce)", async function () {
      const scheme = 2;
      const { rng, pk, keyId, messageHash } = await setup(scheme);
      // advance nonce to 1 with a genuine attestation
      const c0 = await att.attestChallenge(keyId, 0, messageHash);
      await att.attest(keyId, messageHash, genuine(scheme, pk, c0, rng));
      // now craft a signature for the OLD nonce 0 and try again -> reject
      const staleSig = genuine(scheme, pk, c0, rng);
      await expect(att.attest(keyId, messageHash, staleSig)).to.be.revertedWithCustomError(att, "PQCVerificationFailed");
    });

    it("reverts on unknown keyId", async function () {
      await expect(att.attest(99, ethers.ZeroHash, "0x" + "00".repeat(80))).to.be.revertedWithCustomError(att, "UnknownKey");
    });

    it("treats an empty precompile return (pre-fork / stale) as invalid", async function () {
      const scheme = 2;
      const { rng, pk, keyId, messageHash, challenge } = await setup(scheme);
      const sig = genuine(scheme, pk, challenge, rng);
      // Wipe the precompile: an empty account returns success + empty returndata.
      await network.provider.send("hardhat_setCode", [PRECOMPILE[scheme], "0x"]);
      expect(await att.verifySignature(scheme, pk, challenge, sig)).to.equal(false);
      await expect(att.attest(keyId, messageHash, sig)).to.be.revertedWithCustomError(att, "PQCVerificationFailed");
      expect(await att.attestationCount()).to.equal(0n);
    });
  });

  // -----------------------------------------------------------------------
  // Explicit encoding check: the input the contract sends matches the SPEC layout.
  // -----------------------------------------------------------------------
  describe("encoding", function () {
    it("Falcon-1024: mock accepts spec-encoded input and rejects a shifted message", async function () {
      const rng = makeRng("enc");
      const scheme = 2;
      const m = META[scheme];
      const pk = ethers.getBytes(makePubKey(scheme, rng));
      const message = ethers.getBytes(ethers.keccak256(ethers.toUtf8Bytes("enc-msg")));
      const commitment = ethers.getBytes(ethers.keccak256(ethers.concat([pk, 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 input = ethers.hexlify(ethers.concat([pk, sm]));

      // direct call to the installed precompile mock
      let ret = await ethers.provider.call({ to: PRECOMPILE[scheme], data: input });
      expect(BigInt(ret)).to.equal(1n);

      // shift the message by one byte -> commitment no longer matches -> invalid
      const badMsg = Uint8Array.from(message);
      badMsg[0] ^= 0xff;
      const smBad = ethers.concat([new Uint8Array([(sigLen >> 8) & 0xff, sigLen & 0xff]), nonce, badMsg, esig]);
      ret = await ethers.provider.call({ to: PRECOMPILE[scheme], data: ethers.hexlify(ethers.concat([pk, smBad])) });
      expect(BigInt(ret)).to.equal(0n);
    });
  });

  // -----------------------------------------------------------------------
  // Seeded invariant fuzz
  // -----------------------------------------------------------------------
  describe("seeded invariant fuzz", function () {
    it("holds forgery / replay / key-binding invariants over many random rounds", async function () {
      const rng = makeRng("fuzz-seed-v1");
      const ROUNDS = 48;
      let expectedCount = 0n;

      for (let r = 0; r < ROUNDS; r++) {
        const scheme = [1, 2, 3, 4][rng.int(4)];
        const pk = makePubKey(scheme, rng);
        const keyId = await register(scheme, pk);
        const messageHash = ethers.hexlify(rng.bytes(32));

        // --- genuine attestation succeeds; state advances by exactly one ---
        const nonce0 = Number(await att.nonceOf(keyId));
        const challenge0 = await att.attestChallenge(keyId, nonce0, messageHash);
        const good = genuine(scheme, pk, challenge0, rng);
        await att.attest(keyId, messageHash, good);
        expectedCount += 1n;
        expect(await att.attestationCount()).to.equal(expectedCount);
        expect(Number(await att.nonceOf(keyId))).to.equal(nonce0 + 1);
        expect(await att.isValidAttestation(keyId, nonce0, messageHash)).to.equal(true);

        // --- a battery of forgeries at the new nonce: ALL rejected, state frozen ---
        const nonce1 = Number(await att.nonceOf(keyId));
        const challenge1 = await att.attestChallenge(keyId, nonce1, messageHash);
        const forgeries = [];

        // (a) tampered commitment
        forgeries.push(tamperCommitment(scheme, genuine(scheme, pk, challenge1, rng)));
        // (b) genuine-but-for-a-different-message
        {
          const otherMsg = ethers.hexlify(rng.bytes(32));
          const otherCh = await att.attestChallenge(keyId, nonce1, otherMsg);
          forgeries.push(genuine(scheme, pk, otherCh, rng));
        }
        // (c) genuine-but-by-a-different-key
        {
          const otherPk = makePubKey(scheme, rng);
          forgeries.push(genuine(scheme, otherPk, challenge1, rng));
        }
        // (d) replay of the just-consumed signature (challenge was nonce0)
        forgeries.push(good);
        // (e) stale: signature for the previous nonce
        forgeries.push(genuine(scheme, pk, challenge0, rng));

        for (const f of forgeries) {
          await expect(att.attest(keyId, messageHash, f)).to.be.revertedWithCustomError(att, "PQCVerificationFailed");
        }
        // invariants unchanged after the whole battery
        expect(await att.attestationCount()).to.equal(expectedCount);
        expect(Number(await att.nonceOf(keyId))).to.equal(nonce1);
        expect((await att.getAttestation(keyId, nonce1)).exists).to.equal(false);

        // --- a second genuine attestation at the new nonce still works ---
        const good1 = genuine(scheme, pk, challenge1, rng);
        await att.attest(keyId, messageHash, good1);
        expectedCount += 1n;
        expect(await att.attestationCount()).to.equal(expectedCount);
        expect(Number(await att.nonceOf(keyId))).to.equal(nonce1 + 1);
      }
    });
  });
});
