var Mocks = require("mocks");
var Runtime = require('runtime').default;
var WebSocketPrioritizedCachedStrategy = require('core/strategies/websocket_prioritized_cached_strategy')
  .default;
var Util = require('core/util').default;

describe("WebSocketPrioritizedCachedStrategy", function() {
  beforeEach(function() {
    jasmine.clock().uninstall();
    jasmine.clock().install();
  });

  afterEach(function() {
    jasmine.clock().uninstall();
  });

  describe("after calling isSupported", function() {
    it("should return true when the substrategy is supported", function() {
      var substrategy = Mocks.getStrategy(true);
      var strategy = new WebSocketPrioritizedCachedStrategy(substrategy, {}, {});
      expect(strategy.isSupported()).toBe(true);
    });

    it("should return false when the substrategy is not supported", function() {
      var substrategy = Mocks.getStrategy(false);
      var strategy = new WebSocketPrioritizedCachedStrategy(substrategy, {}, {});
      expect(strategy.isSupported()).toBe(false);
    });
  });

  describe("on browsers not supporting localStorage", function() {
    beforeEach(function() {
      spyOn(Runtime, "getLocalStorage").and.returnValue(undefined);
    });

    it("should try the substrategy immediately", function() {
      var substrategy = Mocks.getStrategy(false);
      var strategy = new WebSocketPrioritizedCachedStrategy(substrategy, {}, {});
      var callback = jasmine.createSpy("callback");
      strategy.connect(0, callback);
      expect(substrategy.connect).toHaveBeenCalled();
    });
  });

  describe("on browsers supporting localStorage", function() {
    var localStorage;

    beforeEach(function() {
      localStorage = {};
      spyOn(Runtime, "getLocalStorage").and.returnValue(localStorage);
    });

    function buildCachedTransportTests(useTLS) {
      var TLS_KEY = "pusherTransportTLS";
      var NON_TLS_KEY = "pusherTransportNonTLS";

      var usedKey = useTLS ? TLS_KEY : NON_TLS_KEY;
      var unusedKey = useTLS ? NON_TLS_KEY : TLS_KEY;

      var substrategy, transports, timeline;
      var strategy;
      var callback;

      beforeEach(function() {
        substrategy = Mocks.getStrategy(true);
        transports = {
          test: Mocks.getStrategy(true),
          ws: Mocks.getStrategy(true)
        };
        timeline = Mocks.getTimeline();

        strategy = new WebSocketPrioritizedCachedStrategy(substrategy, transports, {
          useTLS: useTLS,
          timeline: timeline
        });
        callback = jasmine.createSpy("callback");
      });

      describe("without cached transport, useTLS=" + useTLS, function() {
        beforeEach(function() {
          delete localStorage[usedKey];
          localStorage[unusedKey] = "mock";
        });

        it("should not report anything to timeline", function() {
          strategy.connect(0, callback);
          expect(timeline.info).not.toHaveBeenCalled();
        });

        it("should try the substrategy immediately when cache is empty", function() {
          strategy.connect(0, callback);
          expect(substrategy.connect).toHaveBeenCalled();
        });

        it("should try the substrategy immediately when cache is stale", function() {
          localStorage[usedKey] = JSON.stringify({
            timestamp: Util.now() - 1801*1000 // default ttl is 1800s
          });

          strategy.connect(0, callback);
          expect(substrategy.connect).toHaveBeenCalled();
        });

        it("should abort the substrategy when requested", function() {
          var runner = strategy.connect(0, callback);
          runner.abort();
          expect(substrategy._abort).toHaveBeenCalled();
        });

        describe("on forceMinPriority", function() {
          it("should force the new priority on the substrategy", function() {
            var runner = strategy.connect(0, callback);
            runner.forceMinPriority(10);
            expect(substrategy._forceMinPriority).toHaveBeenCalledWith(10);
          });
        });

        describe("after connecting successfully", function() {
          var transport;
          var startTimestamp;

          beforeEach(function() {
            startTimestamp = Util.now();
            spyOn(Util, "now").and.returnValue(startTimestamp);

            strategy.connect(0, callback);
            Util.now.and.returnValue(startTimestamp + 1000);

            transport = Mocks.getTransport(true);
            transport.name = "test";
            substrategy._callback(null, { transport: transport });
          });

          it("should call back with the transport", function() {
            expect(callback).toHaveBeenCalledWith(null, {
              transport: transport
            });
          });

          it("should cache the connected transport", function() {
            expect(JSON.parse(localStorage[usedKey])).toEqual({
              timestamp: Util.now(),
              transport: "test",
              latency: 1000,
              cacheSkipCount: 0
            });
            expect(localStorage[unusedKey]).toEqual("mock");
          });
        });

        describe("after receiving an error", function() {
          beforeEach(function() {
            // set a very low timestamp to have something unused in the cache
            localStorage[usedKey] = JSON.stringify({
              timestamp: 0
            });
            strategy.connect(0, callback);
            substrategy._callback(1);
          });

          it("should call back after receiving an error from the substrategy", function() {
            expect(callback).toHaveBeenCalledWith(1);
          });

          it("should flush the appropriate transport cache", function() {
            expect(localStorage.pusherTransport).toBe(undefined);
            expect(localStorage[unusedKey]).toEqual("mock");
          });
        });
      });

      describe("with cached transport, useTLS=" + useTLS, function() {
        var t0 = Util.now();

        beforeEach(function() {
          cachedStrategy = Mocks.getStrategy(true);
          localStorage[usedKey] = JSON.stringify({
            timestamp: t0,
            transport: "test",
            latency: 1000,
            cacheSkipCount: 4
          });
          localStorage[unusedKey] = "mock";
        });

        it("should try the cached strategy first", function() {
          strategy.connect(0, callback);
          expect(transports.test.connect).toHaveBeenCalled();
        });

        it("should report to timeline when using cached strategy", function() {
          strategy.connect(0, callback);
          expect(timeline.info).toHaveBeenCalledWith({
            cached: true,
            transport: 'test',
            latency: 1000
          });
        });

        it("should abort the cached transport and not call the substrategy", function() {
          var runner = strategy.connect(0, callback);
          runner.abort();
          expect(transports.test._abort).toHaveBeenCalled();
          expect(substrategy.connect).not.toHaveBeenCalled();
        });

        describe("on forceMinPriority", function() {
          it("should force the new priority on the cached strategy", function() {
            var runner = strategy.connect(0, callback);
            runner.forceMinPriority(1000);
            expect(transports.test._forceMinPriority).toHaveBeenCalledWith(1000);
          });
        });

        describe("after connecting successfully with the cached transport", function() {
          var transport;

          beforeEach(function() {
            startTimestamp = Util.now();
            spyOn(Util, "now").and.returnValue(startTimestamp);

            strategy.connect(0, callback);
            Util.now.and.returnValue(startTimestamp + 2000);

            transport = Mocks.getTransport(true);
            transport.name = "test";
            transport.options = { useTLS: false };
            transports.test._callback(null, { transport: transport });
          });

          it("should call back with the transport", function() {
            expect(callback).toHaveBeenCalledWith(null, { transport: transport });
          });

          it("should not try the substrategy", function() {
            expect(substrategy.connect).not.toHaveBeenCalled();
          });

          it("should cache the connected transport", function() {
            expect(JSON.parse(localStorage[usedKey])).toEqual({
              timestamp: Util.now(),
              transport: 'test',
              latency: 2000,
              cacheSkipCount: 4
            });
            expect(localStorage[unusedKey]).toEqual("mock");
          });
        });

        describe("after double the cached latency + 1s", function() {
          beforeEach(function() {
            startTimestamp = Util.now();
            spyOn(Util, "now").and.returnValue(startTimestamp);

            strategy.connect(0, callback);

            Util.now.and.returnValue(startTimestamp + 3000);
            jasmine.clock().tick(3000);
          });

          it("should abort the cached strategy", function() {
            expect(transports.test._abort).toHaveBeenCalled();
          });

          it("should fall back to the substrategy", function() {
            expect(substrategy.connect).toHaveBeenCalled();
          });

          it("should flush the appropriate transport cache", function() {
            expect(localStorage[usedKey]).toBe(undefined);
            expect(localStorage[unusedKey]).toEqual("mock");
          });
        });

        describe("after failing to connect with the cached transport", function() {
          var runner;

          beforeEach(function() {
            startTimestamp = Util.now();
            spyOn(Util, "now").and.returnValue(startTimestamp);

            runner = strategy.connect(0, callback);
            runner.forceMinPriority(666);
            Util.now.and.returnValue(startTimestamp + 2000);
            transports.test._callback("error");
            Util.now.and.returnValue(startTimestamp + 2500);
          });

          it("should fall back to the substrategy", function() {
            expect(substrategy.connect).toHaveBeenCalled();
          });

          it("should flush the appropriate transport cache", function() {
            expect(localStorage[usedKey]).toBe(undefined);
            expect(localStorage[unusedKey]).toEqual("mock");
          });

          it("should abort the substrategy when requested", function() {
            runner.abort();
            expect(cachedStrategy._abort).not.toHaveBeenCalled();
            expect(substrategy._abort).toHaveBeenCalled();
          });

          describe("on forceMinPriority", function() {
            it("should force the previously set priority on the substrategy", function() {
              // priority is forced in beforeEach
              expect(substrategy.connect)
                .toHaveBeenCalledWith(666, jasmine.any(Function));
            });

            it("should force the new priority on the substrategy", function() {
              runner.forceMinPriority(3);
              expect(substrategy._forceMinPriority).toHaveBeenCalledWith(3);
            });
          });

          describe("and connecting successfully using the substrategy", function() {
            var transport;

            beforeEach(function() {
              transport = Mocks.getTransport(true);
              transport.name = "test";
              substrategy._callback(null, { transport: transport });
            });

            it("should call back with the connection", function() {
              expect(callback).toHaveBeenCalledWith(null, {
                transport: transport
              });
            });

            it("should cache the connected transport", function() {
              expect(JSON.parse(localStorage[usedKey])).toEqual({
                timestamp: Util.now(),
                transport: 'test',
                latency: 500,
                cacheSkipCount: 4
              });
              expect(localStorage[unusedKey]).toEqual("mock");
            });
          });

          describe("and failing to connect using the substrategy", function() {
            beforeEach(function() {
              substrategy._callback("error again");
            });

            it("should call back with the error", function() {
              expect(callback).toHaveBeenCalledWith("error again");
            });

            it("should flush the appropriate transport cache", function() {
              expect(localStorage[usedKey]).toBe(undefined);
              expect(localStorage[unusedKey]).toEqual("mock");
            });
          });
        });
      });

      describe('websocket prioritized', function() {
        beforeEach(function() {
          localStorage[unusedKey] = 'mock';
          localStorage[usedKey] = JSON.stringify({
            timestamp: Util.now(),
            transport: 'test',
            latency: 1000,
            cacheSkipCount: 2
          });
          localStorage[usedKey] = "{}";
        });

        it('should try websocket strategy again', function() {
          strategy.connect(0, callback);
          expect(substrategy.connect).toHaveBeenCalled();
        });

        it('should try cached strategy when already attempted default strategy', function() {
          localStorage[usedKey] = JSON.stringify({
            timestamp: Util.now(),
            transport: 'test',
            latency: 1000,
            cacheSkipCount: 4
          });

          strategy.connect(0, callback);
          expect(transports.test.connect).toHaveBeenCalled();
          expect(substrategy.connect).not.toHaveBeenCalled();

          expect(JSON.parse(localStorage[usedKey])).toEqual(jasmine.objectContaining({
            transport: 'test',
            cacheSkipCount: 4,
          }));
        });
      });
    }

    buildCachedTransportTests(false);
    buildCachedTransportTests(true);
  });
});
