// AereZkQbftLightClient — the OUTBOUND zk light client: an external verifier advances a
// trust-minimized view of AERE's OWN QBFT finality by checking an SP1 Groth16 proof that one
// chain-2800 block is final (>= 2f+1 valid ECDSA committed seals from the trusted validator set).
//
// The SP1 verifier is MOCKED (MockSp1Verifier: verifyProof reverts unless the exact
// (vkey, publicValues, proof) triple was pre-registered — mirroring the real gateway, which
// reverts on an invalid proof). So these tests exercise the CONTRACT's job: decode the guest's
// PublicValues byte-for-byte, bind them to the trusted validator-set anchor, enforce a strictly
// advancing block number + finalized flag, and record the new head. The finality math itself is
// proven separately in `zk-light-client/qbft-finality/tests/real_headers.rs` against real headers.
const { expect } = require("chai");
const { ethers } = require("hardhat");

const VKEY = "0x" + "ab".repeat(32);
const WRONG_VKEY = "0x" + "cd".repeat(32);
const PROOF = "0x1234567890"; // opaque proof bytes; the mock keys acceptance on the triple

// EXACTLY the guest's PublicValues struct order (sol! { bytes32 blockHash; uint64 blockNumber;
// bytes32 committedSealHash; bytes32 validatorSetRoot; bool finalized; bool falconVerified; }).
const PV_TYPES = ["bytes32", "uint64", "bytes32", "bytes32", "bool", "bool"];
function encodePV({ blockHash, blockNumber, committedSealHash, validatorSetRoot, finalized, falconVerified }) {
  return ethers.AbiCoder.defaultAbiCoder().encode(PV_TYPES, [
    blockHash, blockNumber, committedSealHash, validatorSetRoot, finalized, falconVerified,
  ]);
}
function marker(vkey, pv, proof) {
  return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(["bytes32", "bytes", "bytes"], [vkey, pv, proof]));
}
// validator_set_root exactly as the guest: keccak256(addr_0 ‖ addr_1 ‖ … ) in stored order.
function validatorSetRoot(addrs) {
  return ethers.keccak256(ethers.concat(addrs.map((a) => ethers.getBytes(a))));
}

const VALIDATORS = [
  "0x6ef891a60bdb5907f8e4cc539fb818edbdfec552",
  "0x93fbd0a959e976d8d91bad36e250487f2da69c69",
  "0x42405515f7d8f85dcb2183fef5d6b980c64fc417",
];
const VSROOT = validatorSetRoot(VALIDATORS);
const BOOT_NUM = 9241600n;
const BOOT_HASH = "0x" + "11".repeat(32);

