text: "use strict";
  const PushController = require('../lib/Controllers/PushController').PushController;
  const StatusHandler = require('../lib/StatusHandler');
  const Config = require('../lib/Config');
  const validatePushType = require('../lib/Push/utils').validatePushType;
  
  const successfulTransmissions = function(body, installations) {
  
    const promises = installations.map((device) => {
      return Promise.resolve({
        transmitted: true,
        device: device,
      })
    });
  
    return Promise.all(promises);
  }
  
  const successfulIOS = function(body, installations) {
  
    const promises = installations.map((device) => {
      return Promise.resolve({
        transmitted: device.deviceType == "ios",
        device: device,
      })
    });
  
    return Promise.all(promises);
  }
  
  describe('PushController', () => {
    it('can validate device type when no device type is set', (done) => {
      // Make query condition
      const where = {
      };
      const validPushTypes = ['ios', 'android'];
  
      expect(function(){
        validatePushType(where, validPushTypes);
      }).not.toThrow();
      done();
    });
  
    it('can validate device type when single valid device type is set', (done) => {
      // Make query condition
      const where = {
        'deviceType': 'ios'
      };
      const validPushTypes = ['ios', 'android'];
  
      expect(function(){
        validatePushType(where, validPushTypes);
      }).not.toThrow();
      done();
    });
  
    it('can validate device type when multiple valid device types are set', (done) => {
      // Make query condition
      const where = {
        'deviceType': {
          '$in': ['android', 'ios']
        }
      };
      const validPushTypes = ['ios', 'android'];
  
      expect(function(){
        validatePushType(where, validPushTypes);
      }).not.toThrow();
      done();
    });
  
    it('can throw on validateDeviceType when single invalid device type is set', (done) => {
      // Make query condition
      const where = {
        'deviceType': 'osx'
      };
      const validPushTypes = ['ios', 'android'];
  
      expect(function(){
        validatePushType(where, validPushTypes);
      }).toThrow();
      done();
    });
  
    it('can throw on validateDeviceType when single invalid device type is set', (done) => {
      // Make query condition
      const where = {
        'deviceType': 'osx'
      };
      const validPushTypes = ['ios', 'android'];
  
      expect(function(){
        validatePushType(where, validPushTypes);
      }).toThrow();
      done();
    });
  
    it('can get expiration time in string format', (done) => {
      // Make mock request
      const timeStr = '2015-03-19T22:05:08Z';
      const body = {
        'expiration_time': timeStr
      }
  
      const time = PushController.getExpirationTime(body);
      expect(time).toEqual(new Date(timeStr).valueOf());
      done();
    });
  
    it('can get expiration time in number format', (done) => {
      // Make mock request
      const timeNumber = 1426802708;
      const body = {
        'expiration_time': timeNumber
      }
  
      const time = PushController.getExpirationTime(body);
      expect(time).toEqual(timeNumber * 1000);
      done();
    });
  
    it('can throw on getExpirationTime in invalid format', (done) => {
      // Make mock request
      const body = {
        'expiration_time': 'abcd'
      }
  
      expect(function(){
        PushController.getExpirationTime(body);
      }).toThrow();
      done();
    });
  
    it('can get push time in string format', (done) => {
      // Make mock request
      const timeStr = '2015-03-19T22:05:08Z';
      const body = {
        'push_time': timeStr
      }
  
      const { date } = PushController.getPushTime(body);
      expect(date).toEqual(new Date(timeStr));
      done();
    });
  
    it('can get push time in number format', (done) => {
      // Make mock request
      const timeNumber = 1426802708;
      const body = {
        'push_time': timeNumber
      }
  
      const { date } = PushController.getPushTime(body);
      expect(date.valueOf()).toEqual(timeNumber * 1000);
      done();
    });
  
    it('can throw on getPushTime in invalid format', (done) => {
      // Make mock request
      const body = {
        'push_time': 'abcd'
      }
  
      expect(function(){
        PushController.getPushTime(body);
      }).toThrow();
      done();
    });
  
    it('properly increment badges', (done) => {
      const pushAdapter = {
        send: function(body, installations) {
          const badge = body.data.badge;
          installations.forEach((installation) => {
            expect(installation.badge).toEqual(badge);
            expect(installation.originalBadge + 1).toEqual(installation.badge);
          })
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios", "android"];
        }
      }
      const payload = {data:{
        alert: "Hello World!",
        badge: "Increment",
      }}
      const installations = [];
      while(installations.length != 10) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      while(installations.length != 15) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length);
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "android");
        installations.push(installation);
      }
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
  
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        return Parse.Object.saveAll(installations)
      }).then(() => {
        return pushController.sendPush(payload, {}, config, auth);
      }).then(() => {
        // Wait so the push is completed.
        return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
      }).then(() => {
        // Check we actually sent 15 pushes.
        const query = new Parse.Query('_PushStatus');
        return query.find({ useMasterKey: true })
      }).then((results) => {
        expect(results.length).toBe(1);
        const pushStatus = results[0];
        expect(pushStatus.get('numSent')).toBe(15);
      }).then(() => {
        // Check that the installations were actually updated.
        const query = new Parse.Query('_Installation');
        return query.find({ useMasterKey: true })
      }).then((results) => {
        expect(results.length).toBe(15);
        for (let i = 0; i < 15; i++) {
          const installation = results[i];
          expect(installation.get('badge')).toBe(parseInt(installation.get('originalBadge')) + 1);
        }
        done()
      }).catch((err) => {
        jfail(err);
        done();
      });
    });
  
    it('properly increment badges by more than 1', (done) => {
      const pushAdapter = {
        send: function(body, installations) {
          const badge = body.data.badge;
          installations.forEach((installation) => {
            expect(installation.badge).toEqual(badge);
            expect(installation.originalBadge + 3).toEqual(installation.badge);
          })
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios", "android"];
        }
      }
      const payload = {data:{
        alert: "Hello World!",
        badge: { __op: 'Increment', amount: 3 },
      }}
      const installations = [];
      while(installations.length != 10) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      while(installations.length != 15) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length);
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "android");
        installations.push(installation);
      }
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
  
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        return Parse.Object.saveAll(installations)
      }).then(() => {
        return pushController.sendPush(payload, {}, config, auth);
      }).then(() => {
        // Wait so the push is completed.
        return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
      }).then(() => {
        // Check we actually sent 15 pushes.
        const query = new Parse.Query('_PushStatus');
        return query.find({ useMasterKey: true })
      }).then((results) => {
        expect(results.length).toBe(1);
        const pushStatus = results[0];
        expect(pushStatus.get('numSent')).toBe(15);
      }).then(() => {
        // Check that the installations were actually updated.
        const query = new Parse.Query('_Installation');
        return query.find({ useMasterKey: true })
      }).then((results) => {
        expect(results.length).toBe(15);
        for (let i = 0; i < 15; i++) {
          const installation = results[i];
          expect(installation.get('badge')).toBe(parseInt(installation.get('originalBadge')) + 3);
        }
        done()
      }).catch((err) => {
        jfail(err);
        done();
      });
    });
  
    it('properly set badges to 1', (done) => {
  
      const pushAdapter = {
        send: function(body, installations) {
          const badge = body.data.badge;
          installations.forEach((installation) => {
            expect(installation.badge).toEqual(badge);
            expect(1).toEqual(installation.badge);
          })
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const payload = {data: {
        alert: "Hello World!",
        badge: 1,
      }}
      const installations = [];
      while(installations.length != 10) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
  
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        return Parse.Object.saveAll(installations)
      }).then(() => {
        return pushController.sendPush(payload, {}, config, auth);
      }).then(() => {
        // Wait so the push is completed.
        return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
      }).then(() => {
        // Check we actually sent the pushes.
        const query = new Parse.Query('_PushStatus');
        return query.find({ useMasterKey: true })
      }).then((results) => {
        expect(results.length).toBe(1);
        const pushStatus = results[0];
        expect(pushStatus.get('numSent')).toBe(10);
      }).then(() => {
        // Check that the installations were actually updated.
        const query = new Parse.Query('_Installation');
        return query.find({ useMasterKey: true })
      }).then((results) => {
        expect(results.length).toBe(10);
        for (let i = 0; i < 10; i++) {
          const installation = results[i];
          expect(installation.get('badge')).toBe(1);
        }
        done()
      }).catch((err) => {
        jfail(err);
        done();
      });
    });
  
    it('properly set badges to 1 with complex query #2903 #3022', (done) => {
  
      const payload = {
        data: {
          alert: "Hello World!",
          badge: 1,
        }
      }
      const installations = [];
      while(installations.length != 10) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
      let matchedInstallationsCount = 0;
      const pushAdapter = {
        send: function(body, installations) {
          matchedInstallationsCount += installations.length;
          const badge = body.data.badge;
          installations.forEach((installation) => {
            expect(installation.badge).toEqual(badge);
            expect(1).toEqual(installation.badge);
          })
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
      const pushController = new PushController();
      reconfigureServer({
        push: {
          adapter: pushAdapter
        }
      }).then(() => {
        return Parse.Object.saveAll(installations)
      }).then((installations) => {
        const objectIds = installations.map(installation => {
          return installation.id;
        })
        const where = {
          objectId: {'$in': objectIds.slice(0, 5)}
        }
        return pushController.sendPush(payload, where, config, auth);
      }).then(() => {
        return new Promise((res) => {
          setTimeout(res, 300);
        });
      }).then(() => {
        expect(matchedInstallationsCount).toBe(5);
        const query = new Parse.Query(Parse.Installation);
        query.equalTo('badge', 1);
        return query.find({useMasterKey: true});
      }).then((installations) => {
        expect(installations.length).toBe(5);
        done();
      }).catch(() => {
        fail("should not fail");
        done();
      });
    });
  
    it('properly creates _PushStatus', (done) => {
      const pushStatusAfterSave = {
        handler: function() {}
      };
      const spy = spyOn(pushStatusAfterSave, 'handler').and.callThrough();
      Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler);
      const installations = [];
      while(installations.length != 10) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      while(installations.length != 15) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("deviceType", "android");
        installations.push(installation);
      }
      const payload = {data: {
        alert: "Hello World!",
        badge: 1,
      }}
  
      const pushAdapter = {
        send: function(body, installations) {
          return successfulIOS(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        return Parse.Object.saveAll(installations);
      })
        .then(() => {
          return pushController.sendPush(payload, {}, config, auth);
        }).then(() => {
          // it is enqueued so it can take time
          return new Promise((resolve) => {
            setTimeout(() => {
              resolve();
            }, 1000);
          });
        }).then(() => {
          const query = new Parse.Query('_PushStatus');
          return query.find({useMasterKey: true});
        }).then((results) => {
          expect(results.length).toBe(1);
          const result = results[0];
          expect(result.createdAt instanceof Date).toBe(true);
          expect(result.updatedAt instanceof Date).toBe(true);
          expect(result.id.length).toBe(10);
          expect(result.get('source')).toEqual('rest');
          expect(result.get('query')).toEqual(JSON.stringify({}));
          expect(typeof result.get('payload')).toEqual("string");
          expect(JSON.parse(result.get('payload'))).toEqual(payload.data);
          expect(result.get('status')).toEqual('succeeded');
          expect(result.get('numSent')).toEqual(10);
          expect(result.get('sentPerType')).toEqual({
            'ios': 10 // 10 ios
          });
          expect(result.get('numFailed')).toEqual(5);
          expect(result.get('failedPerType')).toEqual({
            'android': 5 // android
          });
          // Try to get it without masterKey
          const query = new Parse.Query('_PushStatus');
          return query.find();
        }).catch((error) => {
          expect(error.code).toBe(119);
        })
        .then(() => {
          function getPushStatus(callIndex) {
            return spy.calls.all()[callIndex].args[0].object;
          }
          expect(spy).toHaveBeenCalled();
          expect(spy.calls.count()).toBe(4);
          const allCalls = spy.calls.all();
          allCalls.forEach((call) => {
            expect(call.args.length).toBe(1);
            const object = call.args[0].object;
            expect(object instanceof Parse.Object).toBe(true);
          });
          expect(getPushStatus(0).get('status')).toBe('pending');
          expect(getPushStatus(1).get('status')).toBe('running');
          expect(getPushStatus(1).get('numSent')).toBe(0);
          expect(getPushStatus(2).get('status')).toBe('running');
          expect(getPushStatus(2).get('numSent')).toBe(10);
          expect(getPushStatus(2).get('numFailed')).toBe(5);
          // Those are updated from a nested . operation, this would
          // not render correctly before
          expect(getPushStatus(2).get('failedPerType')).toEqual({
            android: 5
          });
          expect(getPushStatus(2).get('sentPerType')).toEqual({
            ios: 10
          });
          expect(getPushStatus(3).get('status')).toBe('succeeded');
        })
        .then(done).catch(done.fail);
    });
  
    it('properly creates _PushStatus without serverURL', (done) => {
      const pushStatusAfterSave = {
        handler: function() {}
      };
      Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler);
      const installation = new Parse.Object("_Installation");
      installation.set("installationId", "installation");
      installation.set("deviceToken","device_token")
      installation.set("badge", 0);
      installation.set("originalBadge", 0);
      installation.set("deviceType", "ios");
  
      const payload = {data: {
        alert: "Hello World!",
        badge: 1,
      }}
  
      const pushAdapter = {
        send: function(body, installations) {
          return successfulIOS(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
      const pushController = new PushController();
      return installation.save().then(() => {
        return reconfigureServer({
          serverURL: 'http://localhost:8378/', // server with borked URL
          push: { adapter: pushAdapter }
        })
      })
        .then(() => {
          return pushController.sendPush(payload, {}, config, auth);
        }).then(() => {
          // it is enqueued so it can take time
          return new Promise((resolve) => {
            setTimeout(() => {
              resolve();
            }, 1000);
          });
        }).then(() => {
          Parse.serverURL = 'http://localhost:8378/1'; // GOOD url
          const query = new Parse.Query('_PushStatus');
          return query.find({useMasterKey: true});
        }).then((results) => {
          expect(results.length).toBe(1);
        })
        .then(done).catch(done.fail);
    });
  
    it('should properly report failures in _PushStatus', (done) => {
      const pushAdapter = {
        send: function(body, installations) {
          return installations.map((installation) => {
            return Promise.resolve({
              deviceType: installation.deviceType
            })
          })
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
      const where = { 'channels': {
        '$ins': ['Giants', 'Mets']
      }};
      const payload = {data: {
        alert: "Hello World!",
        badge: 1,
      }}
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        return pushController.sendPush(payload, where, config, auth)
      }).then(() => {
        fail('should not succeed');
        done();
      }).catch(() => {
        const query = new Parse.Query('_PushStatus');
        query.find({useMasterKey: true}).then((results) => {
          expect(results.length).toBe(1);
          const pushStatus = results[0];
          expect(pushStatus.get('status')).toBe('failed');
          done();
        });
      });
    });
  
    it('should support full RESTQuery for increment', (done) => {
      const payload = {data: {
        alert: "Hello World!",
        badge: 'Increment',
      }}
  
      const pushAdapter = {
        send: function(body, installations) {
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
  
      const where = {
        'deviceToken': {
          '$in': ['device_token_0', 'device_token_1', 'device_token_2']
        }
      }
  
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        const installations = [];
        while (installations.length != 5) {
          const installation = new Parse.Object("_Installation");
          installation.set("installationId", "installation_" + installations.length);
          installation.set("deviceToken", "device_token_" + installations.length)
          installation.set("badge", installations.length);
          installation.set("originalBadge", installations.length);
          installation.set("deviceType", "ios");
          installations.push(installation);
        }
        return Parse.Object.saveAll(installations);
      }).then(() => {
        return pushController.sendPush(payload, where, config, auth);
      }).then(() => {
        // Wait so the push is completed.
        return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
      }).then(() => {
        const query = new Parse.Query('_PushStatus');
        return query.find({ useMasterKey: true })
      }).then((results) => {
        expect(results.length).toBe(1);
        const pushStatus = results[0];
        expect(pushStatus.get('numSent')).toBe(3);
        done();
      }).catch((err) => {
        jfail(err);
        done();
      });
    });
  
    it('should support object type for alert', (done) => {
      const payload = {data: {
        alert: {
          'loc-key': 'hello_world',
        },
      }}
  
      const pushAdapter = {
        send: function(body, installations) {
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
  
      const where = {
        'deviceType': 'ios'
      }
  
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        const installations = [];
        while (installations.length != 5) {
          const installation = new Parse.Object("_Installation");
          installation.set("installationId", "installation_" + installations.length);
          installation.set("deviceToken", "device_token_" + installations.length)
          installation.set("badge", installations.length);
          installation.set("originalBadge", installations.length);
          installation.set("deviceType", "ios");
          installations.push(installation);
        }
        return Parse.Object.saveAll(installations);
      }).then(() => {
        return pushController.sendPush(payload, where, config, auth)
      }).then(() => {
        // Wait so the push is completed.
        return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
      }).then(() => {
        const query = new Parse.Query('_PushStatus');
        return query.find({ useMasterKey: true })
      }).then((results) => {
        expect(results.length).toBe(1);
        const pushStatus = results[0];
        expect(pushStatus.get('numSent')).toBe(5);
        done();
      }).catch(() => {
        fail('should not fail');
        done();
      });
    });
  
    it('should flatten', () => {
      const res = StatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]])
      expect(res).toEqual([1,2,3,4,5,6]);
    });
  
    it('properly transforms push time', () => {
      expect(PushController.getPushTime()).toBe(undefined);
      expect(PushController.getPushTime({
        'push_time': 1000
      }).date).toEqual(new Date(1000 * 1000));
      expect(PushController.getPushTime({
        'push_time': '2017-01-01'
      }).date).toEqual(new Date('2017-01-01'));
  
      expect(() => {PushController.getPushTime({
        'push_time': 'gibberish-time'
      })}).toThrow();
      expect(() => {PushController.getPushTime({
        'push_time': Number.NaN
      })}).toThrow();
  
      expect(PushController.getPushTime({
        push_time: '2017-09-06T13:42:48.369Z'
      })).toEqual({
        date: new Date('2017-09-06T13:42:48.369Z'),
        isLocalTime: false,
      });
      expect(PushController.getPushTime({
        push_time: '2007-04-05T12:30-02:00',
      })).toEqual({
        date: new Date('2007-04-05T12:30-02:00'),
        isLocalTime: false,
      });
      expect(PushController.getPushTime({
        push_time: '2007-04-05T12:30',
      })).toEqual({
        date: new Date('2007-04-05T12:30'),
        isLocalTime: true,
      });
    });
  
    it('should not schedule push when not configured', (done) => {
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
      const pushAdapter = {
        send: function(body, installations) {
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const pushController = new PushController();
      const payload = {
        data: {
          alert: 'hello',
        },
        push_time: new Date().getTime()
      }
  
      const installations = [];
      while(installations.length != 10) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        return Parse.Object.saveAll(installations).then(() => {
          return pushController.sendPush(payload, {}, config, auth);
        }).then(() => new Promise(resolve => setTimeout(resolve, 300)));
      }).then(() => {
        const query = new Parse.Query('_PushStatus');
        return query.find({useMasterKey: true}).then((results) => {
          expect(results.length).toBe(1);
          const pushStatus = results[0];
          expect(pushStatus.get('status')).not.toBe('scheduled');
          done();
        });
      }).catch((err) => {
        console.error(err);
        fail('should not fail');
        done();
      });
    });
  
    it('should schedule push when configured', (done) => {
      const auth = {
        isMaster: true
      }
      const pushAdapter = {
        send: function(body, installations) {
          const promises = installations.map((device) => {
            if (!device.deviceToken) {
              // Simulate error when device token is not set
              return Promise.reject();
            }
            return Promise.resolve({
              transmitted: true,
              device: device,
            })
          });
  
          return Promise.all(promises);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const pushController = new PushController();
      const payload = {
        data: {
          alert: 'hello',
        },
        push_time: new Date().getTime() / 1000
      }
  
      const installations = [];
      while(installations.length != 10) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      reconfigureServer({
        push: { adapter: pushAdapter },
        scheduledPush: true
      }).then(() => {
        const config = Config.get(Parse.applicationId);
        return Parse.Object.saveAll(installations).then(() => {
          return pushController.sendPush(payload, {}, config, auth);
        }).then(() => new Promise(resolve => setTimeout(resolve, 300)));
      }).then(() => {
        const query = new Parse.Query('_PushStatus');
        return query.find({useMasterKey: true}).then((results) => {
          expect(results.length).toBe(1);
          const pushStatus = results[0];
          expect(pushStatus.get('status')).toBe('scheduled');
        });
      }).then(done).catch(done.err);
    });
  
    it('should not enqueue push when device token is not set', (done) => {
      const auth = {
        isMaster: true
      }
      const pushAdapter = {
        send: function(body, installations) {
          const promises = installations.map((device) => {
            if (!device.deviceToken) {
              // Simulate error when device token is not set
              return Promise.reject();
            }
            return Promise.resolve({
              transmitted: true,
              device: device,
            })
          });
  
          return Promise.all(promises);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const pushController = new PushController();
      const payload = {
        data: {
          alert: 'hello',
        },
        push_time: new Date().getTime() / 1000
      }
  
      const installations = [];
      while(installations.length != 5) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("deviceToken","device_token_" + installations.length)
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      while(installations.length != 15) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        const config = Config.get(Parse.applicationId);
        return Parse.Object.saveAll(installations).then(() => {
          return pushController.sendPush(payload, {}, config, auth);
        }).then(() => new Promise(resolve => setTimeout(resolve, 100)));
      }).then(() => {
        const query = new Parse.Query('_PushStatus');
        return query.find({useMasterKey: true}).then((results) => {
          expect(results.length).toBe(1);
          const pushStatus = results[0];
          expect(pushStatus.get('numSent')).toBe(5);
          expect(pushStatus.get('status')).toBe('succeeded');
          done();
        });
      }).catch((err) => {
        console.error(err);
        fail('should not fail');
        done();
      });
    });
  
    it('should not mark the _PushStatus as failed when audience has no deviceToken', (done) => {
      const auth = {
        isMaster: true
      }
      const pushAdapter = {
        send: function(body, installations) {
          const promises = installations.map((device) => {
            if (!device.deviceToken) {
              // Simulate error when device token is not set
              return Promise.reject();
            }
            return Promise.resolve({
              transmitted: true,
              device: device,
            })
          });
  
          return Promise.all(promises);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const pushController = new PushController();
      const payload = {
        data: {
          alert: 'hello',
        },
        push_time: new Date().getTime() / 1000
      }
  
      const installations = [];
      while(installations.length != 5) {
        const installation = new Parse.Object("_Installation");
        installation.set("installationId", "installation_" + installations.length);
        installation.set("badge", installations.length);
        installation.set("originalBadge", installations.length);
        installation.set("deviceType", "ios");
        installations.push(installation);
      }
  
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        const config = Config.get(Parse.applicationId);
        return Parse.Object.saveAll(installations).then(() => {
          return pushController.sendPush(payload, {}, config, auth)
        }).then(() => new Promise(resolve => setTimeout(resolve, 100)));
      }).then(() => {
        const query = new Parse.Query('_PushStatus');
        return query.find({useMasterKey: true}).then((results) => {
          expect(results.length).toBe(1);
          const pushStatus = results[0];
          expect(pushStatus.get('numSent')).toBe(0);
          expect(pushStatus.get('status')).toBe('succeeded');
          done();
        });
      }).catch((err) => {
        console.error(err);
        fail('should not fail');
        done();
      });
    });
  
    it('should support localized payload data', (done) => {
      const payload = {data: {
        alert: 'Hello!',
        'alert-fr': 'Bonjour',
        'alert-es': 'Ola'
      }}
  
      const pushAdapter = {
        send: function(body, installations) {
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
  
      const where = {
        'deviceType': 'ios'
      }
      spyOn(pushAdapter, 'send').and.callThrough();
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        const installations = [];
        while (installations.length != 5) {
          const installation = new Parse.Object("_Installation");
          installation.set("installationId", "installation_" + installations.length);
          installation.set("deviceToken", "device_token_" + installations.length)
          installation.set("badge", installations.length);
          installation.set("originalBadge", installations.length);
          installation.set("deviceType", "ios");
          installations.push(installation);
        }
        installations[0].set('localeIdentifier', 'fr-CA');
        installations[1].set('localeIdentifier', 'fr-FR');
        installations[2].set('localeIdentifier', 'en-US');
        return Parse.Object.saveAll(installations);
      }).then(() => {
        return pushController.sendPush(payload, where, config, auth)
      }).then(() => {
        // Wait so the push is completed.
        return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
      }).then(() => {
        expect(pushAdapter.send.calls.count()).toBe(2);
        const firstCall = pushAdapter.send.calls.first();
        expect(firstCall.args[0].data).toEqual({
          alert: 'Hello!'
        });
        expect(firstCall.args[1].length).toBe(3); // 3 installations
  
        const lastCall = pushAdapter.send.calls.mostRecent();
        expect(lastCall.args[0].data).toEqual({
          alert: 'Bonjour'
        });
        expect(lastCall.args[1].length).toBe(2); // 2 installations
        // No installation is in es so only 1 call for fr, and another for default
        done();
      }).catch(done.fail);
    });
  
    it('should update audiences', (done) => {
      const pushAdapter = {
        send: function(body, installations) {
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes: function() {
          return ["ios"];
        }
      }
  
      const config = Config.get(Parse.applicationId);
      const auth = {
        isMaster: true
      }
  
      let audienceId = null;
      const now = new Date();
      let timesUsed = 0;
  
      const where = {
        'deviceType': 'ios'
      }
      spyOn(pushAdapter, 'send').and.callThrough();
      const pushController = new PushController();
      reconfigureServer({
        push: { adapter: pushAdapter }
      }).then(() => {
        const installations = [];
        while (installations.length != 5) {
          const installation = new Parse.Object("_Installation");
          installation.set("installationId", "installation_" + installations.length);
          installation.set("deviceToken","device_token_" + installations.length)
          installation.set("badge", installations.length);
          installation.set("originalBadge", installations.length);
          installation.set("deviceType", "ios");
          installations.push(installation);
        }
        return Parse.Object.saveAll(installations);
      }).then(() => {
        // Create an audience
        const query = new Parse.Query("_Audience");
        query.descending("createdAt");
        query.equalTo("query", JSON.stringify(where));
        const parseResults = (results) => {
          if (results.length > 0) {
            audienceId = results[0].id;
            timesUsed = results[0].get('timesUsed');
            if (!isFinite(timesUsed)) {
              timesUsed = 0;
            }
          }
        }
        const audience = new Parse.Object("_Audience");
        audience.set("name", "testAudience")
        audience.set("query", JSON.stringify(where));
        return Parse.Object.saveAll(audience).then(() => {
          return query.find({ useMasterKey: true }).then(parseResults);
        });
      }).then(() => {
        const body = {
          data: { alert: 'hello' },
          audience_id: audienceId
        }
        return pushController.sendPush(body, where, config, auth)
      }).then(() => {
        // Wait so the push is completed.
        return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); });
      }).then(() => {
        expect(pushAdapter.send.calls.count()).toBe(1);
        const firstCall = pushAdapter.send.calls.first();
        expect(firstCall.args[0].data).toEqual({
          alert: 'hello'
        });
        expect(firstCall.args[1].length).toBe(5);
      }).then(() => {
        // Get the audience we used above.
        const query = new Parse.Query("_Audience");
        query.equalTo("objectId", audienceId);
        return query.find({ useMasterKey: true })
      }).then((results) => {
        const audience = results[0];
        expect(audience.get('query')).toBe(JSON.stringify(where));
        expect(audience.get('timesUsed')).toBe(timesUsed + 1);
        expect(audience.get('lastUsed')).not.toBeLessThan(now);
      }).then(() => {
        done();
      }).catch(done.fail);
    });
  
    describe('pushTimeHasTimezoneComponent', () => {
      it('should be accurate', () => {
        expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048Z'))
          .toBe(true, 'UTC time');
        expect(PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30-02:00'))
          .toBe(true, 'Timezone offset');
        expect(PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30:00.000Z-02:00'))
          .toBe(true, 'Seconds + Milliseconds + Timezone offset');
  
        expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048'))
          .toBe(false, 'No timezone');
        expect(PushController.pushTimeHasTimezoneComponent('2017-09-06'))
          .toBe(false, 'YY-MM-DD');
      });
    });
  
    describe('formatPushTime', () => {
      it('should format as ISO string', () => {
        expect(PushController.formatPushTime({
          date: new Date('2017-09-06T17:14:01.048Z'),
          isLocalTime: false,
        })).toBe('2017-09-06T17:14:01.048Z', 'UTC time');
        expect(PushController.formatPushTime({
          date: new Date('2007-04-05T12:30-02:00'),
          isLocalTime: false
        })).toBe('2007-04-05T14:30:00.000Z', 'Timezone offset');
  
        const noTimezone = new Date('2017-09-06T17:14:01.048')
        const expectedHour = 17 + noTimezone.getTimezoneOffset() / 60;
        expect(PushController.formatPushTime({
          date: noTimezone,
          isLocalTime: true,
        })).toBe(`2017-09-06T${expectedHour}:14:01.048`, 'No timezone');
        expect(PushController.formatPushTime({
          date: new Date('2017-09-06'),
          isLocalTime: true
        })).toBe('2017-09-06T00:00:00.000', 'YY-MM-DD');
      });
    });
  
    describe('Scheduling pushes in local time', () => {
      it('should preserve the push time', (done) => {
        const auth = {isMaster: true};
        const pushAdapter = {
          send(body, installations) {
            return successfulTransmissions(body, installations);
          },
          getValidPushTypes() {
            return ["ios"];
          }
        };
  
        const pushTime = '2017-09-06T17:14:01.048';
        const expectedHour = 17 + new Date(pushTime).getTimezoneOffset() / 60;
  
        reconfigureServer({
          push: {adapter: pushAdapter},
          scheduledPush: true
        })
          .then(() => {
            const config = Config.get(Parse.applicationId);
            return new Promise((resolve, reject) => {
              const pushController = new PushController();
              pushController.sendPush({
                data: {
                  alert: "Hello World!",
                  badge: "Increment",
                },
                push_time: pushTime
              }, {}, config, auth, resolve)
                .catch(reject);
            })
          })
          .then((pushStatusId) => {
            const q = new Parse.Query('_PushStatus');
            return q.get(pushStatusId, {useMasterKey: true});
          })
          .then((pushStatus) => {
            expect(pushStatus.get('status')).toBe('scheduled');
            expect(pushStatus.get('pushTime')).toBe(`2017-09-06T${expectedHour}:14:01.048`);
          })
          .then(done, done.fail);
      });
    });
  
    describe('With expiration defined', () => {
      const auth = {isMaster: true};
      const pushController = new PushController();
  
      let config = Config.get(Parse.applicationId);
  
      const pushes = [];
      const pushAdapter = {
        send(body, installations) {
          pushes.push(body);
          return successfulTransmissions(body, installations);
        },
        getValidPushTypes() {
          return ["ios"];
        }
      };
  
      beforeEach((done) => {
        reconfigureServer({
          push: {adapter: pushAdapter},
        })
          .then(() => {
            config = Config.get(Parse.applicationId);
          })
          .then(done, done.fail);
      });
  
      it('should throw if both expiration_time and expiration_interval are set', () => {
        expect(() => pushController.sendPush({
          expiration_time: '2017-09-25T13:21:20.841Z',
          expiration_interval: 1000,
        }, {}, config, auth)).toThrow()
      });
  
      it('should throw on invalid expiration_interval', () => {
        expect(() => pushController.sendPush({
          expiration_interval: -1
        }, {}, config, auth)).toThrow();
        expect(() => pushController.sendPush({
          expiration_interval: '',
        }, {}, config, auth)).toThrow();
        expect(() => pushController.sendPush({
          expiration_time: {},
        }, {}, config, auth)).toThrow();
      });
  
      describe('For immediate pushes',() => {
        it('should transform the expiration_interval into an absolute time', (done) => {
          const now = new Date('2017-09-25T13:30:10.452Z');
  
          reconfigureServer({
            push: {adapter: pushAdapter},
          })
            .then(() =>
              new Promise((resolve) => {
                pushController.sendPush({
                  data: {
                    alert: 'immediate push',
                  },
                  expiration_interval: 20 * 60, // twenty minutes
                }, {}, Config.get(Parse.applicationId), auth, resolve, now)
              }))
            .then((pushStatusId) => {
              const p = new Parse.Object('_PushStatus');
              p.id = pushStatusId;
              return p.fetch({useMasterKey: true});
            })
            .then((pushStatus) => {
              expect(pushStatus.get('expiry')).toBeDefined('expiry must be set');
              expect(pushStatus.get('expiry'))
                .toEqual(new Date('2017-09-25T13:50:10.452Z').valueOf());
  
              expect(pushStatus.get('expiration_interval')).toBeDefined('expiration_interval must be defined');
              expect(pushStatus.get('expiration_interval')).toBe(20 * 60);
            })
            .then(done, done.fail);
        });
      });
    });
  });
  