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

// ---------------------------------------------------------------------------
// AereOutboundVerifier — trust-minimised OUTBOUND interop (AERE -> dest chain).
//
// The zk-interop SP1 guest commits ABI-encoded public values:
//   (uint64 chainId, uint64 blockNumber, bytes32 blockHash, bytes32 root,
//    uint8 claimKind, bytes32 commitment, bool finalized, bool falconVerified)
// where `commitment` = interop_core::log_commitment(0) over the Outbox log:
//   keccak256( cid(32) ‖ blockHash(32) ‖ addr(32) ‖ topic0 ‖ topic1 ‖ topic2
//              ‖ keccak256(data) )
//
// Hardhat has no SP1 Groth16 verifier, so we install a MockSP1Verifier that,
// like a real verifier, accepts ONLY a registered (vkey, publicValues, proof)
// triple. The commitment is rebuilt here byte-for-byte the way the Rust guest
// builds it, so what the verifier checks on-chain is exactly what the guest
// proves off-chain. (The real guest logic is separately validated against live
// chain-2800 data in zk-interop/interop-core/tests/real_inclusion.rs.)
// ---------------------------------------------------------------------------

const AERE_CHAIN_ID = 2800n;
const abi = ethers.AbiCoder.defaultAbiCoder();

// keccak256 of the event signature string (topic0).
const EVENT_SIG = ethers.keccak256(
  ethers.toUtf8Bytes(
    "AereCrossChainMessage(bytes32,uint256,uint256,address,address,bytes)"
  )
);

// 32-byte left-padded word from a uint / address, matching the Rust padding.
function word(v) {
  return ethers.zeroPadValue(ethers.toBeHex(BigInt(v)), 32);
}
function addrWord(a) {
  return ethers.zeroPadValue(ethers.getAddress(a), 32);
}

// Rebuild the guest commitment for log index 0 of the Outbox event.
function buildCommitment({ blockHash, outbox, messageId, destChainId, dataHash }) {
  return ethers.keccak256(
    ethers.solidityPacked(
      ["bytes32", "bytes32", "bytes32", "bytes32", "bytes32", "bytes32", "bytes32"],
      [
        word(AERE_CHAIN_ID), // cid
        blockHash,
        addrWord(outbox),
        EVENT_SIG, // topic0
        messageId, // topic1
        word(destChainId), // topic2
        dataHash,
      ]
    )
  );
}

function messageIdOf(destChainId, nonce, sender, targetHandler, payload) {
  return ethers.keccak256(
    abi.encode(
      ["uint256", "uint256", "uint256", "address", "address", "bytes"],
      [AERE_CHAIN_ID, destChainId, nonce, sender, targetHandler, payload]
    )
  );
}
function dataHashOf(nonce, sender, targetHandler, payload) {
  return ethers.keccak256(
    abi.encode(
      ["uint256", "address", "address", "bytes"],
      [nonce, sender, targetHandler, payload]
    )
  );
}

// The existing MockSp1Verifier keys acceptance on a single marker that binds
// vkey + publicValues + proof, exactly like the real gateway's (vkey, pv, proof).
function proofMarker(vkey, pv, proof) {
  return ethers.keccak256(
    abi.encode(["bytes32", "bytes", "bytes"], [vkey, pv, proof])
  );
}

// Encode the guest's committed public values.
function encodePublicValues(pv) {
  return abi.encode(
    ["uint64", "uint64", "bytes32", "bytes32", "uint8", "bytes32", "bool", "bool"],
    [
      pv.chainId,
      pv.blockNumber,
      pv.blockHash,
      pv.root,
      pv.claimKind,
      pv.commitment,
      pv.finalized,
      pv.falconVerified,
    ]
  );
}

