import { describe, expect, it, jest } from '@jest/globals';
import ProcessingQueue from '../js/async/queue.js';
import { PromiseStatus, promiseStatus } from '../js/async/promise.js';
import '../js/ext/polyfills.js';

describe('ProcessingQueue(action)', () => {

    // Expose the queue for testing
    class ProcessingQueueTest extends ProcessingQueue {
        get queuedItems() { return this._items; }
    }

    it('put(item) places the item at the end of the queue for processing', () => {
        const data = [
            { value: 42 },
            { value: 18 }
        ];
        const queue = new ProcessingQueueTest(() => true);
        data.forEach(x => queue.put(x));

        expect(queue.queuedItems).toHaveLength(2);
        data.forEach((x,i) => expect(queue.queuedItems[i]).toStrictEqual(x));
    })

    it('processed() resolves when processing ends', async () => {
        const action = jest.fn();
        const queue = new ProcessingQueue(action);
        const data = [42, 'test', 18, 'end'];

        data.forEach(item => queue.put(item));
        await queue.processed();
        expect(action).toBeCalledTimes(data.length);
    })

    it('action(item, queue) is called for each item on the queue', async () => {

        const action = jest.fn();
        const queue = new ProcessingQueue(action);
        const data = [42, 'test', 18, 'end'];

        data.forEach(item => queue.put(item));
        await queue.processed();

        expect(action).toBeCalledTimes(data.length);
        data.forEach((item, i) => {
            expect(action.mock.calls[i][0]).toStrictEqual(item);
            expect(action.mock.calls[i][1]).toStrictEqual(queue);
        });
    })

    it('processing is done asynchronously', async () => {
        const action = jest.fn();
        const queue = new ProcessingQueue(action);
        [42, 'test', 18, 'end'].forEach(item => queue.put(item));

        expect(action.mock.calls.length).toBeLessThanOrEqual(1);
        await queue.processed();
        expect(action.mock.calls.length).toEqual(4);
    })

    it('only items on which action returns a true value are removed from the queue', async () => {

        const queue = new ProcessingQueueTest(x => typeof x === 'string');

        [42, 'test', 18, 'end'].forEach(item => queue.put(item));
        await queue.processed();

        expect(queue.queuedItems).toHaveLength(2);
        [42,18].forEach((item, i) => expect(queue.queuedItems[i]).toEqual(item));
    })

    it('if action throws then the item is left on the queue, but processing keeps going on', async () => {

        const queue = new ProcessingQueueTest(x => {
            if (typeof x == 'number') {
                throw new Error('test');
            }
            return true;
        });

        [42, 'test', 18, 'end'].forEach(item => queue.put(item));
        await queue.processed();

        expect(queue.queuedItems).toHaveLength(2);
        [42,18].forEach((item, i) => expect(queue.queuedItems[i]).toEqual(item));
    })

    it('active: is true when the queue is processing items', async () => {
        let wasActive = 0;
        const queue = new ProcessingQueue(x => {
            if (queue.active) wasActive++;
        });
        [42, 'test', 18, 'end'].forEach(item => queue.put(item));

        expect(queue.active).toBeTruthy();
        await queue.processed();
        expect(queue.active).toBeFalsy();
        expect(wasActive).toEqual(4);
    })

    it('paused: true if processing is interrupted with pause() and set to false via resume()', async () => {
        const action = jest.fn();
        const queue = new ProcessingQueue(action);
        expect(queue.paused).toBeFalsy();
    
        queue.pause();
        expect(queue.paused).toBeTruthy();

        [0,1,2].forEach(item => queue.put(item));
        await new Promise(res => setTimeout(res, 10));

        expect(queue.size).toBe(3);
        expect(queue.active).toBeFalsy();
        expect(action).not.toBeCalled();

        queue.resume();
        expect(queue.paused).toBeFalsy();
        await queue.processed();
        expect(action).toBeCalledTimes(3);
    })

    it('size: the current size of the queue', async () => {
        const queue = new ProcessingQueue(x => !!x);
        [0, 1, 0, 2, 3, 0].forEach(item => queue.put(item));
        expect(queue.size).toEqual(6);
        await queue.processed();
        expect(queue.size).toEqual(3);
    })

    it('empty: true if there are no elements in the queue', async () => {
        const queue = new ProcessingQueue(x => !!x);
        expect(queue.empty).toBeTruthy();

        [1, 2, 3].forEach(item => queue.put(item));
        expect(queue.empty).toBeFalsy();

        await queue.processed();
        expect(queue.empty).toBeTruthy();

        queue.put(0);
        await queue.processed(0);
        expect(queue.empty).toBeFalsy();
    })

    it('waitForEmpty(): resolves once the queue is empty', async () => {
        let removeItems;
        let emptyPromise;
        const queue = new ProcessingQueue(x => removeItems);

        await expect(promiseStatus(queue.waitForEmpty())).resolves.toEqual(PromiseStatus.RESOLVED);

        removeItems = true;
        [1, 2, 3].forEach(item => queue.put(item));
        emptyPromise = queue.waitForEmpty();
        await expect(promiseStatus(emptyPromise)).resolves.toEqual(PromiseStatus.PENDING);
        await queue.processed();
        await expect(promiseStatus(emptyPromise)).resolves.toEqual(PromiseStatus.RESOLVED);

        removeItems = false;
        [1, 2, 3].forEach(item => queue.put(item));
        emptyPromise = queue.waitForEmpty();
        await queue.processed();
        await expect(promiseStatus(emptyPromise)).resolves.toEqual(PromiseStatus.PENDING);
    })


    it('clear() removes all elements from the queue and resolves promises', async () => {
        const queue = new ProcessingQueue(x => false);
        let emptyPromise;

        [1,2,3].forEach(item => queue.put(item));
        emptyPromise = queue.waitForEmpty();
        await queue.processed();
        expect(queue.empty).toBeFalsy();
        await expect(promiseStatus(emptyPromise)).resolves.toEqual(PromiseStatus.PENDING);
        queue.clear();
        expect(queue.empty).toBeTruthy();
        await expect(promiseStatus(emptyPromise)).resolves.toEqual(PromiseStatus.RESOLVED);

        queue.pause();
        [1,2,3].forEach(item => queue.put(item));
        emptyPromise = queue.waitForEmpty();
        expect(queue.empty).toBeFalsy();
        queue.clear();
        expect(queue.empty).toBeTruthy();
        await expect(promiseStatus(emptyPromise)).resolves.toEqual(PromiseStatus.RESOLVED);
    })

})