import { jest, describe, expect, test, beforeEach, afterEach } from '@jest/globals';

import './test-utils/toBeWithinRange';
import { deferredPromise, deferWhileRunning } from '../src';

type MockFunction = (arg1: string, arg2: string, arg3: string) => Promise<string>;

function mockHelper(func?: MockFunction) {
    return jest.fn(func);
}

function sleep(ms: number) {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}

describe('deferredPromise', () => {
    test('resolves when resolve is called', async () => {
        const deferred = deferredPromise();
        deferred.resolve('test');
        await expect(deferred.promise).resolves.toEqual('test');
    });

    test('rejects when reject is called', async () => {
        const deferred = deferredPromise();
        deferred.reject('error');
        await expect(deferred.promise).rejects.toEqual('error');
    });
});

describe('deferWhileRunning', () => {
    beforeEach(() => {
        jest.useFakeTimers();
        jest.spyOn(global, 'setTimeout');
    });

    afterEach(() => {
        (setTimeout as jest.MockedFunction<typeof setTimeout>).mockClear();
        jest.useRealTimers();
    });

    test('invokes the function immediately if no previous calls were made and resolves with the resolved value', async () => {
        const mockFn = mockHelper((arg1, arg2, arg3) => Promise.resolve(arg1 + arg2 + arg3));
        const deferred = deferWhileRunning(mockFn);
        const result = deferred('a', 'b', 'c');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);
        await expect(result).resolves.toEqual('abc');
    });

    test('invokes the function immediately if no previous calls were made and rejects with the rejected value', async () => {
        const mockFn = mockHelper((arg1, arg2, arg3) => Promise.reject({ error: arg1 + arg2 + arg3 }));
        const deferred = deferWhileRunning(mockFn);
        const result = deferred('a', 'b', 'c');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);
        await expect(result).rejects.toEqual({ error: 'abc' });
    });

    test('invokes the function immediately if no previous calls were made and rejects with the thrown error', async () => {
        const mockFn = mockHelper(async (arg1, arg2, arg3) => {
            throw new Error(arg1 + arg2 + arg3);
        });
        const deferred = deferWhileRunning(mockFn);
        const result = deferred('a', 'b', 'c');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);
        try {
            await result;
        } catch (e: any) {
            expect(e.message).toEqual('abc');
        }
    });

    test('invokes the next function immediately if the previous invocation was resolved', async () => {
        const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => arg1 + arg2 + arg3);
        const deferred = deferWhileRunning(mockFn);

        const result1 = deferred('a', 'b', 'c');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);
        await expect(result1).resolves.toEqual('abc');

        mockFn.mockClear();

        const result2 = await deferred('d', 'e', 'f');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']);
        expect(result2).toEqual('def');
    });

    test('invokes the next function immediately if the previous invocation was rejected', async () => {
        const mockFn = mockHelper()
            .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                return Promise.reject({ error: arg1 + arg2 + arg3 });
            })
            .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                return arg1 + arg2 + arg3;
            });
        const deferred = deferWhileRunning(mockFn);

        const result1 = deferred('a', 'b', 'c');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);
        await expect(result1).rejects.toEqual({ error: 'abc' });

        mockFn.mockClear();

        const result2 = await deferred('d', 'e', 'f');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']);
        expect(result2).toEqual('def');
    });

    test('debounce the function call and resolves with the resolved value after debounce delay', async () => {
        const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => {
            return arg1 + arg2 + arg3;
        });
        const deferred = deferWhileRunning(mockFn, { debounceDelay: 100 });

        const result = deferred('a', 'b', 'c');

        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100);
        expect(mockFn).not.toBeCalled();
        jest.advanceTimersByTime(100);

        await expect(result).resolves.toEqual('abc');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);
    });

    test('debounce the function call and rejects with the rejected value after debounce delay', async () => {
        const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => {
            return Promise.reject({ error: arg1 + arg2 + arg3 });
        });
        const deferred = deferWhileRunning(mockFn, { debounceDelay: 100 });

        const result = deferred('a', 'b', 'c');

        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100);
        expect(mockFn).not.toBeCalled();
        jest.advanceTimersByTime(100);

        await expect(result).rejects.toEqual({ error: 'abc' });
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);
    });

    test('debounce the function call and rejects with the thrown error after debounce delay', async () => {
        const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => {
            throw new Error(arg1 + arg2 + arg3);
        });
        const deferred = deferWhileRunning(mockFn, { debounceDelay: 100 });

        const result = deferred('a', 'b', 'c');

        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100);
        expect(mockFn).not.toBeCalled();
        jest.advanceTimersByTime(100);

        await expect(result).rejects.toThrow('abc');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);
    });

    test('debounce the function three times then invokes the function with the last provided arguments and resolves all calls after debounce delay', async () => {
        const mockFn = jest.fn(async (arg1: string) => arg1);
        const deferred = deferWhileRunning(mockFn, { debounceDelay: 150 });

        // first call is debounced for 150ms
        const result1 = deferred('a');

        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150);
        expect(mockFn).not.toBeCalled();

        jest.advanceTimersByTime(100);

        // second call is made after 100ms, debounced for 150ms, and scheduled for 250ms (from start)
        const result2 = deferred('b');

        expect(setTimeout).toHaveBeenCalledTimes(2);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150);
        expect(mockFn).not.toBeCalled();

        jest.advanceTimersByTime(100);

        // third call is made after 200ms (from start), debounced for 150ms, and scheduled for 350ms (from start)
        const result3 = deferred('c');

        expect(setTimeout).toHaveBeenCalledTimes(3);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150);
        expect(mockFn).not.toBeCalled();

        jest.advanceTimersByTime(150);

        await expect(result1).resolves.toEqual('c');
        await expect(result2).resolves.toEqual('c');
        await expect(result3).resolves.toEqual('c');

        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.calls[0]).toEqual(['c']);
    });

    test('debounce the function twice then invokes the function due to debounceMaxDelay, then defers the third call until the previous invocation is resolved', async () => {
        const mockFn = jest.fn(async (arg1: string) => {
            await sleep(100);
            return arg1;
        });
        const deferred = deferWhileRunning(mockFn, {
            debounceDelay: 200,
            debounceMaxDelay: 300
        });

        // const deferred = deferWhileRunning(mockFn);
        //
        //                                  mockFn('b')  mockFn('c')
        //                                       ↓         ↓
        //        o------ debounce delay -------●o========●o=========●
        //        o---------------------------------------●
        //        ↑              o------------------------●
        //        ↑              ↑                   o---------------●
        //        ↑              ↑                   ↑
        // time:--0---------100--150--200-------300--350--400--------------->
        //        ↑              ↑                   ↑
        //  deferred('a')  deferred('b')       deferred('c')

        // first call is debounced for 300ms
        const result1 = deferred('a');

        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 200);
        expect(mockFn).not.toBeCalled();

        // second call is made after 150ms, debounced for 150ms because max debounce delay is 300ms,
        // and scheduled for 300ms (from start) and runs for 100ms
        jest.advanceTimersByTime(150);
        const result2 = deferred('b');

        expect(setTimeout).toHaveBeenCalledTimes(2);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150);
        expect(mockFn).not.toBeCalled();

        jest.advanceTimersByTime(150);
        expect(mockFn).toHaveBeenCalledTimes(1);
        expect(mockFn).toHaveBeenLastCalledWith('b');
        // this setTimeout called from within the mocked function
        expect(setTimeout).toHaveBeenCalledTimes(3);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100);

        // third call is made after 350ms (from start) while the second call is running,
        // so this call will be deferred until after the second call invocation completes
        // which will be at 400ms from the start
        jest.advanceTimersByTime(50);
        const result3 = deferred('c');
        expect(mockFn).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenCalledTimes(3);

        jest.advanceTimersByTime(50);
        await expect(result1).resolves.toEqual('b');
        await expect(result2).resolves.toEqual('b');

        expect(setTimeout).toHaveBeenCalledTimes(4);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 100);

        jest.advanceTimersByTime(100);
        expect(mockFn).toHaveBeenCalledTimes(2);
        expect(mockFn).toHaveBeenLastCalledWith('c');
        await expect(result3).resolves.toEqual('c');
    });

    test('debounce the function three times then invokes the function with arguments as defined by the argsResolver then resolves with the resolved value after debounce delay', async () => {
        const mockFn = jest.fn(async (arg1: string, arg2: number) => {
            return [arg1, arg2];
        });
        const deferred = deferWhileRunning(mockFn, {
            debounceDelay: 200,
            argsResolver: ({ nextArgs, prevArgs }) => {
                return prevArgs ? ([prevArgs[0] + nextArgs[0], prevArgs[1] + nextArgs[1]] as [string, number]) : nextArgs;
            }
        });

        // const deferred = deferWhileRunning(mockFn);
        //
        //                               mockFn('abc', 6)
        //                                      ↓
        //        o------ debounce delay -------●o===●
        //        o-----------------------------●
        //        ↑         o-------------------●
        //        ↑         ↑         o---------●
        //        ↑         ↑         ↑
        // time:--0---------100-------200-------300----->
        //        ↑         ↑         ↑
        //        ↑         ↑   deferred('c', 3)
        //        ↑   deferred('a', 2)
        //  deferred('a', 1)

        // first call is debounced for 200ms
        const result1 = deferred('a', 1);
        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 200);
        expect(mockFn).not.toBeCalled();

        // second call is made after 100ms, debounced for 200ms, and scheduled for 300ms (from start)
        jest.advanceTimersByTime(100);
        const result2 = deferred('b', 2);
        expect(setTimeout).toHaveBeenCalledTimes(2);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 200);
        expect(mockFn).not.toBeCalled();

        // third call is made after 200ms (from start), debounced for 200ms, and scheduled for 400ms (from start)
        jest.advanceTimersByTime(100);
        const result3 = deferred('c', 3);
        expect(setTimeout).toHaveBeenCalledTimes(3);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 200);
        expect(mockFn).not.toBeCalled();

        jest.advanceTimersByTime(200);

        await expect(result1).resolves.toEqual(['abc', 6]);
        await expect(result2).resolves.toEqual(['abc', 6]);
        await expect(result3).resolves.toEqual(['abc', 6]);
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.calls[0]).toEqual(['abc', 6]);
    });

    test('defers the next function call until the previous invocation is resolved', async () => {
        const wait = deferredPromise<void>();
        const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => {
            await wait.promise;
            return arg1 + arg2 + arg3;
        });
        const deferred = deferWhileRunning(mockFn);
        const result1 = deferred('a', 'b', 'c');
        const result2 = deferred('d', 'e', 'f');

        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);

        mockFn.mockClear();
        wait.resolve();
        await expect(result1).resolves.toEqual('abc');

        await expect(result2).resolves.toEqual('def');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']);
    });

    test('defers the next function call until the previous invocation is rejected', async () => {
        const wait = deferredPromise<void>();
        const mockFn = mockHelper()
            .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                await wait.promise;
                return Promise.reject({ error: arg1 + arg2 + arg3 });
            })
            .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                return arg1 + arg2 + arg3;
            });
        const deferred = deferWhileRunning(mockFn);

        const result1 = deferred('a', 'b', 'c');
        const result2 = deferred('d', 'e', 'f');

        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);

        mockFn.mockClear();
        wait.resolve();
        await expect(result1).rejects.toEqual({ error: 'abc' });

        await expect(result2).resolves.toEqual('def');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']);
    });

    test('defers the next function call until the previous invocation throws', async () => {
        const wait = deferredPromise<void>();
        const mockFn = mockHelper()
            .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                await wait.promise;
                throw new Error(arg1 + arg2 + arg3);
            })
            .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                return arg1 + arg2 + arg3;
            });
        const deferred = deferWhileRunning(mockFn);

        const result1 = deferred('a', 'b', 'c');
        const result2 = deferred('d', 'e', 'f');

        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);

        mockFn.mockClear();
        wait.resolve();
        try {
            await result1;
        } catch (e: any) {
            expect(e.message).toEqual('abc');
        }

        await expect(result2).resolves.toEqual('def');
        expect(mockFn.mock.calls.length).toEqual(1);
        expect(mockFn.mock.lastCall).toEqual(['d', 'e', 'f']);
    });

    test(
        'defers multiple function calls until the previous invocation is resolved,' +
            'then invokes the function with the last provided arguments and resolves all calls',
        async () => {
            const wait = deferredPromise<void>();
            const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => {
                await wait.promise;
                return arg1 + arg2 + arg3;
            });
            const deferred = deferWhileRunning(mockFn);
            const result1 = deferred('a', 'b', 'c');
            const result2 = deferred('d', 'e', 'f');
            const result3 = deferred('g', 'h', 'i');
            const result4 = deferred('j', 'k', 'l');

            expect(mockFn.mock.calls.length).toEqual(1);
            expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);

            mockFn.mockClear();
            wait.resolve();
            await expect(result1).resolves.toEqual('abc');

            await expect(result2).resolves.toEqual('jkl');
            await expect(result3).resolves.toEqual('jkl');
            await expect(result4).resolves.toEqual('jkl');
            expect(mockFn.mock.calls.length).toEqual(1);
            expect(mockFn.mock.lastCall).toEqual(['j', 'k', 'l']);
        }
    );

    test(
        'defers multiple function calls until the previous invocation is resolved,' +
            'then invokes the function with the last provided arguments and rejects all calls',
        async () => {
            const wait = deferredPromise<void>();
            const mockFn = mockHelper()
                .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                    await wait.promise;
                    return arg1 + arg2 + arg3;
                })
                .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                    return Promise.reject({ error: arg1 + arg2 + arg3 });
                });
            const deferred = deferWhileRunning(mockFn);
            const result1 = deferred('a', 'b', 'c');
            const result2 = deferred('d', 'e', 'f');
            const result3 = deferred('g', 'h', 'i');
            const result4 = deferred('j', 'k', 'l');

            expect(mockFn.mock.calls.length).toEqual(1);
            expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);

            mockFn.mockClear();
            wait.resolve();
            await expect(result1).resolves.toEqual('abc');

            await expect(result2).rejects.toEqual({ error: 'jkl' });
            await expect(result3).rejects.toEqual({ error: 'jkl' });
            await expect(result4).rejects.toEqual({ error: 'jkl' });
            expect(mockFn.mock.calls.length).toEqual(1);
            expect(mockFn.mock.lastCall).toEqual(['j', 'k', 'l']);
        }
    );

    test(
        'defers multiple function calls until the previous invocation is rejected, ' +
            'then invokes the function with the last provided arguments and resolves all calls',
        async () => {
            const wait = deferredPromise<void>();
            const mockFn = mockHelper()
                .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                    await wait.promise;
                    return Promise.reject({ error: arg1 + arg2 + arg3 });
                })
                .mockImplementationOnce(async (arg1: string, arg2: string, arg3: string) => {
                    return arg1 + arg2 + arg3;
                });
            const deferred = deferWhileRunning(mockFn);
            const result1 = deferred('a', 'b', 'c');
            const result2 = deferred('d', 'e', 'f');
            const result3 = deferred('g', 'h', 'i');
            const result4 = deferred('j', 'k', 'l');

            expect(mockFn.mock.calls.length).toEqual(1);
            expect(mockFn.mock.lastCall).toEqual(['a', 'b', 'c']);

            mockFn.mockClear();
            wait.resolve();
            await expect(result1).rejects.toEqual({ error: 'abc' });

            await expect(result2).resolves.toEqual('jkl');
            await expect(result3).resolves.toEqual('jkl');
            await expect(result4).resolves.toEqual('jkl');
            expect(mockFn.mock.calls.length).toEqual(1);
            expect(mockFn.mock.lastCall).toEqual(['j', 'k', 'l']);
        }
    );

    test('invokes the function immediately if no previous calls in the same group made and resolves with the resolved value', async () => {
        const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => {
            return arg1 + '-' + arg2 + arg3;
        });
        const deferred = deferWhileRunning(mockFn, {
            groupResolver: (group: string) => group
        });
        const result1 = deferred('group1', 'a', 'b');
        const result2 = deferred('group2', 'c', 'd');
        const result3 = deferred('group3', 'e', 'f');

        expect(mockFn.mock.calls.length).toEqual(3);
        expect(mockFn.mock.calls[0]).toEqual(['group1', 'a', 'b']);
        expect(mockFn.mock.calls[1]).toEqual(['group2', 'c', 'd']);
        expect(mockFn.mock.calls[2]).toEqual(['group3', 'e', 'f']);

        await expect(result1).resolves.toEqual('group1-ab');
        await expect(result2).resolves.toEqual('group2-cd');
        await expect(result3).resolves.toEqual('group3-ef');
    });

    test(
        'defers multiple function calls in a group until the previous invocation in the same group is resolved,' +
            'then invokes the function with the last provided arguments to the group and resolves all group calls',
        async () => {
            const wait = deferredPromise<void>();
            const mockFn = jest.fn(async (arg1: string, arg2: string, arg3: string) => {
                await wait.promise;
                return arg1 + '-' + arg2 + arg3;
            });
            const deferred = deferWhileRunning(mockFn, {
                groupResolver: (group: string) => group
            });
            const result1a = deferred('group1', 'a', 'b');
            const result1b = deferred('group1', 'c', 'd');
            const result1c = deferred('group1', 'e', 'f');
            const result2a = deferred('group2', 'a', 'b');
            const result2b = deferred('group2', 'c', 'd');
            const result2c = deferred('group2', 'e', 'f');
            const result3a = deferred('group3', 'a', 'b');
            const result3b = deferred('group3', 'c', 'd');
            const result3c = deferred('group3', 'e', 'f');

            expect(mockFn.mock.calls.length).toEqual(3);
            expect(mockFn.mock.calls[0]).toEqual(['group1', 'a', 'b']);
            expect(mockFn.mock.calls[1]).toEqual(['group2', 'a', 'b']);
            expect(mockFn.mock.calls[2]).toEqual(['group3', 'a', 'b']);

            wait.resolve();

            await expect(result1a).resolves.toEqual('group1-ab');
            await expect(result2a).resolves.toEqual('group2-ab');
            await expect(result3a).resolves.toEqual('group3-ab');

            await expect(result1b).resolves.toEqual('group1-ef');
            await expect(result1c).resolves.toEqual('group1-ef');
            await expect(result2b).resolves.toEqual('group2-ef');
            await expect(result2c).resolves.toEqual('group2-ef');
            await expect(result3b).resolves.toEqual('group3-ef');
            await expect(result3c).resolves.toEqual('group3-ef');

            expect(mockFn.mock.calls.length).toEqual(6);
            expect(mockFn.mock.calls[3]).toEqual(['group1', 'e', 'f']);
            expect(mockFn.mock.calls[4]).toEqual(['group2', 'e', 'f']);
            expect(mockFn.mock.calls[5]).toEqual(['group3', 'e', 'f']);
        }
    );

    test(
        'defers multiple function calls in a group until the previous invocation in the same group is resolved,' +
            'then invokes the function with arguments as defined by argsResolver to the group and resolves all group calls',
        async () => {
            const wait = deferredPromise<void>();
            const mockFn = mockHelper(async (arg1: string, arg2: string, arg3: string) => {
                await wait.promise;
                return arg1 + '-' + arg2 + arg3;
            });
            const deferred = deferWhileRunning(mockFn, {
                groupResolver: (group: string) => group,
                argsResolver: ({ nextArgs, prevArgs }) => {
                    return [nextArgs[0], 'A', 'B'] as [string, string, string];
                }
            });
            const result1a = deferred('group1', 'a', 'b');
            const result1b = deferred('group1', 'c', 'd');
            const result1c = deferred('group1', 'e', 'f');
            const result2a = deferred('group2', 'a', 'b');
            const result2b = deferred('group2', 'c', 'd');
            const result2c = deferred('group2', 'e', 'f');
            const result3a = deferred('group3', 'a', 'b');
            const result3b = deferred('group3', 'c', 'd');
            const result3c = deferred('group3', 'e', 'f');

            expect(mockFn.mock.calls.length).toEqual(3);
            expect(mockFn.mock.calls[0]).toEqual(['group1', 'a', 'b']);
            expect(mockFn.mock.calls[1]).toEqual(['group2', 'a', 'b']);
            expect(mockFn.mock.calls[2]).toEqual(['group3', 'a', 'b']);

            wait.resolve();

            await expect(result1a).resolves.toEqual('group1-ab');
            await expect(result2a).resolves.toEqual('group2-ab');
            await expect(result3a).resolves.toEqual('group3-ab');

            await expect(result1b).resolves.toEqual('group1-AB');
            await expect(result1c).resolves.toEqual('group1-AB');
            await expect(result2b).resolves.toEqual('group2-AB');
            await expect(result2c).resolves.toEqual('group2-AB');
            await expect(result3b).resolves.toEqual('group3-AB');
            await expect(result3c).resolves.toEqual('group3-AB');

            expect(mockFn.mock.calls.length).toEqual(6);
            expect(mockFn.mock.calls[3]).toEqual(['group1', 'A', 'B']);
            expect(mockFn.mock.calls[4]).toEqual(['group2', 'A', 'B']);
            expect(mockFn.mock.calls[5]).toEqual(['group3', 'A', 'B']);
        }
    );
});
