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

/**
 * Real drand quicknet beacon verification test.
 *
 * Vectors are GENUINE published values from the League of Entropy quicknet chain
 * (chain hash 52db9ba7...84e971), fetched from https://api.drand.sh:
 *
 *   /info                -> public_key, period 3, genesis 1692803367,
 *                           schemeID "bls-unchained-g1-rfc9380"
 *   /public/1000000      -> randomness + signature below
 *   /public/1000001      -> randomness + signature below
 *
 * The EIP-2537 pairing assertions require the BLS12-381 precompiles. They are
 * live on Aere mainnet (Prague parity) and are available in the local EDR EVM
 * ONLY at the "prague" hardfork. This test detects availability at runtime:
 *   - with EIP-2537: it runs the full on-chain pairing verification (real pass/fail)
 *   - without it: it asserts the contract FAILS CLOSED (never returns true), and
 *     the pairing case is reported as [MEASURE: run under a prague/EIP-2537 EVM].
 *
 * Run the full on-chain verification with:
 *   npx hardhat test test/AereRandomnessBeaconV2.test.js --config hardhat.config.prague.js
 */

// ── genuine drand quicknet vectors ──
const SIG_1000000 =
  "0x83ad29e4c409f9470fc2ef02f90214df49e02b441a1a241a82d622d9f608ef98fd8b11a029f1bee9d9e83b45088abe72";
const RAND_1000000 =
  "0xb22aad4794f7451896f7a371aa46106fd84d919f3f569acd5b2fddf1d1440af3";
const SIG_1000001 =
  "0xa5bd91e5e2d8c0bf51bffdfad87eef34348fd9c0b2df2bee39db90bdef7e1399b1a77bb2fe98b24d84c0936a306c4218";
const RAND_1000001 =
  "0x9f45f439afd81e9846b3b4dc5e3e6051922c73c8459d18e9d507b52ddbd884ff";

// ── reference constants derived independently with @noble/curves ──
const MSG_1000000 =
  "0xce59b701970051bef0d7efdc1a4196c49ce1bbaaf9c5403626ad7adcc41737e7";
const U0_1000000 =
  "0x0000000000000000000000000000000016468f66eb172d70cfafac794eddcd7a34c20a0f1509693fbd5e08dc01e5a7676e010f194278e2dc5bb159fdacd30c9d";
const U1_1000000 =
  "0x000000000000000000000000000000000923cb3c137f776f1a4d1434af4d0d92e90e02940ba28612e87569548e64ab786ef8cc8b970c3cd1c96a19d070eff3b6";
const SIG_1000000_UNCOMPRESSED =
  "0x0000000000000000000000000000000003ad29e4c409f9470fc2ef02f90214df49e02b441a1a241a82d622d9f608ef98fd8b11a029f1bee9d9e83b45088abe720000000000000000000000000000000001776ff7408b39c5f6f9fa50746efd7eea17fbb61f2e7b9c849ff0528e5a3deeedd029d0df345199963d75ba93b5a02a";
const HM_1000000_G1 =
  "0x000000000000000000000000000000000533fbfd488393ea4e2fe335e2b768d112934e2477f335d4360c1ed8b96907740b49a16947fe30f7fc76433c70d0940b0000000000000000000000000000000005cf7470bbc5e814f87f58627718949c2ba851c3f1e29220da68829d3502953f3b557c97d72fc76caf341ea244730002";

const lc = (s) => s.toLowerCase();

