// AerePQCEncryptedIntent - quantum-safe encrypted-intent settlement rail.
//
// A user hands a designated solver an order whose CONTENTS are hidden from front-runners until
// settlement. The symmetric key protecting the intent is transported with ML-KEM-768 (FIPS 203),
// so the confidentiality is post-quantum. On chain the contract holds only ciphertext HASHES, a
// commitment, escrow and timeout logic; the heavy KEM/AEAD math runs off-chain.
//
// This suite drives the on-chain rail directly. It does NOT run real ML-KEM: the contract only
// checks byte LENGTHS (ek = 1184, kem ciphertext = 1088) and HASHES, so fixed-length filler bytes
// exercise every on-chain branch faithfully. Coverage:
//   - submit (happy path + every validation revert)
//   - honest settle (claim -> revealAndSettle with a matching reveal; escrow + bond paid to solver)
//   - solver-timeout refund (no-claim path, and claim-then-stall path where the bond is forfeited)
//   - reveal-mismatch (a wrong reveal reverts and the state is untouched; a later correct reveal wins)
//
// Run: npx hardhat test test/AerePQCEncryptedIntent.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");

const coder = ethers.AbiCoder.defaultAbiCoder();

const EK_LEN = 1184; // ML-KEM-768 encapsulation key
const CT_LEN = 1088; // ML-KEM-768 ciphertext
const HOUR = 3600;
const DAY = 24 * HOUR;

// deterministic filler bytes (keccak hash-chain), so tests are reproducible.
function fill(n, seedText) {
  const out = new Uint8Array(n);
  let seed = ethers.keccak256(ethers.toUtf8Bytes(seedText));
  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 ethers.hexlify(out);
}
// commitment == keccak256(abi.encode(intentPlaintext, salt)) - mirrors the contract exactly.
function commitmentFor(plaintext, salt) {
  return ethers.keccak256(coder.encode(["bytes", "bytes32"], [plaintext, salt]));
}

