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

// -----------------------------------------------------------------------------
// AereEthMessageInbox — finality (via light client) + receipts-trie MPT inclusion +
// replay protection. The receipts trie is built by an INDEPENDENT JS MPT builder;
// the on-chain MerklePatriciaProof verifier walks it. Finality is isolated by
// bootstrapping the light client directly with the finalized body root that commits
// the receiptsRoot (the sync-committee path is covered in the light-client suite).
// -----------------------------------------------------------------------------

const GVR = "0x" + "22".repeat(32);
// Synthetic execution-payload branch shape: receipts_root committed under the beacon
// body root at (depth, subtreeIndex). In production these are the fork-specific
// generalized index for ExecutionPayload.receipts_root under BeaconBlockBody.
const EXEC_DEPTH = 9;
const EXEC_INDEX = 291;
const EXEC_GINDEX = (1 << EXEC_DEPTH) + EXEC_INDEX; // 803

const SOURCE = ethers.getAddress("0x00000000000000000000000000000000000000A1");
const OTHER = ethers.getAddress("0x00000000000000000000000000000000000000B2");
const TOPIC0 = "0x" + "aa".repeat(32);
const TOPIC1 = "0x" + "bb".repeat(32);

// Build receipts, the receipts trie, and the finalized body tree that commits its root.
function buildScenario(receipts) {
  const entries = receipts.map((r, i) => ({ key: T.txIndexKey(i), value: r.value }));
  const { root: receiptsRoot, nodes } = T.buildTrie(entries);

  // finalized beacon body tree: receiptsRoot at EXEC_GINDEX.
  const assign = {};
  assign[EXEC_GINDEX] = receiptsRoot;
  const bodyTree = H.buildTree(EXEC_DEPTH, assign);

  return { receiptsRoot, nodes, entries, bodyTree };
}

async function deployStack(bodyRoot, source = SOURCE) {
  const bootstrapFinalized = {
    slot: 5000,
    proposerIndex: 3,
    parentRoot: "0x" + "00".repeat(32),
    stateRoot: "0x" + "01".repeat(32),
    bodyRoot: H.hex(bodyRoot), // commits the receiptsRoot at EXEC_GINDEX
  };
  const LC = await ethers.getContractFactory("AereEthLightClient");
  // any nonzero committee root; the inbox path doesn't process sync updates.
  const lc = await LC.deploy(H.hex(H.DST), GVR, "0x" + "07".repeat(32), bootstrapFinalized);
  await lc.waitForDeployment();

  const Inbox = await ethers.getContractFactory("AereEthMessageInbox");
  const inbox = await Inbox.deploy(await lc.getAddress(), source, EXEC_DEPTH, EXEC_INDEX);
  await inbox.waitForDeployment();
  return { lc, inbox };
}

function receiptWith(type, logs) {
  return { value: T.encodeReceipt(type, { status: 1, cumulativeGas: 21000, logs }) };
}
function proofFor(scn, txIndex, logIndex) {
  const key = T.txIndexKey(txIndex);
  const mptNodes = T.getProof(scn.receiptsRoot, scn.nodes, key).map((b) => "0x" + b.toString("hex"));
  return {
    receiptsRoot: H.hex(scn.receiptsRoot),
    finalizedBodyRoot: H.hex(scn.bodyTree.root),
    execBranch: scn.bodyTree.branch(EXEC_GINDEX).map(H.hex),
    txIndex,
    mptNodes,
    logIndex,
  };
}

