import * as callbacks from "../../src/realtime-callbacks";

let wallTime = 1234567890;
jest.useFakeTimers();

describe("realtime-callbacks", function() {
    function tick(millis) {
        wallTime += millis;
        jest.advanceTimersByTime(millis);
    }

    beforeEach(function() {
        callbacks.setNow(() => wallTime);
    });

    afterEach(function() {
        callbacks.setNow();
    });

    describe("setTimeout", function() {
        it("should call the callback after the timeout", function() {
            const callback = jest.fn();
            callbacks.setTimeout(callback, 100);

            expect(callback).not.toHaveBeenCalled();
            tick(100);
            expect(callback).toHaveBeenCalled();
        });

        it("should default to a zero timeout", function() {
            const callback = jest.fn();
            callbacks.setTimeout(callback);

            expect(callback).not.toHaveBeenCalled();
            tick(0);
            expect(callback).toHaveBeenCalled();
        });

        it("should pass any parameters to the callback", function() {
            const callback = jest.fn();
            callbacks.setTimeout(callback, 0, "a", "b", "c");
            tick(0);
            expect(callback).toHaveBeenCalledWith("a", "b", "c");
        });

        it("should set 'this' to the global object", function() {
            let passed = false;
            const callback = function() {
                expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
                expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
                passed = true;
            };
            callbacks.setTimeout(callback);
            tick(0);
            expect(passed).toBe(true);
        });

        it("should handle timeouts of several seconds", function() {
            const callback = jest.fn();
            callbacks.setTimeout(callback, 2000);

            expect(callback).not.toHaveBeenCalled();
            for (let i = 0; i < 4; i++) {
                tick(500);
            }
            expect(callback).toHaveBeenCalled();
        });

        it("should call multiple callbacks in the right order", function() {
            const callback1 = jest.fn();
            const callback2 = jest.fn();
            const callback3 = jest.fn();
            callbacks.setTimeout(callback2, 200);
            callbacks.setTimeout(callback1, 100);
            callbacks.setTimeout(callback3, 300);

            expect(callback1).not.toHaveBeenCalled();
            expect(callback2).not.toHaveBeenCalled();
            expect(callback3).not.toHaveBeenCalled();
            tick(100);
            expect(callback1).toHaveBeenCalled();
            expect(callback2).not.toHaveBeenCalled();
            expect(callback3).not.toHaveBeenCalled();
            tick(100);
            expect(callback1).toHaveBeenCalled();
            expect(callback2).toHaveBeenCalled();
            expect(callback3).not.toHaveBeenCalled();
            tick(100);
            expect(callback1).toHaveBeenCalled();
            expect(callback2).toHaveBeenCalled();
            expect(callback3).toHaveBeenCalled();
        });

        it("should treat -ve timeouts the same as a zero timeout", function() {
            const callback1 = jest.fn();
            const callback2 = jest.fn();

            // check that cb1 is called before cb2
            callback1.mockImplementation(function() {
                expect(callback2).not.toHaveBeenCalled();
            });

            callbacks.setTimeout(callback1);
            callbacks.setTimeout(callback2, -100);

            expect(callback1).not.toHaveBeenCalled();
            expect(callback2).not.toHaveBeenCalled();
            tick(0);
            expect(callback1).toHaveBeenCalled();
            expect(callback2).toHaveBeenCalled();
        });

        it("should not get confused by chained calls", function() {
            const callback2 = jest.fn();
            const callback1 = jest.fn(function() {
                callbacks.setTimeout(callback2, 0);
                expect(callback2).not.toHaveBeenCalled();
            });

            callbacks.setTimeout(callback1);
            expect(callback1).not.toHaveBeenCalled();
            expect(callback2).not.toHaveBeenCalled();
            tick(0);
            expect(callback1).toHaveBeenCalled();
            // the fake timer won't actually run callbacks registered during
            // one tick until the next tick.
            tick(1);
            expect(callback2).toHaveBeenCalled();
        });

        it("should be immune to exceptions", function() {
            const callback1 = jest.fn(function() {
                throw new Error("prepare to die");
            });
            const callback2 = jest.fn();
            callbacks.setTimeout(callback1, 0);
            callbacks.setTimeout(callback2, 0);

            expect(callback1).not.toHaveBeenCalled();
            expect(callback2).not.toHaveBeenCalled();
            tick(0);
            expect(callback1).toHaveBeenCalled();
            expect(callback2).toHaveBeenCalled();
        });
    });

    describe("cancelTimeout", function() {
        it("should cancel a pending timeout", function() {
            const callback = jest.fn();
            const k = callbacks.setTimeout(callback);
            callbacks.clearTimeout(k);
            tick(0);
            expect(callback).not.toHaveBeenCalled();
        });

        it("should not affect sooner timeouts", function() {
            const callback1 = jest.fn();
            const callback2 = jest.fn();

            callbacks.setTimeout(callback1, 100);
            const k = callbacks.setTimeout(callback2, 200);
            callbacks.clearTimeout(k);

            tick(100);
            expect(callback1).toHaveBeenCalled();
            expect(callback2).not.toHaveBeenCalled();

            tick(150);
            expect(callback2).not.toHaveBeenCalled();
        });
    });
});
