// AereStateRootAnchor tests.
//
// What is REAL here:
//  - The AERE genesis block header (chain 2800) is real data; encodeShanghaiHeader
//    reconstructs its canonical hash 0xd86d57a8... exactly. The anchor's keccak
//    match + state-root decode is tested against it (genesis is outside the live
//    256-block BLOCKHASH window, so the mock injects its real canonical hash).
//  - The real BLOCKHASH opcode path is tested end-to-end on the local chain:
//    a freshly mined block is reconstructed, keccak-verified against blockhash,
//    then anchored through the PRODUCTION contract (no mock).
//  - proveAgainstAnchor is tested end-to-end against a faithful stand-in for the
//    live AereStorageProofVerifier's verify/submitProofWithExpectedRoot ABI.

const { expect } = require("chai");
const { ethers } = require("hardhat");
const { anyUint } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
const { encodeHeader } = require("./header-rlp");
const genesis = require("./fixtures/aere-genesis-header.json");

const ZERO32 = "0x" + "00".repeat(32);

// abi.encode(chainId, blockNumber, stateRoot, account, slot, value) -> 192 bytes
function publicValues({ chainId = 2800n, blockNumber, stateRoot, account, slot, value }) {
  return ethers.AbiCoder.defaultAbiCoder().encode(
    ["uint256", "uint256", "bytes32", "address", "bytes32", "bytes32"],
    [chainId, blockNumber, stateRoot, account, slot, value]
  );
}

