// Tests for channel ACL — membership-gated messaging.
// Uses bun:test. Tests the logic from btmsg-db.ts channel operations.

import { describe, it, expect, beforeEach } from 'bun:test';

// ── In-memory channel store (replicated logic from btmsg-db.ts) ─────────────

interface ChannelMessage {
  id: string;
  channelId: string;
  fromAgent: string;
  content: string;
}

function createChannelStore() {
  const channels = new Map<string, { id: string; name: string }>();
  const members = new Map<string, Set<string>>();    // channelId -> Set<agentId>
  const messages: ChannelMessage[] = [];
  let msgCounter = 0;

  return {
    createChannel(id: string, name: string): void {
      channels.set(id, { id, name });
      members.set(id, new Set());
    },

    joinChannel(channelId: string, agentId: string): void {
      const ch = channels.get(channelId);
      if (!ch) throw new Error(`Channel '${channelId}' not found`);
      members.get(channelId)!.add(agentId);
    },

    leaveChannel(channelId: string, agentId: string): void {
      members.get(channelId)?.delete(agentId);
    },

    sendChannelMessage(channelId: string, fromAgent: string, content: string): string {
      const memberSet = members.get(channelId);
      if (!memberSet || !memberSet.has(fromAgent)) {
        throw new Error(`Agent '${fromAgent}' is not a member of channel '${channelId}'`);
      }
      const id = `msg-${++msgCounter}`;
      messages.push({ id, channelId, fromAgent, content });
      return id;
    },

    getChannelMembers(channelId: string): string[] {
      return Array.from(members.get(channelId) ?? []);
    },

    getMessages(channelId: string): ChannelMessage[] {
      return messages.filter(m => m.channelId === channelId);
    },
  };
}

// ── Tests ───────────────────────────────────────────────────────────────────

describe('channel membership', () => {
  let store: ReturnType<typeof createChannelStore>;

  beforeEach(() => {
    store = createChannelStore();
    store.createChannel('general', 'General');
  });

  it('joinChannel adds member', () => {
    store.joinChannel('general', 'agent-1');
    expect(store.getChannelMembers('general')).toContain('agent-1');
  });

  it('joinChannel to nonexistent channel throws', () => {
    expect(() => store.joinChannel('nonexistent', 'agent-1')).toThrow('not found');
  });

  it('leaveChannel removes member', () => {
    store.joinChannel('general', 'agent-1');
    store.leaveChannel('general', 'agent-1');
    expect(store.getChannelMembers('general')).not.toContain('agent-1');
  });

  it('leaveChannel is idempotent', () => {
    store.leaveChannel('general', 'agent-1');
    expect(store.getChannelMembers('general')).toHaveLength(0);
  });

  it('getChannelMembers returns all members', () => {
    store.joinChannel('general', 'agent-1');
    store.joinChannel('general', 'agent-2');
    store.joinChannel('general', 'agent-3');
    expect(store.getChannelMembers('general')).toHaveLength(3);
  });

  it('duplicate join is idempotent (Set semantics)', () => {
    store.joinChannel('general', 'agent-1');
    store.joinChannel('general', 'agent-1');
    expect(store.getChannelMembers('general')).toHaveLength(1);
  });
});

describe('channel message ACL', () => {
  let store: ReturnType<typeof createChannelStore>;

  beforeEach(() => {
    store = createChannelStore();
    store.createChannel('ops', 'Operations');
    store.joinChannel('ops', 'manager');
  });

  it('member can send message', () => {
    const id = store.sendChannelMessage('ops', 'manager', 'hello team');
    expect(id).toBeTruthy();
    const msgs = store.getMessages('ops');
    expect(msgs).toHaveLength(1);
    expect(msgs[0].content).toBe('hello team');
    expect(msgs[0].fromAgent).toBe('manager');
  });

  it('non-member is rejected', () => {
    expect(() => {
      store.sendChannelMessage('ops', 'outsider', 'sneaky message');
    }).toThrow("not a member");
  });

  it('former member is rejected after leaving', () => {
    store.leaveChannel('ops', 'manager');
    expect(() => {
      store.sendChannelMessage('ops', 'manager', 'should fail');
    }).toThrow("not a member");
  });

  it('rejoined member can send again', () => {
    store.leaveChannel('ops', 'manager');
    store.joinChannel('ops', 'manager');
    const id = store.sendChannelMessage('ops', 'manager', 'back again');
    expect(id).toBeTruthy();
  });
});

describe('channel isolation', () => {
  let store: ReturnType<typeof createChannelStore>;

  beforeEach(() => {
    store = createChannelStore();
    store.createChannel('ch-a', 'Channel A');
    store.createChannel('ch-b', 'Channel B');
    store.joinChannel('ch-a', 'agent-1');
  });

  it('member of channel A cannot send to channel B', () => {
    expect(() => {
      store.sendChannelMessage('ch-b', 'agent-1', 'wrong channel');
    }).toThrow("not a member");
  });

  it('messages are channel-scoped', () => {
    store.joinChannel('ch-b', 'agent-2');
    store.sendChannelMessage('ch-a', 'agent-1', 'msg in A');
    store.sendChannelMessage('ch-b', 'agent-2', 'msg in B');
    expect(store.getMessages('ch-a')).toHaveLength(1);
    expect(store.getMessages('ch-b')).toHaveLength(1);
    expect(store.getMessages('ch-a')[0].content).toBe('msg in A');
    expect(store.getMessages('ch-b')[0].content).toBe('msg in B');
  });
});
