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

// ---------------------------------------------------------------------------
// AereComputeMarketV3: proof-carrying DePIN compute marketplace.
//
// The three verification modes, the Falcon-512 post-quantum settlement gate, and
// the escrow-solvency invariant are exercised against faithful test doubles:
//
//   * MockSp1Verifier          - the deployed SP1 gateway stand-in. verifyProof
//     REVERTS on an invalid proof (exact production semantics), so a ZK_VERIFIED
//     job cannot pay without a valid proof.
//   * MockPQCPrecompile(1)     - installed at 0x0AE1 via hardhat_setCode. Parses
//     the Falcon-512 SPEC offsets and accepts iff the embedded commitment equals
//     keccak256(pk || message). The market builds the precompile input itself, so a
//     positive here proves the market's real precompile-calling path is correct.
//   * MockERC20Lending         - an allowlisted ERC-20 reward asset.
//
// Real Falcon crypto and real SP1 proving are external [MEASURE]; the wire
// encoding of the Falcon input is separately KAT-proven in AerePQCMessageVerifier
// and against the live mainnet precompile.
// ---------------------------------------------------------------------------

const AE1 = ethers.getAddress("0x0000000000000000000000000000000000000ae1"); // Falcon-512 precompile
const ZERO = ethers.ZeroAddress;

const Mode = { REPLAY: 0, ZK_VERIFIED: 1, OPTIMISTIC: 2 };
const Status = {
  None: 0, Open: 1, Claimed: 2, Submitted: 3, Disputed: 4, Paid: 5, Refunded: 6, Slashed: 7,
};

const SETTLEMENT_DOMAIN = ethers.keccak256(ethers.toUtf8Bytes("AereComputeMarketV3.v1.settlement"));

// Deterministic seeded RNG (keccak hash-chain), reproducible.
function makeRng(seedText) {
  let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
  return {
    bytes(n) {
      const out = new Uint8Array(n);
      let i = 0;
      while (i < n) {
        seed = ethers.keccak256(seed);
        const chunk = ethers.getBytes(seed);
        for (let j = 0; j < chunk.length && i < n; j++, i++) out[i] = chunk[j];
      }
      return out;
    },
  };
}

// A well-formed Falcon-512 public key (897 bytes, header 0x09).
function falconPubKey(rng) {
  const raw = rng.bytes(897);
  raw[0] = 0x09;
  return ethers.hexlify(raw);
}

// Build a Falcon envelope the mock treats as a genuine signature by `pk` over the
// 32-byte `message`: nonce(40) || esig ; esig = 0x29 || keccak256(pk || message).
function falconMockEnvelope(pk, message32, rng) {
  const commitment = ethers.keccak256(ethers.concat([pk, message32]));
  const nonce = rng.bytes(40);
  const esig = ethers.concat([new Uint8Array([0x29]), commitment]);
  return ethers.hexlify(ethers.concat([nonce, esig]));
}

function abi() {
  return ethers.AbiCoder.defaultAbiCoder();
}

// Reproduce settlementDigest(id, provider, token, amount, resultHash) off-chain.
function settlementDigest(chainId, market, id, provider, token, amount, resultHash) {
  return ethers.keccak256(
    abi().encode(
      ["bytes32", "uint256", "address", "uint256", "address", "address", "uint256", "bytes32"],
      [SETTLEMENT_DOMAIN, chainId, market, id, provider, token, amount, resultHash]
    )
  );
}

