// Hardhat tests for AereVectorStore (Aere VSM, verifiable semantic memory).
//
// Covers exactly the acceptance cases:
//   - commit a store version (with a mock-Falcon-512-verified signature via 0x0AE1)
//   - commit a second version (append-only history preserved)
//   - a tampered Falcon signature is rejected fail-closed (nothing recorded)
//   - a retrieval attestation against a valid version records; a missing version reverts
//   - an inclusion proof for a committed vector verifies; a non-member fails
//   - AERE402 paid-query rail: paidQuery settles through the real facilitator, and a priced
//     store's attestRetrieval is gated on the payment receipt (unpaid reverts fail-closed)
//   - classical (owner-signed) store commits + non-owner rejection
//   - createStore fail-closed on a malformed Falcon key / inconsistent price config
//
// Falcon is MOCKED here: Hardhat has no PQC precompile, so we install MockPQCPrecompile
// (scheme 1) at 0x0AE1 via hardhat_setCode. The mock parses the input at the precompile's
// SPEC offsets and accepts iff commitment == keccak256(pk || message). The REAL Falcon-512
// encoding is separately proven against the LIVE mainnet precompile (0x0AE1) elsewhere in the
// repo (AerePQCAttestation). The embedding model + similarity search + vector data are
// off-chain [MEASURE]; this test exercises only the on-chain provenance / attestation / pay path.
//
// Run: npx hardhat test test/aere-vector-store.test.js

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

const ONE = 10n ** 18n;
const FALCON512 = ethers.getAddress("0x0000000000000000000000000000000000000ae1");

// ---------------------------------------------------------------------------
// Falcon-512 mock-signature helpers (mirror AerePQCAttestation.test.js, scheme 1).
// ---------------------------------------------------------------------------
const PK_LEN = 897;
const PK_HEADER = 0x09;
const ESIG_HEADER = 0x29;

function randBytes(n, salt) {
  // deterministic keccak hash-chain so the test is fully reproducible
  const out = new Uint8Array(n);
  let seed = ethers.keccak256(ethers.toUtf8Bytes("vsm-seed-" + salt));
  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;
}

function makeFalconPubKey(salt) {
  const raw = randBytes(PK_LEN, salt);
  raw[0] = PK_HEADER; // Falcon-512 requires the header byte
  return ethers.hexlify(raw);
}

function commitmentFor(pubKey, message) {
  return ethers.keccak256(ethers.concat([pubKey, message]));
}

// Falcon envelope handed to commit(): nonce(40) || esig ; esig = 0x29 || commitment(32).
function falconEnvelope(commitment, salt) {
  const nonce = randBytes(40, "nonce-" + salt);
  const esig = ethers.concat([new Uint8Array([ESIG_HEADER]), ethers.getBytes(commitment)]);
  return ethers.hexlify(ethers.concat([nonce, esig]));
}

function genuineFalconSig(pubKey, message, salt) {
  return falconEnvelope(commitmentFor(pubKey, message), salt);
}

function tamperFalconSig(sigHex) {
  const b = ethers.getBytes(sigHex);
  b[41] ^= 0x01; // flip a byte inside the commitment region -> genuine forgery
  return ethers.hexlify(b);
}

// ---------------------------------------------------------------------------
// Minimal OpenZeppelin-compatible Merkle tree (sorted-pair keccak256).
// ---------------------------------------------------------------------------
function hashPair(a, b) {
  return BigInt(a) < BigInt(b)
    ? ethers.keccak256(ethers.concat([a, b]))
    : ethers.keccak256(ethers.concat([b, a]));
}

function buildTree(leaves) {
  const layers = [leaves.slice()];
  while (layers[layers.length - 1].length > 1) {
    const prev = layers[layers.length - 1];
    const next = [];
    for (let i = 0; i < prev.length; i += 2) {
      if (i + 1 < prev.length) next.push(hashPair(prev[i], prev[i + 1]));
      else next.push(prev[i]); // odd node: promote
    }
    layers.push(next);
  }
  return layers;
}

function treeRoot(layers) {
  return layers[layers.length - 1][0];
}

function treeProof(layers, index) {
  const proof = [];
  let idx = index;
  for (let l = 0; l < layers.length - 1; l++) {
    const layer = layers[l];
    const pairIndex = idx ^ 1;
    if (pairIndex < layer.length) proof.push(layer[pairIndex]);
    idx = Math.floor(idx / 2);
  }
  return proof;
}

