import { describe, expect, jest } from '@jest/globals';
import { defineEvents } from '../js/async/events.js';
import '../js/ext/polyfills.js';

class Dispatcher extends EventTarget { }

const beforeOwn = Object.getOwnPropertyNames(new Dispatcher());
const beforeProto = Object.getOwnPropertyNames(Dispatcher.prototype);

defineEvents(Dispatcher, 'aaa', 'bbb');


describe("defineEvents(class, event1, event2, ...)", () => {

    it ("adds an non-enumerable onNNN property for each name", () => {

        const expectNonEnumerableProperty = (object, name) => {
            const prop = Object.getOwnPropertyDescriptor(object, name)
            expect(prop).toMatchObject({
                enumerable: false,
                get: expect.any(Function),
                set: expect.any(Function)
            })
        }
        expectNonEnumerableProperty(Dispatcher.prototype, 'onaaa');
        expectNonEnumerableProperty(Dispatcher.prototype, 'onbbb');
    });

    it ("no more methods or properties are added", () => {
        
        const own = Object.getOwnPropertyNames(new Dispatcher());
        const proto = Object.getOwnPropertyNames(Dispatcher.prototype);

        expect(own.length).toEqual(beforeOwn.length);
        expect(proto.length).toEqual(beforeProto.length + 2);
    });

    it ("the methods are inherited by derived classes", () => {

        class DerDispatcher extends Dispatcher { }

        expect(DerDispatcher.prototype).toHaveProperty('onaaa');
        expect(DerDispatcher.prototype).toHaveProperty('onbbb');

    });

    it ("can be safely called on a derived class", () => {

        class DerDispatcher extends Dispatcher { }

        defineEvents(DerDispatcher, 'ccc');
        expect(DerDispatcher.prototype).toHaveProperty('onaaa');
        expect(DerDispatcher.prototype).toHaveProperty('onbbb');
        expect(DerDispatcher.prototype).toHaveProperty('onccc');
    });

});

describe("addEventListener(eventName, handler, options?)", () => {

    it ("registers an event handler", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();

        disp.addEventListener('aaa', handler);
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).toBeCalledTimes(1);
    });

    it ("can register event handlers for events not listed in defineEvents()", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();

        disp.addEventListener('ccc', handler);
        disp.dispatchEvent(new Event('ccc'));
        expect(handler).toBeCalledTimes(1);
    });

    it ("handlers are called in the order of registration", () => {
        const disp = new Dispatcher();
        const calls = [];

        disp.addEventListener('aaa', () => calls.push(1));
        disp.addEventListener('aaa', () => calls.push(2));
        disp.dispatchEvent(new Event('aaa'));

        expect(calls).toEqual([1,2]);
    });

    it ("the handler is removed when used with options.once = true", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();
        disp.addEventListener('aaa', handler, {once:true});
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).toBeCalledTimes(1);
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).toBeCalledTimes(1);
    });

    it ("when options.signal is specified, then aborting the signal removes the listener", async () => {
        const ctl = new AbortController();
        const disp = new Dispatcher();
        const handler = jest.fn();

        disp.addEventListener('aaa', handler, { signal: ctl.signal });
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).toBeCalled();

        handler.mockClear();
        ctl.abort();
        await new Promise(res => setTimeout(res, 10));
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).not.toBeCalled();
    });

    it ("the handler is ignored when the signal is already aborted", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();

        disp.addEventListener('aaa', handler, { signal: AbortSignal.abort() });
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).not.toBeCalled();
    });

});

describe("removeEventListener(eventName, handler)", () => {

    it ("removes a previously registered event handler", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();
        disp.addEventListener('aaa', handler);
        disp.removeEventListener('aaa', handler);
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).not.toBeCalled();
    });

    it ("does nothing when no handler is found", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();
        disp.removeEventListener('aaa', handler);
    });

});

