import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';
import Messenger from '../js/messenger.js';
import { UrlFactory } from '../js/medium/urlfactory.js';
import { parseWebSocketMessage } from '../js/medium/nchan.js';
import { Message, Response, Request } from '../js/medium/protocol.js';
import { wait, waitForEvent, isResolved, isSettled, isRejected, AbortError } from '../js/async/promise.js';
import { ChangeEvent } from '../js/async/events.js';

import '../js/ext/polyfills.js';
import './mocks/websocket.js';
import MockFetch from './mocks/fetch.js';
import spyOn from './mocks/spy.js';
import createController from './helpers/messenger-controller.js';

jest.setTimeout(1000);

const resolvesImmediately = promise => wait(0).then(() => isResolved(promise));
const rejectsImmediately  = promise => wait(0).then(() => isRejected(promise));
const settlesImmediately  = promise => wait(0).then(() => isSettled(promise));

const resolvesWithin = (promise, ms) => Promise.race([promise.then(() => true, () => false), wait(ms, false)]);
const rejectsWithin  = (promise, ms) => Promise.race([promise.then(() => false, () => true), wait(ms, false)]);
const settlesWithin  = (promise, ms) => Promise.race([promise.finally(() => true), wait(ms, false)]);

const memoryLogger = () => ({
    logs: [],
    infos: [],
    warnings: [],
    errors: [],
    debugs: [],
    log(...args) { this.logs.push(args); },
    debug(...args) { this.debugs.push(args); },
    info(...args) { this.infos.push(args); },
    warning(...args) { this.warnings.push(args); },
    error(...args) { this.errors.push(args); }
});

const defaultUrlFactory = new UrlFactory('https://test.com/path', 'tester', { pub: 'p1,p2', sub: 's1,s2' });
const defaultAccessToken = 'token';

const createMessenger = (urlFactory, token, options) => {
    const messenger = new Messenger(
        urlFactory || defaultUrlFactory,
        { logger: memoryLogger(), ...options }
    );
    if (token !== false) messenger.tokens.add(token || defaultAccessToken);
    return messenger;
};

const createMessengerPair = (urlFactory, token, options) => {
    const messenger = createMessenger(urlFactory, token, options);
    const controller = createController(messenger);
    return { messenger, controller };
};

const testMessage = (received, expected) => {
    expect(received).toBeInstanceOf(Message);
    ['channel', 'name', 'timestamp', 'id'].forEach(prop =>
        expected.hasOwnProperty(prop) &&
            expect(received).toHaveProperty(prop, expected[prop])
    );
    expect(received).toHaveProperty('payload');
    expect(received['payload']).toEqual(expected['payload']);
};

const messengerStates = [
    Messenger.WAITING,
    Messenger.CONNECTING,
    Messenger.CONFIGURING,
    Messenger.CONNECTED,
    Messenger.CLOSING,
    Messenger.CLOSED
];