describe("AereComputeMarketV3", function () {
  let market, sp1, token;
  let deployer, requester, provider, challenger, arbiter, governance, other;
  let chainId;
  let falconMockCode;

  before(async function () {
    // Compile + cache the Falcon-512 mock precompile code once.
    const M = await ethers.getContractFactory("MockPQCPrecompile");
    const f = await M.deploy(1); // scheme 1 = Falcon-512
    await f.waitForDeployment();
    falconMockCode = await ethers.provider.getCode(await f.getAddress());
  });

  beforeEach(async function () {
    [deployer, requester, provider, challenger, arbiter, governance, other] = await ethers.getSigners();
    chainId = (await ethers.provider.getNetwork()).chainId;

    // Install the faithful Falcon-512 mock at the live precompile address.
    await network.provider.send("hardhat_setCode", [AE1, falconMockCode]);

    sp1 = await (await ethers.getContractFactory("MockSp1Verifier")).deploy();
    await sp1.waitForDeployment();

    token = await (await ethers.getContractFactory("MockERC20Lending")).deploy("USD Coin e", "USDC.e", 6);
    await token.waitForDeployment();

    market = await (await ethers.getContractFactory("AereComputeMarketV3")).deploy(
      await sp1.getAddress(),
      arbiter.address,
      governance.address
    );
    await market.waitForDeployment();
  });

  // Assert the escrow-solvency invariant for both assets: contract balance >= liabilities.
  async function assertSolvent() {
    expect(await market.isSolvent(ZERO), "native solvency").to.equal(true);
    expect(await market.isSolvent(await token.getAddress()), "token solvency").to.equal(true);
    // Cross-check the actual balances against the tracked liabilities.
    const mAddr = await market.getAddress();
    const nativeBal = await ethers.provider.getBalance(mAddr);
    const nativeLia = await market.totalLiabilities(ZERO);
    expect(nativeBal >= nativeLia, "native bal>=liab").to.equal(true);
    const tokBal = await token.balanceOf(mAddr);
    const tokLia = await market.totalLiabilities(await token.getAddress());
    expect(tokBal >= tokLia, "token bal>=liab").to.equal(true);
  }

  function baseParams(overrides) {
    return Object.assign(
      {
        token: ZERO,
        reward: ethers.parseEther("10"),
        providerBond: ethers.parseEther("2"),
        specHash: ethers.keccak256(ethers.toUtf8Bytes("spec:matmul-128x128")),
        zkProgramVKey: ethers.ZeroHash,
        mode: Mode.OPTIMISTIC,
        deadline: 0, // filled below
        challengeWindow: 3600,
        pqcSettlement: false,
        falconPubKey: "0x",
      },
      overrides || {}
    );
  }

  async function post(p, valueOverride) {
    const now = (await ethers.provider.getBlock("latest")).timestamp;
    const params = baseParams(p);
    if (params.deadline === 0) params.deadline = now + 7 * 24 * 3600;
    const id = await market.jobCount();
    const value = valueOverride !== undefined ? valueOverride : (params.token === ZERO ? params.reward : 0n);
    await market.connect(requester).postJob(params, { value });
    return id;
  }

  // =========================================================================
  // 0. Construction guards (L1 fail-open)
  // =========================================================================
  describe("construction guards", function () {
    // L1 (LOW) fix: verifyProof returns void, so a code-less ZK verifier makes the call succeed
    // silently and a ZK_VERIFIED job would pay with NO valid proof (fund loss). The constructor must
    // reject a code-less verifier, exactly like the sibling AereFinalityCertificateVerifier.
    it("rejects a code-less ZK verifier at construction (L1 fail-open guard)", async function () {
      const F = await ethers.getContractFactory("AereComputeMarketV3");
      // A non-zero EOA has no code.
      await expect(
        F.deploy(other.address, arbiter.address, governance.address)
      ).to.be.revertedWithCustomError(market, "VerifierHasNoCode");
      // A zero verifier still reverts on the earlier ZeroAddress check.
      await expect(
        F.deploy(ZERO, arbiter.address, governance.address)
      ).to.be.revertedWithCustomError(market, "ZeroAddress");
      // Control: the real (code-bearing) mock gateway deploys fine.
      const ok = await F.deploy(await sp1.getAddress(), arbiter.address, governance.address);
      await ok.waitForDeployment();
      expect(await ok.ZK_VERIFIER()).to.equal(await sp1.getAddress());
    });
  });

  // =========================================================================
  // 1. ZK_VERIFIED: pay only on a valid proof, reject an invalid one
  // =========================================================================
  describe("ZK_VERIFIED mode", function () {
    const vkey = ethers.keccak256(ethers.toUtf8Bytes("sp1-program:aere-verifiable-compute-v1"));

    async function postZK() {
      return post({
        mode: Mode.ZK_VERIFIED,
        reward: ethers.parseEther("10"),
        providerBond: 0n,
        zkProgramVKey: vkey,
        challengeWindow: 0,
      });
    }

    it("pays the provider on a valid proof, and only then", async function () {
      const id = await postZK();
      await assertSolvent();
      await market.connect(provider).claimJob(id, { value: 0 });
      await assertSolvent();

      const specHash = (await market.getJob(id)).specHash;
      const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result:0xC0FFEE"));
      const publicValues = abi().encode(["bytes32", "bytes32"], [specHash, resultHash]);
      const proof = "0x1122334455667788";

      // Register this exact (vkey, publicValues, proof) as valid in the gateway.
      const marker = ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof]));
      await sp1.setValid(marker, true);

      const before = await ethers.provider.getBalance(provider.address);
      // A third party submits on the provider's behalf? No: only the job provider may submit.
      await expect(market.connect(provider).submitResultZK(id, publicValues, proof, "0x"))
        .to.emit(market, "ResultVerifiedZK")
        .and.to.emit(market, "JobPaid");
      const after = await ethers.provider.getBalance(provider.address);

      const j = await market.getJob(id);
      expect(Number(j.status)).to.equal(Status.Paid);
      expect(j.resultHash).to.equal(resultHash);
      // Provider received ~10 AERE (minus its own gas for the submit tx).
      expect(after > before).to.equal(true);
      await assertSolvent();
      expect(await market.totalLiabilities(ZERO)).to.equal(0n);
    });

    it("rejects an invalid proof and pays nothing (fail-closed)", async function () {
      const id = await postZK();
      await market.connect(provider).claimJob(id, { value: 0 });

      const specHash = (await market.getJob(id)).specHash;
      const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result:fabricated"));
      const publicValues = abi().encode(["bytes32", "bytes32"], [specHash, resultHash]);
      const proof = "0xdeadbeef"; // never registered as valid

      await expect(
        market.connect(provider).submitResultZK(id, publicValues, proof, "0x")
      ).to.be.revertedWithCustomError(sp1, "MockInvalidProof");

      const j = await market.getJob(id);
      expect(Number(j.status)).to.equal(Status.Claimed); // unchanged, no payment
      await assertSolvent();
      expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("10"));
    });

    it("rejects a proof whose public values bind a different spec", async function () {
      const id = await postZK();
      await market.connect(provider).claimJob(id, { value: 0 });

      const wrongSpec = ethers.keccak256(ethers.toUtf8Bytes("spec:some-other-job"));
      const resultHash = ethers.keccak256(ethers.toUtf8Bytes("result"));
      const publicValues = abi().encode(["bytes32", "bytes32"], [wrongSpec, resultHash]);
      const proof = "0x1234";
      const marker = ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, publicValues, proof]));
      await sp1.setValid(marker, true); // even a "valid" proof for the wrong spec must be rejected

      await expect(
        market.connect(provider).submitResultZK(id, publicValues, proof, "0x")
      ).to.be.revertedWithCustomError(market, "BadPublicValues");
      await assertSolvent();
    });
  });

  // =========================================================================
  // 2. OPTIMISTIC mode: finalize after the window; dispute slashes on a bad result
  // =========================================================================
  describe("OPTIMISTIC mode", function () {
    it("post + escrow + claim + submit + finalize pays provider and returns its bond", async function () {
      const id = await post({ mode: Mode.OPTIMISTIC });
      await assertSolvent();

      await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
      await assertSolvent();
      expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("12")); // 10 reward + 2 bond

      await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("optimistic-result"));
      expect(Number((await market.getJob(id)).status)).to.equal(Status.Submitted);

      // Cannot finalize before the window closes.
      await expect(market.connect(other).finalize(id, "0x")).to.be.revertedWithCustomError(market, "WindowOpen");

      await network.provider.send("evm_increaseTime", [3601]);
      await network.provider.send("evm_mine", []);

      const before = await ethers.provider.getBalance(provider.address);
      // A third party finalizes so the provider pays no gas: delta is exactly reward + bond.
      await expect(market.connect(other).finalize(id, "0x")).to.emit(market, "JobPaid");
      const after = await ethers.provider.getBalance(provider.address);
      expect(after - before).to.equal(ethers.parseEther("12"));

      expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid);
      expect(await market.totalLiabilities(ZERO)).to.equal(0n);
      await assertSolvent();
    });

    it("a successful dispute slashes the provider bond to the challenger and refunds the requester", async function () {
      const id = await post({ mode: Mode.OPTIMISTIC });
      await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
      await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("bad-result"));
      await assertSolvent();

      // Challenger disputes within the window, posting a matching bond.
      await expect(
        market.connect(challenger).dispute(id, ethers.ZeroHash, { value: ethers.parseEther("2") })
      ).to.emit(market, "JobDisputed");
      expect(Number((await market.getJob(id)).status)).to.equal(Status.Disputed);
      expect(await market.totalLiabilities(ZERO)).to.equal(ethers.parseEther("14")); // 10 + 2 + 2
      await assertSolvent();

      // Only the arbiter can resolve.
      await expect(market.connect(other).resolveDispute(id, false)).to.be.revertedWithCustomError(market, "NotArbiter");

      const reqBefore = await ethers.provider.getBalance(requester.address);
      const chBefore = await ethers.provider.getBalance(challenger.address);

      // Arbiter upholds the dispute (provider lost). Resolve is sent by a neutral account so the
      // requester/challenger balance deltas are exact.
      await market.connect(arbiter).resolveDispute(id, false);

      const reqAfter = await ethers.provider.getBalance(requester.address);
      const chAfter = await ethers.provider.getBalance(challenger.address);

      // Requester refunded the full reward.
      expect(reqAfter - reqBefore).to.equal(ethers.parseEther("10"));
      // Challenger gets its own bond back (2) plus the slashed provider bond (2) = 4.
      expect(chAfter - chBefore).to.equal(ethers.parseEther("4"));

      const j = await market.getJob(id);
      expect(Number(j.status)).to.equal(Status.Slashed);
      expect(await market.totalLiabilities(ZERO)).to.equal(0n);
      await assertSolvent();
    });
  });

  // =========================================================================
  // 3. REPLAY mode: dispute path (deterministic recompute on record)
  // =========================================================================
  describe("REPLAY mode", function () {
    it("records the challenger's recomputed hash; arbiter can uphold the provider", async function () {
      const id = await post({ mode: Mode.REPLAY, challengeWindow: 1800, providerBond: ethers.parseEther("1") });
      await market.connect(provider).claimJob(id, { value: ethers.parseEther("1") });
      const result = ethers.toUtf8Bytes("deterministic-output-42");
      await market.connect(provider).submitResult(id, result);
      const submittedHash = ethers.keccak256(result);
      expect((await market.getJob(id)).resultHash).to.equal(submittedHash);
      await assertSolvent();

      // A challenger claims a different recomputation and disputes.
      const challengerHash = ethers.keccak256(ethers.toUtf8Bytes("challenger-claims-this"));
      await market.connect(challenger).dispute(id, challengerHash, { value: ethers.parseEther("1") });
      expect(await market.challengeResultHashOf(id)).to.equal(challengerHash);
      await assertSolvent();

      // Off-chain re-run agrees with the provider: arbiter upholds the provider.
      const provBefore = await ethers.provider.getBalance(provider.address);
      await market.connect(arbiter).resolveDispute(id, true);
      const provAfter = await ethers.provider.getBalance(provider.address);

      // Provider gets reward (10) + own bond back (1) + challenger's forfeited bond (1) = 12.
      expect(provAfter - provBefore).to.equal(ethers.parseEther("12"));
      expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid);
      expect(await market.totalLiabilities(ZERO)).to.equal(0n);
      await assertSolvent();
    });

    it("re-run verification off-chain matches the on-chain resultHash for a good result", async function () {
      const id = await post({ mode: Mode.REPLAY, challengeWindow: 60, providerBond: ethers.parseEther("1") });
      await market.connect(provider).claimJob(id, { value: ethers.parseEther("1") });
      const result = ethers.toUtf8Bytes("f(input)=output");
      await market.connect(provider).submitResult(id, result);

      // Anyone re-runs f(input) locally and checks determinism against the on-chain anchor.
      const onChain = (await market.getJob(id)).resultHash;
      expect(onChain).to.equal(ethers.keccak256(result));

      await network.provider.send("evm_increaseTime", [61]);
      await network.provider.send("evm_mine", []);
      await market.connect(other).finalize(id, "0x");
      expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid);
      await assertSolvent();
    });
  });

  // =========================================================================
  // 4. ERC-20 reward asset (allowlist)
  // =========================================================================
  describe("allowlisted ERC-20 reward", function () {
    it("escrows and settles an allowlisted token; rejects a non-allowlisted one", async function () {
      const tokenAddr = await token.getAddress();
      await token.mint(requester.address, ethers.parseUnits("1000", 6));
      await token.connect(requester).approve(await market.getAddress(), ethers.parseUnits("1000", 6));

      // Not allowlisted yet.
      await expect(
        post({ mode: Mode.OPTIMISTIC, token: tokenAddr, reward: ethers.parseUnits("100", 6) }, 0n)
      ).to.be.revertedWithCustomError(market, "TokenNotAllowed");

      // Governance allowlists it (governance cannot move funds).
      await expect(market.connect(other).setTokenAllowed(tokenAddr, true)).to.be.revertedWithCustomError(market, "NotGovernance");
      await market.connect(governance).setTokenAllowed(tokenAddr, true);

      const id = await post({ mode: Mode.OPTIMISTIC, token: tokenAddr, reward: ethers.parseUnits("100", 6) }, 0n);
      await assertSolvent();
      expect(await market.totalLiabilities(tokenAddr)).to.equal(ethers.parseUnits("100", 6));

      // Bond is native AERE even though the reward is an ERC-20.
      await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
      await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("erc20-result"));
      await network.provider.send("evm_increaseTime", [3601]);
      await network.provider.send("evm_mine", []);

      const before = await token.balanceOf(provider.address);
      await market.connect(other).finalize(id, "0x");
      const after = await token.balanceOf(provider.address);
      expect(after - before).to.equal(ethers.parseUnits("100", 6));
      expect(await market.totalLiabilities(tokenAddr)).to.equal(0n);
      await assertSolvent();
    });
  });

  // =========================================================================
  // 5. Falcon-512 post-quantum settlement authorization
  // =========================================================================
  describe("PQC settlement (Falcon-512 via 0x0AE1)", function () {
    it("finalizes only with a valid Falcon-512 authorization; a tampered one fails closed", async function () {
      const rng = makeRng("pqc-settle");
      const pk = falconPubKey(rng);

      const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: true, falconPubKey: pk });
      await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
      await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("pqc-guarded-result"));
      await assertSolvent();

      await network.provider.send("evm_increaseTime", [3601]);
      await network.provider.send("evm_mine", []);

      const j = await market.getJob(id);
      const digest = settlementDigest(
        chainId, await market.getAddress(), id, provider.address, ZERO, j.reward, j.resultHash
      );
      // Sanity: our off-chain digest matches the contract's.
      expect(await market.settlementDigest(id, provider.address, ZERO, j.reward, j.resultHash)).to.equal(digest);

      const goodSig = falconMockEnvelope(pk, digest, rng);

      // Tampered signature: flip a byte inside the commitment. Payout reverts, nothing paid.
      const bad = ethers.getBytes(goodSig);
      bad[41] ^= 0x01; // esig = nonce(40) || 0x29 || commitment ; byte 41 is inside the commitment
      await expect(
        market.connect(other).finalize(id, ethers.hexlify(bad))
      ).to.be.revertedWithCustomError(market, "NotPQCAuthorized");
      expect(Number((await market.getJob(id)).status)).to.equal(Status.Submitted); // unchanged
      await assertSolvent();

      // Genuine Falcon-512 authorization: payout succeeds.
      const before = await ethers.provider.getBalance(provider.address);
      await expect(market.connect(other).finalize(id, goodSig))
        .to.emit(market, "PQCSettlementAuthorized")
        .and.to.emit(market, "JobPaid");
      const after = await ethers.provider.getBalance(provider.address);
      expect(after - before).to.equal(ethers.parseEther("12")); // reward 10 + bond 2
      expect(Number((await market.getJob(id)).status)).to.equal(Status.Paid);
      await assertSolvent();
    });

    it("rejects a Falcon authorization for the wrong amount (digest binds the payout)", async function () {
      const rng = makeRng("pqc-amount");
      const pk = falconPubKey(rng);
      const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: true, falconPubKey: pk });
      await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
      await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("r"));
      await network.provider.send("evm_increaseTime", [3601]);
      await network.provider.send("evm_mine", []);

      const j = await market.getJob(id);
      // Sign a digest for the WRONG amount (5 AERE instead of 10).
      const wrongDigest = settlementDigest(
        chainId, await market.getAddress(), id, provider.address, ZERO, ethers.parseEther("5"), j.resultHash
      );
      const wrongSig = falconMockEnvelope(pk, wrongDigest, rng);
      await expect(
        market.connect(other).finalize(id, wrongSig)
      ).to.be.revertedWithCustomError(market, "NotPQCAuthorized");
      await assertSolvent();
    });

    it("rejects a PQC signature supplied for a non-PQC job", async function () {
      const rng = makeRng("nonpqc");
      const pk = falconPubKey(rng);
      const id = await post({ mode: Mode.OPTIMISTIC, pqcSettlement: false });
      await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
      await market.connect(provider).submitResult(id, ethers.toUtf8Bytes("r"));
      await network.provider.send("evm_increaseTime", [3601]);
      await network.provider.send("evm_mine", []);
      const junkSig = falconMockEnvelope(pk, ethers.ZeroHash, rng);
      await expect(
        market.connect(other).finalize(id, junkSig)
      ).to.be.revertedWithCustomError(market, "PQCSigNotAllowed");
    });
  });

  // =========================================================================
  // 6. Escrow-solvency invariant across a mixed workload, and no-double-pay
  // =========================================================================
  describe("escrow solvency and no double-pay", function () {
    it("holds the solvency invariant across many concurrent jobs and terminal states", async function () {
      const vkey = ethers.keccak256(ethers.toUtf8Bytes("vk"));
      // A batch of jobs in various modes and states.
      const idA = await post({ mode: Mode.OPTIMISTIC });
      const idB = await post({ mode: Mode.REPLAY, challengeWindow: 100, providerBond: ethers.parseEther("1") });
      const idC = await post({ mode: Mode.ZK_VERIFIED, providerBond: 0n, zkProgramVKey: vkey, challengeWindow: 0 });
      const idD = await post({ mode: Mode.OPTIMISTIC }); // will be cancelled while Open
      await assertSolvent();

      await market.connect(provider).claimJob(idA, { value: ethers.parseEther("2") });
      await market.connect(provider).claimJob(idB, { value: ethers.parseEther("1") });
      await market.connect(provider).claimJob(idC, { value: 0 });
      await assertSolvent();

      // Cancel D while Open.
      await market.connect(requester).cancelJob(idD);
      await assertSolvent();

      // A submits + disputed + slashed.
      await market.connect(provider).submitResult(idA, ethers.toUtf8Bytes("A"));
      await market.connect(challenger).dispute(idA, ethers.ZeroHash, { value: ethers.parseEther("2") });
      await market.connect(arbiter).resolveDispute(idA, false);
      await assertSolvent();

      // B submits + finalized.
      await market.connect(provider).submitResult(idB, ethers.toUtf8Bytes("B"));
      await network.provider.send("evm_increaseTime", [101]);
      await network.provider.send("evm_mine", []);
      await market.connect(other).finalize(idB, "0x");
      await assertSolvent();

      // C settles via ZK.
      const specHashC = (await market.getJob(idC)).specHash;
      const rhC = ethers.keccak256(ethers.toUtf8Bytes("C-result"));
      const pvC = abi().encode(["bytes32", "bytes32"], [specHashC, rhC]);
      const proofC = "0xabcdef01";
      await sp1.setValid(ethers.keccak256(abi().encode(["bytes32", "bytes", "bytes"], [vkey, pvC, proofC])), true);
      await market.connect(provider).submitResultZK(idC, pvC, proofC, "0x");
      await assertSolvent();

      // All liabilities cleared.
      expect(await market.totalLiabilities(ZERO)).to.equal(0n);

      // No double-pay: finalizing/settling a terminal job reverts.
      await expect(market.connect(other).finalize(idB, "0x")).to.be.revertedWithCustomError(market, "WrongStatus");
      await expect(market.connect(provider).submitResultZK(idC, pvC, proofC, "0x")).to.be.revertedWithCustomError(market, "WrongStatus");
      await expect(market.connect(arbiter).resolveDispute(idA, true)).to.be.revertedWithCustomError(market, "WrongStatus");
    });

    it("reclaims a claimed job that blew its deadline, slashing the provider bond to the requester", async function () {
      const now = (await ethers.provider.getBlock("latest")).timestamp;
      const id = await post({ mode: Mode.OPTIMISTIC, deadline: now + 100 });
      await market.connect(provider).claimJob(id, { value: ethers.parseEther("2") });
      await assertSolvent();

      await network.provider.send("evm_increaseTime", [101]);
      await network.provider.send("evm_mine", []);

      const before = await ethers.provider.getBalance(requester.address);
      const tx = await market.connect(requester).reclaimExpired(id);
      const rc = await tx.wait();
      const gas = rc.gasUsed * rc.gasPrice;
      const after = await ethers.provider.getBalance(requester.address);
      // Requester recovers reward (10) + slashed provider bond (2), minus gas.
      expect(after - before + gas).to.equal(ethers.parseEther("12"));
      expect(Number((await market.getJob(id)).status)).to.equal(Status.Refunded);
      await assertSolvent();
    });
  });
});
