/**
 * Unit tests for WebSocket client functionality
 * Tests connection, reconnection, authentication, error handling, and message dispatch
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';

// Mock WebSocket class
class MockWebSocket {
    constructor(url) {
        this.url = url;
        this.readyState = MockWebSocket.CONNECTING;
        this.CONNECTING = 0;
        this.OPEN = 1;
        this.CLOSING = 2;
        this.CLOSED = 3;
        
        // Event handlers
        this.onopen = null;
        this.onclose = null;
        this.onerror = null;
        this.onmessage = null;
        
        // Store instance for testing
        MockWebSocket._lastInstance = this;
        
        // Auto-connect after a tick
        setTimeout(() => {
            if (this.readyState === MockWebSocket.CONNECTING) {
                this.readyState = MockWebSocket.OPEN;
                if (this.onopen) this.onopen({ type: 'open' });
            }
        }, 0);
    }
    
    send(data) {
        if (this.readyState !== MockWebSocket.OPEN) {
            throw new Error('WebSocket is not open');
        }
        this._lastSent = data;
    }
    
    close(code, reason) {
        this.readyState = MockWebSocket.CLOSING;
        setTimeout(() => {
            this.readyState = MockWebSocket.CLOSED;
            if (this.onclose) {
                this.onclose({
                    type: 'close',
                    code: code || 1000,
                    reason: reason || '',
                    wasClean: code === 1000
                });
            }
        }, 0);
    }
    
    // Test helper: simulate message received
    _simulateMessage(data) {
        if (this.onmessage) {
            this.onmessage({
                type: 'message',
                data: typeof data === 'string' ? data : JSON.stringify(data)
            });
        }
    }
    
    // Test helper: simulate error
    _simulateError(error) {
        if (this.onerror) {
            this.onerror({
                type: 'error',
                error: error || new Error('WebSocket error')
            });
        }
    }
    
    // Test helper: simulate connection close
    _simulateClose(code = 1006, reason = 'Connection lost') {
        this.readyState = MockWebSocket.CLOSED;
        if (this.onclose) {
            this.onclose({
                type: 'close',
                code,
                reason,
                wasClean: false
            });
        }
    }
}

// Static properties
MockWebSocket.CONNECTING = 0;
MockWebSocket.OPEN = 1;
MockWebSocket.CLOSING = 2;
MockWebSocket.CLOSED = 3;

// Import WebSocket client code (we'll need to evaluate it)
// For testing, we'll load the actual file
let WebSocketClient;

describe('WebSocket Client - Initialization', () => {
    beforeEach(() => {
        // Mock global WebSocket
        global.WebSocket = MockWebSocket;
        
        // Clear any timers
        vi.useFakeTimers();
        
        // Load WebSocketClient class by evaluating the source
        // In a real setup, this would be imported
        const sourceCode = `
        class WebSocketClient {
            constructor(url, options = {}) {
                this.url = url;
                this.ws = null;
                this.isConnected = false;
                this.reconnectAttempts = 0;
                this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
                this.reconnectDelay = options.reconnectDelay || 1000;
                this.autoReconnect = options.autoReconnect !== false;
                this.eventHandlers = new Map();
                this.messageQueue = [];
                this.rooms = new Set();
            }
            
            getWebSocketUrl() {
                const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
                const host = window.location.host;
                return \`\${protocol}//\${host}\${this.url}\`;
            }
            
            connect() {
                try {
                    const wsUrl = this.getWebSocketUrl();
                    this.ws = new WebSocket(wsUrl);
                    
                    this.ws.onopen = (event) => {
                        this.isConnected = true;
                        this.reconnectAttempts = 0;
                        this.emit('connect');
                        this.rejoinRooms();
                        this.processMessageQueue();
                    };
                    
                    this.ws.onmessage = (event) => {
                        this.handleMessage(event);
                    };
                    
                    this.ws.onerror = (event) => {
                        console.error('WebSocket error:', event);
                        this.emit('error', event.error || new Error('WebSocket error'));
                    };
                    
                    this.ws.onclose = (event) => {
                        this.isConnected = false;
                        this.emit('disconnect', event.reason);
                        
                        if (this.autoReconnect && !event.wasClean && 
                            this.reconnectAttempts < this.maxReconnectAttempts) {
                            this.reconnectAttempts++;
                            const delay = this.reconnectDelay * this.reconnectAttempts;
                            console.log(\`Reconnecting in \${delay}ms (attempt \${this.reconnectAttempts}/\${this.maxReconnectAttempts})...\`);
                            setTimeout(() => this.connect(), delay);
                        } else if (this.reconnectAttempts >= this.maxReconnectAttempts) {
                            this.emit('reconnect_failed');
                        }
                    };
                } catch (error) {
                    console.error('Failed to create WebSocket:', error);
                    this.emit('error', error);
                }
            }
            
            disconnect() {
                if (this.ws) {
                    this.autoReconnect = false;
                    this.ws.close(1000, 'Client disconnect');
                }
            }
            
            handleMessage(event) {
                try {
                    const message = JSON.parse(event.data);
                    const { type, ...data } = message;
                    if (type) {
                        this.emit(type, data);
                    }
                } catch (error) {
                    console.error('Failed to parse message:', error);
                    this.emit('error', error);
                }
            }
            
            on(event, handler) {
                if (!this.eventHandlers.has(event)) {
                    this.eventHandlers.set(event, []);
                }
                this.eventHandlers.get(event).push(handler);
            }
            
            off(event, handler) {
                if (this.eventHandlers.has(event)) {
                    const handlers = this.eventHandlers.get(event);
                    const index = handlers.indexOf(handler);
                    if (index !== -1) {
                        handlers.splice(index, 1);
                    }
                }
            }
            
            emit(event, data) {
                if (this.eventHandlers.has(event)) {
                    this.eventHandlers.get(event).forEach(handler => {
                        try {
                            handler(data);
                        } catch (error) {
                            console.error(\`Error in event handler for '\${event}':\`, error);
                        }
                    });
                }
            }
            
            send(action, data) {
                const message = JSON.stringify({ action, ...data });
                if (this.connected()) {
                    this.ws.send(message);
                } else {
                    this.messageQueue.push(message);
                }
            }
            
            join(room) {
                this.rooms.add(room);
                if (this.connected()) {
                    this.send('join', { room });
                }
            }
            
            leave(room) {
                this.rooms.delete(room);
                if (this.connected()) {
                    this.send('leave', { room });
                }
            }
            
            rejoinRooms() {
                this.rooms.forEach(room => {
                    this.send('join', { room });
                });
            }
            
            processMessageQueue() {
                while (this.messageQueue.length > 0 && this.connected()) {
                    const message = this.messageQueue.shift();
                    this.ws.send(message);
                }
            }
            
            connected() {
                return this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN;
            }
        }
        
        function io(url) {
            const client = new WebSocketClient(url);
            client.connect();
            return client;
        }
        
        globalThis.WebSocketClient = WebSocketClient;
        globalThis.io = io;
        `;
        
        eval(sourceCode);
        WebSocketClient = globalThis.WebSocketClient;
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
        delete globalThis.WebSocketClient;
        delete globalThis.io;
    });
    
    it('should create WebSocket client with default options', () => {
        const client = new WebSocketClient('/ws');
        
        expect(client.url).toBe('/ws');
        expect(client.maxReconnectAttempts).toBe(5);
        expect(client.reconnectDelay).toBe(1000);
        expect(client.autoReconnect).toBe(true);
        expect(client.isConnected).toBe(false);
        expect(client.reconnectAttempts).toBe(0);
    });
    
    it('should create WebSocket client with custom options', () => {
        const client = new WebSocketClient('/ws', {
            maxReconnectAttempts: 10,
            reconnectDelay: 2000,
            autoReconnect: false
        });
        
        expect(client.maxReconnectAttempts).toBe(10);
        expect(client.reconnectDelay).toBe(2000);
        expect(client.autoReconnect).toBe(false);
    });
    
    it('should initialize empty event handlers map', () => {
        const client = new WebSocketClient('/ws');
        
        expect(client.eventHandlers).toBeInstanceOf(Map);
        expect(client.eventHandlers.size).toBe(0);
    });
    
    it('should initialize empty message queue', () => {
        const client = new WebSocketClient('/ws');
        
        expect(client.messageQueue).toEqual([]);
    });
    
    it('should initialize empty rooms set', () => {
        const client = new WebSocketClient('/ws');
        
        expect(client.rooms).toBeInstanceOf(Set);
        expect(client.rooms.size).toBe(0);
    });
});

describe('WebSocket Client - Connection', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        
        // Mock window.location
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
        
        const sourceCode = `${/* Same source as above */}`;
        eval(sourceCode);
        WebSocketClient = globalThis.WebSocketClient;
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
        delete global.window;
    });
    
    it('should generate correct WebSocket URL with http protocol', () => {
        const client = new WebSocketClient('/ws/updates');
        expect(client.getWebSocketUrl()).toBe('ws://localhost:8000/ws/updates');
    });
    
    it('should generate correct WebSocket URL with https protocol', () => {
        global.window.location.protocol = 'https:';
        const client = new WebSocketClient('/ws/updates');
        expect(client.getWebSocketUrl()).toBe('wss://localhost:8000/ws/updates');
    });
    
    it('should create WebSocket connection on connect()', async () => {
        const client = new WebSocketClient('/ws');
        client.connect();
        
        expect(MockWebSocket._lastInstance).toBeDefined();
        expect(MockWebSocket._lastInstance.url).toBe('ws://localhost:8000/ws');
    });
    
    it('should emit connect event when connection opens', async () => {
        const client = new WebSocketClient('/ws');
        const connectHandler = vi.fn();
        client.on('connect', connectHandler);
        
        client.connect();
        await vi.runAllTimersAsync();
        
        expect(connectHandler).toHaveBeenCalledTimes(1);
        expect(client.isConnected).toBe(true);
    });
    
    it('should reset reconnect attempts on successful connection', async () => {
        const client = new WebSocketClient('/ws');
        client.reconnectAttempts = 3;
        
        client.connect();
        await vi.runAllTimersAsync();
        
        expect(client.reconnectAttempts).toBe(0);
    });
});

describe('WebSocket Client - Reconnection Logic', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
        delete global.window;
    });
    
    it('should attempt reconnection after unclean close', async () => {
        const client = new WebSocketClient('/ws');
        client.connect();
        await vi.runAllTimersAsync();
        
        // Simulate connection loss
        const ws = MockWebSocket._lastInstance;
        ws._simulateClose(1006, 'Connection lost');
        await vi.runAllTimersAsync();
        
        // Should trigger reconnection after delay
        expect(client.reconnectAttempts).toBe(1);
    });
    
    it('should use exponential backoff for reconnection delays', async () => {
        const client = new WebSocketClient('/ws', { reconnectDelay: 1000 });
        client.connect();
        await vi.runAllTimersAsync();
        
        // First reconnection attempt: 1000ms delay
        let ws = MockWebSocket._lastInstance;
        ws._simulateClose(1006);
        
        vi.advanceTimersByTime(999);
        expect(client.reconnectAttempts).toBe(1);
        
        vi.advanceTimersByTime(1);
        await vi.runAllTimersAsync();
        
        // Second reconnection attempt: 2000ms delay
        ws = MockWebSocket._lastInstance;
        ws._simulateClose(1006);
        
        vi.advanceTimersByTime(1999);
        expect(client.reconnectAttempts).toBe(2);
        
        vi.advanceTimersByTime(1);
        await vi.runAllTimersAsync();
        
        // Third reconnection attempt: 3000ms delay
        expect(client.reconnectAttempts).toBe(3);
    });
    
    it('should stop reconnecting after max attempts', async () => {
        const client = new WebSocketClient('/ws', { 
            maxReconnectAttempts: 3,
            reconnectDelay: 100
        });
        const reconnectFailedHandler = vi.fn();
        client.on('reconnect_failed', reconnectFailedHandler);
        
        client.connect();
        await vi.runAllTimersAsync();
        
        // Simulate 3 connection failures
        for (let i = 0; i < 3; i++) {
            const ws = MockWebSocket._lastInstance;
            ws._simulateClose(1006);
            await vi.runAllTimersAsync();
        }
        
        expect(client.reconnectAttempts).toBe(3);
        expect(reconnectFailedHandler).toHaveBeenCalledTimes(1);
        
        // Should not attempt another reconnection
        const attemptsBefore = client.reconnectAttempts;
        await vi.advanceTimersByTimeAsync(5000);
        expect(client.reconnectAttempts).toBe(attemptsBefore);
    });
    
    it('should not reconnect after clean disconnect', async () => {
        const client = new WebSocketClient('/ws');
        client.connect();
        await vi.runAllTimersAsync();
        
        const attemptsBefore = client.reconnectAttempts;
        client.disconnect();
        await vi.runAllTimersAsync();
        
        expect(client.reconnectAttempts).toBe(attemptsBefore);
        expect(client.autoReconnect).toBe(false);
    });
    
    it('should not reconnect when autoReconnect is disabled', async () => {
        const client = new WebSocketClient('/ws', { autoReconnect: false });
        client.connect();
        await vi.runAllTimersAsync();
        
        const ws = MockWebSocket._lastInstance;
        ws._simulateClose(1006);
        await vi.runAllTimersAsync();
        
        expect(client.reconnectAttempts).toBe(0);
    });
});

describe('WebSocket Client - Event Handling', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
    });
    
    it('should register event handlers', () => {
        const client = new WebSocketClient('/ws');
        const handler = vi.fn();
        
        client.on('test_event', handler);
        
        expect(client.eventHandlers.has('test_event')).toBe(true);
        expect(client.eventHandlers.get('test_event')).toContain(handler);
    });
    
    it('should register multiple handlers for same event', () => {
        const client = new WebSocketClient('/ws');
        const handler1 = vi.fn();
        const handler2 = vi.fn();
        
        client.on('test_event', handler1);
        client.on('test_event', handler2);
        
        expect(client.eventHandlers.get('test_event').length).toBe(2);
    });
    
    it('should emit events to registered handlers', () => {
        const client = new WebSocketClient('/ws');
        const handler = vi.fn();
        
        client.on('test_event', handler);
        client.emit('test_event', { message: 'test' });
        
        expect(handler).toHaveBeenCalledWith({ message: 'test' });
    });
    
    it('should remove event handlers with off()', () => {
        const client = new WebSocketClient('/ws');
        const handler = vi.fn();
        
        client.on('test_event', handler);
        client.off('test_event', handler);
        
        client.emit('test_event', { message: 'test' });
        expect(handler).not.toHaveBeenCalled();
    });
    
    it('should handle errors in event handlers gracefully', () => {
        const client = new WebSocketClient('/ws');
        const errorHandler = vi.fn(() => {
            throw new Error('Handler error');
        });
        const normalHandler = vi.fn();
        
        client.on('test_event', errorHandler);
        client.on('test_event', normalHandler);
        
        // Should not throw, should continue to next handler
        expect(() => client.emit('test_event', {})).not.toThrow();
        expect(errorHandler).toHaveBeenCalled();
        expect(normalHandler).toHaveBeenCalled();
    });
});

describe('WebSocket Client - Message Handling', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
    });
    
    it('should parse and emit JSON messages', async () => {
        const client = new WebSocketClient('/ws');
        const handler = vi.fn();
        client.on('download_progress', handler);
        
        client.connect();
        await vi.runAllTimersAsync();
        
        const ws = MockWebSocket._lastInstance;
        ws._simulateMessage({
            type: 'download_progress',
            episode_id: '123',
            progress: 50
        });
        
        expect(handler).toHaveBeenCalledWith({
            episode_id: '123',
            progress: 50
        });
    });
    
    it('should handle malformed JSON messages', async () => {
        const client = new WebSocketClient('/ws');
        const errorHandler = vi.fn();
        client.on('error', errorHandler);
        
        client.connect();
        await vi.runAllTimersAsync();
        
        const ws = MockWebSocket._lastInstance;
        ws._simulateMessage('not valid json{');
        
        expect(errorHandler).toHaveBeenCalled();
    });
    
    it('should emit error for messages without type', async () => {
        const client = new WebSocketClient('/ws');
        const testHandler = vi.fn();
        client.on('test', testHandler);
        
        client.connect();
        await vi.runAllTimersAsync();
        
        const ws = MockWebSocket._lastInstance;
        ws._simulateMessage({ data: 'no type field' });
        
        // Should not emit to any handler without type
        expect(testHandler).not.toHaveBeenCalled();
    });
});

describe('WebSocket Client - Message Queueing', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
    });
    
    it('should queue messages when disconnected', () => {
        const client = new WebSocketClient('/ws');
        
        client.send('test_action', { data: 'test' });
        
        expect(client.messageQueue.length).toBe(1);
        expect(client.messageQueue[0]).toContain('test_action');
    });
    
    it('should send messages immediately when connected', async () => {
        const client = new WebSocketClient('/ws');
        client.connect();
        await vi.runAllTimersAsync();
        
        const ws = MockWebSocket._lastInstance;
        client.send('test_action', { data: 'test' });
        
        expect(ws._lastSent).toContain('test_action');
        expect(client.messageQueue.length).toBe(0);
    });
    
    it('should process queued messages on reconnection', async () => {
        const client = new WebSocketClient('/ws');
        
        // Queue messages while disconnected
        client.send('action1', { data: '1' });
        client.send('action2', { data: '2' });
        
        expect(client.messageQueue.length).toBe(2);
        
        // Connect and process queue
        client.connect();
        await vi.runAllTimersAsync();
        
        expect(client.messageQueue.length).toBe(0);
    });
});

describe('WebSocket Client - Room Management', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
    });
    
    it('should add room to rooms set on join', () => {
        const client = new WebSocketClient('/ws');
        
        client.join('downloads');
        
        expect(client.rooms.has('downloads')).toBe(true);
    });
    
    it('should send join message when connected', async () => {
        const client = new WebSocketClient('/ws');
        client.connect();
        await vi.runAllTimersAsync();
        
        const ws = MockWebSocket._lastInstance;
        client.join('downloads');
        
        expect(ws._lastSent).toContain('join');
        expect(ws._lastSent).toContain('downloads');
    });
    
    it('should remove room from rooms set on leave', async () => {
        const client = new WebSocketClient('/ws');
        client.join('downloads');
        
        client.leave('downloads');
        
        expect(client.rooms.has('downloads')).toBe(false);
    });
    
    it('should rejoin all rooms on reconnection', async () => {
        const client = new WebSocketClient('/ws', { reconnectDelay: 100 });
        client.join('downloads');
        client.join('progress');
        
        client.connect();
        await vi.runAllTimersAsync();
        
        // Simulate disconnect and reconnect
        let ws = MockWebSocket._lastInstance;
        ws._simulateClose(1006);
        await vi.runAllTimersAsync();
        
        // Check that join messages were sent for both rooms
        ws = MockWebSocket._lastInstance;
        const sentMessages = [];
        const originalSend = ws.send.bind(ws);
        ws.send = (data) => {
            sentMessages.push(data);
            return originalSend(data);
        };
        
        // Trigger rejoin
        client.rejoinRooms();
        
        expect(sentMessages.some(msg => msg.includes('downloads'))).toBe(true);
        expect(sentMessages.some(msg => msg.includes('progress'))).toBe(true);
    });
});

describe('WebSocket Client - Error Handling', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
    });
    
    it('should emit error event on WebSocket error', async () => {
        const client = new WebSocketClient('/ws');
        const errorHandler = vi.fn();
        client.on('error', errorHandler);
        
        client.connect();
        await vi.runAllTimersAsync();
        
        const ws = MockWebSocket._lastInstance;
        ws._simulateError(new Error('Connection failed'));
        
        expect(errorHandler).toHaveBeenCalled();
    });
    
    it('should emit disconnect event on connection close', async () => {
        const client = new WebSocketClient('/ws');
        const disconnectHandler = vi.fn();
        client.on('disconnect', disconnectHandler);
        
        client.connect();
        await vi.runAllTimersAsync();
        
        const ws = MockWebSocket._lastInstance;
        ws._simulateClose(1006, 'Connection lost');
        
        expect(disconnectHandler).toHaveBeenCalledWith('Connection lost');
    });
    
    it('should set isConnected to false on close', async () => {
        const client = new WebSocketClient('/ws');
        client.connect();
        await vi.runAllTimersAsync();
        
        expect(client.isConnected).toBe(true);
        
        const ws = MockWebSocket._lastInstance;
        ws._simulateClose(1000);
        await vi.runAllTimersAsync();
        
        expect(client.isConnected).toBe(false);
    });
});

describe('WebSocket Client - Connection State', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
    });
    
    it('should return false when not connected', () => {
        const client = new WebSocketClient('/ws');
        expect(client.connected()).toBe(false);
    });
    
    it('should return true when connected', async () => {
        const client = new WebSocketClient('/ws');
        client.connect();
        await vi.runAllTimersAsync();
        
        expect(client.connected()).toBe(true);
    });
    
    it('should return false after disconnection', async () => {
        const client = new WebSocketClient('/ws');
        client.connect();
        await vi.runAllTimersAsync();
        
        client.disconnect();
        await vi.runAllTimersAsync();
        
        expect(client.connected()).toBe(false);
    });
});

describe('WebSocket Client - Socket.IO Compatibility', () => {
    beforeEach(() => {
        global.WebSocket = MockWebSocket;
        vi.useFakeTimers();
        global.window = {
            location: {
                protocol: 'http:',
                host: 'localhost:8000'
            }
        };
    });
    
    afterEach(() => {
        vi.restoreAllMocks();
        vi.useRealTimers();
    });
    
    it('should create and connect client using io() function', async () => {
        const client = globalThis.io('/ws');
        await vi.runAllTimersAsync();
        
        expect(client).toBeInstanceOf(WebSocketClient);
        expect(client.isConnected).toBe(true);
    });
    
    it('should support Socket.IO-like event interface', async () => {
        const client = globalThis.io('/ws');
        const handler = vi.fn();
        client.on('test', handler);
        
        await vi.runAllTimersAsync();
        
        client.emit('test', { data: 'value' });
        expect(handler).toHaveBeenCalledWith({ data: 'value' });
    });
});
