import { describe, it, expect, beforeEach } from 'vitest';
import { __resetNotificationBus, notificationSubscriberCount, publishNotification, subscribeUserNotifications, } from '../../realtime/notification-bus.js';
beforeEach(() => {
    __resetNotificationBus();
});
function makeDto(overrides = {}) {
    return {
        id: 'n-1',
        userId: 'user-a',
        kind: 'comment_reply',
        documentId: 'doc-1',
        commentId: 'c-1',
        payload: {
            kind: 'comment_reply',
            actorId: 'user-b',
            actorName: 'B',
            excerpt: 'hi',
        },
        readAt: null,
        createdAt: new Date().toISOString(),
        ...overrides,
    };
}
describe('notification-bus', () => {
    it('delivers published notifications to subscribed userIds only', () => {
        const a = [];
        const b = [];
        subscribeUserNotifications('user-a', (n) => a.push(n));
        subscribeUserNotifications('user-b', (n) => b.push(n));
        publishNotification(makeDto({ id: 'a-1', userId: 'user-a' }));
        publishNotification(makeDto({ id: 'b-1', userId: 'user-b' }));
        expect(a.map((n) => n.id)).toEqual(['a-1']);
        expect(b.map((n) => n.id)).toEqual(['b-1']);
    });
    it('returns true only when at least one subscriber received the event', () => {
        expect(publishNotification(makeDto({ userId: 'nobody' }))).toBe(false);
        subscribeUserNotifications('user-a', () => undefined);
        expect(publishNotification(makeDto({ userId: 'user-a' }))).toBe(true);
    });
    it('isolates throwing listeners so siblings still receive the event', () => {
        const seen = [];
        subscribeUserNotifications('user-a', () => {
            throw new Error('boom');
        });
        subscribeUserNotifications('user-a', (n) => seen.push(n.id));
        publishNotification(makeDto({ id: 'iso-1', userId: 'user-a' }));
        expect(seen).toEqual(['iso-1']);
    });
    it('unsubscribe drops the listener and clears empty buckets', () => {
        const seen = [];
        const off = subscribeUserNotifications('user-a', (n) => seen.push(n));
        expect(notificationSubscriberCount('user-a')).toBe(1);
        publishNotification(makeDto({ id: 'pre' }));
        off();
        expect(notificationSubscriberCount('user-a')).toBe(0);
        publishNotification(makeDto({ id: 'post' }));
        expect(seen.map((n) => n.id)).toEqual(['pre']);
    });
    it('snapshots the listener set so synchronous unsubscribes from inside a listener do not skip neighbours', () => {
        const seen = [];
        let off2;
        subscribeUserNotifications('user-a', (n) => {
            seen.push(`first:${n.id}`);
            // Synchronous teardown — exactly the shape the SSE stream uses
            // when its own controller throws. Must not poison the rest of
            // the broadcast pass.
            off2?.();
        });
        off2 = subscribeUserNotifications('user-a', (n) => {
            seen.push(`second:${n.id}`);
        });
        publishNotification(makeDto({ id: 'snap' }));
        expect(seen).toEqual(['first:snap', 'second:snap']);
    });
    it('rejects empty userId at subscribe time to prevent silent cross-user leaks', () => {
        expect(() => subscribeUserNotifications('', () => undefined)).toThrow();
    });
});
//# sourceMappingURL=notification-bus.test.js.map