describe ('Messenger states', () => {

    it('there are six connection state constants', () => {

        expect(Messenger.WAITING).toBeDefined()
        expect(Messenger.CONNECTING).toBeDefined()
        expect(Messenger.CONFIGURING).toBeDefined()
        expect(Messenger.CONNECTED).toBeDefined()
        expect(Messenger.CLOSING).toBeDefined()
        expect(Messenger.CLOSED).toBeDefined()

        const states = new Set(messengerStates);
        expect(states.size).toEqual(6);
    })

    it('.desiredState is CONNECTED or CLOSED, depending on .state', () => {
        const { messenger, controller } = createMessengerPair();
        for (const state of messengerStates) {
            controller.setState(state);
            if (state === Messenger.CLOSING || state === Messenger.CLOSED)
                expect(messenger.desiredState).toBe(Messenger.CLOSED);
            else
                expect(messenger.desiredState).toBe(Messenger.CONNECTED);
        }
    })

    it('.isReady returns true exactly when .state is CONNECTED', () => {
        const { messenger, controller } = createMessengerPair();
        for (const state of messengerStates) {
            controller.setState(state);
            if (state === Messenger.CONNECTED)
                expect(messenger.isReady).toBeTruthy();
            else
                expect(messenger.isReady).toBeFalsy();
        }
    })

    it('changing a state triggers a statechange event', async () => {
        const { messenger, controller } = createMessengerPair();
        const waiter = waitForEvent(messenger, 'statechange');
        controller.changeState(Messenger.CONNECTED);
        const event = await waiter;

        expect(event).toBeInstanceOf(ChangeEvent);
        expect(event.type).toEqual('statechange');
        expect(event.previousValue).toEqual(Messenger.CLOSED);
        expect(event.detail.previous).toEqual(Messenger.CLOSED);
        expect(event.currentValue).toEqual(Messenger.CONNECTED);
        expect(event.detail.current).toEqual(Messenger.CONNECTED);
    })

    it('connected() resolves once the state changes to CONFIGURING', async () => {
        const { messenger, controller } = createMessengerPair();
        const connected = messenger.connected();
        
        // The promise resolves once the state is set to CONNECTED
        await expect(isSettled(connected)).resolves.toBeFalsy();
        controller.changeState(Messenger.CONFIGURING);
        await expect(resolvesImmediately(connected)).resolves.toBeTruthy();

        // In CONFIGURING or CONNECTED state the promise resolves immediately
        await expect(resolvesImmediately(messenger.connected())).resolves.toBeTruthy();
        controller.changeState(Messenger.CONNECTED);
        await expect(resolvesImmediately(messenger.connected())).resolves.toBeTruthy();
    })

    it('opened() resolves once the state changes to CONNECTED', async () => {
        const { messenger, controller } = createMessengerPair();
        const opened = messenger.opened();
        
        // The promise resolves once the state is set to CONNECTED
        await expect(isSettled(opened)).resolves.toBeFalsy();
        controller.changeState(Messenger.CONNECTED);
        await expect(resolvesImmediately(opened)).resolves.toBeTruthy();

        // In CONNECTED state the promise resolves immediately
        const alreadyOpened = messenger.opened();
        await expect(resolvesImmediately(alreadyOpened)).resolves.toBeTruthy();
    })

    it('closed() resolves once the state changes to CLOSED', async () => {
        const { messenger, controller } = createMessengerPair();
        // Simulate an opened connection
        controller.setState(Messenger.CONNECTED);

        const closed = messenger.closed();
        
        // The promise resolves once the state is set to CLOSED
        await expect(isSettled(closed)).resolves.toBeFalsy();
        controller.changeState(Messenger.CLOSED);
        await expect(resolvesImmediately(closed)).resolves.toBeTruthy();

        // In CLOSED state the promise resolves immediately
        const alreadyClosed = messenger.closed();
        await expect(resolvesImmediately(alreadyClosed)).resolves.toBeTruthy();
    })

    it('connected() can be cancelled with a signal', async () => {
        const { messenger, controller } = createMessengerPair();
        const abortController = new AbortController();

        const connected = messenger.connected({ signal: abortController.signal });
        abortController.abort();
        await expect(connected).rejects.toThrowError(AbortError);
    })

    it('opened() can be cancelled with a signal', async () => {
        const { messenger, controller } = createMessengerPair();
        const abortController = new AbortController();

        const opened = messenger.opened({ signal: abortController.signal });
        abortController.abort();
        await expect(opened).rejects.toThrowError(AbortError);
    })

    it('closed() can be cancelled with a signal', async () => {
        const { messenger, controller } = createMessengerPair();
        const abortController = new AbortController();
        // Simulate an opened connection
        controller.setState(Messenger.CONNECTED);

        const closed = messenger.closed({ signal: abortController.signal });
        abortController.abort();
        await expect(closed).rejects.toThrowError(AbortError);
    })
})

