const { expect } = require("chai");
const { ethers } = require("hardhat");
const H = require("./helpers");

// -----------------------------------------------------------------------------
// AereEthLightClient — verified against AERE's LIVE EIP-2537 BLS12-381 precompiles
// under the Prague hardfork (real crypto, NOT a mock). SSZ roots and Merkle branches
// are cross-checked between the Solidity contract and an independent JS implementation.
// -----------------------------------------------------------------------------

// A light-client update verifies ~512 pubkeys + BLS aggregation on precompiles;
// measured ~32M gas (rotation ~2x). AERE's block gas is effectively unlimited; the
// test just needs an explicit limit above hardhat's ~30M auto-send default.
const GAS = { gasLimit: 400_000_000 };

const GVR = "0x" + "11".repeat(32); // synthetic genesis_validators_root
const FORK_VERSION = "0x02000000"; // synthetic fork version (Bellatrix-shaped)
const DOMAIN_SYNC = "0x07000000";

// Generalized indices (consensus-spec): finalized_checkpoint.root=105 (depth6),
// next_sync_committee=55 (depth5).
const GI_FINALIZED = 105;
const GI_NEXT = 55;

function beaconHeaderStruct(h) {
  return {
    slot: h.slot,
    proposerIndex: h.proposerIndex,
    parentRoot: H.hex(h.parentRoot),
    stateRoot: H.hex(h.stateRoot),
    bodyRoot: H.hex(h.bodyRoot),
  };
}

// Build a complete, internally-consistent light-client update.
// opts: { committee, finalizedSlot, attestedSlot, participants, nextCommittee }
function buildUpdate(committee, opts) {
  const finalizedHeader = {
    slot: opts.finalizedSlot,
    proposerIndex: 7,
    parentRoot: Buffer.from("aa".repeat(32), "hex"),
    stateRoot: Buffer.from("bb".repeat(32), "hex"),
    bodyRoot: opts.finalizedBodyRoot || Buffer.from("cc".repeat(32), "hex"),
  };
  const finalizedRoot = H.beaconHeaderRoot(finalizedHeader);

  // Assign leaves in the attested state tree (depth 6 covers gindex 55 and 105).
  const assign = {};
  assign[GI_FINALIZED] = finalizedRoot;
  if (opts.nextCommittee) assign[GI_NEXT] = opts.nextCommittee.root;
  const tree = H.buildTree(6, assign);

  const attestedHeader = {
    slot: opts.attestedSlot,
    proposerIndex: 9,
    parentRoot: Buffer.from("dd".repeat(32), "hex"),
    stateRoot: tree.root, // commits both finality and (optionally) next committee
    bodyRoot: Buffer.from("ee".repeat(32), "hex"),
  };
  const attestedRoot = H.beaconHeaderRoot(attestedHeader);
  const domain = H.computeDomain(
    Buffer.from(DOMAIN_SYNC.slice(2), "hex"),
    Buffer.from(FORK_VERSION.slice(2), "hex"),
    Buffer.from(GVR.slice(2), "hex")
  );
  const signingRoot = H.shaPair(attestedRoot, domain);

  const { bits, aggSigUncompressed } = H.signSubset(committee, signingRoot, opts.participants);

  const finalityBranch = tree.branch(GI_FINALIZED).map(H.hex);
  const nextBranch = opts.nextCommittee ? tree.branch(GI_NEXT).map(H.hex) : [];

  return {
    attestedHeader: beaconHeaderStruct(attestedHeader),
    finalizedHeader: beaconHeaderStruct(finalizedHeader),
    finalityBranch,
    participationBits: H.hex(bits),
    signature: H.hex(aggSigUncompressed),
    pubkeys: H.hex(H.cat(...committee.uncompressed)),
    aggregatePubkey: H.hex(committee.aggUncompressed),
    signatureForkVersion: FORK_VERSION,
    signedByNextCommittee: opts.signedByNextCommittee || false,
    hasNextCommittee: !!opts.nextCommittee,
    nextPubkeys: opts.nextCommittee ? H.hex(H.cat(...opts.nextCommittee.uncompressed)) : "0x",
    nextAggregatePubkey: opts.nextCommittee ? H.hex(opts.nextCommittee.aggUncompressed) : "0x",
    nextSyncCommitteeBranch: nextBranch,
    _finalizedRoot: finalizedRoot,
    _signingRoot: signingRoot,
    _attestedRoot: attestedRoot,
    _domain: domain,
  };
}

