import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { performance } from 'node:perf_hooks';
import WebSocket from 'ws';
import { WebSocketClient } from './websocket-client.js';
class FakeSocket extends EventEmitter {
    readyState;
    sentPayloads = [];
    constructor() {
        super();
        this.readyState = WebSocket.CONNECTING;
    }
    close() {
        this.readyState = WebSocket.CLOSED;
        this.emit('close', 1000, Buffer.from('manual close'));
    }
    terminate() {
        this.readyState = WebSocket.CLOSED;
    }
    send(payload) {
        this.sentPayloads.push(payload);
    }
    ping() {
        setImmediate(() => this.emit('pong'));
    }
}
class SilentPingSocket extends FakeSocket {
    ping() {
        // intentionally no pong
    }
    terminate() {
        this.readyState = WebSocket.CLOSED;
        this.emit('close', 1006, Buffer.from('heartbeat timeout'));
    }
}
describe('WebSocketClient reconnect behavior', () => {
    it('should include runtime token in websocket URL when configured', async () => {
        let connectedUrl = '';
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 20,
            maxReconnectAttempts: 1,
            runtimeAccessToken: 'aws_rt_test_token',
        }, (url) => {
            connectedUrl = url;
            const socket = new FakeSocket();
            setImmediate(() => {
                socket.readyState = WebSocket.OPEN;
                socket.emit('open');
            });
            return socket;
        });
        try {
            client.on('error', () => { });
            await client.connect('agent-test');
            const parsedUrl = new URL(connectedUrl);
            assert.equal(parsedUrl.searchParams.get('agentId'), 'agent-test');
            assert.equal(parsedUrl.searchParams.get('runtimeAccessToken'), 'aws_rt_test_token');
        }
        finally {
            client.disconnect();
        }
    });
    it('should force refresh runtime token once after policy rejection', async () => {
        const requestedBodies = [];
        const originalFetch = globalThis.fetch;
        globalThis.fetch = (async (_url, init) => {
            requestedBodies.push(JSON.parse(String(init?.body || '{}')));
            return new Response(JSON.stringify({ ok: true, runtimeAccessToken: 'fresh-token' }), {
                status: 200,
                headers: { 'Content-Type': 'application/json' },
            });
        });
        const connectedUrls = [];
        let attempt = 0;
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 10,
            maxReconnectAttempts: 2,
            runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
            userId: 'user-a',
            runtimeAccessToken: 'stale-token',
        }, (url) => {
            connectedUrls.push(url);
            attempt += 1;
            const socket = new FakeSocket();
            if (attempt === 1) {
                setImmediate(() => {
                    socket.readyState = WebSocket.OPEN;
                    socket.emit('open');
                    socket.readyState = WebSocket.CLOSED;
                    socket.emit('close', 1008, Buffer.from('policy violation'));
                });
            }
            else {
                setImmediate(() => {
                    socket.readyState = WebSocket.OPEN;
                    socket.emit('open');
                });
            }
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            await new Promise((resolve, reject) => {
                const timer = setTimeout(() => reject(new Error('expected reconnect after token refresh')), 500);
                client.on('connected', () => {
                    if (attempt >= 2) {
                        clearTimeout(timer);
                        resolve();
                    }
                });
            });
            assert.equal(requestedBodies.length, 1);
            assert.equal(requestedBodies[0]?.forceRefresh, true);
            assert.equal(new URL(connectedUrls[0] || '').searchParams.get('runtimeAccessToken'), 'stale-token');
            assert.equal(new URL(connectedUrls[1] || '').searchParams.get('runtimeAccessToken'), 'fresh-token');
        }
        finally {
            client.disconnect();
            globalThis.fetch = originalFetch;
        }
    });
    it('should not hammer token refresh endpoint after refresh failure for same token', async () => {
        let refreshRequests = 0;
        const originalFetch = globalThis.fetch;
        globalThis.fetch = (async () => {
            refreshRequests += 1;
            return new Response(JSON.stringify({ error: 'refresh failed' }), {
                status: 500,
                headers: { 'Content-Type': 'application/json' },
            });
        });
        let attempt = 0;
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 10,
            maxReconnectAttempts: 3,
            runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
            userId: 'user-a',
            runtimeAccessToken: 'stale-token',
        }, () => {
            attempt += 1;
            const socket = new FakeSocket();
            setImmediate(() => {
                socket.readyState = WebSocket.OPEN;
                socket.emit('open');
                socket.readyState = WebSocket.CLOSED;
                socket.emit('close', 1008, Buffer.from('policy violation'));
            });
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            await new Promise((resolve, reject) => {
                const timer = setTimeout(() => reject(new Error('expected repeated reconnects')), 500);
                client.on('disconnected', () => {
                    if (attempt >= 3) {
                        clearTimeout(timer);
                        resolve();
                    }
                });
            });
            assert.equal(refreshRequests, 1);
        }
        finally {
            client.disconnect();
            globalThis.fetch = originalFetch;
        }
    });
    it('should abort runtime token refresh after configured timeout', async () => {
        const originalFetch = globalThis.fetch;
        let signalSeen = false;
        globalThis.fetch = (async (_url, init) => {
            signalSeen = init?.signal instanceof AbortSignal;
            return await new Promise((_resolve, reject) => {
                init?.signal?.addEventListener('abort', () => reject(new Error('aborted by signal')), { once: true });
            });
        });
        let attempt = 0;
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 10,
            maxReconnectAttempts: 1,
            runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
            userId: 'user-a',
            runtimeAccessToken: 'stale-token',
            mcpHttpRequestTimeoutMs: 10,
        }, () => {
            attempt += 1;
            const socket = new FakeSocket();
            setImmediate(() => {
                socket.readyState = WebSocket.OPEN;
                socket.emit('open');
                socket.readyState = WebSocket.CLOSED;
                socket.emit('close', 1008, Buffer.from('policy violation'));
            });
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            await new Promise((resolve, reject) => {
                const timer = setTimeout(() => reject(new Error('expected reconnect after refresh timeout')), 500);
                client.on('disconnected', () => {
                    if (attempt >= 2) {
                        clearTimeout(timer);
                        resolve();
                    }
                });
            });
            assert.equal(signalSeen, true);
        }
        finally {
            client.disconnect();
            globalThis.fetch = originalFetch;
        }
    });
    it('should refresh again when a different runtime token is later rejected', async () => {
        const requestedBodies = [];
        const refreshedTokens = ['fresh-token-1', 'fresh-token-2'];
        const originalFetch = globalThis.fetch;
        globalThis.fetch = (async (_url, init) => {
            requestedBodies.push(JSON.parse(String(init?.body || '{}')));
            return new Response(JSON.stringify({ ok: true, runtimeAccessToken: refreshedTokens.shift() }), {
                status: 200,
                headers: { 'Content-Type': 'application/json' },
            });
        });
        let attempt = 0;
        const createdSockets = [];
        const config = {
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 10,
            maxReconnectAttempts: 5,
            runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
            userId: 'user-a',
            runtimeAccessToken: 'stale-token-1',
        };
        const client = new WebSocketClient(config, () => {
            attempt += 1;
            const socket = new FakeSocket();
            createdSockets.push(socket);
            setImmediate(() => {
                socket.readyState = WebSocket.OPEN;
                socket.emit('open');
                if (attempt === 1) {
                    socket.readyState = WebSocket.CLOSED;
                    socket.emit('close', 1008, Buffer.from('policy violation'));
                }
            });
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            await new Promise((resolve, reject) => {
                const timer = setTimeout(() => reject(new Error('expected second token refresh')), 500);
                const interval = setInterval(() => {
                    if (requestedBodies.length >= 2 && config.runtimeAccessToken === 'fresh-token-2') {
                        clearInterval(interval);
                        clearTimeout(timer);
                        resolve();
                    }
                }, 5);
                client.on('connected', () => {
                    if (attempt === 2) {
                        config.runtimeAccessToken = 'stale-token-2';
                        const activeSocket = createdSockets[createdSockets.length - 1];
                        if (activeSocket) {
                            activeSocket.readyState = WebSocket.CLOSED;
                            activeSocket.emit('close', 1008, Buffer.from('policy violation'));
                        }
                    }
                });
            });
            assert.equal(requestedBodies.length, 2);
            assert.equal(config.runtimeAccessToken, 'fresh-token-2');
        }
        finally {
            client.disconnect();
            globalThis.fetch = originalFetch;
        }
    });
    it('should keep retrying at interval after scheduler restart until connected', async () => {
        const createdSockets = [];
        let attempt = 0;
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 20,
            maxReconnectAttempts: -1
        }, (_url) => {
            const socket = new FakeSocket();
            createdSockets.push(socket);
            attempt += 1;
            if (attempt === 1 || attempt >= 4) {
                setImmediate(() => {
                    socket.readyState = WebSocket.OPEN;
                    socket.emit('open');
                });
            }
            else {
                setImmediate(() => {
                    socket.emit('error', new Error('ECONNREFUSED'));
                    socket.emit('close', 1006, Buffer.from('connect fail'));
                });
            }
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            const firstSocket = createdSockets[0];
            assert.ok(firstSocket, 'first socket should exist');
            firstSocket.readyState = WebSocket.CLOSED;
            firstSocket.emit('close', 1006, Buffer.from('server restart'));
            await new Promise((resolve, reject) => {
                const timer = setTimeout(() => {
                    reject(new Error('expected reconnect to eventually succeed'));
                }, 500);
                client.on('connected', () => {
                    if (attempt >= 4) {
                        clearTimeout(timer);
                        resolve();
                    }
                });
            });
            assert.equal(attempt, 4, 'should attempt initial + retries until success');
            assert.equal(client.isConnected(), true);
        }
        finally {
            client.disconnect();
        }
    });
    it('should wait reconnect interval before first reconnect attempt', async () => {
        const reconnectInterval = 80;
        const attemptTimes = [];
        let attempt = 0;
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval,
            maxReconnectAttempts: 2,
        }, (_url) => {
            const socket = new FakeSocket();
            attempt += 1;
            attemptTimes.push(performance.now());
            if (attempt === 1) {
                setImmediate(() => {
                    socket.readyState = WebSocket.OPEN;
                    socket.emit('open');
                });
            }
            else {
                setImmediate(() => {
                    socket.readyState = WebSocket.OPEN;
                    socket.emit('open');
                });
            }
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            const firstSocketTime = attemptTimes[0];
            const firstSocket = client.ws;
            assert.ok(firstSocket, 'first socket should exist');
            firstSocket.readyState = WebSocket.CLOSED;
            firstSocket.emit('close', 1006, Buffer.from('server restart'));
            await new Promise((resolve, reject) => {
                const timer = setTimeout(() => {
                    reject(new Error('expected reconnect attempt'));
                }, 500);
                const check = setInterval(() => {
                    if (attempt >= 2) {
                        clearInterval(check);
                        clearTimeout(timer);
                        resolve();
                    }
                }, 5);
            });
            assert.ok(attemptTimes[1] - firstSocketTime >= reconnectInterval - 15, `first reconnect should wait about ${reconnectInterval}ms, actual=${attemptTimes[1] - firstSocketTime}ms`);
        }
        finally {
            client.disconnect();
        }
    });
    it('should terminate and reconnect when heartbeat response times out', async () => {
        const createdSockets = [];
        let attempt = 0;
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 10,
            heartbeatTimeout: 20,
            reconnectInterval: 10,
            maxReconnectAttempts: 2,
        }, (_url) => {
            attempt += 1;
            const socket = attempt === 1 ? new SilentPingSocket() : new FakeSocket();
            createdSockets.push(socket);
            setImmediate(() => {
                socket.readyState = WebSocket.OPEN;
                socket.emit('open');
            });
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            await new Promise((resolve, reject) => {
                const timer = setTimeout(() => {
                    reject(new Error('expected reconnect after heartbeat timeout'));
                }, 500);
                client.on('connected', () => {
                    if (attempt >= 2) {
                        clearTimeout(timer);
                        resolve();
                    }
                });
            });
            assert.equal(createdSockets[0]?.readyState, WebSocket.CLOSED);
            assert.equal(attempt, 2);
            assert.equal(client.isConnected(), true);
        }
        finally {
            client.disconnect();
        }
    });
    it('should acknowledge reliable direct messages when received', async () => {
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 20,
            maxReconnectAttempts: 1,
        }, (_url) => {
            const socket = new FakeSocket();
            setImmediate(() => {
                socket.readyState = WebSocket.OPEN;
                socket.emit('open');
            });
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            const socket = client.ws;
            assert.ok(socket, 'socket should exist');
            socket.emit('message', Buffer.from(JSON.stringify({
                type: 'DM_MSG',
                data: {
                    msgId: 'dm-1',
                    _msgId: 'delivery-1',
                    _requireAck: true,
                    senderId: 'agent-a',
                    senderName: 'Agent A',
                    receiverId: 'agent-test',
                    content: 'hello',
                    requireReply: false,
                    timestamp: '2026-05-06T10:00:00.000Z',
                },
                timestamp: '2026-05-06T10:00:00.000Z',
            })));
            const ackPayload = socket.sentPayloads
                .map(payload => JSON.parse(payload))
                .find(payload => payload.type === 'MESSAGE_ACK');
            assert.deepEqual(ackPayload, {
                type: 'MESSAGE_ACK',
                data: { msgId: 'delivery-1' },
                timestamp: ackPayload?.timestamp,
            });
        }
        finally {
            client.disconnect();
        }
    });
    it('should ignore terminal status messages without warning', async () => {
        const originalWarn = console.warn;
        const warnMessages = [];
        console.warn = (...args) => {
            warnMessages.push(args.map(String).join(' '));
        };
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 20,
            maxReconnectAttempts: 1,
        }, (_url) => {
            const socket = new FakeSocket();
            setImmediate(() => {
                socket.readyState = WebSocket.OPEN;
                socket.emit('open');
            });
            return socket;
        });
        try {
            client.on('error', () => {
                // suppress unhandled error event in test environment
            });
            await client.connect('agent-test');
            const socket = client.ws;
            assert.ok(socket, 'socket should exist');
            socket.emit('message', Buffer.from(JSON.stringify({
                type: 'TERMINAL_STATUS',
                data: {
                    agentId: 'agent-test',
                    status: 'running',
                    sessionId: 'session-1',
                },
                timestamp: '2026-05-06T10:00:00.000Z',
            })));
            assert.equal(warnMessages.some(message => message.includes('未知消息类型')), false);
        }
        finally {
            client.disconnect();
            console.warn = originalWarn;
        }
    });
    it('should ignore MCP node log broadcasts without warning', async () => {
        const originalWarn = console.warn;
        const warnMessages = [];
        console.warn = (...args) => {
            warnMessages.push(args.map(String).join(' '));
        };
        const client = new WebSocketClient({
            serverUrl: 'ws://localhost:8080/ws/agent',
            heartbeatInterval: 1000,
            heartbeatTimeout: 3000,
            reconnectInterval: 20,
            maxReconnectAttempts: 1,
        }, (_url) => {
            const socket = new FakeSocket();
            setImmediate(() => {
                socket.readyState = WebSocket.OPEN;
                socket.emit('open');
            });
            return socket;
        });
        try {
            client.on('error', () => { });
            await client.connect('agent-test');
            const socket = client.ws;
            assert.ok(socket, 'socket should exist');
            socket.emit('message', Buffer.from(JSON.stringify({
                type: 'MCP_NODE_LOG',
                data: {
                    agentId: 'agent-test',
                    level: 'info',
                    message: 'AI request started',
                    timestamp: 1_780_000_000_000,
                },
                timestamp: '2026-05-06T10:00:00.000Z',
            })));
            assert.equal(warnMessages.some(message => message.includes('MCP_NODE_LOG')), false);
        }
        finally {
            client.disconnect();
            console.warn = originalWarn;
        }
    });
});
//# sourceMappingURL=websocket-client.test.js.map