describe("AereZkQbftLightClient (outbound zk light client of AERE QBFT finality)", function () {
  let mock, lc, prover;

  function pv(over = {}) {
    return encodePV({
      blockHash: "0x" + "22".repeat(32),
      blockNumber: 9241622n,
      committedSealHash: "0x" + "33".repeat(32),
      validatorSetRoot: VSROOT,
      finalized: true,
      falconVerified: false,
      ...over,
    });
  }
  async function accept(publicValues, proof = PROOF) {
    await mock.setValid(marker(VKEY, publicValues, proof), true);
  }

  beforeEach(async function () {
    [prover] = await ethers.getSigners();
    mock = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
    await mock.waitForDeployment();
    lc = await (await ethers.getContractFactory("AereZkQbftLightClient")).deploy(
      await mock.getAddress(), VKEY, VSROOT, BOOT_NUM, BOOT_HASH
    );
    await lc.waitForDeployment();
  });

  it("bootstraps the anchor + initial head immutably", async function () {
    expect(await lc.SP1_VERIFIER()).to.equal(await mock.getAddress());
    expect(await lc.PROGRAM_VKEY()).to.equal(VKEY);
    expect(await lc.trustedValidatorSetRoot()).to.equal(VSROOT);
    expect(await lc.finalizedNumber()).to.equal(BOOT_NUM);
    expect(await lc.finalizedBlockHash()).to.equal(BOOT_HASH);
    expect(await lc.updateCount()).to.equal(0n);
  });

  it("the guest PublicValues ABI-encoding is exactly 192 bytes (6 x 32)", async function () {
    expect(ethers.getBytes(pv()).length).to.equal(192);
  });

  it("advances the tracked finalized head on a valid proof + emits FinalityAdvanced", async function () {
    const p = pv();
    await accept(p);
    await expect(lc.processProof(p, PROOF))
      .to.emit(lc, "FinalityAdvanced")
      .withArgs(9241622n, "0x" + "22".repeat(32), "0x" + "33".repeat(32), false, prover.address);
    expect(await lc.finalizedNumber()).to.equal(9241622n);
    expect(await lc.finalizedBlockHash()).to.equal("0x" + "22".repeat(32));
    expect(await lc.lastCommittedSealHash()).to.equal("0x" + "33".repeat(32));
    expect(await lc.lastFalconVerified()).to.equal(false);
    expect(await lc.updateCount()).to.equal(1n);
  });

  it("advances again for a strictly higher block number", async function () {
    const p1 = pv({ blockNumber: 9241622n });
    await accept(p1); await lc.processProof(p1, PROOF);
    const p2 = pv({ blockNumber: 9241650n, blockHash: "0x" + "44".repeat(32) });
    await accept(p2); await lc.processProof(p2, PROOF);
    expect(await lc.finalizedNumber()).to.equal(9241650n);
    expect(await lc.finalizedBlockHash()).to.equal("0x" + "44".repeat(32));
    expect(await lc.updateCount()).to.equal(2n);
  });

  it("rejects a proof whose validator-set root != the trusted anchor", async function () {
    const bad = pv({ validatorSetRoot: "0x" + "ff".repeat(32) });
    await accept(bad);
    await expect(lc.processProof(bad, PROOF)).to.be.revertedWithCustomError(lc, "ValidatorSetMismatch");
  });

  it("rejects a non-advancing block number (equal to current head)", async function () {
    const eq = pv({ blockNumber: BOOT_NUM });
    await accept(eq);
    await expect(lc.processProof(eq, PROOF)).to.be.revertedWithCustomError(lc, "StaleFinality");
  });

  it("rejects a lower block number after advancing", async function () {
    const p = pv({ blockNumber: 9241650n });
    await accept(p); await lc.processProof(p, PROOF);
    const lower = pv({ blockNumber: 9241640n, blockHash: "0x" + "55".repeat(32) });
    await accept(lower);
    await expect(lc.processProof(lower, PROOF)).to.be.revertedWithCustomError(lc, "StaleFinality");
  });

  it("rejects finalized=false (guest would never emit it, but the contract re-checks)", async function () {
    const nf = pv({ finalized: false });
    await accept(nf);
    await expect(lc.processProof(nf, PROOF)).to.be.revertedWithCustomError(lc, "NotFinalized");
  });

  it("rejects a zero block hash", async function () {
    const zh = pv({ blockHash: ethers.ZeroHash });
    await accept(zh);
    await expect(lc.processProof(zh, PROOF)).to.be.revertedWithCustomError(lc, "ZeroBlockHash");
  });

  it("rejects public values of the wrong length", async function () {
    const short = "0x" + "00".repeat(160); // 160 != 192
    await mock.setValid(marker(VKEY, short, PROOF), true);
    await expect(lc.processProof(short, PROOF)).to.be.revertedWithCustomError(lc, "BadPublicValuesLength");
  });

  it("rejects an invalid proof (mock not registered => gateway reverts)", async function () {
    const p = pv(); // not registered with the mock
    await expect(lc.processProof(p, PROOF)).to.be.revertedWithCustomError(lc, "InvalidProof");
  });

  it("rejects a proof registered under the WRONG vkey", async function () {
    const p = pv();
    await mock.setValid(marker(WRONG_VKEY, p, PROOF), true); // right pv, wrong program
    await expect(lc.processProof(p, PROOF)).to.be.revertedWithCustomError(lc, "InvalidProof");
  });

  it("records falconVerified when the reserved PQC slot is set (forward-compat)", async function () {
    const p = pv({ falconVerified: true });
    await accept(p);
    await lc.processProof(p, PROOF);
    expect(await lc.lastFalconVerified()).to.equal(true);
  });

  it("verify() is stateless: returns decoded fields, records nothing", async function () {
    const p = pv();
    await accept(p);
    const res = await lc.verify.staticCall(p, PROOF);
    expect(res[0]).to.equal("0x" + "22".repeat(32)); // blockHash
    expect(res[1]).to.equal(9241622n);               // blockNumber
    expect(res[3]).to.equal(VSROOT);                 // validatorSetRoot
    expect(res[4]).to.equal(true);                   // finalized
    // no state change
    expect(await lc.finalizedNumber()).to.equal(BOOT_NUM);
    expect(await lc.updateCount()).to.equal(0n);
  });

  it("verify() reverts on a wrong anchor even with a valid proof", async function () {
    const bad = pv({ validatorSetRoot: "0x" + "ee".repeat(32) });
    await accept(bad);
    await expect(lc.verify(bad, PROOF)).to.be.revertedWithCustomError(lc, "ValidatorSetMismatch");
  });
});