describe("AereEthMessageInbox (finality + MPT receipt inclusion)", function () {
  it("delivers a real message: finalized receiptsRoot + MPT inclusion + correct source", async function () {
    const msgData = "0x" + "1234abcd".repeat(4);
    const receipts = [
      receiptWith(2, [{ address: SOURCE, topics: [TOPIC0, TOPIC1], data: msgData }]),
    ];
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);

    const p = proofFor(scn, 0, 0);
    await expect(inbox.deliver(p))
      .to.emit(inbox, "MessageDelivered")
      .withArgs(H.hex(scn.receiptsRoot), 0, 0, SOURCE, [TOPIC0, TOPIC1], msgData);

    const id = ethers.keccak256(
      ethers.solidityPacked(["bytes32", "uint256", "uint256"], [H.hex(scn.receiptsRoot), 0, 0])
    );
    expect(await inbox.delivered(id)).to.equal(true);
  });

  it("selects the right receipt out of a multi-receipt trie (branch/extension nodes)", async function () {
    const receipts = [];
    for (let i = 0; i < 6; i++) {
      const addr = i === 3 ? SOURCE : OTHER;
      receipts.push(receiptWith(2, [{ address: addr, topics: [TOPIC0], data: "0x" + "0".repeat(8) }]));
    }
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);
    // receipt at txIndex 3 was emitted by SOURCE.
    const p = proofFor(scn, 3, 0);
    await expect(inbox.deliver(p)).to.emit(inbox, "MessageDelivered");
  });

  it("supports legacy (type-0) receipts too", async function () {
    const receipts = [receiptWith(0, [{ address: SOURCE, topics: [TOPIC0], data: "0xdead" }])];
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);
    await expect(inbox.deliver(proofFor(scn, 0, 0))).to.emit(inbox, "MessageDelivered");
  });

  it("parses the correct log from a multi-log receipt", async function () {
    const receipts = [
      receiptWith(2, [
        { address: OTHER, topics: [TOPIC0], data: "0x00" },
        { address: SOURCE, topics: [TOPIC0, TOPIC1], data: "0xbeef" },
      ]),
    ];
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);
    // logIndex 1 is the SOURCE log.
    await expect(inbox.deliver(proofFor(scn, 0, 1)))
      .to.emit(inbox, "MessageDelivered")
      .withArgs(H.hex(scn.receiptsRoot), 0, 1, SOURCE, [TOPIC0, TOPIC1], "0xbeef");
    // logIndex 0 is emitted by OTHER -> wrong source.
    const { inbox: inbox2 } = await deployStack(scn.bodyTree.root);
    await expect(inbox2.deliver(proofFor(scn, 0, 0))).to.be.revertedWith("Inbox:wrong source");
  });

  it("rejects replay (same receipt+log delivered twice)", async function () {
    const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])];
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);
    await inbox.deliver(proofFor(scn, 0, 0));
    await expect(inbox.deliver(proofFor(scn, 0, 0))).to.be.revertedWith("Inbox:already delivered");
  });

  it("rejects a receiptsRoot not committed under the finalized body (bad exec branch)", async function () {
    const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])];
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);
    const p = proofFor(scn, 0, 0);
    const b = p.execBranch.slice();
    b[0] = "0x" + "cc".repeat(32); // corrupt the execution-payload branch
    p.execBranch = b;
    await expect(inbox.deliver(p)).to.be.revertedWith("Inbox:not finalized");
  });

  it("rejects a body root the light client never finalized", async function () {
    const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])];
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);
    const p = proofFor(scn, 0, 0);
    p.finalizedBodyRoot = "0x" + "77".repeat(32); // never finalized
    await expect(inbox.deliver(p)).to.be.revertedWith("Inbox:not finalized");
  });

  it("rejects a forged receipt (tampered receipt value breaks MPT inclusion)", async function () {
    const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])];
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);
    const p = proofFor(scn, 0, 0);
    // corrupt the sole proof node so keccak(node) != receiptsRoot.
    const nodes = p.mptNodes.slice();
    const bad = Buffer.from(nodes[0].slice(2), "hex");
    bad[bad.length - 1] ^= 0x01;
    nodes[0] = "0x" + bad.toString("hex");
    p.mptNodes = nodes;
    await expect(inbox.deliver(p)).to.be.reverted; // hash mismatch -> missing node
  });

  it("rejects the wrong txIndex key (proves a different, absent slot)", async function () {
    const receipts = [receiptWith(2, [{ address: SOURCE, topics: [TOPIC0], data: "0x01" }])];
    const scn = buildScenario(receipts);
    const { inbox } = await deployStack(scn.bodyTree.root);
    const p = proofFor(scn, 0, 0);
    p.txIndex = 7; // key rlp(7) is not in the single-entry trie
    await expect(inbox.deliver(p)).to.be.reverted;
  });
});