describe("AereOutboundVerifier", function () {
  const GOOD_VKEY =
    "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
  const WRONG_VKEY =
    "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
  const PROOF = "0xa1b2c3d4"; // opaque Groth16 proof bytes (mock).

  let deployer, sender, other;
  let sp1, handler, outbox, verifier;
  let destChainId, blockHash, root;
  let msg; // { nonce, sender, targetHandler, payload }
  let messageId, dataHash, commitment, publicValues;

  beforeEach(async function () {
    [deployer, sender, other] = await ethers.getSigners();

    // Destination chain id = the chain the verifier runs on (hardhat = 31337).
    destChainId = BigInt((await ethers.provider.getNetwork()).chainId);

    sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
    handler = await (
      await ethers.getContractFactory("MockAereMessageHandler")
    ).deploy();
    outbox = await (
      await ethers.getContractFactory("AereOutboundOutbox")
    ).deploy();

    verifier = await (
      await ethers.getContractFactory("AereOutboundVerifier")
    ).deploy(await sp1.getAddress(), GOOD_VKEY, await outbox.getAddress());

    // The message fields.
    msg = {
      nonce: 0n,
      sender: sender.address,
      targetHandler: await handler.getAddress(),
      payload: "0xcafe1234",
    };

    // A QBFT-final block that carries the Outbox log (arbitrary in the mock).
    blockHash =
      "0x6c9dbe1c0000000000000000000000000000000000000000000000000015e900";
    root =
      "0x5ceadcd400000000000000000000000000000000000000000000000000a03500";

    messageId = messageIdOf(
      destChainId,
      msg.nonce,
      msg.sender,
      msg.targetHandler,
      msg.payload
    );
    dataHash = dataHashOf(
      msg.nonce,
      msg.sender,
      msg.targetHandler,
      msg.payload
    );
    commitment = buildCommitment({
      blockHash,
      outbox: await outbox.getAddress(),
      messageId,
      destChainId,
      dataHash,
    });

    publicValues = encodePublicValues({
      chainId: AERE_CHAIN_ID,
      blockNumber: 9261789n,
      blockHash,
      root,
      claimKind: 1,
      commitment,
      finalized: true,
      falconVerified: false,
    });

    // Register the known-good proof triple with the mock verifier.
    await sp1.setValid(proofMarker(GOOD_VKEY, publicValues, PROOF), true);
  });

  function callDeliver(v = verifier, pv = publicValues, proof = PROOF, m = msg) {
    return v.deliver(
      pv,
      proof,
      destChainId,
      m.nonce,
      m.sender,
      m.targetHandler,
      m.payload
    );
  }

  it("Outbox.send emits the canonical event and matching messageId", async function () {
    const tx = await outbox
      .connect(sender)
      .send(destChainId, msg.targetHandler, msg.payload);
    await expect(tx)
      .to.emit(outbox, "AereCrossChainMessage")
      .withArgs(
        messageId,
        destChainId,
        0n,
        sender.address,
        msg.targetHandler,
        msg.payload
      );
    expect(await outbox.nonce()).to.equal(1n);
  });

  it("delivers a valid proof with an unused nonce (exactly once)", async function () {
    await expect(callDeliver())
      .to.emit(verifier, "MessageDelivered")
      .withArgs(messageId, sender.address, msg.targetHandler, 9261789n);

    expect(await verifier.delivered(messageId)).to.equal(true);
    expect(await handler.callCount()).to.equal(1n);
    expect(await handler.lastSrcChainId()).to.equal(AERE_CHAIN_ID);
    expect(await handler.lastSender()).to.equal(sender.address);
    expect(await handler.lastPayload()).to.equal(msg.payload);
  });

  it("rejects a replay of the same message", async function () {
    await callDeliver();
    await expect(callDeliver())
      .to.be.revertedWithCustomError(verifier, "AlreadyDelivered")
      .withArgs(messageId);
    expect(await handler.callCount()).to.equal(1n); // no second delivery
  });

  it("rejects a proof verified against the wrong pinned vkey", async function () {
    const wrongVerifier = await (
      await ethers.getContractFactory("AereOutboundVerifier")
    ).deploy(await sp1.getAddress(), WRONG_VKEY, await outbox.getAddress());
    // The mock only accepted (GOOD_VKEY, pv, proof); WRONG_VKEY is unregistered.
    await expect(callDeliver(wrongVerifier)).to.be.revertedWithCustomError(
      sp1,
      "MockInvalidProof"
    );
  });

  it("rejects tampered public inputs (SP1 layer catches it)", async function () {
    // Flip blockNumber — not part of the commitment, so only the SP1 proof
    // binding can catch it. The mock has no registered triple for this pv.
    const tampered = encodePublicValues({
      chainId: AERE_CHAIN_ID,
      blockNumber: 9999999n,
      blockHash,
      root,
      claimKind: 1,
      commitment,
      finalized: true,
      falconVerified: false,
    });
    await expect(callDeliver(verifier, tampered)).to.be.revertedWithCustomError(
      sp1,
      "MockInvalidProof"
    );
  });

  it("rejects delivered fields that do not match the proven commitment", async function () {
    // Correct proof/publicValues, but deliver a DIFFERENT payload: the
    // re-derived commitment no longer matches the proven one.
    const tamperedMsg = { ...msg, payload: "0xdeadbeef" };
    await expect(
      callDeliver(verifier, publicValues, PROOF, tamperedMsg)
    ).to.be.revertedWithCustomError(verifier, "CommitmentMismatch");
  });

  it("rejects a proof for the wrong source chain", async function () {
    // chainId != 2800. Rebuild a self-consistent, registered proof so the SP1
    // layer passes and the chain-id gate is what rejects it.
    const badChainId = 9999n;
    const pv = encodePublicValues({
      chainId: badChainId,
      blockNumber: 9261789n,
      blockHash,
      root,
      claimKind: 1,
      commitment,
      finalized: true,
      falconVerified: false,
    });
    await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
    await expect(callDeliver(verifier, pv))
      .to.be.revertedWithCustomError(verifier, "WrongSourceChain")
      .withArgs(badChainId);
  });

  it("rejects a non-final source block", async function () {
    const pv = encodePublicValues({
      chainId: AERE_CHAIN_ID,
      blockNumber: 9261789n,
      blockHash,
      root,
      claimKind: 1,
      commitment,
      finalized: false,
      falconVerified: false,
    });
    await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
    await expect(callDeliver(verifier, pv)).to.be.revertedWithCustomError(
      verifier,
      "NotFinal"
    );
  });

  it("rejects a non-receipt claim kind", async function () {
    const pv = encodePublicValues({
      chainId: AERE_CHAIN_ID,
      blockNumber: 9261789n,
      blockHash,
      root,
      claimKind: 0, // storage claim, not a message log
      commitment,
      finalized: true,
      falconVerified: false,
    });
    await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
    await expect(callDeliver(verifier, pv))
      .to.be.revertedWithCustomError(verifier, "NotReceiptClaim")
      .withArgs(0);
  });

  it("rejects an unbacked post-quantum (falconVerified) claim", async function () {
    const pv = encodePublicValues({
      chainId: AERE_CHAIN_ID,
      blockNumber: 9261789n,
      blockHash,
      root,
      claimKind: 1,
      commitment,
      finalized: true,
      falconVerified: true, // mainnet consensus is ECDSA; must be false
    });
    await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
    await expect(callDeliver(verifier, pv)).to.be.revertedWithCustomError(
      verifier,
      "UnexpectedFalconFlag"
    );
  });

  it("rejects a message addressed to the wrong destination chain", async function () {
    const badDest = destChainId + 1n;
    const mid = messageIdOf(
      badDest,
      msg.nonce,
      msg.sender,
      msg.targetHandler,
      msg.payload
    );
    const dh = dataHashOf(msg.nonce, msg.sender, msg.targetHandler, msg.payload);
    const cmt = buildCommitment({
      blockHash,
      outbox: await outbox.getAddress(),
      messageId: mid,
      destChainId: badDest,
      dataHash: dh,
    });
    const pv = encodePublicValues({
      chainId: AERE_CHAIN_ID,
      blockNumber: 9261789n,
      blockHash,
      root,
      claimKind: 1,
      commitment: cmt,
      finalized: true,
      falconVerified: false,
    });
    await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
    await expect(
      verifier.deliver(
        pv,
        PROOF,
        badDest,
        msg.nonce,
        msg.sender,
        msg.targetHandler,
        msg.payload
      )
    )
      .to.be.revertedWithCustomError(verifier, "WrongDestinationChain")
      .withArgs(badDest);
  });

  it("rejects a log claimed from an untrusted (wrong) outbox address", async function () {
    // Build a commitment as if a DIFFERENT contract emitted the log. The
    // verifier is pinned to the real outbox, so re-derivation mismatches.
    const cmt = buildCommitment({
      blockHash,
      outbox: other.address, // not the pinned outbox
      messageId,
      destChainId,
      dataHash,
    });
    const pv = encodePublicValues({
      chainId: AERE_CHAIN_ID,
      blockNumber: 9261789n,
      blockHash,
      root,
      claimKind: 1,
      commitment: cmt,
      finalized: true,
      falconVerified: false,
    });
    await sp1.setValid(proofMarker(GOOD_VKEY, pv, PROOF), true);
    await expect(callDeliver(verifier, pv)).to.be.revertedWithCustomError(
      verifier,
      "CommitmentMismatch"
    );
  });

  it("propagates a handler revert (message not marked delivered)", async function () {
    await handler.setShouldRevert(true);
    await expect(callDeliver()).to.be.revertedWith("handler: forced revert");
    // State-changing revert rolls back the delivered flag too.
    expect(await verifier.delivered(messageId)).to.equal(false);
  });
});
