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

// Status enum: None=0 Open=1 Claimed=2 Submitted=3 Disputed=4 Paid=5 Refunded=6 Slashed=7
describe("AereComputeMarketV2 (DePIN, self-contained spec/result)", function () {
  const MIN = ethers.parseEther("0.1");
  const PAY = ethers.parseEther("0.01");
  const spec = ethers.toUtf8Bytes(JSON.stringify({ task: "keccak-chain", seed: "0xabcd", iters: 1000 }));
  const result = ethers.toUtf8Bytes(JSON.stringify({ task: "keccak-chain", iters: 1000, digest: "0xdead" }));

  async function deploy() {
    const [foundation, provider, requester, other] = await ethers.getSigners();
    const M = await ethers.getContractFactory("AereComputeMarketV2");
    const m = await M.deploy(foundation.address, MIN);
    await m.waitForDeployment();
    return { m, foundation, provider, requester, other };
  }

  it("full happy path: register -> post(spec) -> claim -> submit(result) -> confirm -> paid", async function () {
    const { m, provider, requester } = await deploy();
    await m.connect(provider).registerProvider(1000, "https://p.example", { value: MIN });
    const tx = await m.connect(requester).postJob(spec, { value: PAY });
    await tx.wait();
    // job anchors keccak256(spec)
    expect((await m.jobs(0)).specHash).to.equal(ethers.keccak256(spec));

    await m.connect(provider).claimJob(0);
    await m.connect(provider).submitResult(0, result);
    // result anchor is keccak256(result) - verifiable determinism
    expect((await m.jobs(0)).resultHash).to.equal(ethers.keccak256(result));

    const before = await ethers.provider.getBalance(provider.address);
    await m.connect(requester).confirmJob(0);
    const after = await ethers.provider.getBalance(provider.address);
    expect(after - before).to.be.closeTo(PAY, ethers.parseEther("0.001"));
    expect((await m.jobs(0)).status).to.equal(5); // Paid
  });

  it("emits the full spec bytes in JobPosted so any watcher learns the workload", async function () {
    const { m, requester } = await deploy();
    await expect(m.connect(requester).postJob(spec, { value: PAY }))
      .to.emit(m, "JobPosted")
      .withArgs(0, requester.address, PAY, ethers.keccak256(spec), ethers.hexlify(spec));
  });

  it("emits the full result bytes in JobSubmitted for off-chain re-run", async function () {
    const { m, provider, requester } = await deploy();
    await m.connect(provider).registerProvider(1, "x", { value: MIN });
    await m.connect(requester).postJob(spec, { value: PAY });
    await m.connect(provider).claimJob(0);
    await expect(m.connect(provider).submitResult(0, result))
      .to.emit(m, "JobSubmitted")
      .withArgs(0, ethers.keccak256(result), ethers.hexlify(result));
  });

  it("rejects under-stake provider, empty spec, zero payment, non-provider claim", async function () {
    const { m, provider, requester, other } = await deploy();
    await expect(m.connect(provider).registerProvider(1, "x", { value: ethers.parseEther("0.09") }))
      .to.be.revertedWithCustomError(m, "BadStake");
    await expect(m.connect(requester).postJob("0x", { value: PAY }))
      .to.be.revertedWithCustomError(m, "EmptySpec");
    await expect(m.connect(requester).postJob(spec, { value: 0 }))
      .to.be.revertedWithCustomError(m, "ZeroPayment");
    await m.connect(requester).postJob(spec, { value: PAY });
    await expect(m.connect(other).claimJob(0)).to.be.revertedWithCustomError(m, "NotProvider");
  });

  it("top-up staking: reach the floor first, later top-ups can be any size", async function () {
    const { m, provider } = await deploy();
    // first deposit must meet the floor
    await expect(m.connect(provider).registerProvider(1, "x", { value: ethers.parseEther("0.06") }))
      .to.be.revertedWithCustomError(m, "BadStake");
    await m.connect(provider).registerProvider(1, "x", { value: MIN });
    // once at the floor a top-up of any size is accepted
    await m.connect(provider).registerProvider(1, "x", { value: ethers.parseEther("0.02") });
    expect((await m.providers(provider.address)).stake).to.equal(ethers.parseEther("0.12"));
  });

  it("cancel unclaimed job refunds requester", async function () {
    const { m, requester } = await deploy();
    await m.connect(requester).postJob(spec, { value: PAY });
    await m.connect(requester).cancelJob(0);
    expect((await m.jobs(0)).status).to.equal(6); // Refunded
  });

  it("autoConfirm after window pays provider, not before", async function () {
    const { m, provider, requester } = await deploy();
    await m.connect(provider).registerProvider(1, "x", { value: MIN });
    await m.connect(requester).postJob(spec, { value: PAY });
    await m.connect(provider).claimJob(0);
    await m.connect(provider).submitResult(0, result);
    await expect(m.autoConfirm(0)).to.be.revertedWithCustomError(m, "WindowOpen");
    await ethers.provider.send("evm_increaseTime", [3 * 24 * 3600 + 1]);
    await ethers.provider.send("evm_mine", []);
    await m.autoConfirm(0);
    expect((await m.jobs(0)).status).to.equal(5); // Paid
  });

  it("dispute -> Disputed status, provider cannot re-submit to dodge arbitration", async function () {
    const { m, provider, requester } = await deploy();
    await m.connect(provider).registerProvider(1, "x", { value: MIN });
    await m.connect(requester).postJob(spec, { value: PAY });
    await m.connect(provider).claimJob(0);
    await m.connect(provider).submitResult(0, ethers.toUtf8Bytes("bad"));
    await m.connect(requester).disputeJob(0);
    expect((await m.jobs(0)).status).to.equal(4); // Disputed
    // the V1 griefing path is closed: cannot re-submit once disputed
    await expect(m.connect(provider).submitResult(0, result))
      .to.be.revertedWithCustomError(m, "WrongStatus");
  });

  it("dispute -> Foundation slashes losing provider, refunds requester", async function () {
    const { m, foundation, provider, requester } = await deploy();
    await m.connect(provider).registerProvider(1, "x", { value: MIN });
    await m.connect(requester).postJob(spec, { value: PAY });
    await m.connect(provider).claimJob(0);
    await m.connect(provider).submitResult(0, ethers.toUtf8Bytes("bad"));
    await m.connect(requester).disputeJob(0);
    await m.connect(foundation).resolveDispute(0, false); // provider loses
    expect((await m.jobs(0)).status).to.equal(7); // Slashed
    expect((await m.providers(provider.address)).stake).to.equal(ethers.parseEther("0.05")); // 50% slashed
    expect(await m.slashPool()).to.equal(ethers.parseEther("0.05"));
  });

  it("dispute -> Foundation can rule for provider, pays provider", async function () {
    const { m, foundation, provider, requester } = await deploy();
    await m.connect(provider).registerProvider(1, "x", { value: MIN });
    await m.connect(requester).postJob(spec, { value: PAY });
    await m.connect(provider).claimJob(0);
    await m.connect(provider).submitResult(0, result);
    await m.connect(requester).disputeJob(0);
    const before = await ethers.provider.getBalance(provider.address);
    await m.connect(foundation).resolveDispute(0, true);
    const after = await ethers.provider.getBalance(provider.address);
    expect(after - before).to.equal(PAY);
    expect((await m.jobs(0)).status).to.equal(5); // Paid
  });

  it("only Foundation can resolve disputes", async function () {
    const { m, provider, requester, other } = await deploy();
    await m.connect(provider).registerProvider(1, "x", { value: MIN });
    await m.connect(requester).postJob(spec, { value: PAY });
    await m.connect(provider).claimJob(0);
    await m.connect(provider).submitResult(0, result);
    await m.connect(requester).disputeJob(0);
    await expect(m.connect(other).resolveDispute(0, true)).to.be.revertedWith("not foundation");
  });

  it("provider cannot deregister with active jobs", async function () {
    const { m, provider, requester } = await deploy();
    await m.connect(provider).registerProvider(1, "x", { value: MIN });
    await m.connect(requester).postJob(spec, { value: PAY });
    await m.connect(provider).claimJob(0);
    await expect(m.connect(provider).deregisterProvider()).to.be.revertedWithCustomError(m, "HasActiveJobs");
  });

  it("configurable MIN_STAKE is enforced from the constructor", async function () {
    const { m } = await deploy();
    expect(await m.MIN_STAKE()).to.equal(MIN);
    const M = await ethers.getContractFactory("AereComputeMarketV2");
    await expect(M.deploy(ethers.ZeroAddress, MIN)).to.be.revertedWithCustomError(m, "ZeroAddress");
    await expect(M.deploy((await ethers.getSigners())[0].address, 0)).to.be.revertedWithCustomError(m, "BadStake");
  });
});
