// AereZKScreen + AereAIProof tests.

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

async function deployZkStack() {
  const [deployer, alice, bob] = await ethers.getSigners();
  const Verifier = await ethers.getContractFactory("MockSp1Verifier");
  const verifier = await Verifier.deploy();
  await verifier.waitForDeployment();
  const ZK = await ethers.getContractFactory("AereZKScreen");
  const zk = await ZK.deploy(await verifier.getAddress());
  await zk.waitForDeployment();
  return { deployer, alice, bob, verifier, zk };
}

// Build publicValues + pre-program the void mock to accept the exact triple.
async function makeProof(verifier, vKey, user, attestedAt, extraDataHash, proof = "0xdeadbeef") {
  const pv = ethers.AbiCoder.defaultAbiCoder().encode(
    ["uint256", "address", "uint256", "bytes32"],
    [(await ethers.provider.getNetwork()).chainId, user, attestedAt, extraDataHash]
  );
  const marker = ethers.keccak256(ethers.AbiCoder.defaultAbiCoder().encode(
    ["bytes32", "bytes", "bytes"], [vKey, pv, proof]
  ));
  await verifier.setValid(marker, true);
  return { pv, proof };
}

describe("AereZKScreen", function () {
  const ROOT = ethers.id("authorized-allowlist-root-v1");

  it("Foundation registers program; duplicate rejected; zero root rejected", async function () {
    const { zk } = await deployZkStack();
    const vKey = ethers.id("OFAC-screen-v1");
    await zk.registerProgram(vKey, 90 * 86400, ROOT, "OFAC SDN screen v1");
    await expect(zk.registerProgram(vKey, 0, ROOT, "")).to.be.revertedWithCustomError(zk, "AlreadyRegistered");
    const vKey2 = ethers.id("zero-root");
    await expect(zk.registerProgram(vKey2, 0, ethers.ZeroHash, "")).to.be.revertedWithCustomError(zk, "ZeroRoot");
  });

  it("submits valid proof (root == authorizedRoot); isCleared true", async function () {
    const { alice, verifier, zk } = await deployZkStack();
    const vKey = ethers.id("OFAC-screen-v1");
    await zk.registerProgram(vKey, 90 * 86400, ROOT, "OFAC");
    const attestedAt = (await ethers.provider.getBlock("latest")).timestamp - 60;
    const { pv, proof } = await makeProof(verifier, vKey, alice.address, attestedAt, ROOT);
    await zk.connect(alice).submitProof(vKey, pv, proof);
    expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
  });

  it("rejects proof whose root != authorizedRoot (self-clear attack)", async function () {
    const { alice, verifier, zk } = await deployZkStack();
    const vKey = ethers.id("OFAC-screen-v1");
    await zk.registerProgram(vKey, 0, ROOT, "OFAC");
    // Attacker proves membership in a root of their OWN making.
    const attackerRoot = ethers.id("attacker-picked-root");
    const attestedAt = (await ethers.provider.getBlock("latest")).timestamp - 60;
    const { pv, proof } = await makeProof(verifier, vKey, alice.address, attestedAt, attackerRoot);
    await expect(zk.connect(alice).submitProof(vKey, pv, proof))
      .to.be.revertedWithCustomError(zk, "UnauthorizedRoot");
    expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(false);
  });

  it("rejects proof for wrong user", async function () {
    const { alice, bob, verifier, zk } = await deployZkStack();
    const vKey = ethers.id("OFAC-screen-v1");
    await zk.registerProgram(vKey, 0, ROOT, "OFAC");
    const attestedAt = (await ethers.provider.getBlock("latest")).timestamp - 60;
    const { pv, proof } = await makeProof(verifier, vKey, bob.address, attestedAt, ROOT);
    await expect(zk.connect(alice).submitProof(vKey, pv, proof))
      .to.be.revertedWithCustomError(zk, "UserMismatch");
  });

  it("rejects invalid proof", async function () {
    const { alice, zk } = await deployZkStack();
    const vKey = ethers.id("v");
    await zk.registerProgram(vKey, 0, ROOT, "x");
    const pv = ethers.AbiCoder.defaultAbiCoder().encode(
      ["uint256", "address", "uint256", "bytes32"],
      [(await ethers.provider.getNetwork()).chainId, alice.address, 100, ROOT]
    );
    await expect(zk.connect(alice).submitProof(vKey, pv, "0x"))
      .to.be.revertedWithCustomError(zk, "InvalidProof");
  });

  it("expiry causes isCleared to flip false", async function () {
    const { alice, verifier, zk } = await deployZkStack();
    const vKey = ethers.id("expiry-test");
    await zk.registerProgram(vKey, 100, ROOT, "short");
    const ts = (await ethers.provider.getBlock("latest")).timestamp - 50;
    const { pv, proof } = await makeProof(verifier, vKey, alice.address, ts, ROOT, "0x01");
    await zk.connect(alice).submitProof(vKey, pv, proof);
    expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
    await ethers.provider.send("evm_increaseTime", [200]);
    await ethers.provider.send("evm_mine", []);
    expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(false);
  });

  it("setAuthorizedRoot rotates the accepted root", async function () {
    const { alice, verifier, zk } = await deployZkStack();
    const vKey = ethers.id("rotate");
    await zk.registerProgram(vKey, 0, ROOT, "rot");
    const newRoot = ethers.id("rotated-root");
    // Proof against the OLD root now fails after rotation.
    await zk.setAuthorizedRoot(vKey, newRoot);
    const attestedAt = (await ethers.provider.getBlock("latest")).timestamp - 30;
    const old = await makeProof(verifier, vKey, alice.address, attestedAt, ROOT);
    await expect(zk.connect(alice).submitProof(vKey, old.pv, old.proof))
      .to.be.revertedWithCustomError(zk, "UnauthorizedRoot");
    // Proof against the NEW root succeeds.
    const fresh = await makeProof(verifier, vKey, alice.address, attestedAt, newRoot);
    await zk.connect(alice).submitProof(vKey, fresh.pv, fresh.proof);
    expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
    await expect(zk.setAuthorizedRoot(vKey, ethers.ZeroHash)).to.be.revertedWithCustomError(zk, "ZeroRoot");
  });

  it("revoke voids a clearance; a fresh newer proof re-clears", async function () {
    const { alice, verifier, zk } = await deployZkStack();
    const vKey = ethers.id("revoke-test");
    await zk.registerProgram(vKey, 0, ROOT, "rev");
    const t1 = (await ethers.provider.getBlock("latest")).timestamp - 100;
    const p1 = await makeProof(verifier, vKey, alice.address, t1, ROOT);
    await zk.connect(alice).submitProof(vKey, p1.pv, p1.proof);
    expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);

    await zk.revoke(alice.address, vKey);
    expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(false);

    // A fresh proof with attestedAt strictly after the revocation re-clears.
    await ethers.provider.send("evm_increaseTime", [10]);
    await ethers.provider.send("evm_mine", []);
    const t2 = (await ethers.provider.getBlock("latest")).timestamp;
    const p2 = await makeProof(verifier, vKey, alice.address, t2, ROOT);
    await zk.connect(alice).submitProof(vKey, p2.pv, p2.proof);
    expect(await zk.isCleared(alice.address, vKey, 0)).to.equal(true);
  });

  it("only owner can register, rotate, revoke", async function () {
    const { alice, zk } = await deployZkStack();
    const vKey = ethers.id("owner-test");
    await expect(zk.connect(alice).registerProgram(vKey, 0, ROOT, "x"))
      .to.be.revertedWith("Ownable: caller is not the owner");
    await zk.registerProgram(vKey, 0, ROOT, "x");
    await expect(zk.connect(alice).setAuthorizedRoot(vKey, ethers.id("y")))
      .to.be.revertedWith("Ownable: caller is not the owner");
    await expect(zk.connect(alice).revoke(alice.address, vKey))
      .to.be.revertedWith("Ownable: caller is not the owner");
  });
});

