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

describe("AereInferNet", () => {
  let provider, regulator, outsider;
  let net;
  const MODEL_ID = ethers.keccak256(ethers.toUtf8Bytes("claude-4.7-sonnet-20251022"));
  const MODEL_DIGEST = ethers.keccak256(ethers.toUtf8Bytes("model-weights-blob"));
  const ATT_DIGEST = ethers.keccak256(ethers.toUtf8Bytes("sgx-attestation-quote"));

  beforeEach(async () => {
    [provider, regulator, outsider] = await ethers.getSigners();
    const F = await ethers.getContractFactory("AereInferNet");
    net = await F.deploy();
    await net.waitForDeployment();
  });

  function buildLeaf(req, resp, ts, deployer) {
    return ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
      ["bytes32", "bytes32", "uint64", "bytes32"], [req, resp, ts, deployer]
    ));
  }

  // tiny sort-pair Merkle: builds a 4-leaf tree and returns root + a proof for leaf 0
  function tinyTree(leaves) {
    const hashPair = (a, b) => {
      const [lo, hi] = BigInt(a) <= BigInt(b) ? [a, b] : [b, a];
      return ethers.keccak256(ethers.concat([lo, hi]));
    };
    while (leaves.length > 1) {
      const next = [];
      for (let i = 0; i < leaves.length; i += 2) {
        if (i + 1 < leaves.length) next.push(hashPair(leaves[i], leaves[i + 1]));
        else next.push(leaves[i]);
      }
      leaves = next;
    }
    return leaves[0];
  }

  function buildTreeWithProof(allLeaves, idx) {
    // returns { root, proof } for the leaf at idx in a sorted-pair tree.
    const hashPair = (a, b) => {
      const [lo, hi] = BigInt(a) <= BigInt(b) ? [a, b] : [b, a];
      return ethers.keccak256(ethers.concat([lo, hi]));
    };
    let layer = allLeaves.slice();
    let proof = [];
    let cur = idx;
    while (layer.length > 1) {
      const sibling = cur ^ 1;
      if (sibling < layer.length) proof.push(layer[sibling]);
      const next = [];
      for (let i = 0; i < layer.length; i += 2) {
        if (i + 1 < layer.length) next.push(hashPair(layer[i], layer[i + 1]));
        else next.push(layer[i]);
      }
      layer = next;
      cur = Math.floor(cur / 2);
    }
    return { root: layer[0], proof };
  }

  it("registers a model + reads it back", async () => {
    await expect(
      net.connect(provider).registerModel(
        MODEL_ID, MODEL_DIGEST, ATT_DIGEST, 2, "claude-4.7-sonnet-20251022", "ipfs://attestation"
      )
    ).to.emit(net, "ModelRegistered");
    const m = await net.modelOf(provider.address, MODEL_ID);
    expect(m.exists).to.equal(true);
    expect(m.modelDigest).to.equal(MODEL_DIGEST);
    expect(m.risk).to.equal(2);
  });

  it("rejects duplicate model registration + malformed inputs", async () => {
    await net.connect(provider).registerModel(MODEL_ID, MODEL_DIGEST, ATT_DIGEST, 2, "n", "u");
    await expect(
      net.connect(provider).registerModel(MODEL_ID, MODEL_DIGEST, ATT_DIGEST, 2, "n", "u")
    ).to.be.revertedWithCustomError(net, "ModelExists");
    await expect(
      net.connect(outsider).registerModel(MODEL_ID, ethers.ZeroHash, ATT_DIGEST, 2, "n", "u")
    ).to.be.revertedWithCustomError(net, "ZeroDigest");
    await expect(
      net.connect(outsider).registerModel(MODEL_ID, MODEL_DIGEST, ATT_DIGEST, 2, "", "u")
    ).to.be.revertedWithCustomError(net, "EmptyName");
  });

  it("commits an epoch only when model is registered", async () => {
    const root = ethers.keccak256(ethers.toUtf8Bytes("root-x"));
    const today = BigInt(Math.floor((await ethers.provider.getBlock("latest")).timestamp / 86400));
    await expect(
      net.connect(provider).commitEpoch(MODEL_ID, today, root, 100, "ipfs://meta")
    ).to.be.revertedWithCustomError(net, "ModelUnknown");
    await net.connect(provider).registerModel(MODEL_ID, MODEL_DIGEST, ATT_DIGEST, 2, "n", "u");
    await expect(
      net.connect(provider).commitEpoch(MODEL_ID, today, root, 100, "ipfs://meta")
    ).to.emit(net, "EpochCommitted").withArgs(provider.address, MODEL_ID, today, root, 100, "ipfs://meta");
    const e = await net.epochOf(provider.address, MODEL_ID, today);
    expect(e.root).to.equal(root);
    expect(e.inferenceCount).to.equal(100n);
    expect(await net.modelEpochCount(provider.address, MODEL_ID)).to.equal(1n);
  });

  it("rejects re-committing the same epoch + zero root + out-of-range epoch (audit fix HIGH #26)", async () => {
    const root = ethers.keccak256(ethers.toUtf8Bytes("root"));
    const today = BigInt(Math.floor((await ethers.provider.getBlock("latest")).timestamp / 86400));
    await net.connect(provider).registerModel(MODEL_ID, MODEL_DIGEST, ATT_DIGEST, 2, "n", "u");
    // Old epoch (>1 day in past) rejected.
    await expect(
      net.connect(provider).commitEpoch(MODEL_ID, 1n, root, 1, "")
    ).to.be.revertedWithCustomError(net, "EpochOutOfRange");
    // Future epoch rejected.
    await expect(
      net.connect(provider).commitEpoch(MODEL_ID, today + 5n, root, 1, "")
    ).to.be.revertedWithCustomError(net, "EpochOutOfRange");
    await expect(
      net.connect(provider).commitEpoch(MODEL_ID, today, ethers.ZeroHash, 1, "")
    ).to.be.revertedWithCustomError(net, "ZeroDigest");
    await net.connect(provider).commitEpoch(MODEL_ID, today, root, 1, "");
    await expect(
      net.connect(provider).commitEpoch(MODEL_ID, today, root, 1, "")
    ).to.be.revertedWithCustomError(net, "EpochExists");
  });

  it("verifyInference proves inclusion against committed root", async () => {
    await net.connect(provider).registerModel(MODEL_ID, MODEL_DIGEST, ATT_DIGEST, 2, "n", "u");

    const leaves = [];
    const targets = [];
    for (let i = 0; i < 4; i++) {
      const req = ethers.keccak256(ethers.toUtf8Bytes(`req-${i}`));
      const resp = ethers.keccak256(ethers.toUtf8Bytes(`resp-${i}`));
      const ts = BigInt(1717880000000 + i * 1000);
      const dep = ethers.keccak256(ethers.toUtf8Bytes(`deployer-${i}`));
      leaves.push(buildLeaf(req, resp, ts, dep));
      targets.push({ req, resp, ts, dep });
    }

    const idx = 2;
    const { root, proof } = buildTreeWithProof(leaves, idx);
    const today = BigInt(Math.floor((await ethers.provider.getBlock("latest")).timestamp / 86400));
    await net.connect(provider).commitEpoch(MODEL_ID, today, root, 4, "");

    const t = targets[idx];
    expect(await net.verifyInference(provider.address, MODEL_ID, today, t.req, t.resp, t.ts, t.dep, proof))
      .to.equal(true);

    // Wrong leaf → false.
    expect(await net.verifyInference(
      provider.address, MODEL_ID, today,
      ethers.keccak256(ethers.toUtf8Bytes("not-req")), t.resp, t.ts, t.dep, proof
    )).to.equal(false);

    // Unknown epoch → false (no revert).
    expect(await net.verifyInference(provider.address, MODEL_ID, 999999n, t.req, t.resp, t.ts, t.dep, proof))
      .to.equal(false);
  });
});
