import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createPreviewStream, createPreviewStreamResponse, encodeSseEvent, PREVIEW_SSE_HEADERS, } from '../preview/sse-stream.js';
import { __resetPreviewEventBus, notifyPreviewUpdate } from '../preview/event-bus.js';
beforeEach(() => {
    __resetPreviewEventBus();
});
const decoder = new TextDecoder();
async function readNext(reader, timeoutMs = 1000) {
    let timer;
    const timeout = new Promise((_, reject) => {
        timer = setTimeout(() => reject(new Error(`SSE read timed out after ${timeoutMs}ms`)), timeoutMs);
    });
    try {
        const result = await Promise.race([reader.read(), timeout]);
        if (result.done)
            throw new Error('Stream closed before a chunk arrived');
        return decoder.decode(result.value);
    }
    finally {
        if (timer)
            clearTimeout(timer);
    }
}
describe('encodeSseEvent', () => {
    it('serializes default (anonymous) messages', () => {
        expect(encodeSseEvent({ hello: 'world' })).toBe('data: {"hello":"world"}\n\n');
    });
    it('emits an event tag when provided', () => {
        expect(encodeSseEvent({ ok: true }, 'update')).toBe('event: update\ndata: {"ok":true}\n\n');
    });
    it('splits multi-line strings into multiple data lines', () => {
        expect(encodeSseEvent('line one\nline two')).toBe('data: line one\ndata: line two\n\n');
    });
    it('passes strings through without re-stringifying', () => {
        expect(encodeSseEvent('hello')).toBe('data: hello\n\n');
    });
});
describe('createPreviewStream', () => {
    let reader;
    afterEach(async () => {
        if (reader) {
            await reader.cancel().catch(() => undefined);
            reader = undefined;
        }
    });
    it('sends a `ready` event on connection', async () => {
        const stream = createPreviewStream({ collection: 'posts', documentId: 'doc-1' }, { heartbeatMs: 60_000, maxLifetimeMs: 60_000 });
        reader = stream.getReader();
        const chunk = await readNext(reader);
        expect(chunk).toContain('event: ready');
        expect(chunk).toContain('"collection":"posts"');
        expect(chunk).toContain('"documentId":"doc-1"');
    });
    it('forwards `notifyPreviewUpdate` payloads as `update` events', async () => {
        const stream = createPreviewStream({ collection: 'posts', documentId: 'doc-1' }, { heartbeatMs: 60_000, maxLifetimeMs: 60_000 });
        reader = stream.getReader();
        await readNext(reader); // discard the ready handshake
        notifyPreviewUpdate('posts', 'doc-1', { values: { title: 'New' } });
        const chunk = await readNext(reader);
        expect(chunk).toContain('event: update');
        expect(chunk).toContain('"values":{"title":"New"}');
    });
    it('ignores notifications for other documents', async () => {
        const stream = createPreviewStream({ collection: 'posts', documentId: 'doc-1' }, { heartbeatMs: 60_000, maxLifetimeMs: 60_000 });
        reader = stream.getReader();
        await readNext(reader); // ready
        notifyPreviewUpdate('posts', 'doc-2', { values: { title: 'Other doc' } });
        notifyPreviewUpdate('posts', 'doc-1', { values: { title: 'Our doc' } });
        const chunk = await readNext(reader);
        expect(chunk).toContain('"title":"Our doc"');
        expect(chunk).not.toContain('"title":"Other doc"');
    });
    it('sends heartbeat comments on the heartbeat interval', async () => {
        const stream = createPreviewStream({ collection: 'posts', documentId: 'doc-1' }, { heartbeatMs: 10, maxLifetimeMs: 60_000 });
        reader = stream.getReader();
        await readNext(reader); // ready
        const chunk = await readNext(reader, 500);
        expect(chunk).toContain(': heartbeat');
    });
    it('closes after `maxLifetimeMs` so EventSource can reconnect', async () => {
        const stream = createPreviewStream({ collection: 'posts', documentId: 'doc-1' }, { heartbeatMs: 60_000, maxLifetimeMs: 25 });
        reader = stream.getReader();
        await readNext(reader); // ready
        // Drain pending chunks until the stream closes naturally.
        const start = Date.now();
        let closed = false;
        while (Date.now() - start < 1000) {
            const result = await reader.read();
            if (result.done) {
                closed = true;
                break;
            }
        }
        expect(closed).toBe(true);
    });
    it('removes the subscription on `cancel`', async () => {
        const { previewSubscriberCount } = await import('../preview/event-bus.js');
        const stream = createPreviewStream({ collection: 'posts', documentId: 'doc-1' }, { heartbeatMs: 60_000, maxLifetimeMs: 60_000 });
        reader = stream.getReader();
        await readNext(reader); // ready — guarantees `start` ran
        expect(previewSubscriberCount('posts', 'doc-1')).toBe(1);
        await reader.cancel();
        reader = undefined;
        expect(previewSubscriberCount('posts', 'doc-1')).toBe(0);
    });
});
describe('createPreviewStreamResponse', () => {
    it('returns a Response with SSE headers', async () => {
        const res = createPreviewStreamResponse({ collection: 'posts', documentId: 'doc-1' }, { heartbeatMs: 60_000, maxLifetimeMs: 60_000 });
        expect(res.status).toBe(200);
        for (const [key, value] of Object.entries(PREVIEW_SSE_HEADERS)) {
            expect(res.headers.get(key)).toBe(value);
        }
        await res.body?.cancel().catch(() => undefined);
    });
});
//# sourceMappingURL=preview-sse-stream.test.js.map