describe ('waitForConnectionState(desiredState, { signal, timeout })', () => {

    it('resolves when the desired state is reached', async () => {
        const { messenger, controller } = createMessengerPair();
        const waitedState = Messenger.CLOSING;

        const promise = messenger.waitForConnectionState(waitedState);
        for (const state of messengerStates) {
            if (state === waitedState) continue;
            controller.changeState(state);
            await expect(settlesImmediately(promise)).resolves.toBeFalsy();
        }
        controller.changeState(waitedState);
        await expect(settlesImmediately(promise)).resolves.toBeTruthy();
    })

    it('resolves immediately for the current state', async () => {
        const messenger = createMessenger();
        const promise = messenger.waitForConnectionState(messenger.state);
        await expect(isResolved(promise)).resolves.toBeTruthy();
    })

    it('rejects when signal is aborted', async () => {
        const messenger = createMessenger();
        const controller = new AbortController();
        const promise = messenger.waitForConnectionState(Messenger.CONNECTED, { signal: controller.signal });
        controller.abort();
        await expect(promise).rejects.toThrowError(AbortError);
    });

})

describe ('Messenger token handling', () => {

    beforeAll(() => MockFetch.setUp());
    afterAll(() => MockFetch.tearDown());
    afterEach(() => MockFetch.mockReset());

    it ('.tokens is a set-like collection of tokens', () => {

        const messenger = new Messenger(defaultUrlFactory);
        const tokens = ['one', 'two', 'three'];

        // Initially the list is empty
        expect(messenger.tokens.size).toStrictEqual(0);

        // Several tokens can be added at once
        messenger.tokens.add(...tokens);
        expect(messenger.tokens.size).toStrictEqual(3);
        tokens.forEach(token => expect(messenger.tokens.has(token)).toBeTruthy());
        expect(messenger.tokens.has('four')).toBeFalsy();

        // Adding tokens for the second time does nothing
        messenger.tokens.add(...tokens);
        expect(messenger.tokens.size).toStrictEqual(3);

        // Several tokens can be removed at once
        messenger.tokens.delete('one', 'three', 'four');
        expect(messenger.tokens.size).toStrictEqual(1);
        expect(messenger.tokens.has('one')).toBeFalsy();
        expect(messenger.tokens.has('two')).toBeTruthy();
        expect(messenger.tokens.has('three')).toBeFalsy();

        // The collection can be cleared with clear()
        messenger.tokens.add(...tokens);
        expect(messenger.tokens.size).toStrictEqual(3);
        messenger.tokens.clear();
        expect(messenger.tokens.size).toStrictEqual(0);
    })

    it ('.tokens.merged() resolves with a merged token', async () => {

        const mergedTokenName = 'mergedtoken';
        MockFetch.mockJsonOnce({
            success: true,
            data: mergedTokenName
        });

        const messenger = new Messenger(defaultUrlFactory);
        const tokens = ['one', 'two', 'three'];

        messenger.tokens.add(...tokens);
        const merged = await messenger.tokens.merged();

        expect(merged).toEqual(mergedTokenName);
        expect(fetch).toBeCalledTimes(1);

        const fetchUrl = fetch.mock.calls[0][0],
              fetchMethod = fetch.mock.calls[0][1].method,
              fetchBody = fetch.mock.calls[0][1].body;
        expect(fetchUrl).toEqual(defaultUrlFactory.restEndpoint('token/merge'));
        expect(fetchMethod).toEqual('POST');
        expect(typeof fetchBody).toEqual('string');
        expect(JSON.parse(fetchBody)).toHaveProperty('tokens', tokens);
    })

    it ('.tokens.merged() calls REST API only at the first time', async () => {

        const messenger = new Messenger(defaultUrlFactory);
        messenger.tokens.add('one', 'two', 'three');

        const tokenNameA = 'mergedTokenA';
        const tokenNameB = 'mergedTokenB';
        MockFetch.mockJsonOnce({ success: true, data: tokenNameA });
        MockFetch.mockJsonOnce({ success: true, data: tokenNameB });

        const token1 = await messenger.tokens.merged();
        const token2 = await messenger.tokens.merged();

        expect(fetch).toBeCalledTimes(1);
        expect(token1).toEqual(tokenNameA);
        expect(token2).toEqual(tokenNameA);
    })

    it ('.tokens.merged() returns the owned token if only one has been assigned', async () => {

        const messenger = new Messenger(defaultUrlFactory);
        messenger.tokens.add('unique');

        const token = await messenger.tokens.merged();

        expect(fetch).not.toBeCalled();
        expect(token).toEqual('unique');
    })

    it ('.tokens.merged({ signal }) aborts on signal', async () => {
        const messenger = createMessenger();
        const controller = new AbortController();
        MockFetch.mockPending();

        // Set several tokens, so that merge() uses Fetch API
        messenger.tokens.add('one', 'two', 'three');
        // Make a request and abort it
        const tokenPromise = messenger.tokens.merged({ signal: controller.signal });
        await wait(10);
        controller.abort();
        await expect(tokenPromise).rejects.toThrowError(DOMException);
        
        // Try again with an already aborted signal
        messenger.tokens.add('four');
        const secondPromise = messenger.tokens.merged({ signal: controller.signal });
        await expect(secondPromise).rejects.toThrowError(DOMException);
    })
})