describe("AerePQCEncryptedIntent (quantum-safe encrypted-intent rail)", function () {
  let c, user, solver, otherSolver, relayer, stranger;
  let ek, ekHash;

  // A canonical intent plaintext + fresh salt + its commitment, and the two ciphertext blobs.
  const PLAINTEXT = ethers.hexlify(ethers.toUtf8Bytes("SWAP 100 AERE -> USDC minOut=95 deadline=+1h"));
  const SALT = ethers.keccak256(ethers.toUtf8Bytes("intent-salt-0"));
  const COMMIT = commitmentFor(PLAINTEXT, SALT);
  const KEM_CT = fill(CT_LEN, "kem-ct");
  const ENC_PAYLOAD = fill(96, "aead-blob"); // nonce(12)||ct||tag(16), any nonempty blob
  const ESCROW = ethers.parseEther("1");
  const MIN_BOND = ethers.parseEther("0.25");
  const CLAIM_WINDOW = 2 * HOUR;
  const SETTLE_WINDOW = 3 * HOUR;

  beforeEach(async function () {
    [user, solver, otherSolver, relayer, stranger] = await ethers.getSigners();
    const F = await ethers.getContractFactory("AerePQCEncryptedIntent");
    c = await F.deploy();
    await c.waitForDeployment();

    ek = fill(EK_LEN, "solver-ek-0");
    ekHash = ethers.keccak256(ek);
    await (await c.connect(solver).publishEncapKey(ek)).wait();
  });

  // Submit a standard intent from `user` to `solver`; returns the assigned intentId.
  async function submitStd(overrides = {}) {
    const o = {
      solver: solver.address,
      ekHash,
      kemCt: KEM_CT,
      payload: ENC_PAYLOAD,
      commit: COMMIT,
      claimWindow: CLAIM_WINDOW,
      settleWindow: SETTLE_WINDOW,
      minBond: MIN_BOND,
      escrow: ESCROW,
      ...overrides,
    };
    const id = await c.nextIntentId();
    await (
      await c
        .connect(user)
        .submitIntent(o.solver, o.ekHash, o.kemCt, o.payload, o.commit, o.claimWindow, o.settleWindow, o.minBond, {
          value: o.escrow,
        })
    ).wait();
    return id;
  }

  // =========================================================================
  //  Solver key registry
  // =========================================================================
  describe("encapsulation key registry", function () {
    it("publishes a key, stores its hash, and emits the full key", async function () {
      const sk = await c.solverKeyOf(solver.address);
      expect(sk.keyHash).to.equal(ekHash);
      expect(sk.epoch).to.equal(1n);
      expect(sk.active).to.equal(true);
    });

    it("rejects a wrong-length encapsulation key", async function () {
      await expect(c.connect(otherSolver).publishEncapKey(fill(EK_LEN - 1, "short")))
        .to.be.revertedWithCustomError(c, "BadEncapKeyLength");
    });

    it("rotation bumps the epoch and changes the bound hash", async function () {
      const ek2 = fill(EK_LEN, "solver-ek-1");
      await (await c.connect(solver).publishEncapKey(ek2)).wait();
      const sk = await c.solverKeyOf(solver.address);
      expect(sk.epoch).to.equal(2n);
      expect(sk.keyHash).to.equal(ethers.keccak256(ek2));
      // an intent that binds the OLD hash is now stale
      await expect(submitStd({ ekHash })).to.be.revertedWithCustomError(c, "StaleEncapKey");
    });

    it("retire blocks new intents but does not touch existing ones", async function () {
      const id = await submitStd();
      await (await c.connect(solver).retireEncapKey()).wait();
      await expect(submitStd()).to.be.revertedWithCustomError(c, "SolverKeyInactive");
      // the already-open intent can still be claimed + settled
      await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait();
      await (await c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)).wait();
      expect(await c.statusOf(id)).to.equal(3n); // Settled
    });
  });

  // =========================================================================
  //  submit validation
  // =========================================================================
  describe("submitIntent validation", function () {
    it("stores hashes + commitment and locks the escrow; emits IntentSubmitted", async function () {
      const id = await c.nextIntentId();
      await expect(
        c
          .connect(user)
          .submitIntent(solver.address, ekHash, KEM_CT, ENC_PAYLOAD, COMMIT, CLAIM_WINDOW, SETTLE_WINDOW, MIN_BOND, {
            value: ESCROW,
          })
      )
        .to.emit(c, "IntentSubmitted")
        .withArgs(id, user.address, solver.address, ekHash, COMMIT, ESCROW, MIN_BOND, anyValue, KEM_CT, ENC_PAYLOAD);

      const it = await c.getIntent(id);
      expect(it.user).to.equal(user.address);
      expect(it.solver).to.equal(solver.address);
      expect(it.status).to.equal(1n); // Open
      expect(it.escrow).to.equal(ESCROW);
      expect(it.kemCtHash).to.equal(ethers.keccak256(KEM_CT));
      expect(it.payloadHash).to.equal(ethers.keccak256(ENC_PAYLOAD));
      expect(it.commitment).to.equal(COMMIT);
      expect(await ethers.provider.getBalance(await c.getAddress())).to.equal(ESCROW);
    });

    it("rejects an inactive/unknown solver", async function () {
      await expect(submitStd({ solver: otherSolver.address }))
        .to.be.revertedWithCustomError(c, "SolverKeyInactive");
    });

    it("rejects a stale ekHash", async function () {
      await expect(submitStd({ ekHash: ethers.ZeroHash }))
        .to.be.revertedWithCustomError(c, "StaleEncapKey");
    });

    it("rejects a wrong-length KEM ciphertext", async function () {
      await expect(submitStd({ kemCt: fill(CT_LEN - 1, "badct") }))
        .to.be.revertedWithCustomError(c, "BadCiphertextLength");
    });

    it("rejects an empty payload", async function () {
      await expect(submitStd({ payload: "0x" })).to.be.revertedWithCustomError(c, "EmptyPayload");
    });

    it("rejects a zero commitment", async function () {
      await expect(submitStd({ commit: ethers.ZeroHash })).to.be.revertedWithCustomError(c, "ZeroCommitment");
    });

    it("rejects out-of-range claim/settle windows", async function () {
      await expect(submitStd({ claimWindow: 8 * DAY })).to.be.revertedWithCustomError(c, "ClaimWindowOutOfRange");
      await expect(submitStd({ claimWindow: 0 })).to.be.revertedWithCustomError(c, "ClaimWindowOutOfRange");
      await expect(submitStd({ settleWindow: 8 * DAY })).to.be.revertedWithCustomError(c, "SettleWindowOutOfRange");
      await expect(submitStd({ settleWindow: 0 })).to.be.revertedWithCustomError(c, "SettleWindowOutOfRange");
    });

    it("commitmentFor view matches the off-chain derivation", async function () {
      expect(await c.commitmentFor(PLAINTEXT, SALT)).to.equal(COMMIT);
    });
  });

  // =========================================================================
  //  Honest settle: submit -> claim -> revealAndSettle
  // =========================================================================
  describe("honest settle", function () {
    it("solver claims, reveals the committed intent, and receives escrow + returned bond", async function () {
      const id = await submitStd();

      // only the designated solver may claim
      await expect(c.connect(stranger).claimIntent(id, { value: MIN_BOND }))
        .to.be.revertedWithCustomError(c, "NotDesignatedSolver");
      // bond below minimum is rejected
      await expect(c.connect(solver).claimIntent(id, { value: MIN_BOND - 1n }))
        .to.be.revertedWithCustomError(c, "InsufficientBond");

      const bond = ethers.parseEther("0.4"); // >= minBond
      await expect(c.connect(solver).claimIntent(id, { value: bond })).to.emit(c, "IntentClaimed");
      let it = await c.getIntent(id);
      expect(it.status).to.equal(2n); // Claimed
      expect(it.bond).to.equal(bond);

      // settle with the correct reveal; measure the solver's net gain (escrow + bond) net of gas.
      const before = await ethers.provider.getBalance(solver.address);
      const tx = await c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT);
      const rc = await tx.wait();
      const after = await ethers.provider.getBalance(solver.address);
      expect(after - before).to.equal(ESCROW + bond - rc.fee);

      it = await c.getIntent(id);
      expect(it.status).to.equal(3n); // Settled
      expect(it.escrow).to.equal(0n);
      expect(it.bond).to.equal(0n);
      // contract fully drained for this intent
      expect(await ethers.provider.getBalance(await c.getAddress())).to.equal(0n);
    });

    it("emits IntentSettled carrying the now-public plaintext", async function () {
      const id = await submitStd();
      await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait();
      await expect(c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT))
        .to.emit(c, "IntentSettled")
        .withArgs(id, solver.address, ESCROW, PLAINTEXT);
    });

    it("works with zero escrow and zero minBond", async function () {
      const id = await submitStd({ escrow: 0n, minBond: 0n });
      await (await c.connect(solver).claimIntent(id, { value: 0 })).wait();
      await (await c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)).wait();
      expect(await c.statusOf(id)).to.equal(3n);
    });

    it("rejects settle before claim (must be Claimed)", async function () {
      const id = await submitStd();
      await expect(c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT))
        .to.be.revertedWithCustomError(c, "WrongStatus");
    });

    it("only the designated solver may settle", async function () {
      const id = await submitStd();
      await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait();
      await expect(c.connect(stranger).revealAndSettle(id, PLAINTEXT, SALT))
        .to.be.revertedWithCustomError(c, "NotDesignatedSolver");
    });

    it("cannot claim after the claim window closes", async function () {
      const id = await submitStd();
      await time.increase(CLAIM_WINDOW + 60);
      await expect(c.connect(solver).claimIntent(id, { value: MIN_BOND }))
        .to.be.revertedWithCustomError(c, "ClaimWindowClosed");
    });

    it("cannot settle after the settle window closes", async function () {
      const id = await submitStd();
      await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait();
      await time.increase(SETTLE_WINDOW + 60);
      await expect(c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT))
        .to.be.revertedWithCustomError(c, "SettleWindowClosed");
    });

    it("cannot double-claim a claimed intent", async function () {
      const id = await submitStd();
      await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait();
      await expect(c.connect(solver).claimIntent(id, { value: MIN_BOND }))
        .to.be.revertedWithCustomError(c, "WrongStatus");
    });
  });

  // =========================================================================
  //  Solver-timeout refund
  // =========================================================================
  describe("solver-timeout refund", function () {
    it("no-claim: after the claim window the user reclaims the full escrow", async function () {
      const id = await submitStd();
      // too early
      await expect(c.connect(relayer).timeoutRefund(id)).to.be.revertedWithCustomError(c, "NotYetRefundable");
      expect(await c.isRefundable(id)).to.equal(false);

      await time.increase(CLAIM_WINDOW + 60);
      expect(await c.isRefundable(id)).to.equal(true);

      // permissionless poke; the refund always goes to the user (not the caller)
      await expect(c.connect(relayer).timeoutRefund(id)).to.changeEtherBalances(
        [user, await c.getAddress()],
        [ESCROW, -ESCROW]
      );
      expect(await c.statusOf(id)).to.equal(4n); // Refunded
      // cannot refund twice
      await expect(c.connect(relayer).timeoutRefund(id)).to.be.revertedWithCustomError(c, "WrongStatus");
    });

    it("claim-then-stall: the solver's bond is forfeited to the user with the escrow", async function () {
      const id = await submitStd();
      const bond = ethers.parseEther("0.5");
      await (await c.connect(solver).claimIntent(id, { value: bond })).wait();

      // still not refundable during the settle window
      await time.increase(SETTLE_WINDOW - 60);
      await expect(c.connect(relayer).timeoutRefund(id)).to.be.revertedWithCustomError(c, "NotYetRefundable");

      // past the settle deadline: user gets escrow + forfeited bond, contract drains to 0
      await time.increase(120);
      await expect(c.connect(relayer).timeoutRefund(id))
        .to.emit(c, "IntentRefunded")
        .withArgs(id, user.address, ESCROW, bond);
      await expect(c.connect(relayer).timeoutRefund(id)).to.be.revertedWithCustomError(c, "WrongStatus"); // already refunded
      expect(await ethers.provider.getBalance(await c.getAddress())).to.equal(0n);
    });

    it("timeoutRefund reverts for an unknown intent", async function () {
      await expect(c.connect(relayer).timeoutRefund(999)).to.be.revertedWithCustomError(c, "UnknownIntent");
    });
  });

  // =========================================================================
  //  Reveal mismatch
  // =========================================================================
  describe("reveal mismatch", function () {
    it("a wrong plaintext is rejected and the intent is untouched; the correct reveal then settles", async function () {
      const id = await submitStd();
      await (await c.connect(solver).claimIntent(id, { value: MIN_BOND })).wait();

      const wrongPlaintext = ethers.hexlify(ethers.toUtf8Bytes("SWAP 100 AERE -> USDC minOut=1 (tampered)"));
      await expect(c.connect(solver).revealAndSettle(id, wrongPlaintext, SALT))
        .to.be.revertedWithCustomError(c, "CommitmentMismatch");
      // still Claimed, escrow intact
      let it = await c.getIntent(id);
      expect(it.status).to.equal(2n);
      expect(it.escrow).to.equal(ESCROW);

      // a wrong SALT with the right plaintext is also rejected
      await expect(c.connect(solver).revealAndSettle(id, PLAINTEXT, ethers.keccak256(ethers.toUtf8Bytes("wrong-salt"))))
        .to.be.revertedWithCustomError(c, "CommitmentMismatch");

      // the correct reveal still works (retry within the window)
      await (await c.connect(solver).revealAndSettle(id, PLAINTEXT, SALT)).wait();
      it = await c.getIntent(id);
      expect(it.status).to.equal(3n); // Settled
    });
  });
});
