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

describe('VaultfireForumRegistry', function () {
  let registry;
  let alice, bob, carol;

  // Deterministic groupId fixtures (opaque MLS bytes — exact contents irrelevant)
  const GROUP_GENERAL    = '0x' + 'aa'.repeat(32);
  const GROUP_TECHNICAL  = '0x' + 'bb'.repeat(32);
  const GROUP_GENERAL_V2 = '0x' + 'cc'.repeat(32);
  const GROUP_LONG       = '0x' + 'dd'.repeat(256); // exact max length

  beforeEach(async function () {
    [alice, bob, carol] = await ethers.getSigners();
    const Factory = await ethers.getContractFactory('VaultfireForumRegistry');
    registry = await Factory.deploy();
    await registry.waitForDeployment();
  });

  describe('Initial state', function () {
    it('starts with zero categories', async function () {
      expect(await registry.categoryCount()).to.equal(0n);
      expect(await registry.listCategories()).to.deep.equal([]);
      expect(await registry.isRegistered('general')).to.equal(false);
    });

    it('returns zero values for an unregistered category', async function () {
      const [gid, pub, pAt, uAt] = await registry.getGroup('general');
      expect(gid).to.equal('0x');
      expect(pub).to.equal(ethers.ZeroAddress);
      expect(pAt).to.equal(0n);
      expect(uAt).to.equal(0n);
    });
  });

  describe('First registration (first-write-wins)', function () {
    it('lets anyone register a fresh category', async function () {
      const tx = await registry.connect(alice).registerGroup('general', GROUP_GENERAL);
      const receipt = await tx.wait();
      const block = await ethers.provider.getBlock(receipt.blockNumber);

      const [gid, pub, pAt, uAt] = await registry.getGroup('general');
      expect(gid).to.equal(GROUP_GENERAL);
      expect(pub).to.equal(alice.address);
      expect(pAt).to.equal(BigInt(block.timestamp));
      expect(uAt).to.equal(BigInt(block.timestamp));

      expect(await registry.isRegistered('general')).to.equal(true);
      expect(await registry.categoryCount()).to.equal(1n);
      expect(await registry.listCategories()).to.deep.equal(['general']);
    });

    it('emits GroupRegistered with all fields', async function () {
      await expect(registry.connect(alice).registerGroup('technical', GROUP_TECHNICAL))
        .to.emit(registry, 'GroupRegistered');
      // (timestamp varies — explicit field check above already verifies storage)
    });

    it('rejects an empty category', async function () {
      await expect(registry.connect(alice).registerGroup('', GROUP_GENERAL))
        .to.be.revertedWith('category required');
    });

    it('rejects a category > 64 bytes', async function () {
      const longCat = 'a'.repeat(65);
      await expect(registry.connect(alice).registerGroup(longCat, GROUP_GENERAL))
        .to.be.revertedWith('category too long');
    });

    it('rejects empty groupId bytes', async function () {
      await expect(registry.connect(alice).registerGroup('general', '0x'))
        .to.be.revertedWith('groupId required');
    });

    it('rejects a groupId > 256 bytes', async function () {
      const tooLong = '0x' + 'ee'.repeat(257);
      await expect(registry.connect(alice).registerGroup('general', tooLong))
        .to.be.revertedWith('groupId too long');
    });

    it('accepts a groupId of exactly 256 bytes', async function () {
      await expect(registry.connect(alice).registerGroup('general', GROUP_LONG)).to.not.be.reverted;
      const [gid] = await registry.getGroup('general');
      expect(gid).to.equal(GROUP_LONG);
    });
  });

  describe('Drive-by hijack protection', function () {
    it('rejects a second writer on an existing category', async function () {
      await registry.connect(alice).registerGroup('general', GROUP_GENERAL);
      await expect(registry.connect(bob).registerGroup('general', GROUP_GENERAL_V2))
        .to.be.revertedWith('not original publisher');
    });

    it('protects every category independently', async function () {
      await registry.connect(alice).registerGroup('general', GROUP_GENERAL);
      // Bob can still claim a different category
      await expect(registry.connect(bob).registerGroup('technical', GROUP_TECHNICAL)).to.not.be.reverted;
      // But not Alice's
      await expect(registry.connect(carol).registerGroup('general', GROUP_GENERAL_V2))
        .to.be.revertedWith('not original publisher');
    });
  });

  describe('Rotation by original publisher', function () {
    it('lets the original publisher overwrite their own entry', async function () {
      await registry.connect(alice).registerGroup('general', GROUP_GENERAL);
      const [, , publishedAt0] = await registry.getGroup('general');

      // Wait at least one block so updatedAt differs
      await ethers.provider.send('evm_increaseTime', [60]);
      await ethers.provider.send('evm_mine', []);

      const tx = await registry.connect(alice).registerGroup('general', GROUP_GENERAL_V2);
      const rcpt = await tx.wait();
      const block = await ethers.provider.getBlock(rcpt.blockNumber);

      const [gid, pub, pAt, uAt] = await registry.getGroup('general');
      expect(gid).to.equal(GROUP_GENERAL_V2);
      expect(pub).to.equal(alice.address);
      expect(pAt).to.equal(publishedAt0);                     // unchanged
      expect(uAt).to.equal(BigInt(block.timestamp));          // bumped
      expect(uAt).to.be.gt(pAt);
    });

    it('emits GroupRotated on rotation, not GroupRegistered', async function () {
      await registry.connect(alice).registerGroup('general', GROUP_GENERAL);
      const tx = registry.connect(alice).registerGroup('general', GROUP_GENERAL_V2);
      await expect(tx).to.emit(registry, 'GroupRotated');
      await expect(tx).to.not.emit(registry, 'GroupRegistered');
    });

    it('does not duplicate the category in listCategories on rotation', async function () {
      await registry.connect(alice).registerGroup('general', GROUP_GENERAL);
      await registry.connect(alice).registerGroup('general', GROUP_GENERAL_V2);
      expect(await registry.categoryCount()).to.equal(1n);
      expect(await registry.listCategories()).to.deep.equal(['general']);
    });
  });

  describe('Multi-category enumeration', function () {
    it('registers and lists all 5 production categories in order', async function () {
      const cats = ['general', 'technical', 'governance', 'tasks', 'announcements'];
      const groups = cats.map((_, i) => '0x' + (10 + i).toString(16).padStart(2, '0').repeat(32));

      for (let i = 0; i < cats.length; i++) {
        await registry.connect(alice).registerGroup(cats[i], groups[i]);
      }

      expect(await registry.categoryCount()).to.equal(5n);
      expect(await registry.listCategories()).to.deep.equal(cats);

      for (let i = 0; i < cats.length; i++) {
        const [gid, pub] = await registry.getGroup(cats[i]);
        expect(gid).to.equal(groups[i]);
        expect(pub).to.equal(alice.address);
      }
    });

    it('handles different publishers for different categories', async function () {
      await registry.connect(alice).registerGroup('general', GROUP_GENERAL);
      await registry.connect(bob).registerGroup('technical', GROUP_TECHNICAL);

      const [, pubA] = await registry.getGroup('general');
      const [, pubB] = await registry.getGroup('technical');
      expect(pubA).to.equal(alice.address);
      expect(pubB).to.equal(bob.address);
    });
  });

  describe('Privacy / mission alignment', function () {
    it('stores no personal data — only groupId, publisher, and timestamps', async function () {
      // Verified by absence: contract has no email/name/identity fields.
      // This test is a structural assertion that the public surface area
      // contains only the expected fields.
      await registry.connect(alice).registerGroup('general', GROUP_GENERAL);
      const result = await registry.getGroup('general');
      // 4 return values: groupId, publisher, publishedAt, updatedAt
      expect(result.length).to.equal(4);
    });
  });
});