describe ('Messenger publisher', () => {

    beforeAll(() => MockFetch.setUp());
    afterAll(() => MockFetch.tearDown());
    afterEach(() => MockFetch.mockReset());
    beforeEach(() => MockFetch.mockJson({}, { status: 201 }));

    const channels = ['ch1', 'ch2', 'ch3'];

    it ('publish() makes a POST request to a publisher location' , async () => {
        const { messenger, controller } = createMessengerPair();
        controller.channels.pub = channels;

        const payload = { name: 'test', value: 42 };
        const messageName = 'update'
        const pubChannels = ['channel1', 'xyz'];
        await messenger.publish(messageName, payload, { channels: pubChannels });

        expect(fetch).toBeCalledTimes(1);
        expect(fetch.mock.lastCall[0]).toEqual(defaultUrlFactory.publisherEndpoint(pubChannels));

        const fetchOptions = fetch.mock.lastCall[1];
        expect(fetchOptions).toHaveProperty('method', 'POST');
        expect(fetchOptions).toHaveProperty('headers.Authorization', defaultAccessToken);
        expect(fetchOptions).toHaveProperty('headers.Accept');
        expect(fetchOptions).toHaveProperty('headers.Content-Type');
        expect(fetchOptions.headers.Accept).toMatch(/^(text|application)\/json$/);
        expect(fetchOptions.headers['Content-Type']).toMatch(/^(text|application)\/json$/);

        // Request is authenticated by the token and contains fields
        //   type: message
        //   name: <first parameter>
        //   payoad: <second parameter>
        expect(typeof fetchOptions.body).toEqual('string');
        const body = JSON.parse(fetchOptions.body);
        expect(body).toHaveProperty('type', 'message');
        expect(body).toHaveProperty('name', messageName);
        expect(body).toHaveProperty('payload', payload);
    })

    it ('publish() calls tokens.merged() to obtain the authorization token', async () => {
        const messenger = createMessenger();
        const spiedMerged = spyOn(messenger.tokens, 'merged');

        await messenger.publish('name', null, { channels: ['channel', 'xyz'] });

        // publish() gets the token
        expect(spiedMerged.called).toEqual(1);
    })

    it ('publish() puts the message on the internal queue if no channels are specified', async () => {
        const { messenger, controller } = createMessengerPair();
        const spiedMerged = spyOn(messenger.tokens, 'merged');

        await messenger.publish('name', null);

        expect(fetch).not.toBeCalled();
        expect(spiedMerged.called).toEqual(0);
        expect(controller.queue.size).toBe(1);
    })

    it ('publish() puts the message on the internal queue if all pub channels are specified', async () => {
        const { messenger, controller } = createMessengerPair();
        const spiedMerged = spyOn(messenger.tokens, 'merged');
        fetch.mockClear();

        await messenger.publish('name', null, { channels: controller.channels.pub });

        expect(fetch).not.toBeCalled();
        expect(spiedMerged.called).toEqual(0);
        expect(controller.queue.size).toBe(1);
    })

    it ('publish() forwards fetch errors', async () => {
        const testError = Error('test error');
        MockFetch.mockExceptionOnce(testError);

        const messenger = createMessenger();
        const promise1 = messenger.publish('name', null, { channels: ['xyz'] });
        await expect(promise1).rejects.toThrow(testError);

        MockFetch.mockJsonOnce({}, { status: 404 });
        const promise2 = messenger.publish('name', null, { channels: ['xyz'] });
        await expect(promise2).rejects.toThrowError(Error);
    })

})