describe("AereStateRootAnchor", function () {
  let verifier, mockAnchor, prodAnchor, user;

  beforeEach(async function () {
    [user] = await ethers.getSigners();

    const V = await ethers.getContractFactory("MockStorageProofVerifier");
    verifier = await V.deploy();
    await verifier.waitForDeployment();
    const vAddr = await verifier.getAddress();

    const M = await ethers.getContractFactory("MockBlockhashAnchor");
    mockAnchor = await M.deploy(vAddr);
    await mockAnchor.waitForDeployment();

    const P = await ethers.getContractFactory("AereStateRootAnchor");
    prodAnchor = await P.deploy(vAddr);
    await prodAnchor.waitForDeployment();
  });

  describe("constructor", function () {
    it("stores the verifier and rejects the zero address", async function () {
      expect(await prodAnchor.VERIFIER()).to.equal(await verifier.getAddress());
      expect(await prodAnchor.BLOCKHASH_WINDOW()).to.equal(256n);

      const P = await ethers.getContractFactory("AereStateRootAnchor");
      await expect(P.deploy(ethers.ZeroAddress)).to.be.revertedWithCustomError(
        prodAnchor,
        "ZeroVerifier"
      );
    });
  });

  describe("encoder / fixture sanity (REAL AERE genesis)", function () {
    it("reconstructs the real canonical AERE genesis block hash", function () {
      const header = encodeHeader(genesis);
      expect(ethers.keccak256(header)).to.equal(genesis.hash);
    });

    it("previewStateRoot decodes the real genesis state root", async function () {
      const header = encodeHeader(genesis);
      expect(await prodAnchor.previewStateRoot(header)).to.equal(genesis.stateRoot);
    });
  });

  describe("anchorRecentHeader (against REAL AERE genesis via injected canonical hash)", function () {
    it("anchors the real genesis header and records the real state root", async function () {
      const header = encodeHeader(genesis);
      await mockAnchor.setBlockHash(0, genesis.hash);

      await expect(mockAnchor.anchorRecentHeader(0, header))
        .to.emit(mockAnchor, "HeaderAnchored")
        .withArgs(0, genesis.hash, genesis.stateRoot, anyUint);

      expect(await mockAnchor.isAnchored(0)).to.equal(true);
      expect(await mockAnchor.anchoredStateRoot(0)).to.equal(genesis.stateRoot);

      const rec = await mockAnchor.getAnchoredRoot(0);
      expect(rec.exists).to.equal(true);
      expect(rec.stateRoot).to.equal(genesis.stateRoot);
      expect(rec.blockHash).to.equal(genesis.hash);
      expect(rec.blockNum).to.equal(0n);
      expect(rec.verifiedAt).to.be.gt(0n);
    });

    it("reverts HeaderHashMismatch when the header is tampered", async function () {
      const header = encodeHeader(genesis);
      await mockAnchor.setBlockHash(0, genesis.hash);

      // Flip one byte inside the state-root region of the header.
      const bytesArr = ethers.getBytes(header);
      bytesArr[60] ^= 0x01;
      const tampered = ethers.hexlify(bytesArr);

      await expect(mockAnchor.anchorRecentHeader(0, tampered)).to.be.revertedWithCustomError(
        mockAnchor,
        "HeaderHashMismatch"
      );
    });

    it("reverts MalformedHeader when a hash-matching input is not a valid header", async function () {
      // A short RLP list whose keccak we inject: passes the hash check, then
      // fails structural decode.
      const bogus = "0xc0"; // empty RLP list
      await mockAnchor.setBlockHash(7, ethers.keccak256(bogus));
      await expect(mockAnchor.anchorRecentHeader(7, bogus)).to.be.revertedWithCustomError(
        mockAnchor,
        "MalformedHeader"
      );
    });

    it("reverts OutOfWindow when no canonical hash is available", async function () {
      const header = encodeHeader(genesis);
      // No injected hash for block 0 -> _blockHashOf returns 0.
      await expect(mockAnchor.anchorRecentHeader(0, header)).to.be.revertedWithCustomError(
        mockAnchor,
        "OutOfWindow"
      );
    });

    it("decodes an arbitrary (real) state root value carried in a valid header", async function () {
      // Same real genesis layout but carrying AERE recent-block state root
      // 0x9592f7...; proves the decode reads whatever root the header commits.
      const recentRoot =
        "0x9592f7acd3ec0a4694b5fa23cd9b8b814d49a19c5c9fcf6c47a523611e5b0245";
      const header = encodeHeader({ ...genesis, stateRoot: recentRoot });
      await mockAnchor.setBlockHash(123, ethers.keccak256(header));
      await mockAnchor.anchorRecentHeader(123, header);
      expect(await mockAnchor.anchoredStateRoot(123)).to.equal(recentRoot);
    });
  });

  describe("real BLOCKHASH opcode path (PRODUCTION contract, no mock)", function () {
    it("anchors a freshly mined local block via the real blockhash() opcode", async function () {
      // Produce a few normal blocks.
      for (let i = 0; i < 3; i++) await ethers.provider.send("evm_mine", []);
      const head = await ethers.provider.getBlockNumber();
      const target = head - 1; // inside the 256 window, not the current block

      const raw = await ethers.provider.send("eth_getBlockByNumber", [
        "0x" + target.toString(16),
        false,
      ]);
      const header = encodeHeader(raw);

      // Hard gate: our reconstruction must equal the chain's block hash AND the
      // value the contract sees from the real BLOCKHASH opcode.
      expect(ethers.keccak256(header)).to.equal(raw.hash);
      expect(await prodAnchor.canonicalBlockHash(target)).to.equal(raw.hash);
      expect(await prodAnchor.isWithinWindow(target)).to.equal(true);

      await prodAnchor.anchorRecentHeader(target, header);
      expect(await prodAnchor.anchoredStateRoot(target)).to.equal(raw.stateRoot);
    });

    it("reverts OutOfWindow for the current/future block (blockhash == 0)", async function () {
      const head = await ethers.provider.getBlockNumber();
      const header = encodeHeader(genesis); // content irrelevant; window check is first
      // A future block number: BLOCKHASH returns 0 for the current and any
      // future block, so the window check reverts before the hash check.
      await expect(
        prodAnchor.anchorRecentHeader(head + 100, header)
      ).to.be.revertedWithCustomError(prodAnchor, "OutOfWindow");
    });

    it("reverts OutOfWindow for a block older than 256 (past the BLOCKHASH window)", async function () {
      await ethers.provider.send("hardhat_mine", ["0x160"]); // mine 352 blocks
      const header = encodeHeader(genesis);
      // Block 1 is now far outside the last-256 window.
      await expect(prodAnchor.anchorRecentHeader(1, header)).to.be.revertedWithCustomError(
        prodAnchor,
        "OutOfWindow"
      );
    });
  });

  describe("proveAgainstAnchor (end-to-end with faithful verifier stand-in)", function () {
    const account = "0x00000000000000000000000000000000000000A1";
    const slot = "0x" + "00".repeat(31) + "05";
    const value = "0x" + "00".repeat(31) + "2a";

    beforeEach(async function () {
      // Anchor the real genesis root at block 0 on the mock anchor.
      const header = encodeHeader(genesis);
      await mockAnchor.setBlockHash(0, genesis.hash);
      await mockAnchor.anchorRecentHeader(0, header);
    });

    it("records a proof whose committed root equals the anchored root", async function () {
      const pv = publicValues({
        blockNumber: 0n,
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });
      await expect(mockAnchor.proveAgainstAnchor(pv, "0x"))
        .to.emit(mockAnchor, "ProvenAgainstAnchor")
        .withArgs(0, genesis.stateRoot, account, slot, value);

      expect(await verifier.recorded(ethers.keccak256(pv))).to.equal(true);
    });

    it("reverts RootNotCanonical when the proof's root is not the anchored root", async function () {
      const pv = publicValues({
        blockNumber: 0n,
        stateRoot: ZERO32, // not the anchored genesis root
        account,
        slot,
        value,
      });
      await expect(mockAnchor.proveAgainstAnchor(pv, "0x")).to.be.revertedWithCustomError(
        mockAnchor,
        "RootNotCanonical"
      );
    });

    it("reverts NotAnchored when the proof's block is not anchored", async function () {
      const pv = publicValues({
        blockNumber: 999n,
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });
      await expect(mockAnchor.proveAgainstAnchor(pv, "0x")).to.be.revertedWithCustomError(
        mockAnchor,
        "NotAnchored"
      );
    });

    it("reverts when the underlying SP1 proof is invalid", async function () {
      await verifier.setValidate(false);
      const pv = publicValues({
        blockNumber: 0n,
        stateRoot: genesis.stateRoot,
        account,
        slot,
        value,
      });
      await expect(mockAnchor.proveAgainstAnchor(pv, "0x")).to.be.reverted;
    });
  });
});
