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

describe("AereAgentMemoryVault", () => {
  let user, agent1, agent2, outsider;
  let v;
  const SLOT_PREFS = ethers.keccak256(ethers.toUtf8Bytes("user.preferences"));
  const SLOT_TONE = ethers.keccak256(ethers.toUtf8Bytes("user.tone"));
  const HASH = ethers.keccak256(ethers.toUtf8Bytes("encrypted-blob"));

  beforeEach(async () => {
    [user, agent1, agent2, outsider] = await ethers.getSigners();
    const F = await ethers.getContractFactory("AereAgentMemoryVault");
    v = await F.deploy();
    await v.waitForDeployment();
  });

  async function nowTs() {
    return (await ethers.provider.getBlock("latest")).timestamp;
  }

  it("user grants permission + agent writes + head updates", async () => {
    await v.connect(user).grant(agent1.address, SLOT_PREFS, true, true, 0);
    await expect(
      v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "ipfs://blob")
    ).to.emit(v, "EntryWritten").withArgs(user.address, SLOT_PREFS, agent1.address, 1n, HASH, "ipfs://blob");

    const head = await v.headOf(user.address, SLOT_PREFS);
    expect(head.contentHash).to.equal(HASH);
    expect(head.writer).to.equal(agent1.address);
    expect(head.version).to.equal(1n);
    expect(await v.nextVersion(user.address, SLOT_PREFS)).to.equal(1n);
  });

  it("write reverts without canWrite or with empty hash", async () => {
    await expect(
      v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "")
    ).to.be.revertedWithCustomError(v, "NotWriter");
    await v.connect(user).grant(agent1.address, SLOT_PREFS, true, false, 0); // read-only
    await expect(
      v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "")
    ).to.be.revertedWithCustomError(v, "NotWriter");
    await v.connect(user).grant(agent1.address, SLOT_PREFS, true, true, 0);
    await expect(
      v.connect(agent1).write(user.address, SLOT_PREFS, ethers.ZeroHash, "")
    ).to.be.revertedWithCustomError(v, "EmptyHash");
  });

  it("permission expiry blocks new writes", async () => {
    const t = await nowTs();
    await v.connect(user).grant(agent1.address, SLOT_PREFS, true, true, t + 5);
    await v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "");
    await ethers.provider.send("evm_increaseTime", [10]);
    await ethers.provider.send("evm_mine", []);
    await expect(
      v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "")
    ).to.be.revertedWithCustomError(v, "PermissionExpired");
  });

  it("grant with already-elapsed validUntil reverts", async () => {
    const t = await nowTs();
    await expect(
      v.connect(user).grant(agent1.address, SLOT_PREFS, true, true, t - 1)
    ).to.be.revertedWithCustomError(v, "InvalidValidity");
  });

  it("user revoke blocks future writes + flips canReadNow", async () => {
    await v.connect(user).grant(agent1.address, SLOT_PREFS, true, true, 0);
    expect(await v.canReadNow(user.address, agent1.address, SLOT_PREFS)).to.equal(true);
    await v.connect(user).revoke(agent1.address, SLOT_PREFS);
    expect(await v.canReadNow(user.address, agent1.address, SLOT_PREFS)).to.equal(false);
    await expect(
      v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "")
    ).to.be.revertedWithCustomError(v, "NotWriter");
  });

  it("revoke without prior grant reverts", async () => {
    await expect(
      v.connect(user).revoke(agent1.address, SLOT_PREFS)
    ).to.be.revertedWithCustomError(v, "NoSuchPermission");
  });

  it("portability — second agent gets the same head after fresh grant", async () => {
    await v.connect(user).grant(agent1.address, SLOT_PREFS, true, true, 0);
    await v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "ipfs://v1");
    await v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "ipfs://v2");
    const headV1 = await v.headOf(user.address, SLOT_PREFS);
    expect(headV1.version).to.equal(2n);

    // User onboards agent2 with read-only access.
    await v.connect(user).grant(agent2.address, SLOT_PREFS, true, false, 0);
    expect(await v.canReadNow(user.address, agent2.address, SLOT_PREFS)).to.equal(true);
    const headRead = await v.headOf(user.address, SLOT_PREFS);
    expect(headRead.version).to.equal(2n);

    // But agent2 cannot write yet.
    await expect(
      v.connect(agent2).write(user.address, SLOT_PREFS, HASH, "")
    ).to.be.revertedWithCustomError(v, "NotWriter");
  });

  it("each user namespace is isolated", async () => {
    await v.connect(user).grant(agent1.address, SLOT_PREFS, true, true, 0);
    await v.connect(agent1).write(user.address, SLOT_PREFS, HASH, "");
    // outsider's namespace untouched.
    const otherHead = await v.headOf(outsider.address, SLOT_PREFS);
    expect(otherHead.version).to.equal(0n);
    // Agent has no permission in outsider's namespace.
    await expect(
      v.connect(agent1).write(outsider.address, SLOT_PREFS, HASH, "")
    ).to.be.revertedWithCustomError(v, "NotWriter");
  });

  it("per-slot grain: granting one slot does not grant another", async () => {
    await v.connect(user).grant(agent1.address, SLOT_PREFS, true, true, 0);
    await expect(
      v.connect(agent1).write(user.address, SLOT_TONE, HASH, "")
    ).to.be.revertedWithCustomError(v, "NotWriter");
  });
});
