import { __assign, __awaiter, __generator } from "tslib";
import { pWhile } from '../../../lib/p-while';
import * as timer from '../../../lib/priority-queue/backoff';
import { sourceMiddlewarePlugin, } from '../../../plugins/middleware';
import { Context, ContextCancelation } from '../../context';
import { EventQueue } from '../event-queue';
function flushAll(eq) {
    return __awaiter(this, void 0, void 0, function () {
        var flushed;
        var _this = this;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    flushed = [];
                    return [4 /*yield*/, pWhile(function () { return eq.queue.length > 0; }, function () { return __awaiter(_this, void 0, void 0, function () {
                            var res;
                            return __generator(this, function (_a) {
                                switch (_a.label) {
                                    case 0: return [4 /*yield*/, eq.flush().catch(function () { return []; })];
                                    case 1:
                                        res = _a.sent();
                                        flushed = flushed.concat(res);
                                        return [2 /*return*/];
                                }
                            });
                        }); })];
                case 1:
                    _a.sent();
                    return [2 /*return*/, flushed];
            }
        });
    });
}
var testPlugin = {
    name: 'test',
    type: 'before',
    version: '0.1.0',
    load: function () { return Promise.resolve(); },
    isLoaded: function () { return true; },
};
var ajs = {};
var fruitBasket, basketView, shopper;
beforeEach(function () {
    fruitBasket = new Context({
        type: 'track',
        event: 'Fruit Basket',
        properties: {
            banana: '🍌',
            apple: '🍎',
            grape: '🍇',
        },
    });
    basketView = new Context({
        type: 'page',
    });
    shopper = new Context({
        type: 'identify',
        traits: {
            name: 'Netto Farah',
        },
    });
});
test('can send events', function () { return __awaiter(void 0, void 0, void 0, function () {
    var eq, evt;
    return __generator(this, function (_a) {
        switch (_a.label) {
            case 0:
                eq = new EventQueue();
                return [4 /*yield*/, eq.dispatch(fruitBasket)];
            case 1:
                evt = _a.sent();
                expect(evt).toBe(fruitBasket);
                return [2 /*return*/];
        }
    });
}); });
test('delivers events out of band', function () { return __awaiter(void 0, void 0, void 0, function () {
    var eq;
    return __generator(this, function (_a) {
        switch (_a.label) {
            case 0:
                jest.useFakeTimers();
                eq = new EventQueue();
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                eq.dispatch(fruitBasket);
                expect(setTimeout).toHaveBeenCalled();
                expect(eq.queue.includes(fruitBasket)).toBe(true);
                // run timers and deliver events
                jest.runAllTimers();
                return [4 /*yield*/, eq.flush()];
            case 1:
                _a.sent();
                expect(eq.queue.length).toBe(0);
                return [2 /*return*/];
        }
    });
}); });
test('does not enqueue multiple flushes at once', function () { return __awaiter(void 0, void 0, void 0, function () {
    var eq, anothaOne;
    return __generator(this, function (_a) {
        switch (_a.label) {
            case 0:
                jest.useFakeTimers();
                eq = new EventQueue();
                anothaOne = new Context({
                    type: 'page',
                });
                eq.dispatch(fruitBasket);
                eq.dispatch(anothaOne);
                expect(setTimeout).toHaveBeenCalledTimes(1);
                expect(eq.queue.length).toBe(2);
                jest.useRealTimers();
                return [4 /*yield*/, flushAll(eq)];
            case 1:
                _a.sent();
                expect(eq.queue.length).toBe(0);
                return [2 /*return*/];
        }
    });
}); });
describe('Flushing', function () {
    beforeEach(function () {
        jest.useRealTimers();
    });
    test('works until the queue is empty', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, flushed;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    eq = new EventQueue();
                    eq.dispatch(fruitBasket);
                    eq.dispatch(basketView);
                    eq.dispatch(shopper);
                    expect(eq.queue.length).toBe(3);
                    return [4 /*yield*/, flushAll(eq)];
                case 1:
                    flushed = _a.sent();
                    expect(eq.queue.length).toBe(0);
                    expect(flushed).toEqual([fruitBasket, basketView, shopper]);
                    return [2 /*return*/];
            }
        });
    }); });
    test('re-queues failed events', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, flushed;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    eq = new EventQueue();
                    return [4 /*yield*/, eq.register(Context.system(), __assign(__assign({}, testPlugin), { track: function (ctx) {
                                if (ctx === fruitBasket) {
                                    throw new Error('aaay');
                                }
                                return Promise.resolve(ctx);
                            } }), ajs)];
                case 1:
                    _a.sent();
                    eq.dispatch(fruitBasket);
                    eq.dispatch(basketView);
                    eq.dispatch(shopper);
                    expect(eq.queue.length).toBe(3);
                    return [4 /*yield*/, flushAll(eq)
                        // flushed good events
                    ];
                case 2:
                    flushed = _a.sent();
                    // flushed good events
                    expect(flushed).toEqual([basketView, shopper]);
                    // attempted to deliver multiple times
                    expect(eq.queue.getAttempts(fruitBasket)).toEqual(2);
                    return [2 /*return*/];
            }
        });
    }); });
    test('delivers events on retry', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, flushed;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    jest.useRealTimers();
                    // make sure all backoffs return immediatelly
                    jest.spyOn(timer, 'backoff').mockImplementationOnce(function () { return 100; });
                    eq = new EventQueue();
                    return [4 /*yield*/, eq.register(Context.system(), __assign(__assign({}, testPlugin), { track: function (ctx) {
                                // only fail first attempt
                                if (ctx === fruitBasket && ctx.attempts === 1) {
                                    throw new Error('aaay');
                                }
                                return Promise.resolve(ctx);
                            } }), ajs)];
                case 1:
                    _a.sent();
                    eq.dispatch(fruitBasket);
                    eq.dispatch(basketView);
                    eq.dispatch(shopper);
                    expect(eq.queue.length).toBe(3);
                    return [4 /*yield*/, flushAll(eq)
                        // delivered both basket and shopper
                    ];
                case 2:
                    flushed = _a.sent();
                    // delivered both basket and shopper
                    expect(flushed).toEqual([basketView, shopper]);
                    // wait for the exponential backoff
                    return [4 /*yield*/, new Promise(function (res) { return setTimeout(res, 100); })
                        // second try
                    ];
                case 3:
                    // wait for the exponential backoff
                    _a.sent();
                    return [4 /*yield*/, flushAll(eq)];
                case 4:
                    // second try
                    flushed = _a.sent();
                    expect(eq.queue.length).toBe(0);
                    expect(flushed).toEqual([fruitBasket]);
                    expect(flushed[0].attempts).toEqual(2);
                    return [2 /*return*/];
            }
        });
    }); });
    test('does not retry non retriable cancelations', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, flushed;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    eq = new EventQueue();
                    return [4 /*yield*/, eq.register(Context.system(), __assign(__assign({}, testPlugin), { track: function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
                                return __generator(this, function (_a) {
                                    if (ctx === fruitBasket) {
                                        throw new ContextCancelation({ retry: false, reason: 'Test!' });
                                    }
                                    return [2 /*return*/, ctx];
                                });
                            }); } }), ajs)];
                case 1:
                    _a.sent();
                    eq.dispatch(fruitBasket);
                    eq.dispatch(basketView);
                    eq.dispatch(shopper);
                    expect(eq.queue.length).toBe(3);
                    return [4 /*yield*/, flushAll(eq)
                        // delivered both basket and shopper
                    ];
                case 2:
                    flushed = _a.sent();
                    // delivered both basket and shopper
                    expect(flushed).toEqual([basketView, shopper]);
                    // nothing to retry
                    expect(eq.queue.length).toBe(0);
                    return [2 /*return*/];
            }
        });
    }); });
    test('retries retriable cancelations', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, flushed;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    // make sure all backoffs return immediatelly
                    jest.spyOn(timer, 'backoff').mockImplementationOnce(function () { return 100; });
                    eq = new EventQueue();
                    return [4 /*yield*/, eq.register(Context.system(), __assign(__assign({}, testPlugin), { track: function (ctx) {
                                // only fail first attempt
                                if (ctx === fruitBasket && ctx.attempts === 1) {
                                    ctx.cancel(new ContextCancelation({ retry: true }));
                                }
                                return Promise.resolve(ctx);
                            } }), ajs)];
                case 1:
                    _a.sent();
                    eq.dispatch(fruitBasket);
                    eq.dispatch(basketView);
                    eq.dispatch(shopper);
                    expect(eq.queue.length).toBe(3);
                    return [4 /*yield*/, flushAll(eq)
                        // delivered both basket and shopper
                    ];
                case 2:
                    flushed = _a.sent();
                    // delivered both basket and shopper
                    expect(flushed).toEqual([basketView, shopper]);
                    // wait for the exponential backoff
                    return [4 /*yield*/, new Promise(function (res) { return setTimeout(res, 100); })
                        // second try
                    ];
                case 3:
                    // wait for the exponential backoff
                    _a.sent();
                    return [4 /*yield*/, flushAll(eq)];
                case 4:
                    // second try
                    flushed = _a.sent();
                    expect(eq.queue.length).toBe(0);
                    expect(flushed).toEqual([fruitBasket]);
                    expect(flushed[0].attempts).toEqual(2);
                    return [2 /*return*/];
            }
        });
    }); });
    test('client: can block on delivery', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, fruitBasketDelivery, basketViewDelivery, shopperDelivery, _a, fruitBasketCtx, basketViewCtx, shopperCtx;
        return __generator(this, function (_b) {
            switch (_b.label) {
                case 0:
                    jest.useRealTimers();
                    eq = new EventQueue();
                    return [4 /*yield*/, eq.register(Context.system(), __assign(__assign({}, testPlugin), { track: function (ctx) {
                                // only fail first attempt
                                if (ctx === fruitBasket && ctx.attempts === 1) {
                                    throw new Error('aaay');
                                }
                                return Promise.resolve(ctx);
                            } }), ajs)];
                case 1:
                    _b.sent();
                    fruitBasketDelivery = eq.dispatch(fruitBasket);
                    basketViewDelivery = eq.dispatch(basketView);
                    shopperDelivery = eq.dispatch(shopper);
                    expect(eq.queue.length).toBe(3);
                    return [4 /*yield*/, Promise.all([
                            fruitBasketDelivery,
                            basketViewDelivery,
                            shopperDelivery,
                        ])];
                case 2:
                    _a = _b.sent(), fruitBasketCtx = _a[0], basketViewCtx = _a[1], shopperCtx = _a[2];
                    expect(eq.queue.length).toBe(0);
                    expect(fruitBasketCtx.attempts).toBe(2);
                    expect(basketViewCtx.attempts).toBe(1);
                    expect(shopperCtx.attempts).toBe(1);
                    return [2 /*return*/];
            }
        });
    }); });
    describe('denyList permutations', function () {
        var amplitude = __assign(__assign({}, testPlugin), { name: 'Amplitude', type: 'destination', track: function (ctx) {
                return Promise.resolve(ctx);
            } });
        var mixPanel = __assign(__assign({}, testPlugin), { name: 'Mixpanel', type: 'destination', track: function (ctx) {
                return Promise.resolve(ctx);
            } });
        test('does not delivery to destinations on denyList', function () { return __awaiter(void 0, void 0, void 0, function () {
            var eq, evt, ctx, flushed;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        eq = new EventQueue();
                        jest.spyOn(amplitude, 'track');
                        jest.spyOn(mixPanel, 'track');
                        evt = {
                            type: 'track',
                            integrations: {
                                Mixpanel: false,
                            },
                        };
                        ctx = new Context(evt);
                        return [4 /*yield*/, eq.register(Context.system(), amplitude, ajs)];
                    case 1:
                        _a.sent();
                        return [4 /*yield*/, eq.register(Context.system(), mixPanel, ajs)];
                    case 2:
                        _a.sent();
                        eq.dispatch(ctx);
                        expect(eq.queue.length).toBe(1);
                        return [4 /*yield*/, flushAll(eq)];
                    case 3:
                        flushed = _a.sent();
                        expect(flushed).toEqual([ctx]);
                        expect(mixPanel.track).not.toHaveBeenCalled();
                        expect(amplitude.track).toHaveBeenCalled();
                        return [2 /*return*/];
                }
            });
        }); });
        test('does not deliver to any destination if All: false ', function () { return __awaiter(void 0, void 0, void 0, function () {
            var eq, evt, ctx, flushed;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        eq = new EventQueue();
                        jest.spyOn(amplitude, 'track');
                        jest.spyOn(mixPanel, 'track');
                        evt = {
                            type: 'track',
                            integrations: {
                                All: false,
                            },
                        };
                        ctx = new Context(evt);
                        return [4 /*yield*/, eq.register(Context.system(), amplitude, ajs)];
                    case 1:
                        _a.sent();
                        return [4 /*yield*/, eq.register(Context.system(), mixPanel, ajs)];
                    case 2:
                        _a.sent();
                        eq.dispatch(ctx);
                        expect(eq.queue.length).toBe(1);
                        return [4 /*yield*/, flushAll(eq)];
                    case 3:
                        flushed = _a.sent();
                        expect(flushed).toEqual([ctx]);
                        expect(mixPanel.track).not.toHaveBeenCalled();
                        expect(amplitude.track).not.toHaveBeenCalled();
                        return [2 /*return*/];
                }
            });
        }); });
        test('delivers to destinations if All: false but the destination is allowed', function () { return __awaiter(void 0, void 0, void 0, function () {
            var eq, evt, ctx, flushed;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        eq = new EventQueue();
                        jest.spyOn(amplitude, 'track');
                        jest.spyOn(mixPanel, 'track');
                        evt = {
                            type: 'track',
                            integrations: {
                                All: false,
                                Amplitude: true,
                            },
                        };
                        ctx = new Context(evt);
                        return [4 /*yield*/, eq.register(Context.system(), amplitude, ajs)];
                    case 1:
                        _a.sent();
                        return [4 /*yield*/, eq.register(Context.system(), mixPanel, ajs)];
                    case 2:
                        _a.sent();
                        eq.dispatch(ctx);
                        expect(eq.queue.length).toBe(1);
                        return [4 /*yield*/, flushAll(eq)];
                    case 3:
                        flushed = _a.sent();
                        expect(flushed).toEqual([ctx]);
                        expect(mixPanel.track).not.toHaveBeenCalled();
                        expect(amplitude.track).toHaveBeenCalled();
                        return [2 /*return*/];
                }
            });
        }); });
        test('respect deny lists generated by other plugin', function () { return __awaiter(void 0, void 0, void 0, function () {
            var eq, evt, ctx, skipAmplitude;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        eq = new EventQueue();
                        jest.spyOn(amplitude, 'track');
                        jest.spyOn(mixPanel, 'track');
                        evt = {
                            type: 'track',
                            integrations: {
                                Amplitude: true,
                                MixPanel: true,
                            },
                        };
                        ctx = new Context(evt);
                        return [4 /*yield*/, eq.register(Context.system(), amplitude, ajs)];
                    case 1:
                        _a.sent();
                        return [4 /*yield*/, eq.register(Context.system(), mixPanel, ajs)];
                    case 2:
                        _a.sent();
                        return [4 /*yield*/, eq.dispatch(ctx)];
                    case 3:
                        _a.sent();
                        skipAmplitude = function (_a) {
                            var payload = _a.payload, next = _a.next;
                            if (!payload.obj.integrations) {
                                payload.obj.integrations = {};
                            }
                            payload.obj.integrations['Amplitude'] = false;
                            next(payload);
                        };
                        return [4 /*yield*/, eq.register(Context.system(), sourceMiddlewarePlugin(skipAmplitude), ajs)];
                    case 4:
                        _a.sent();
                        return [4 /*yield*/, eq.dispatch(ctx)];
                    case 5:
                        _a.sent();
                        expect(mixPanel.track).toHaveBeenCalledTimes(2);
                        expect(amplitude.track).toHaveBeenCalledTimes(1);
                        return [2 /*return*/];
                }
            });
        }); });
    });
});
describe('deregister', function () {
    it('remove plugin from plugins list', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, toBeRemoved, plugins, promises;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    eq = new EventQueue();
                    toBeRemoved = __assign(__assign({}, testPlugin), { name: 'remove-me' });
                    plugins = [testPlugin, toBeRemoved];
                    promises = plugins.map(function (p) { return eq.register(Context.system(), p, ajs); });
                    return [4 /*yield*/, Promise.all(promises)];
                case 1:
                    _a.sent();
                    return [4 /*yield*/, eq.deregister(Context.system(), toBeRemoved, ajs)];
                case 2:
                    _a.sent();
                    expect(eq.plugins.length).toBe(1);
                    expect(eq.plugins[0]).toBe(testPlugin);
                    return [2 /*return*/];
            }
        });
    }); });
    it('invokes plugin.unload', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, toBeRemoved, plugins, promises;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    eq = new EventQueue();
                    toBeRemoved = __assign(__assign({}, testPlugin), { name: 'remove-me', unload: jest.fn() });
                    plugins = [testPlugin, toBeRemoved];
                    promises = plugins.map(function (p) { return eq.register(Context.system(), p, ajs); });
                    return [4 /*yield*/, Promise.all(promises)];
                case 1:
                    _a.sent();
                    return [4 /*yield*/, eq.deregister(Context.system(), toBeRemoved, ajs)];
                case 2:
                    _a.sent();
                    expect(toBeRemoved.unload).toHaveBeenCalled();
                    expect(eq.plugins.length).toBe(1);
                    expect(eq.plugins[0]).toBe(testPlugin);
                    return [2 /*return*/];
            }
        });
    }); });
});
describe('dispatchSingle', function () {
    it('dispatches events without placing them on the queue', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, promise;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    eq = new EventQueue();
                    promise = eq.dispatchSingle(fruitBasket);
                    expect(eq.queue.length).toBe(0);
                    return [4 /*yield*/, promise];
                case 1:
                    _a.sent();
                    expect(eq.queue.length).toBe(0);
                    return [2 /*return*/];
            }
        });
    }); });
    it('records delivery metrics', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, ctx;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    eq = new EventQueue();
                    return [4 /*yield*/, eq.dispatchSingle(new Context({
                            type: 'track',
                        }))];
                case 1:
                    ctx = _a.sent();
                    expect(ctx.logs().map(function (l) { return l.message; })).toMatchInlineSnapshot("\n      Array [\n        \"Dispatching\",\n        \"Delivered\",\n      ]\n    ");
                    expect(ctx.stats.metrics.map(function (m) { return m.metric; })).toMatchInlineSnapshot("\n      Array [\n        \"message_dispatched\",\n        \"message_delivered\",\n        \"delivered\",\n      ]\n    ");
                    return [2 /*return*/];
            }
        });
    }); });
    test('retries retriable cancelations', function () { return __awaiter(void 0, void 0, void 0, function () {
        var eq, attempted;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    // make sure all backoffs return immediatelly
                    jest.spyOn(timer, 'backoff').mockImplementationOnce(function () { return 100; });
                    eq = new EventQueue();
                    return [4 /*yield*/, eq.register(Context.system(), __assign(__assign({}, testPlugin), { track: function (ctx) {
                                // only fail first attempt
                                if (ctx === fruitBasket && ctx.attempts === 1) {
                                    ctx.cancel(new ContextCancelation({ retry: true }));
                                }
                                return Promise.resolve(ctx);
                            } }), ajs)];
                case 1:
                    _a.sent();
                    expect(eq.queue.length).toBe(0);
                    return [4 /*yield*/, eq.dispatchSingle(fruitBasket)];
                case 2:
                    attempted = _a.sent();
                    expect(attempted.attempts).toEqual(2);
                    return [2 /*return*/];
            }
        });
    }); });
});
//# sourceMappingURL=event-queue.test.js.map