describe("AereAIProof", function () {
  async function deploy() {
    const [deployer, signer, operator, requester] = await ethers.getSigners();
    const AI = await ethers.getContractFactory("AereAIProof");
    const ai = await AI.deploy();
    await ai.waitForDeployment();
    return { deployer, signer, operator, requester, ai };
  }

  async function signAttestation(wallet, ai, attest) {
    const domain = {
      name: "AereAIProof", version: "1",
      chainId: (await ethers.provider.getNetwork()).chainId,
      verifyingContract: await ai.getAddress(),
    };
    const types = {
      Attestation: [
        { name: "modelId", type: "bytes32" },
        { name: "inputHash", type: "bytes32" },
        { name: "outputHash", type: "bytes32" },
        { name: "requesterHash", type: "bytes32" },
        { name: "timestamp", type: "uint256" },
        { name: "nonce", type: "uint256" },
      ],
    };
    return wallet.signTypedData(domain, types, attest);
  }

  it("operator registers model; signer rotation works", async function () {
    const { operator, signer, requester, ai } = await deploy();
    const modelId = ethers.id("claude-opus-4.7");
    await ai.connect(operator).registerModel(modelId, signer.address, "Claude Opus 4.7");
    const m = await ai.models(modelId);
    expect(m.signer).to.equal(signer.address);
    expect(m.operator).to.equal(operator.address);
    await ai.connect(operator).setSigner(modelId, requester.address);
    expect((await ai.models(modelId)).signer).to.equal(requester.address);
  });

  it("anchors a valid attestation; rejects replay + bad-sig + wrong-chain timestamp", async function () {
    const { operator, signer, requester, ai } = await deploy();
    const modelId = ethers.id("claude-opus-4.7");
    await ai.connect(operator).registerModel(modelId, signer.address, "Claude Opus 4.7");

    const inputHash    = ethers.id("input-X");
    const outputHash   = ethers.id("output-Y");
    const requesterHash= ethers.id("user-Z-nonce-1");
    const ts = (await ethers.provider.getBlock("latest")).timestamp - 10;
    const nonce = 1n;
    const attest = { modelId, inputHash, outputHash, requesterHash, timestamp: ts, nonce };

    const sig = await signAttestation(signer, ai, attest);
    await expect(ai.connect(requester).anchor(modelId, inputHash, outputHash, requesterHash, ts, nonce, sig))
      .to.emit(ai, "Anchored");
    expect(await ai.attestationCount(modelId)).to.equal(1n);

    // Replay rejected.
    await expect(ai.connect(requester).anchor(modelId, inputHash, outputHash, requesterHash, ts, nonce, sig))
      .to.be.revertedWithCustomError(ai, "NonceConsumed");

    // Wrong signer rejected.
    const badSig = await signAttestation(operator, ai, { ...attest, nonce: 2n });
    await expect(ai.connect(requester).anchor(modelId, inputHash, outputHash, requesterHash, ts, 2n, badSig))
      .to.be.revertedWithCustomError(ai, "BadSignature");

    // Future timestamp rejected.
    const future = (await ethers.provider.getBlock("latest")).timestamp + 3600;
    const futureSig = await signAttestation(signer, ai, { ...attest, nonce: 3n, timestamp: future });
    await expect(ai.connect(requester).anchor(modelId, inputHash, outputHash, requesterHash, future, 3n, futureSig))
      .to.be.revertedWithCustomError(ai, "AttestationInFuture");
  });
});