describe("AereRandomnessBeaconV2 (drand quicknet, real BLS12-381 / EIP-2537)", function () {
  let beacon;
  let has2537 = false;

  before(async function () {
    const F = await ethers.getContractFactory("AereRandomnessBeaconV2");
    beacon = await F.deploy();
    await beacon.waitForDeployment();
    // Detect EIP-2537 by attempting a real hash-to-curve (needs MAP_FP_TO_G1).
    const [, ok] = await beacon.hashToG1ForRound(1000000);
    has2537 = ok;
    console.log(
      `      EIP-2537 precompiles available in this EVM: ${has2537 ? "YES (real pairing verification)" : "NO (fail-closed path; pairing = [MEASURE])"}`
    );
  });

  // ─────────── construction logic (SHA-256 + MODEXP only; runs on any EVM) ───────────

  describe("message + hash-to-field construction (no EIP-2537 needed)", function () {
    it("reconstructs the unchained quicknet message = SHA-256(round_be8)", async function () {
      expect(lc(await beacon.messageForRound(1000000))).to.equal(lc(MSG_1000000));
    });

    it("matches the RFC 9380 hash-to-field vectors (expand_message_xmd SHA-256, DST NUL_)", async function () {
      const [u0, u1] = await beacon.hashToFieldForRound(1000000);
      expect(lc(u0)).to.equal(lc(U0_1000000));
      expect(lc(u1)).to.equal(lc(U1_1000000));
    });
  });

  describe("G1 signature decompression (MODEXP only; runs on any EVM)", function () {
    it("decompresses the genuine round-1000000 signature to the reference point", async function () {
      const [pt, ok] = await beacon.decompressSignature(SIG_1000000);
      expect(ok).to.equal(true);
      expect(lc(pt)).to.equal(lc(SIG_1000000_UNCOMPRESSED));
    });

    it("fails closed on a wrong-length signature", async function () {
      const [pt, ok] = await beacon.decompressSignature("0x1234");
      expect(ok).to.equal(false);
      expect(pt).to.equal("0x");
    });

    it("fails closed when x^3+4 is not a square (x not on the curve)", async function () {
      // x = 0x000000030...0001 is a verified quadratic non-residue for x^3+4
      // (found via the Legendre symbol with @noble/curves). Compression flag set
      // on the leading byte (0x80); after masking, x is the non-residue above.
      const bad =
        "0x800000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000001";
      const [pt, ok] = await beacon.decompressSignature(bad);
      expect(ok).to.equal(false);
      expect(pt).to.equal("0x");
    });

    it("fails closed on the point at infinity (infinity flag set)", async function () {
      // 0xc0 = compression flag (0x80) | infinity flag (0x40)
      const inf = "0xc0" + "00".repeat(47);
      const [pt, ok] = await beacon.decompressSignature(inf);
      expect(ok).to.equal(false);
      expect(pt).to.equal("0x");
    });
  });

  describe("canonical drand randomness", function () {
    it("derives randomness = SHA-256(signature), matching api.drand.sh", async function () {
      expect(lc(await beacon.canonicalRandomness(SIG_1000000))).to.equal(lc(RAND_1000000));
      expect(lc(await beacon.canonicalRandomness(SIG_1000001))).to.equal(lc(RAND_1000001));
    });
  });

  describe("submitRound input validation (fail-closed regardless of EIP-2537)", function () {
    it("rejects round 0", async function () {
      await expect(beacon.submitRound(0, SIG_1000000)).to.be.revertedWith("AereDrand: bad round");
    });
    it("rejects a signature that is not 48 bytes", async function () {
      await expect(beacon.submitRound(1000000, "0x1234")).to.be.revertedWith(
        "AereDrand: sig must be 48 bytes (G1 compressed)"
      );
    });
  });

  // ─────────── real on-chain BLS verification (EIP-2537) ───────────

  describe("on-chain BLS12-381 pairing verification (EIP-2537)", function () {
    it("hash-to-curve to G1 matches the independent @noble/curves reference [needs EIP-2537]", async function () {
      if (!has2537) {
        console.log("        [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js");
        this.skip();
      }
      const [pt, ok] = await beacon.hashToG1ForRound(1000000);
      expect(ok).to.equal(true);
      expect(lc(pt)).to.equal(lc(HM_1000000_G1));
    });

    it("VERIFIES a genuine round (1000000) as true [needs EIP-2537]", async function () {
      if (!has2537) {
        console.log("        [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js");
        this.skip();
      }
      expect(await beacon.verifyRound(1000000, SIG_1000000)).to.equal(true);
      expect(await beacon.verifyRound(1000001, SIG_1000001)).to.equal(true);
    });

    it("REJECTS a valid signature submitted for the wrong round [needs EIP-2537]", async function () {
      if (!has2537) {
        console.log("        [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js");
        this.skip();
      }
      // genuine sig for 1000000, claimed as 1000001, and vice-versa
      expect(await beacon.verifyRound(1000001, SIG_1000000)).to.equal(false);
      expect(await beacon.verifyRound(1000000, SIG_1000001)).to.equal(false);
    });

    it("REJECTS a tampered signature [needs EIP-2537]", async function () {
      if (!has2537) {
        console.log("        [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js");
        this.skip();
      }
      // flip one byte of the genuine signature
      const bytes = ethers.getBytes(SIG_1000000);
      bytes[20] ^= 0x01;
      const tampered = ethers.hexlify(bytes);
      expect(await beacon.verifyRound(1000000, tampered)).to.equal(false);
    });
  });

  describe("submitRound end-to-end", function () {
    it("stores canonical randomness for a genuine round, and rejects double-submit [needs EIP-2537]", async function () {
      if (!has2537) {
        console.log("        [MEASURE] skipped: EIP-2537 absent. Run with --config hardhat.config.prague.js");
        this.skip();
      }
      const F = await ethers.getContractFactory("AereRandomnessBeaconV2");
      const b = await F.deploy();
      await b.waitForDeployment();

      await expect(b.submitRound(1000000, SIG_1000000))
        .to.emit(b, "RoundSubmitted");
      expect(lc(await b.getRandomness(1000000))).to.equal(lc(RAND_1000000));
      expect(await b.isAvailable(1000000)).to.equal(true);

      await expect(b.submitRound(1000000, SIG_1000000)).to.be.revertedWith(
        "AereDrand: already submitted"
      );
    });

    it("reverts (fail-closed) when submitting a signature that does not verify", async function () {
      const F = await ethers.getContractFactory("AereRandomnessBeaconV2");
      const b = await F.deploy();
      await b.waitForDeployment();

      // On a NON-EIP-2537 EVM this reverts because verification fails closed
      // (precompiles absent). On an EIP-2537 EVM it reverts because the sig for
      // round 1000001 does not verify against round 1000000's message. Either
      // way: no storage, never accepted as true.
      await expect(b.submitRound(1000000, SIG_1000001)).to.be.revertedWith(
        "AereDrand: BLS verification failed"
      );
      expect(await b.isAvailable(1000000)).to.equal(false);
    });

    it("fail-closed check: without EIP-2537, a GENUINE sig is also rejected (never stub-true)", async function () {
      if (has2537) {
        console.log("        (EIP-2537 present: this fail-closed-absence check is not applicable)");
        this.skip();
      }
      const F = await ethers.getContractFactory("AereRandomnessBeaconV2");
      const b = await F.deploy();
      await b.waitForDeployment();
      // The old stub returned true here. The real verifier must return false
      // when it cannot actually check the pairing.
      expect(await b.verifyRound(1000000, SIG_1000000)).to.equal(false);
      await expect(b.submitRound(1000000, SIG_1000000)).to.be.revertedWith(
        "AereDrand: BLS verification failed"
      );
    });
  });
});