describe ('Messenger subscribers', () => {

    const channels = ['ch1', 'ch2', 'ch3', 'ch4'];
    const getChannels = (...ids) => ids.map(x => channels[x]);

    const urlFactory = new UrlFactory('test', 'https://domain.com/messeger-dev');

    const createSubscriber = () => ({
        handleMessage: jest.fn(),
        handleRequest: jest.fn()
    });

    const createMessage = ({channel=channels[0], type='message', name='test', payload=42} = {}) => {
        const timestamp = Date.now();
        return {
            decoded: { type, name, timestamp, channel, payload },
            raw: [
                `id: ${timestamp}:` + channels.map(ch => channel == ch ? '[0]' : '-').join(','),
                JSON.stringify({ type, name, payload })
            ].join('\n\n'),
            get event() { return { data: this.raw }; }
        };
    };


    it('can be subscribed with .subscribe(subscriber, channels)',  () => {
        const { messenger, controller } = createMessengerPair();
        controller.channels.sub = channels;
        
        const S = createSubscriber();
        const msg = createMessage();

        messenger.subscribe(S, getChannels(0));

        expect(controller.subscribers.get(channels[0]).has(S)).toBeTruthy();

        // simulate a message event
        messenger.handleSocketMessage(msg.event);
        expect(S.handleMessage).toBeCalledTimes(1);
        testMessage(S.handleMessage.mock.calls[0][0], msg.decoded);
    })

    it('.subscribe(subscriber, channels) does not remove previous subscriptions', () => {
        const { messenger, controller } = createMessengerPair();
        controller.channels.sub = channels;

        const S = createSubscriber();
        const msg = createMessage();

        messenger.subscribe(S, getChannels(0));
        messenger.subscribe(S, getChannels(2))
        messenger.handleSocketMessage(msg.event);
        expect(S.handleMessage).toBeCalledTimes(1);
        expect(messenger.logger.errors).toHaveLength(0);
    })

    it('.subscribe(subscriber, channels) does not subscribe twice to the same channel', () => {
        const { messenger, controller } = createMessengerPair();
        controller.channels.sub = channels;

        const S = createSubscriber();
        const msg = createMessage();

        messenger.subscribe(S, getChannels(0));
        messenger.subscribe(S, getChannels(0,2));
        messenger.handleSocketMessage(msg.event);
        expect(S.handleMessage).toBeCalledTimes(1);
        expect(messenger.logger.errors).toHaveLength(0);
    })

    it('.subscribe(subscriber) subscribes to all channels', () => {
        const { messenger, controller } = createMessengerPair();
        controller.channels.sub = channels;

        const S = createSubscriber();
        const msg1 = createMessage({ channel: channels[1] });
        const msg2 = createMessage({ channel: channels[2] });

        messenger.subscribe(S);
        messenger.handleSocketMessage(msg1.event);
        messenger.handleSocketMessage(msg2.event);

        expect(S.handleMessage).toBeCalledTimes(2);
        expect(messenger.logger.errors).toHaveLength(0);
    })

    it('.unsubscribe(subscriber) does not unsubscribe from specific channels', () => {
        const { messenger, controller } = createMessengerPair();
        controller.channels.sub = channels;

        const S = createSubscriber();
        const msg = createMessage();

        messenger.subscribe(S, getChannels(0));
        messenger.unsubscribe(S);
        messenger.handleSocketMessage(msg.event);

        expect(S.handleMessage).toBeCalled();
        expect(messenger.logger.errors).toHaveLength(0);
    })

    it('can be unsubscribed from some channels with .unsubscribe(subscriber, channels)', () => {
        const { messenger, controller } = createMessengerPair();
        controller.channels.sub = channels;

        const S = createSubscriber();
        const msg0 = createMessage({ channel: channels[0] });
        const msg2 = createMessage({ channel: channels[2] });

        messenger.subscribe(S, getChannels(0,2,3));
        messenger.unsubscribe(S, getChannels(2,3));
        messenger.handleSocketMessage(msg0.event);
        messenger.handleSocketMessage(msg2.event);

        expect(S.handleMessage).toBeCalledTimes(1);
        testMessage(S.handleMessage.mock.calls[0][0], msg0.decoded);
        expect(messenger.logger.errors).toHaveLength(0);
    })

    it('.unsubscribe(subscriber, channels) silently ignores channels to which subscriber does not listen', () => {
        const { messenger, controller } = createMessengerPair();
        controller.channels.sub = channels;

        const S = createSubscriber();
        const msg = createMessage({ channel: channels[2] });

        messenger.subscribe(S, getChannels(0));
        messenger.unsubscribe(S, getChannels(2,3));
        messenger.handleSocketMessage(msg.event);

        expect(S.handleMessage).not.toBeCalled();
        expect(messenger.logger.errors).toHaveLength(0);
    })

})