function leafOf(label) {
  return ethers.keccak256(ethers.toUtf8Bytes(label));
}

// ---------------------------------------------------------------------------
// EIP-712 AERE402 payment-auth signer (mirror agentic.test.js).
// ---------------------------------------------------------------------------
async function signAuth(signerWallet, fac, agentId, token, payee, amount, resourceId, nonce, deadline) {
  const domain = {
    name: "AERE402Facilitator",
    version: "1",
    chainId: (await ethers.provider.getNetwork()).chainId,
    verifyingContract: await fac.getAddress(),
  };
  const types = {
    PaymentAuth: [
      { name: "agentId", type: "uint256" },
      { name: "token", type: "address" },
      { name: "payee", type: "address" },
      { name: "amount", type: "uint256" },
      { name: "resourceId", type: "bytes32" },
      { name: "nonce", type: "uint256" },
      { name: "deadline", type: "uint256" },
    ],
  };
  const value = { agentId, token, payee, amount, resourceId, nonce, deadline };
  return signerWallet.signTypedData(domain, types, value);
}

describe("AereVectorStore (Aere VSM)", function () {
  let vsm, mockCode;
  let deployer, owner, provider, operator, signer, consumer;

  before(async function () {
    // Capture MockPQCPrecompile (scheme 1) runtime bytecode for hardhat_setCode installs.
    const F = await ethers.getContractFactory("MockPQCPrecompile");
    const mock = await F.deploy(1);
    await mock.waitForDeployment();
    mockCode = await ethers.provider.getCode(await mock.getAddress());
  });

  beforeEach(async function () {
    [deployer, owner, provider, operator, signer, consumer] = await ethers.getSigners();
    await network.provider.send("hardhat_setCode", [FALCON512, mockCode]);

    // The AERE402 rail: AereAgent registry + facilitator + a simple sink, referenced (not
    // redeployed) by the vector store. Facilitator address is pre-computed so the registry
    // can pin it immutably.
    const sink = await (await ethers.getContractFactory("MockSinkSimple")).deploy();
    await sink.waitForDeployment();
    const nonce0 = await ethers.provider.getTransactionCount(deployer.address);
    const futureFac = ethers.getCreateAddress({ from: deployer.address, nonce: nonce0 + 1 });
    const agent = await (await ethers.getContractFactory("AereAgent")).deploy(futureFac);
    await agent.waitForDeployment();
    const fac = await (await ethers.getContractFactory("AERE402Facilitator")).deploy(
      await agent.getAddress(),
      await sink.getAddress()
    );
    await fac.waitForDeployment();
    expect((await fac.getAddress()).toLowerCase()).to.equal(futureFac.toLowerCase());

    vsm = await (await ethers.getContractFactory("AereVectorStore")).deploy(await fac.getAddress());
    await vsm.waitForDeployment();

    // stash rail handles on the suite for the payment test
    this.agent = agent;
    this.fac = fac;
  });

  // -------------------------------------------------------------------------
  // 1 + 2: PQC commit v1, then v2 (append-only history preserved)
  // -------------------------------------------------------------------------
  it("commits a PQC store version 1, then version 2, preserving history append-only", async function () {
    const storeId = ethers.id("agent-memory://claude/user-42");
    const pk = makeFalconPubKey("store-a");
    await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);

    // vectorRoot v1 = Merkle root over 4 vector commitments
    const leavesV1 = ["v0", "v1", "v2", "v3"].map(leafOf);
    const treeV1 = buildTree(leavesV1);
    const rootV1 = treeRoot(treeV1);
    const uriV1 = "ipfs://QmIndexV1";

    const chal1 = await vsm.commitChallenge(storeId, 1, rootV1, 4, uriV1);
    const sig1 = genuineFalconSig(pk, chal1, "s1");
    await expect(vsm.connect(operator).commit(storeId, rootV1, 4, uriV1, sig1))
      .to.emit(vsm, "Committed")
      .withArgs(storeId, 1, rootV1, 4, chal1, uriV1);

    expect(await vsm.versionCount(storeId)).to.equal(1n);
    let cur = await vsm.currentRoot(storeId);
    expect(cur.version).to.equal(1n);
    expect(cur.vectorRoot).to.equal(rootV1);
    expect(cur.vectorCount).to.equal(4n);

    // version 2 with a larger index (5 vectors)
    const leavesV2 = ["v0", "v1", "v2", "v3", "v4"].map(leafOf);
    const rootV2 = treeRoot(buildTree(leavesV2));
    const uriV2 = "ipfs://QmIndexV2";
    const chal2 = await vsm.commitChallenge(storeId, 2, rootV2, 5, uriV2);
    const sig2 = genuineFalconSig(pk, chal2, "s2");
    await vsm.connect(operator).commit(storeId, rootV2, 5, uriV2, sig2);

    expect(await vsm.versionCount(storeId)).to.equal(2n);
    cur = await vsm.currentRoot(storeId);
    expect(cur.version).to.equal(2n);
    expect(cur.vectorRoot).to.equal(rootV2);

    // append-only: version 1 is still exactly as committed (not overwritten)
    const v1 = await vsm.getVersion(storeId, 1);
    expect(v1.vectorRoot).to.equal(rootV1);
    expect(v1.vectorCount).to.equal(4n);
    expect(v1.uri).to.equal(uriV1);
    const v2 = await vsm.getVersion(storeId, 2);
    expect(v2.vectorRoot).to.equal(rootV2);
    expect(await vsm.commitCount()).to.equal(2n);
  });

  // -------------------------------------------------------------------------
  // 3: tampered Falcon signature rejected fail-closed
  // -------------------------------------------------------------------------
  it("rejects a tampered Falcon signature fail-closed (nothing recorded)", async function () {
    const storeId = ethers.id("agent-memory://claude/tamper");
    const pk = makeFalconPubKey("store-t");
    await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);

    const root = treeRoot(buildTree(["a", "b"].map(leafOf)));
    const uri = "ipfs://QmTamper";
    const chal = await vsm.commitChallenge(storeId, 1, root, 2, uri);
    const bad = tamperFalconSig(genuineFalconSig(pk, chal, "st"));

    await expect(vsm.connect(operator).commit(storeId, root, 2, uri, bad)).to.be.revertedWithCustomError(
      vsm,
      "PQCVerificationFailed"
    );
    // fail-closed: no version, no head, commitCount unchanged
    expect(await vsm.versionCount(storeId)).to.equal(0n);
    await expect(vsm.currentRoot(storeId)).to.be.revertedWithCustomError(vsm, "UnknownVersion");
    expect(await vsm.commitCount()).to.equal(0n);

    // a signature for a DIFFERENT message (wrong version) is also rejected
    const chalWrong = await vsm.commitChallenge(storeId, 2, root, 2, uri);
    const sigWrongVersion = genuineFalconSig(pk, chalWrong, "sw");
    await expect(
      vsm.connect(operator).commit(storeId, root, 2, uri, sigWrongVersion)
    ).to.be.revertedWithCustomError(vsm, "PQCVerificationFailed");

    // a signature by a DIFFERENT key is rejected
    const otherPk = makeFalconPubKey("other");
    const sigOtherKey = genuineFalconSig(otherPk, chal, "so");
    await expect(
      vsm.connect(operator).commit(storeId, root, 2, uri, sigOtherKey)
    ).to.be.revertedWithCustomError(vsm, "PQCVerificationFailed");

    // control: the genuine signature still works after all the rejections
    const good = genuineFalconSig(pk, chal, "sg");
    await vsm.connect(operator).commit(storeId, root, 2, uri, good);
    expect(await vsm.versionCount(storeId)).to.equal(1n);
  });

  it("treats an empty precompile return (pre-fork / stale node) as invalid", async function () {
    const storeId = ethers.id("agent-memory://claude/prefork");
    const pk = makeFalconPubKey("store-pf");
    await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);
    const root = treeRoot(buildTree(["x", "y"].map(leafOf)));
    const chal = await vsm.commitChallenge(storeId, 1, root, 2, "u");
    const good = genuineFalconSig(pk, chal, "pf");
    // wipe the precompile: an empty account returns success + empty returndata
    await network.provider.send("hardhat_setCode", [FALCON512, "0x"]);
    await expect(vsm.connect(operator).commit(storeId, root, 2, "u", good)).to.be.revertedWithCustomError(
      vsm,
      "PQCVerificationFailed"
    );
    expect(await vsm.versionCount(storeId)).to.equal(0n);
  });

  // -------------------------------------------------------------------------
  // 4: retrieval attestation records against a valid version; missing version reverts
  // -------------------------------------------------------------------------
  it("records a retrieval attestation against a committed version and reverts on a missing version", async function () {
    const storeId = ethers.id("agent-memory://claude/retrieval");
    const pk = makeFalconPubKey("store-r");
    await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);

    const leaves = ["r0", "r1", "r2", "r3"].map(leafOf);
    const root = treeRoot(buildTree(leaves));
    const chal = await vsm.commitChallenge(storeId, 1, root, 4, "ipfs://QmR");
    await vsm.connect(operator).commit(storeId, root, 4, "ipfs://QmR", genuineFalconSig(pk, chal, "r"));

    const queryHash = ethers.id("query: what did the user say about post-quantum?");
    const resultRoot = treeRoot(buildTree(["r1", "r2"].map(leafOf))); // provider returned 2 results

    // valid version 1: records
    const tx = await vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, ethers.ZeroHash);
    const rc = await tx.wait();
    const ev = rc.logs.map((l) => vsm.interface.parseLog(l)).find((p) => p && p.name === "RetrievalAttested");
    const retrievalId = ev.args.retrievalId;
    expect(await vsm.retrievalCount(storeId)).to.equal(1n);

    const r = await vsm.getRetrieval(retrievalId);
    expect(r.exists).to.equal(true);
    expect(r.version).to.equal(1n);
    expect(r.provider).to.equal(provider.address);
    expect(r.queryHash).to.equal(queryHash);
    expect(r.resultRoot).to.equal(resultRoot);

    // missing version 2 (never committed): reverts fail-closed
    await expect(
      vsm.connect(provider).attestRetrieval(storeId, 2, queryHash, resultRoot, ethers.ZeroHash)
    ).to.be.revertedWithCustomError(vsm, "UnknownVersion");
    // version 0 is also invalid
    await expect(
      vsm.connect(provider).attestRetrieval(storeId, 0, queryHash, resultRoot, ethers.ZeroHash)
    ).to.be.revertedWithCustomError(vsm, "UnknownVersion");
    // still only one attestation recorded
    expect(await vsm.retrievalCount(storeId)).to.equal(1n);
  });

  // -------------------------------------------------------------------------
  // 5: inclusion proof for a committed vector verifies; non-member fails
  // -------------------------------------------------------------------------
  it("verifies an inclusion proof for a committed vector and rejects a non-member", async function () {
    const storeId = ethers.id("agent-memory://claude/inclusion");
    const pk = makeFalconPubKey("store-i");
    await vsm.connect(owner).createStore(storeId, pk, ethers.ZeroAddress, 0, ethers.ZeroAddress);

    const labels = ["vec-alpha", "vec-beta", "vec-gamma", "vec-delta", "vec-epsilon"];
    const leaves = labels.map(leafOf);
    const tree = buildTree(leaves);
    const root = treeRoot(tree);
    const chal = await vsm.commitChallenge(storeId, 1, root, leaves.length, "ipfs://QmInc");
    await vsm.connect(operator).commit(storeId, root, leaves.length, "ipfs://QmInc", genuineFalconSig(pk, chal, "i"));

    // member: vec-gamma (index 2) proves against the committed vectorRoot
    const memberLeaf = leaves[2];
    const memberProof = treeProof(tree, 2);
    expect(await vsm.verifyVectorInclusion(storeId, 1, memberLeaf, memberProof)).to.equal(true);

    // non-member: a leaf that was never in the tree fails with the same-shaped proof
    const nonMember = leafOf("vec-not-in-store");
    expect(await vsm.verifyVectorInclusion(storeId, 1, nonMember, memberProof)).to.equal(false);

    // result-root inclusion: provider attests a result set, consumer proves a returned result is in it
    const queryHash = ethers.id("q: gamma?");
    const resultLeaves = [leaves[2], leaves[4]]; // gamma + epsilon
    const resultTree = buildTree(resultLeaves);
    const resultRoot = treeRoot(resultTree);
    const tx = await vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, ethers.ZeroHash);
    const rc = await tx.wait();
    const retrievalId = rc.logs
      .map((l) => vsm.interface.parseLog(l))
      .find((p) => p && p.name === "RetrievalAttested").args.retrievalId;

    expect(await vsm.verifyResultInclusion(retrievalId, resultLeaves[0], treeProof(resultTree, 0))).to.equal(true);
    expect(await vsm.verifyResultInclusion(retrievalId, nonMember, treeProof(resultTree, 0))).to.equal(false);
  });

  // -------------------------------------------------------------------------
  // 6: AERE402 paid-query rail gates attestRetrieval on a priced store
  // -------------------------------------------------------------------------
  it("meters a priced store on the AERE402 rail and gates attestRetrieval on payment", async function () {
    const agent = this.agent;
    const fac = this.fac;

    // A classical, PRICED store: owner signs commits; queries cost 1 token; proceeds go to provider.
    const token = await (await ethers.getContractFactory("MockERC20Lending")).deploy("USD Coin e", "USDC.e", 18);
    await token.waitForDeployment();
    const storeId = ethers.id("agent-memory://claude/paid");
    const price = 1n * ONE;
    await vsm
      .connect(owner)
      .createStore(storeId, "0x", await token.getAddress(), price, provider.address);

    // owner commits version 1 (classical auth: msg.sender == owner)
    const leaves = ["p0", "p1", "p2"].map(leafOf);
    const root = treeRoot(buildTree(leaves));
    await vsm.connect(owner).commit(storeId, root, 3, "ipfs://QmPaid", "0x");
    expect(await vsm.versionCount(storeId)).to.equal(1n);

    // register + fund an AERE402 paying agent
    const regTx = await agent
      .connect(operator)
      .registerAgent(signer.address, ethers.ZeroHash, ethers.ZeroHash, 10n * ONE, 0, 0, "ipfs://agent");
    const agentId = (await regTx.wait()).logs.find((l) => l.fragment?.name === "AgentRegistered").args.agentId;
    await token.mint(operator.address, 100n * ONE);
    await token.connect(operator).approve(await agent.getAddress(), 100n * ONE);
    await agent.connect(operator).topUp(agentId, await token.getAddress(), 100n * ONE);

    const queryHash = ethers.id("query: recall my PQC preferences");

    // unpaid retrieval on a priced store is rejected fail-closed
    await expect(
      vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, root, ethers.ZeroHash)
    ).to.be.revertedWithCustomError(vsm, "PaymentRequired");

    // pay through the rail: the agent authorizes payment to the vector store (the payee)
    const amount = price;
    const rail_nonce = 1n;
    const deadline = (await ethers.provider.getBlock("latest")).timestamp + 3600;
    const resourceId = await vsm.queryResourceId(storeId, queryHash);
    const sig = await signAuth(
      signer,
      fac,
      agentId,
      await token.getAddress(),
      await vsm.getAddress(),
      amount,
      resourceId,
      rail_nonce,
      deadline
    );

    const provBefore = await token.balanceOf(provider.address);
    const payTx = await vsm
      .connect(consumer)
      .paidQuery(storeId, queryHash, provider.address, agentId, amount, rail_nonce, deadline, sig);
    const payRc = await payTx.wait();
    const receiptId = payRc.logs
      .map((l) => vsm.interface.parseLog(l))
      .find((p) => p && p.name === "QueryPaid").args.receiptId;

    // fee (25 bps) went to the sink; the remainder was forwarded to the provider (recipient)
    const fee = (amount * 25n) / 10_000n;
    expect((await token.balanceOf(provider.address)) - provBefore).to.equal(amount - fee);
    expect(await fac.consumed(agentId, rail_nonce)).to.equal(true);
    // the store custodies nothing across the call
    expect(await token.balanceOf(await vsm.getAddress())).to.equal(0n);

    const receipt = await vsm.getReceipt(receiptId);
    expect(receipt.exists).to.equal(true);
    expect(receipt.consumed).to.equal(false);
    expect(receipt.storeId).to.equal(storeId);
    expect(receipt.queryHash).to.equal(queryHash);
    expect(receipt.amount).to.equal(amount);
    expect(receipt.provider).to.equal(provider.address); // L2: receipt is bound to the intended provider

    // L2 (LOW) fix: a THIRD PARTY (not the bound provider) cannot front-run and consume the receipt
    // with a junk resultRoot to grief the honest provider. Even the payer (consumer) is not the
    // bound provider, so it too is rejected; the receipt stays unconsumed and usable.
    const junkRoot = ethers.id("junk-result-root-from-griefer");
    await expect(
      vsm.connect(operator).attestRetrieval(storeId, 1, queryHash, junkRoot, receiptId)
    ).to.be.revertedWithCustomError(vsm, "NotReceiptProvider");
    await expect(
      vsm.connect(consumer).attestRetrieval(storeId, 1, queryHash, junkRoot, receiptId)
    ).to.be.revertedWithCustomError(vsm, "NotReceiptProvider");
    expect((await vsm.getReceipt(receiptId)).consumed).to.equal(false);

    // now the retrieval attestation is allowed for the BOUND provider, and consumes the receipt one-shot
    const resultRoot = treeRoot(buildTree([leaves[0], leaves[1]]));
    await vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, receiptId);
    expect(await vsm.retrievalCount(storeId)).to.equal(1n);
    expect((await vsm.getReceipt(receiptId)).consumed).to.equal(true);

    // the same receipt cannot back a second retrieval
    await expect(
      vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, receiptId)
    ).to.be.revertedWithCustomError(vsm, "ReceiptConsumed");

    // a FRESH receipt paid for query B cannot back a retrieval that claims query A (ReceiptMismatch)
    const queryB = ethers.id("query: something else");
    const nonceB = 2n;
    const sigB = await signAuth(
      signer,
      fac,
      agentId,
      await token.getAddress(),
      await vsm.getAddress(),
      amount,
      await vsm.queryResourceId(storeId, queryB),
      nonceB,
      deadline
    );
    const rcB = await (await vsm.connect(consumer).paidQuery(storeId, queryB, provider.address, agentId, amount, nonceB, deadline, sigB)).wait();
    const receiptB = rcB.logs.map((l) => vsm.interface.parseLog(l)).find((p) => p && p.name === "QueryPaid").args.receiptId;
    await expect(
      vsm.connect(provider).attestRetrieval(storeId, 1, queryHash, resultRoot, receiptB)
    ).to.be.revertedWithCustomError(vsm, "ReceiptMismatch");
  });

  // -------------------------------------------------------------------------
  // 7: classical store owner auth + non-owner rejection
  // -------------------------------------------------------------------------
  it("classical store: only the owner may commit", async function () {
    const storeId = ethers.id("agent-memory://classical");
    await vsm.connect(owner).createStore(storeId, "0x", ethers.ZeroAddress, 0, ethers.ZeroAddress);

    const root = treeRoot(buildTree(["c0", "c1"].map(leafOf)));
    // non-owner attempt rejected
    await expect(vsm.connect(provider).commit(storeId, root, 2, "u", "0x")).to.be.revertedWithCustomError(
      vsm,
      "NotOwner"
    );
    // owner commit works (falconSig ignored for a classical store)
    await vsm.connect(owner).commit(storeId, root, 2, "u", "0x");
    expect(await vsm.versionCount(storeId)).to.equal(1n);
  });

  // -------------------------------------------------------------------------
  // 8: createStore fail-closed on malformed config
  // -------------------------------------------------------------------------
  it("createStore rejects a malformed Falcon key and inconsistent price config", async function () {
    const storeId = ethers.id("agent-memory://bad");
    // wrong-length Falcon key
    await expect(
      vsm.connect(owner).createStore(storeId, "0x" + "09" + "00".repeat(800), ethers.ZeroAddress, 0, ethers.ZeroAddress)
    ).to.be.revertedWithCustomError(vsm, "InvalidPubKey");
    // correct length but wrong header
    await expect(
      vsm.connect(owner).createStore(storeId, "0x" + "0a" + "00".repeat(896), ethers.ZeroAddress, 0, ethers.ZeroAddress)
    ).to.be.revertedWithCustomError(vsm, "InvalidPubKey");
    // priced store without a token / recipient
    await expect(
      vsm.connect(owner).createStore(storeId, "0x", ethers.ZeroAddress, 1n * ONE, ethers.ZeroAddress)
    ).to.be.revertedWithCustomError(vsm, "InvalidConfig");
    // free store that nonetheless sets a token
    await expect(
      vsm.connect(owner).createStore(storeId, "0x", owner.address, 0, ethers.ZeroAddress)
    ).to.be.revertedWithCustomError(vsm, "InvalidConfig");
    // duplicate storeId rejected
    await vsm.connect(owner).createStore(storeId, "0x", ethers.ZeroAddress, 0, ethers.ZeroAddress);
    await expect(
      vsm.connect(owner).createStore(storeId, "0x", ethers.ZeroAddress, 0, ethers.ZeroAddress)
    ).to.be.revertedWithCustomError(vsm, "StoreExists");
  });
});