describe("dispatchEvent(eventName, eventData?)", () => {

    // it ("calls handlers registered for the event and returns the number of found handlers", async () => {
    //     const disp = new Dispatcher();
    //     const handlers = 3;
    //     for (let i=0; i < handlers; i++)
    //         disp.addEventListener('aaa', () => true);
    //     const called = await disp.dispatchEvent(new Event('aaa'));
    //     expect(called).toEqual(handlers);
    // });

    // it ("if eventData is not an instance of Event, then it is converted to an instance of the class provided in defineEvents() " +
    //      "(or Event for event types not listed) and the property 'type' is set to eventName",
    // async () => {
    //     const disp = new Dispatcher();
    //     let lastEvent = null;
    //     disp.addEventListener('aaa', evt => lastEvent = evt);
    //     disp.addEventListener('bbb', evt => lastEvent = evt);
        
    //     await disp.dispatchEvent(new Event('aaa'));
    //     expect(lastEvent).toBeInstanceOf(Event);
    //     expect(lastEvent.type).toStrictEqual('aaa');

    //     await disp.dispatchEvent(new BbbEvent('bbb'));
    //     expect(lastEvent).toBeInstanceOf(BbbEvent);
    //     expect(lastEvent.type).toStrictEqual('bbb');

    //     await disp.dispatchEvent('bbb', new Event('test'));
    //     expect(lastEvent).toBeInstanceOf(Event);
    //     expect(lastEvent.type).toStrictEqual('test');
    // });

    it ("each handler receives the same instance of the event class with properties " +
        "type (the name of the event), data (the optional eventData), target (the dispatcher)",
    async () => {
        const disp = new Dispatcher();
        const event = new Event('aaa');
        const events = [];
        disp.addEventListener('aaa', evt => events.push(evt));
        disp.addEventListener('aaa', evt => events.push(evt));
        disp.dispatchEvent(event);

        expect(events.length).toEqual(2)
        expect(events[0]).toStrictEqual(event);
        expect(events[1]).toStrictEqual(event);
        expect(event).toMatchObject({
            type: 'aaa',
            target: disp
        });
    });

    it ("can dispatch events not listed in defineEvents()", () => {
        const disp = new Dispatcher();
        const handler = jest.fn()
        disp.addEventListener('ccc', handler);
        disp.dispatchEvent(new Event('ccc'));
        expect(handler).toBeCalled();
    });

    it ("the handlers are fired immediately", () => {
        const disp = new Dispatcher();
        const calls = [];
        disp.addEventListener('aaa', () => calls.push('event'));
        disp.dispatchEvent(new Event('aaa'));
        expect(calls.length).toEqual(1)
    });

});

describe("onNNN", () => {

    it ("when assigned a function, registers it as a handler for the event NNN", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();
        disp.onaaa = handler;
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).toBeCalledTimes(1);
    });

    it ("subsequent assignments remove previously registered handlers", () => {
        const disp = new Dispatcher();
        const handler1 = jest.fn();
        const handler2 = jest.fn();
        disp.onaaa = handler1;
        disp.onaaa = handler2;
        disp.dispatchEvent(new Event('aaa'));
        expect(handler1).not.toBeCalled();
        expect(handler2).toBeCalled();
    });

    it ("assigning null removes a previously assigned handler", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();
        disp.onaaa = handler;
        disp.onaaa = null;
        disp.dispatchEvent(new Event('aaa'));
        expect(handler).not.toBeCalled();
    });

    it ("assigning a value of a different type does not affect the current handler", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();

        disp.onaaa = handler;
        disp.onaaa = 5;
        disp.onaaa = "abf";
        disp.onaaa = {a:5};
        disp.onaaa = [];

        disp.dispatchEvent(new Event('aaa'));
        expect(handler).toBeCalled();
    });

    it ("the getter for onNNN returns the assigned function or undefined", () => {
        const disp = new Dispatcher();
        const handler = jest.fn();

        disp.onaaa = handler;
        expect(disp.onaaa).toEqual(handler);
        expect(disp.onbbb).toBeUndefined();

        disp.onaaa = null;
        expect(disp.onaaa).toBeUndefined();
    });

    it ("can only be used for events registered with defineEvents(...)", async () => {
        const disp = new Dispatcher();
        const handler = jest.fn();
        disp.onccc = handler;
        disp.dispatchEvent(new Event('ccc'));
        expect(handler).not.toBeCalled();
    });

});