describe ('Messenger.open()', () => {

    const connectingStates = [
        { name: 'CONNECTING', stateValue: Messenger.CONNECTING },
        { name: 'CONFIGURING', stateValue: Messenger.CONFIGURING }
    ];

    it.each(connectingStates) ('on $name does nothing and resolves with the internal promise', async ({ stateValue }) => {
        const { messenger, controller } = createMessengerPair();
        const spiedMerged = spyOn(messenger.tokens, 'merged');
        controller.setState(stateValue);
        // Call open() and run the tests
        // - promise not settled
        // - internal promise exists
        // - resolves with the internal promise
        const openPromise = messenger.open();
        expect(messenger.state).toBe(stateValue);
        expect(controller.promises.opened).toBeDefined;
        await expect(isSettled(openPromise)).resolves.toBeFalsy();
        // Resolve the internal promise and check that
        // - token has not been fetched
        // - backend has not been created
        controller.promises.opened.resolve();
        await expect(openPromise).resolves.not.toThrow();
        expect(spiedMerged.called).toEqual(0);
        expect(WebSocket).not.toBeCalled();
    })

    it ('on CONNECTED does nothing and resolves immediately', async () => {
        const { messenger, controller } = createMessengerPair();
        const spiedMerged = spyOn(messenger.tokens, 'merged');
        controller.setState(Messenger.CONNECTED);
        // Call open() and run the tests
        // - promise resolved
        // - no internal promise exists
        // - token not fetched
        // - backened not created
        const promise = messenger.open();
        expect(messenger.state).toBe(Messenger.CONNECTED);
        expect(controller.promises.opened).not.toBeDefined();
        await expect(isResolved(promise)).resolves.toBeTruthy();
        expect(spiedMerged.called).toEqual(0);
        expect(WebSocket).not.toBeCalled();
    })

    it ('on CLOSING changes state to CONNECTING, aborts close(), creates a new connection backend, and resolves with the internal promise', async () => {
        const { messenger, controller } = createMessengerPair();
        const spiedMerged = spyOn(messenger.tokens, 'merged');
        // Open a connection so that it can be closed
        controller.openConnection().then(
            () => controller.promises.opened.resolve()
        );
        await messenger.open();
        const firstConnection = controller.connection;
        // Call close() and check that we have reached the desired state
        const closePromise = messenger.close();
        expect(messenger.state).toBe(Messenger.CLOSING);
        expect(firstConnection.readyState).toBe(WebSocket.CLOSING);
        // Clear mocks
        spiedMerged.mockClear();
        WebSocket.mockClear();
        // Call open() and run the tests:
        //  - state is CONNECTING
        //  - internal promise "closed" is aborted and removed
        //  - internal promise "opened" is created but not settled yet
        //  - tokens.merged() is called
        //  - a new connection backend is created
        const newConnectionPromise = controller.waitForNewConnection();
        const openPromise = messenger.open();
        expect(messenger.state).toBe(Messenger.CONNECTING);
        expect(controller.promises.closed).not.toBeDefined();
        expect(controller.promises.opened).toBeDefined();
        await expect(closePromise).rejects.toThrowError(AbortError);
        await newConnectionPromise;
        expect(controller.connection).not.toBe(firstConnection);
        expect(spiedMerged.called).toEqual(1);
        expect(WebSocket).toBeCalled();
        await expect(isSettled(openPromise)).resolves.toBeFalsy();
        // Check that the promise resolves with the internal one
        controller.promises.opened.resolve();
        await expect(openPromise).resolves.not.toThrow();
    })

    it ('on CLOSED changes state to CONNECTING, creates a new connection backend, and resolves with the internal promise', async () => {
        const { messenger, controller } = createMessengerPair();
        const spiedMerged = spyOn(messenger.tokens, 'merged');
        // Call open() and run the tests:
        //  - state is CONNECTING
        //  - internal promise "closed" is aborted and removed
        //  - internal promise "opened" is created but not settled yet
        //  - getToken() is called
        //  - a new connection backend is created
        const openPromise = messenger.open();
        expect(messenger.state).toBe(Messenger.CONNECTING);
        expect(controller.promises.opened).toBeDefined();
        await controller.waitForConnection();
        expect(spiedMerged.called).toEqual(1);
        expect(WebSocket).toBeCalled();
        await expect(isSettled(openPromise)).resolves.toBeFalsy();
        // Check that the promise resolves with the internal one
        controller.promises.opened.resolve();
        await expect(openPromise).resolves.not.toThrow();
    })

    it ('changes state to CONFIGURING when a connection is opened', async () => {
        const { messenger, controller } = createMessengerPair();
        const spiedHandler = jest.spyOn(Messenger.prototype, 'handleSocketOpen');
        const openPromise = messenger.open();
        const internalOpenPromise = controller.promises.opened.promise;
        // Create the backend and simulate an opened connection
        await controller.openConnection();
        // Check that the promises are not resolved yet,
        // despite handleSocketOpen has been called
        expect(spiedHandler).toBeCalled();
        await expect(resolvesImmediately(openPromise)).resolves.toBeFalsy();
        await expect(isResolved(internalOpenPromise)).resolves.toBeFalsy();
        expect(messenger.state).toBe(Messenger.CONFIGURING);
        spiedHandler.mockRestore();
    })
    
    it ('changes state to CONNECTED and resolves when a hello message is received', async () => {
        const { messenger, controller } = createMessengerPair();
        const spiedHandler = jest.spyOn(Messenger.prototype, 'handleSocketMessage');
        // Prepare the messenger: create a backend connection but do not open the connection yet
        const openPromise = messenger.open();
        const internalOpenPromise = controller.promises.opened.promise;
        controller.openConnection();
        await messenger.waitForConnectionState(Messenger.CONFIGURING);
        // Send a hello message to finalize the process
        const messageSent = waitForEvent(controller.connection, 'message');
        controller.sendHello();
        await messageSent;
        // Check that the promises has resolved
        expect(spiedHandler).toBeCalled();
        await expect(resolvesImmediately(openPromise)).resolves.toBeTruthy();
        await expect(isResolved(internalOpenPromise)).resolves.toBeTruthy();
        expect(messenger.state).toBe(Messenger.CONNECTED);
    })

    it.skip ('changes state to WAITING and rejects when a connection is dropped', async () => {

    })
})