describe("AereEthLightClient (real EIP-2537 BLS, Prague)", function () {
  let committee, harness, bootstrapFinalized;

  before(async function () {
    this.timeout(300000);
    committee = H.makeCommittee("aere-eth-lc-committee-1");
  });

  beforeEach(async function () {
    bootstrapFinalized = {
      slot: 1000,
      proposerIndex: 1,
      parentRoot: "0x" + "00".repeat(32),
      stateRoot: "0x" + "01".repeat(32),
      bodyRoot: "0x" + "02".repeat(32),
    };
    const F = await ethers.getContractFactory("LightClientHarness");
    harness = await F.deploy(H.hex(H.DST), GVR, H.hex(committee.root), bootstrapFinalized);
    await harness.waitForDeployment();
  });

  describe("cross-checks: Solidity vs independent JS reference", function () {
    it("computes the SyncCommittee SSZ root identically", async function () {
      const onchain = await harness.h_committeeRoot(
        H.hex(H.cat(...committee.uncompressed)),
        H.hex(committee.aggUncompressed)
      );
      expect(onchain).to.equal(H.hex(committee.root));
    });

    it("compresses a G1 pubkey identically to noble", async function () {
      const onchain = await harness.h_compress(H.hex(committee.uncompressed[0]));
      expect(onchain).to.equal(H.hex(committee.compressed[0]));
    });

    it("computes the sync-committee domain identically", async function () {
      const onchain = await harness.h_domain(FORK_VERSION);
      const expected = H.computeDomain(
        Buffer.from(DOMAIN_SYNC.slice(2), "hex"),
        Buffer.from(FORK_VERSION.slice(2), "hex"),
        Buffer.from(GVR.slice(2), "hex")
      );
      expect(onchain).to.equal(H.hex(expected));
    });

    it("hashes a message to G2 identically to noble hashToCurve", async function () {
      const msg = Buffer.from("interop-hash-to-curve-check", "ascii");
      const onchain = await harness.h_hashToG2(H.hex(msg));
      const expected = H.encG2(H.bls.G2.hashToCurve(msg));
      expect(onchain.toLowerCase()).to.equal(H.hex(expected).toLowerCase());
    });
  });

  describe("processUpdate — happy path", function () {
    it("accepts a valid full-participation update and advances finality", async function () {
      this.timeout(300000);
      const participants = [...Array(H.COMMITTEE_SIZE).keys()];
      const u = buildUpdate(committee, {
        finalizedSlot: 2000,
        attestedSlot: 2032,
        participants,
      });
      await expect(harness.processUpdate(u, GAS)).to.emit(harness, "FinalityUpdated");
      expect(await harness.finalizedSlot()).to.equal(2000n);
      expect(await harness.finalizedHeaderRoot()).to.equal(H.hex(u._finalizedRoot));
    });

    it("accepts exactly the 2/3 participation threshold (342 of 512)", async function () {
      this.timeout(300000);
      const participants = [...Array(342).keys()];
      const u = buildUpdate(committee, { finalizedSlot: 2100, attestedSlot: 2132, participants });
      await expect(harness.processUpdate(u, GAS)).to.emit(harness, "FinalityUpdated");
      expect(await harness.finalizedSlot()).to.equal(2100n);
    });
  });

  describe("processUpdate — rejections", function () {
    it("rejects below-threshold participation (341 of 512)", async function () {
      this.timeout(300000);
      const participants = [...Array(341).keys()];
      const u = buildUpdate(committee, { finalizedSlot: 2200, attestedSlot: 2232, participants });
      await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:insufficient participation");
    });

    it("rejects a forged signature (valid participants, tampered signature)", async function () {
      this.timeout(300000);
      const participants = [...Array(400).keys()];
      const u = buildUpdate(committee, { finalizedSlot: 2300, attestedSlot: 2332, participants });
      // Flip the signature to a different valid G2 point (sig of a different message).
      const other = H.signSubset(committee, Buffer.from("99".repeat(32), "hex"), participants);
      u.signature = H.hex(other.aggSigUncompressed);
      await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:bad signature");
    });

    it("rejects a committee whose pubkeys don't hash to the committed root", async function () {
      this.timeout(300000);
      const wrong = H.makeCommittee("a-different-committee");
      const participants = [...Array(400).keys()];
      const u = buildUpdate(committee, { finalizedSlot: 2400, attestedSlot: 2432, participants });
      // swap in the wrong committee's pubkeys (signature no longer matters; root check first)
      u.pubkeys = H.hex(H.cat(...wrong.uncompressed));
      u.aggregatePubkey = H.hex(wrong.aggUncompressed);
      await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:committee mismatch");
    });

    it("rejects a tampered finality branch", async function () {
      this.timeout(300000);
      const participants = [...Array(400).keys()];
      const u = buildUpdate(committee, { finalizedSlot: 2500, attestedSlot: 2532, participants });
      const b = u.finalityBranch.slice();
      b[0] = "0x" + "de".repeat(32);
      u.finalityBranch = b;
      await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:bad finality branch");
    });

    it("rejects stale finality (slot not advancing)", async function () {
      this.timeout(300000);
      const participants = [...Array(400).keys()];
      const u = buildUpdate(committee, { finalizedSlot: 900, attestedSlot: 2000, participants });
      await expect(harness.processUpdate(u, GAS)).to.be.revertedWith("LC:stale finality");
    });
  });

  describe("committee rotation", function () {
    it("stores a next committee, then rotates to it on a next-committee-signed update", async function () {
      this.timeout(300000);
      const next = H.makeCommittee("aere-eth-lc-committee-2");
      const participants = [...Array(400).keys()];

      // Update 1: signed by current committee, carries next_sync_committee.
      const u1 = buildUpdate(committee, {
        finalizedSlot: 3000,
        attestedSlot: 3032,
        participants,
        nextCommittee: next,
      });
      await expect(harness.processUpdate(u1, GAS)).to.emit(harness, "NextCommitteeStored");
      expect(await harness.nextSyncCommitteeRoot()).to.equal(H.hex(next.root));

      // Update 2: signed by the NEXT committee -> current rotates to next.
      const u2 = buildUpdate(next, {
        finalizedSlot: 4000,
        attestedSlot: 4032,
        participants,
        signedByNextCommittee: true,
      });
      await expect(harness.processUpdate(u2, GAS)).to.emit(harness, "CommitteeRotated");
      expect(await harness.currentSyncCommitteeRoot()).to.equal(H.hex(next.root));
      expect(await harness.nextSyncCommitteeRoot()).to.equal("0x" + "00".repeat(32));
      expect(await harness.finalizedSlot()).to.equal(4000n);
    });
  });
});
