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

// ---------------------------------------------------------------------------
// AereDaFraudProofVerifier — on-chain adjudication of the aere-da crate's
// bad-encoding fraud proofs and availability samples.
//
// Every fixture in test/fixtures/da/ was emitted by the Rust crate itself
// (aerenew/da, `cargo run --bin aere-da-export-fixtures`) using the SAME code
// path its own tests exercise: DataSquareCommitment::commit, try_prove_bad_row
// / try_prove_bad_col, and sample(). Each fixture also carries `rustVerdict` =
// the crate's own verify_bad_encoding / verify_sample result. This suite is a
// DIFFERENTIAL test: the Solidity verifier's answer must equal BOTH the
// expected outcome AND the Rust crate's answer, for identical bytes.
//
// SHA-256 hasher: the on-chain verifier recomputes the NMT with the EVM sha256
// precompile (0x02), so the fixtures commit to the crate's SHA-256 hasher.
// ---------------------------------------------------------------------------

const FIX_DIR = path.join(__dirname, "fixtures", "da");
const load = (name) => JSON.parse(fs.readFileSync(path.join(FIX_DIR, name + ".json"), "utf8"));

// Fixture JSON -> ethers struct tuples (ordered to match the Solidity structs).
const nodeT = (nd) => [nd.min, nd.max, nd.hash];
const proofT = (pr) => [pr.index, pr.siblings.map(nodeT)];
const shareT = (sh) => [sh.pos, sh.ns, sh.data, proofT(sh.proof)];
const badT = (p) => [p.axis, p.index, nodeT(p.root), proofT(p.outerProof), p.basis.map(shareT), shareT(p.disputed)];
const sampleT = (s) => [s.row, s.col, s.ns, s.data, proofT(s.rowProof), nodeT(s.rowRoot), proofT(s.outerProof)];

describe("AereDaFraudProofVerifier", function () {
  let verifier;

  before(async function () {
    const F = await ethers.getContractFactory("AereDaFraudProofVerifier");
    verifier = await F.deploy();
    await verifier.waitForDeployment();
  });

  // Sanity: the fixtures directory must actually exist and be populated by the
  // Rust exporter (never silently skip — that would fake a green run).
  it("fixture directory is populated by the Rust crate", function () {
    const files = fs.readdirSync(FIX_DIR).filter((f) => f.endsWith(".json"));
    expect(files.length).to.be.greaterThanOrEqual(7);
    for (const f of ["bad_row", "bad_col", "forged_basis", "spurious_wellencoded", "wrong_dataroot", "sample_ok", "sample_tampered"]) {
      expect(files).to.include(f + ".json");
    }
  });

  describe("bad-encoding fraud proofs (differential vs Rust)", function () {
    const cases = [
      { name: "bad_row" },
      { name: "bad_col" },
      { name: "forged_basis" },
      { name: "spurious_wellencoded" },
      { name: "wrong_dataroot" },
    ];

    for (const c of cases) {
      it(`${c.name}: Solidity verdict == expectConvict == rustVerdict`, async function () {
        const fx = load(c.name);
        // The fixture's own expected outcome and the crate's verdict must agree
        // (guards against a stale/miswired fixture).
        expect(fx.expectConvict).to.equal(fx.rustVerdict);
        const got = await verifier.verifyBadEncoding(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof));
        expect(got).to.equal(fx.expectConvict, fx.description);
        expect(got).to.equal(fx.rustVerdict, "Solidity must match the Rust crate bit-for-bit");
      });
    }
  });

  describe("challenge() state-changing path", function () {
    it("CONVICTS a badly-encoded row: emits BadBlockConvicted", async function () {
      const fx = load("bad_row");
      await expect(verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof)))
        .to.emit(verifier, "BadBlockConvicted")
        .withArgs(fx.dataRoot.hash, fx.proof.axis, fx.proof.index, fx.proof.disputed.pos);
    });

    it("CONVICTS a badly-encoded column: emits BadBlockConvicted", async function () {
      const fx = load("bad_col");
      await expect(verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof)))
        .to.emit(verifier, "BadBlockConvicted")
        .withArgs(fx.dataRoot.hash, fx.proof.axis, fx.proof.index, fx.proof.disputed.pos);
    });

    it("REJECTS a spurious challenge on a well-encoded block: reverts", async function () {
      const fx = load("spurious_wellencoded");
      await expect(
        verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof))
      ).to.be.revertedWith("AereDA: not a valid bad-encoding fraud proof");
    });

    it("REJECTS a doctored-basis proof: reverts", async function () {
      const fx = load("forged_basis");
      await expect(
        verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof))
      ).to.be.revertedWith("AereDA: not a valid bad-encoding fraud proof");
    });

    it("REJECTS a proof against the wrong data root: reverts", async function () {
      const fx = load("wrong_dataroot");
      await expect(
        verifier.challenge(nodeT(fx.dataRoot), fx.k, fx.n, badT(fx.proof))
      ).to.be.revertedWith("AereDA: not a valid bad-encoding fraud proof");
    });
  });

  describe("availability samples (NMT inclusion against the data root)", function () {
    it("sample_ok: honest sample verifies (matches Rust)", async function () {
      const fx = load("sample_ok");
      expect(fx.expectAvailable).to.equal(fx.rustVerdict);
      const got = await verifier.verifySample(nodeT(fx.dataRoot), fx.n, sampleT(fx.sample));
      expect(got).to.equal(true);
    });

    it("sample_tampered: flipped byte fails inclusion (matches Rust)", async function () {
      const fx = load("sample_tampered");
      expect(fx.expectAvailable).to.equal(fx.rustVerdict);
      const got = await verifier.verifySample(nodeT(fx.dataRoot), fx.n, sampleT(fx.sample));
      expect(got).to.equal(false);
    });
  });

  describe("robustness: local tamper must not fool the verifier", function () {
    it("flipping a disputed byte on the spurious proof still does not falsely convict a good root", async function () {
      // Take the spurious (well-encoded) fixture and corrupt ONLY the disputed
      // share's data in the calldata (not re-committed). Its inclusion proof no
      // longer matches the honest root, so the verifier must still REJECT: you
      // cannot manufacture a conviction against an honest block by lying about a
      // cell's value.
      const fx = load("spurious_wellencoded");
      const p = JSON.parse(JSON.stringify(fx.proof));
      // flip first byte of disputed data
      const b = ethers.getBytes(p.disputed.data);
      b[0] ^= 0xff;
      p.disputed.data = ethers.hexlify(b);
      const got = await verifier.verifyBadEncoding(nodeT(fx.dataRoot), fx.k, fx.n, badT(p));
      expect(got).to.equal(false);
    });
  });
});