describe ('Messenger.close()', () => {

    it ('on CLOSING does nothing and resolves with the internal promise', async () => {
        const { messenger, controller } = createMessengerPair();
        // Open so that the connection object is created, then change
        // the state to CLOSING
        controller.openConnection().then(() => controller.promises.opened.resolve());
        await messenger.open();
        controller.setState(Messenger.CLOSING);
        // Call close() and run the tests
        // - promise not settled
        // - internal promise exists
        const closePromise = messenger.close();
        expect(messenger.state).toBe(Messenger.CLOSING);
        expect(controller.promises.closed).toBeDefined();
        await expect(isSettled(closePromise)).resolves.toBeFalsy();
        // Resolve the internal promise and check that
        // - the promise is resolved
        // - backend.close() was not called
        controller.promises.closed.resolve();
        await expect(closePromise).resolves.not.toThrow();
        expect(controller.connection.close).not.toBeCalled();
    })

    it ('on CLOSED does nothing and resolves immediately', async () => {
        const { messenger, controller } = createMessengerPair();
        // Call close() and run the tests
        // - promise resolved
        // - no internal promise exists
        // - token not fetched
        // - backened not created
        const promise = messenger.close();
        expect(messenger.state).toBe(Messenger.CLOSED);
        expect(controller.promises.closed).not.toBeDefined();
        await expect(isResolved(promise)).resolves.toBeTruthy();
        expect(controller.connection).toBeNull();
    })
    
    it ('on CONNECTING with no connection backend yet: changes state to CLOSED, aborts open() and resolves immediately', async () => {
        const { messenger, controller } = createMessengerPair();
        // Start opening the messenger and close it before a connection backened is created
        const openPromise = messenger.open();
        const closePromise = messenger.close();
        // Run the tests:
        //  - state is CLOSED
        //  - internal promise "opened" is aborted and removed
        //  - there is no internal promise "closed"
        expect(messenger.state).toBe(Messenger.CLOSED);
        expect(messenger.desiredState).toBe(Messenger.CLOSED);
        expect(controller.promises.closed).not.toBeDefined();
        expect(controller.promises.opened).not.toBeDefined();
        await expect(isResolved(closePromise)).resolves.toBeTruthy();
        await expect(openPromise).rejects.toThrowError(AbortError);
    })

    it ('on CONNECTING with connection backend: changes state to CLOSING, aborts open() and initiates closing the connection', async () => {
        const { messenger, controller } = createMessengerPair();
        // Create the connection and close it before being opened
        const openPromise = messenger.open();
        await controller.waitForConnection();
        const closePromise = messenger.close();
        // Run the tests:
        //  - state is CLOSING
        //  - connection.close() has been called
        //  - internal promise "opened" is aborted and removed
        //  - internal promise "closed" is created but not settled yet
        expect(messenger.state).toBe(Messenger.CLOSING);
        expect(messenger.desiredState).toBe(Messenger.CLOSED);
        expect(controller.connection.close).toBeCalledTimes(1);
        expect(controller.promises.opened).not.toBeDefined();
        expect(controller.promises.closed).toBeDefined();
        await expect(isSettled(closePromise)).resolves.toBeFalsy();
        await expect(openPromise).rejects.toThrowError(AbortError);
        // Resolve the internal promise and test that the close promise resolves
        controller.promises.closed.resolve();
        await expect(closePromise).resolves.not.toThrow();
    })

    it ('on CONNECTED changes state to CLOSING and calls close() on the connection backend', async () => {
        const { messenger, controller } = createMessengerPair();
        controller.openConnection().then(() => controller.promises.opened.resolve());
        await messenger.open();
        // Execute close() and run the tests
        // - state is CLOSING
        // - connection.close() has been called
        // - internal promise "closed" is created but not settled yet
        const closePromise = messenger.close();
        expect(messenger.state).toBe(Messenger.CLOSING);
        expect(messenger.desiredState).toBe(Messenger.CLOSED);
        expect(controller.connection.close).toBeCalledTimes(1);
        expect(controller.promises.closed).toBeDefined();
        await expect(isSettled(closePromise)).resolves.toBeFalsy();
        // Resolve the internal promise and test that the close promise resolves
        controller.promises.closed.resolve();
        await expect(closePromise).resolves.not.toThrow();
    })

    it ('changes state to CLOSED and resolves once the connection backend is closed', async () => {
        const { messenger, controller } = createMessengerPair();
        // Prepare the messenger: create a backend connection then call close()
        controller.openConnection().then(() => controller.promises.opened.resolve());
        await messenger.open();
        const closePromise = messenger.close();
        const internalClosePromise = controller.promises.closed.promise;
        // Close the backend connection and test the messenger behavior
        // - close promise resolves immediately
        // - state is CLOSED
        controller.closeConnection();
        await expect(resolvesImmediately(closePromise)).resolves.toBeTruthy();
        await expect(isResolved(internalClosePromise)).resolves.toBeTruthy();
        expect(messenger.state).toBe(Messenger.CLOSED);
    })

    it.skip ('on WAITING changes state to CLOSED, stops the reconnection process and resolves immediately', async () => {

    })

})
