import { jest } from '@jest/globals';

class CloseEvent extends Event {
    constructor(type='close', { wasClean=false, code=0, reason='' } = {}) {
        super(type);
        Object.defineProperties(this, {
            code: { enumerable: true, configurable: true, writable: false, value: code },
            reason: { enumerable: true, configurable: true, writable: false, value: reason },
            wasClean: { enumerable: true, configurable: true, writable: false, value: wasClean },
        });
    }
}

class MessageEvent extends Event {
    constructor(type='message', { data }) {
        super(type);
        Object.defineProperties(this, {
            data: { enumerable: true, configurable: true, writable: false, value: data }
        });
    }
}

class MockEvent extends Event {
    constructor(type, { instance, controller }) {
        super(type);
        Object.defineProperties(this, {
            instance: { enumerable: true, configurable: false, writable: false, value: instance },
            controller: { enumerable: true, configurable: false, writable: false, value: controller }
        });
    }
}

const WebSocketEventTarget = new EventTarget();
const WebSocketControllers = new WeakMap();
const WebSocket = jest.fn().mockImplementation((url, protocol) => {

    const eventTarget = new EventTarget();
    let readyState = 0;

    const controller = {
        created: Date.now(),
        sent: [],
        bufferedAmount: 0,
        get url() { return url; },
        get readyState() { return readyState; },
        open() {
            if (readyState === 0) {
                readyState = 1;
                eventTarget.dispatchEvent(new Event('open'));
            }
        },
        close(code=0) {
            if (readyState === 3) return;
            if (readyState !== 1) {
                this.error('Connection failed');
                code = code || 1006;
            } else {
                code = code || 1000;
            }
            readyState = 3;
            eventTarget.dispatchEvent(new CloseEvent('close', { code }));
        },
        message(payload) {
            console.log(`Sending ${payload} in readyState ${readyState}`);
            if (readyState !== 1) return;
            eventTarget.dispatchEvent(new MessageEvent('message', { data: payload }));
        },
        error(reason) {
            eventTarget.dispatchEvent(new ErrorEvent('error', { reason }));
        }
    };

    const directEventHandlers = {};
    ['error','open','close','message'].forEach(evtName => {
        directEventHandlers[evtName] = null;
        eventTarget.addEventListener(evtName, evt => {
            const handler = directEventHandlers[evtName];
            if (typeof handler === 'function') {
                return handler(evt);
            }
        });
    });
    

    const instance = {

        get bufferedAmount() { return controller.bufferedAmount; },
        get url() { return url; },
        get protocol() { return protocol; },
        get readyState() { return readyState; },

        addEventListener: jest.fn((name, handler, options={}) => eventTarget.addEventListener(name, handler, options)),
        removeEventListener: jest.fn((name, handler, options={}) => eventTarget.removeEventListener(name, handler, options)),
        dispatchEvent: jest.fn(evt => eventTarget.dispatchEvent(evt)),

        close: jest.fn(() => {
            if (readyState === 1) readyState = 2;
        }),
        send: jest.fn(msg => {
            controller.sent.push(msg);
            controller.bufferedAmount += msg.length;
        }),

        set onopen(handler)    { directEventHandlers.open = handler; },
        set onclose(handler)   { directEventHandlers.close = handler; },
        set onerror(handler)   { directEventHandlers.error = handler; },
        set onmessage(handler) { directEventHandlers.message = handler; },
    };

    controller.instance = instance;
    WebSocketControllers.set(instance, controller);
    setTimeout(() => WebSocketEventTarget.dispatchEvent(new MockEvent('create', { instance, controller })), 0);

    return instance;

});

WebSocket.controllers = WebSocketControllers;

global.WebSocket = WebSocket;
if (typeof global.CloseEvent === 'undefined') global.CloseEvent = CloseEvent;
if (typeof global.MessageEvent === 'undefined') global.MessageEvent = MessageEvent;

Object.defineProperties(WebSocket, {
    addEventListener: {
        configurable: true,
        enumerable: false,
        value: WebSocketEventTarget.addEventListener.bind(WebSocketEventTarget)
    },
    removeEventListener: {
        configurable: true,
        enumerable: false,
        value: WebSocketEventTarget.removeEventListener.bind(WebSocketEventTarget)
    },
    controllers:{ configurable: true, enumerable: true, writable: false, value: WebSocketControllers },
    CONNECTING: { configurable: true, enumerable: true, writable: false, value: 0 },
    OPEN:       { configurable: true, enumerable: true, writable: false, value: 1 },
    CLOSING:    { configurable: true, enumerable: true, writable: false, value: 2 },
    CLOSED:     { configurable: true, enumerable: true, writable: false, value: 3 },
});
