// cancun-canary.test.js
//
// Proves the Cancun EVM feature set behind AereCancunCanary.sol, compiled
// with solc 0.8.24 evmVersion=cancun via the per-file hardhat override
// (the repo global stays 0.8.23). The local hardhat network runs with
// hardfork: "cancun" (see hardhat.config.js), so these tests exercise the
// real opcodes:
//   - TSTORE/TLOAD same-tx round-trip (EIP-1153)
//   - transient auto-reset between two SEPARATE transactions
//   - transient reentrancy lock (inner self-call must revert)
//   - MCOPY round-trip keccak equality (EIP-5656), multiple sizes
//   - BLOBHASH(0) == 0 and BLOBBASEFEE executes (EIP-4844 / EIP-7516)
//   - PUSH0 (EIP-3855) present at genuine opcode positions in the runtime
//     bytecode (opcode-walk, not a naive substring scan)

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

/**
 * Walk EVM bytecode from offset 0, skipping PUSH1..PUSH32 immediates, and
 * count 0x5f (PUSH0) at genuine opcode positions. Stops at the first
 * INVALID (0xfe) since solc places non-code data (metadata) after it.
 */
function countPush0(bytecodeHex) {
  const code = Buffer.from(bytecodeHex.replace(/^0x/, ""), "hex");
  let i = 0;
  let push0 = 0;
  while (i < code.length) {
    const op = code[i];
    if (op === 0xfe) break; // INVALID: data section follows
    if (op === 0x5f) push0++;
    if (op >= 0x60 && op <= 0x7f) {
      i += op - 0x60 + 1; // skip PUSHn immediate bytes
    }
    i += 1;
  }
  return push0;
}

describe("AereCancunCanary (solc 0.8.24, evmVersion=cancun)", () => {
  let canary, deployer;

  before(async () => {
    [deployer] = await ethers.getSigners();
    const F = await ethers.getContractFactory("AereCancunCanary");
    canary = await F.deploy();
    await canary.waitForDeployment();
  });

  it("constructor self-test passed (TSTORE/TLOAD + MCOPY + blob opcodes)", async () => {
    expect(await canary.deploySelfTest()).to.equal(true);
    // constructor also emitted BlobProbed(0x0, fee)
    const rc = await canary.deploymentTransaction().wait();
    const parsed = rc.logs
      .map((l) => { try { return canary.interface.parseLog(l); } catch { return null; } })
      .filter(Boolean);
    const blob = parsed.find((e) => e.name === "BlobProbed");
    expect(blob, "constructor BlobProbed event").to.not.equal(undefined);
    expect(blob.args.blobHash0).to.equal(ethers.ZeroHash);
  });

  it("TSTORE/TLOAD: same-tx write + read-back round-trip", async () => {
    const value = 0xa3e3n;
    await expect(canary.transientWriteAndRead(value))
      .to.emit(canary, "TransientWithinTx")
      .withArgs(value, value);
  });

  it("transient storage auto-resets between two separate transactions", async () => {
    // tx 1: write a non-zero value into the transient slot
    const tx1 = await canary.transientWriteAndRead(999999n);
    const rc1 = await tx1.wait();

    // tx 2 (a LATER, separate transaction): the slot must read 0
    const tx2 = await canary.transientReadOnly();
    const rc2 = await tx2.wait();
    expect(rc2.blockNumber).to.be.greaterThanOrEqual(rc1.blockNumber);
    expect(tx2.hash).to.not.equal(tx1.hash);

    const parsed = rc2.logs
      .map((l) => { try { return canary.interface.parseLog(l); } catch { return null; } })
      .filter(Boolean);
    const ev = parsed.find((e) => e.name === "TransientCrossTx");
    expect(ev.args.observed).to.equal(0n);

    // and via eth_call as well
    expect(await canary.transientPeek()).to.equal(0n);
  });

  it("transient reentrancy lock: inner self-call reverts, outer succeeds", async () => {
    await expect(canary.guardedEnter(true))
      .to.emit(canary, "ReentrancyLockProbe")
      .withArgs(true, true); // reentry attempted, inner call reverted

    // lock is released after the tx (both by the modifier and by the
    // EVM transient reset)
    expect(await canary.lockPeek()).to.equal(0n);

    // a subsequent non-reentrant call goes straight through
    await expect(canary.guardedEnter(false))
      .to.emit(canary, "ReentrancyLockProbe")
      .withArgs(false, false);
  });

  it("MCOPY: round-trip keccak equality across sizes 0..1024", async () => {
    for (const n of [0, 1, 31, 32, 33, 256, 1024]) {
      const data = n === 0 ? "0x" : ethers.hexlify(ethers.randomBytes(n));
      const [inputHash, copyHash] = await canary.mcopyHash(data);
      expect(copyHash, `mcopy size ${n}`).to.equal(inputHash);
      expect(inputHash).to.equal(ethers.keccak256(data));
    }
    // transaction variant emits ok=true
    const data = ethers.hexlify(ethers.randomBytes(777));
    await expect(canary.mcopyCheck(data))
      .to.emit(canary, "McopyChecked")
      .withArgs(777n, ethers.keccak256(data), ethers.keccak256(data), true);
  });

  it("BLOBHASH(0) == 0 in a non-blob tx; BLOBBASEFEE executes", async () => {
    const [h0, bbf] = await canary.blobView();
    expect(h0).to.equal(ethers.ZeroHash);
    expect(bbf).to.be.a("bigint"); // recorded, not asserted (min is 1 wei)

    const tx = await canary.blobProbe();
    const rc = await tx.wait();
    const parsed = rc.logs
      .map((l) => { try { return canary.interface.parseLog(l); } catch { return null; } })
      .filter(Boolean);
    const ev = parsed.find((e) => e.name === "BlobProbed");
    expect(ev.args.blobHash0).to.equal(ethers.ZeroHash);
  });

  it("PUSH0: deployed runtime bytecode contains genuine 0x5f opcodes", async () => {
    const code = await ethers.provider.getCode(await canary.getAddress());
    expect(code.length).to.be.greaterThan(2);
    const n = countPush0(code);
    expect(n, "PUSH0 count at opcode positions").to.be.greaterThan(0);
    // canonical solc cancun/shanghai revert stub PUSH0 DUP1 REVERT
    expect(code.includes("5f80fd"), "5f80fd revert stub").to.equal(true);
  });
});
