/**
 * Integration test against the real `ws` library — confirms the duck-typed
 * `WebSocketLike` interface works against a production-grade WebSocket
 * server, including binary frame handling (ws emits Node `Buffer`s, which
 * extend `Uint8Array`).
 *
 * This is the canary that catches "works in mocks, breaks on real ws"
 * regressions — for example if we accidentally start requiring methods
 * that ws.WebSocket doesn't expose.
 */
import { createServer } from 'node:http';
import { WebSocketServer, WebSocket } from 'ws';
import { encodeSyncStep1, encodeUpdateMessage, readMessage, Awareness, } from '@actuate-media/realtime';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { createRealtimeGateway, } from '../../realtime/gateway.js';
/**
 * Adapt `ws.WebSocket` to the `WebSocketLike` shape. Real consumers paste
 * this snippet (or one like it) into their upgrade handler.
 */
function adaptWsSocket(ws) {
    return {
        send: (data) => ws.send(data),
        close: (code, reason) => ws.close(code, reason),
        on: (event, handler) => {
            if (event === 'message') {
                ws.on('message', (data) => handler(toUint8(data)));
            }
            else if (event === 'close') {
                ws.on('close', () => handler());
            }
            else if (event === 'error') {
                ws.on('error', (err) => handler(err));
            }
        },
    };
}
function toUint8(data) {
    if (Buffer.isBuffer(data))
        return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
    if (data instanceof ArrayBuffer)
        return new Uint8Array(data);
    // Buffer[] — concatenate.
    return new Uint8Array(Buffer.concat(data));
}
async function startHarness(opts) {
    const server = createServer();
    const wss = new WebSocketServer({ noServer: true });
    let connSeq = 0;
    const gateway = createRealtimeGateway({
        authenticate: opts.authenticate ??
            (async () => ({ documentId: opts.documentId, connectionId: `conn-${++connSeq}` })),
    });
    server.on('upgrade', (req, socket, head) => {
        wss.handleUpgrade(req, socket, head, (ws) => {
            const reqInfo = {
                url: req.url ?? '/',
                headers: req.headers,
            };
            void gateway.handleConnection(adaptWsSocket(ws), reqInfo);
        });
    });
    await new Promise((resolve) => server.listen(0, resolve));
    const port = server.address().port;
    return { server, wss, gateway, port };
}
async function stopHarness(h) {
    await h.gateway.shutdown();
    h.wss.close();
    await new Promise((resolve) => h.server.close(() => resolve()));
}
/**
 * Open a ws client AND register the message listener inline before waiting
 * for the open event. This avoids a race where the server's immediate
 * SyncStep1 fires before the test code attaches its `on('message')` handler.
 */
async function openClient(port, documentId) {
    const ws = new WebSocket(`ws://localhost:${port}/api/cms/realtime/sync?documentId=${documentId}`);
    ws.binaryType = 'nodebuffer';
    const inbox = [];
    ws.on('message', (data) => inbox.push(new Uint8Array(data)));
    await new Promise((resolve, reject) => {
        ws.once('open', () => resolve());
        ws.once('error', (err) => reject(err));
    });
    return { ws, inbox };
}
/** Wait until the predicate returns truthy or the timeout fires. */
async function waitFor(predicate, timeoutMs = 1_000) {
    const start = Date.now();
    while (Date.now() - start < timeoutMs) {
        if (predicate())
            return;
        await new Promise((r) => setTimeout(r, 5));
    }
    throw new Error(`waitFor: predicate did not become truthy within ${timeoutMs}ms`);
}
describe('realtime gateway over real ws sockets', () => {
    let harness;
    beforeEach(async () => {
        harness = await startHarness({ documentId: 'doc-1' });
    });
    afterEach(async () => {
        await stopHarness(harness);
    });
    it('completes a SyncStep1 handshake with a real ws client', async () => {
        const { ws, inbox } = await openClient(harness.port, 'doc-1');
        await waitFor(() => inbox.length >= 1);
        expect(inbox[0][0]).toBe(0 /* MESSAGE_SYNC */);
        expect(inbox[0][1]).toBe(0 /* SyncStep1 */);
        ws.close();
    });
    it('relays an update from one client to another', async () => {
        const { ws: wsA, inbox: inboxA } = await openClient(harness.port, 'doc-1');
        const { ws: wsB, inbox: inboxB } = await openClient(harness.port, 'doc-1');
        // Drain initial SyncStep1 from both clients.
        await waitFor(() => inboxA.length >= 1 && inboxB.length >= 1);
        inboxA.length = 0;
        inboxB.length = 0;
        // A pushes an update.
        const probe = new Y.Doc();
        probe.getText('t').insert(0, 'real-ws-edit');
        wsA.send(encodeUpdateMessage(Y.encodeStateAsUpdate(probe)));
        await waitFor(() => inboxB.length > 0);
        const docB = new Y.Doc();
        const awB = new Awareness(docB);
        for (const m of inboxB)
            readMessage(m, docB, awB, 'test');
        expect(docB.getText('t').toString()).toBe('real-ws-edit');
        wsA.close();
        wsB.close();
    });
    it('closes connections cleanly on gateway shutdown', async () => {
        const { ws } = await openClient(harness.port, 'doc-1');
        const closed = new Promise((resolve) => ws.once('close', () => resolve()));
        await harness.gateway.shutdown();
        await closed;
        // After shutdown the gateway should refuse new business.
        expect(harness.gateway.roomCount).toBe(0);
        expect(harness.gateway.connectionCount).toBe(0);
    });
    it('returns 4401 to clients whose auth check fails', async () => {
        await stopHarness(harness);
        harness = await startHarness({
            documentId: 'doc-1',
            authenticate: async () => null,
        });
        const ws = new WebSocket(`ws://localhost:${harness.port}/api/cms/realtime/sync?documentId=doc-1`);
        const closeInfo = await new Promise((resolve) => {
            ws.once('close', (code) => resolve({ code }));
        });
        expect(closeInfo.code).toBe(4401);
    });
    it('replies to a client-initiated SyncStep1 with the persisted state', async () => {
        // Round-trip a fresh client sending its own SyncStep1: the gateway should
        // reply with the current state (initially empty, but the sync step 2
        // envelope is still returned).
        const { ws, inbox } = await openClient(harness.port, 'doc-1');
        await waitFor(() => inbox.length >= 1);
        inbox.length = 0;
        const clientDoc = new Y.Doc();
        ws.send(encodeSyncStep1(clientDoc));
        await waitFor(() => inbox.length >= 1);
        expect(inbox[0][0]).toBe(0 /* MESSAGE_SYNC */);
        // SyncStep2 sub-type = 1.
        expect(inbox[0][1]).toBe(1);
        ws.close();
    });
});
//# sourceMappingURL=gateway-ws-integration.test.js.map