'use strict';
// This is a port of the test suite:
// hungry/js/test/parse_object_test.js
//
// Things we didn't port:
// Tests that aren't async, because they only test the client.
// Tests that use Relations, because we intentionally do not support
// relations.
// Tests that use 'testDestroy', because they have a complex
// dependency on the terrible qunit start/stop mechanism.
// Tests for unfetching, since that behaves differently in
// single-instance mode and we don't want these tests to run in
// single-instance mode.

describe('Parse.Object testing', () => {
  it('create', function (done) {
    create({ test: 'test' }, function (model) {
      ok(model.id, 'Should have an objectId set');
      equal(model.get('test'), 'test', 'Should have the right attribute');
      done();
    });
  });

  it('update', function (done) {
    create({ test: 'test' }, function (model) {
      const t2 = new TestObject({ objectId: model.id });
      t2.set('test', 'changed');
      t2.save().then(function (model) {
        equal(model.get('test'), 'changed', 'Update should have succeeded');
        done();
      });
    });
  });

  it('save without null', function (done) {
    const object = new TestObject();
    object.set('favoritePony', 'Rainbow Dash');
    object.save().then(
      function (objectAgain) {
        equal(objectAgain, object);
        done();
      },
      function (objectAgain, error) {
        ok(null, 'Error ' + error.code + ': ' + error.message);
        done();
      }
    );
  });

  it('save cycle', done => {
    const a = new Parse.Object('TestObject');
    const b = new Parse.Object('TestObject');
    a.set('b', b);
    a.save()
      .then(function () {
        b.set('a', a);
        return b.save();
      })
      .then(function () {
        ok(a.id);
        ok(b.id);
        strictEqual(a.get('b'), b);
        strictEqual(b.get('a'), a);
      })
      .then(
        function () {
          done();
        },
        function (error) {
          ok(false, error);
          done();
        }
      );
  });

  it('get', function (done) {
    create({ test: 'test' }, function (model) {
      const t2 = new TestObject({ objectId: model.id });
      t2.fetch().then(function (model2) {
        equal(model2.get('test'), 'test', 'Update should have succeeded');
        ok(model2.id);
        equal(model2.id, model.id, 'Ids should match');
        done();
      });
    });
  });

  it('delete', function (done) {
    const t = new TestObject();
    t.set('test', 'test');
    t.save().then(function () {
      t.destroy().then(function () {
        const t2 = new TestObject({ objectId: t.id });
        t2.fetch().then(fail, () => done());
      });
    });
  });

  it('find', function (done) {
    const t = new TestObject();
    t.set('foo', 'bar');
    t.save().then(function () {
      const query = new Parse.Query(TestObject);
      query.equalTo('foo', 'bar');
      query.find().then(function (results) {
        equal(results.length, 1);
        done();
      });
    });
  });

  it('relational fields', function (done) {
    const item = new Item();
    item.set('property', 'x');
    const container = new Container();
    container.set('item', item);

    Parse.Object.saveAll([item, container]).then(function () {
      const query = new Parse.Query(Container);
      query.find().then(function (results) {
        equal(results.length, 1);
        const containerAgain = results[0];
        const itemAgain = containerAgain.get('item');
        itemAgain.fetch().then(function () {
          equal(itemAgain.get('property'), 'x');
          done();
        });
      });
    });
  });

  it('save adds no data keys (other than createdAt and updatedAt)', function (done) {
    const object = new TestObject();
    object.save().then(function () {
      const keys = Object.keys(object.attributes).sort();
      equal(keys.length, 2);
      done();
    });
  });

  it('recursive save', function (done) {
    const item = new Item();
    item.set('property', 'x');
    const container = new Container();
    container.set('item', item);

    container.save().then(function () {
      const query = new Parse.Query(Container);
      query.find().then(function (results) {
        equal(results.length, 1);
        const containerAgain = results[0];
        const itemAgain = containerAgain.get('item');
        itemAgain.fetch().then(function () {
          equal(itemAgain.get('property'), 'x');
          done();
        });
      });
    });
  });

  it('fetch', function (done) {
    const item = new Item({ foo: 'bar' });
    item.save().then(function () {
      const itemAgain = new Item();
      itemAgain.id = item.id;
      itemAgain.fetch().then(function () {
        itemAgain.save({ foo: 'baz' }).then(function () {
          item.fetch().then(function () {
            equal(item.get('foo'), itemAgain.get('foo'));
            done();
          });
        });
      });
    });
  });

  it("createdAt doesn't change", function (done) {
    const object = new TestObject({ foo: 'bar' });
    object.save().then(function () {
      const objectAgain = new TestObject();
      objectAgain.id = object.id;
      objectAgain.fetch().then(function () {
        equal(object.createdAt.getTime(), objectAgain.createdAt.getTime());
        done();
      });
    });
  });

  it('createdAt and updatedAt exposed', function (done) {
    const object = new TestObject({ foo: 'bar' });
    object.save().then(function () {
      notEqual(object.updatedAt, undefined);
      notEqual(object.createdAt, undefined);
      done();
    });
  });

  it('updatedAt gets updated', function (done) {
    const object = new TestObject({ foo: 'bar' });
    object.save().then(function () {
      ok(object.updatedAt, 'initial save should cause updatedAt to exist');
      const firstUpdatedAt = object.updatedAt;
      object.save({ foo: 'baz' }).then(function () {
        ok(object.updatedAt, 'two saves should cause updatedAt to exist');
        notEqual(firstUpdatedAt, object.updatedAt);
        done();
      });
    });
  });

  it('createdAt is reasonable', function (done) {
    const startTime = new Date();
    const object = new TestObject({ foo: 'bar' });
    object.save().then(function () {
      const endTime = new Date();
      const startDiff = Math.abs(startTime.getTime() - object.createdAt.getTime());
      ok(startDiff < 5000);

      const endDiff = Math.abs(endTime.getTime() - object.createdAt.getTime());
      ok(endDiff < 5000);

      done();
    });
  });

  it('can set null', function (done) {
    const obj = new Parse.Object('TestObject');
    obj.set('foo', null);
    obj.save().then(
      function (obj) {
        on_db('mongo', () => {
          equal(obj.get('foo'), null);
        });
        on_db('postgres', () => {
          equal(obj.get('foo'), null);
        });
        done();
      },
      function () {
        fail('should not fail');
        done();
      }
    );
  });

  it('can set boolean', function (done) {
    const obj = new Parse.Object('TestObject');
    obj.set('yes', true);
    obj.set('no', false);
    obj.save().then(
      function (obj) {
        equal(obj.get('yes'), true);
        equal(obj.get('no'), false);
        done();
      },
      function (obj, error) {
        ok(false, error.message);
        done();
      }
    );
  });

  it('cannot set invalid date', async function (done) {
    const obj = new Parse.Object('TestObject');
    obj.set('when', new Date(Date.parse(null)));
    try {
      await obj.save();
    } catch (e) {
      ok(true);
      done();
      return;
    }
    ok(false, 'Saving an invalid date should throw');
    done();
  });

  it('can set authData when not user class', async () => {
    const obj = new Parse.Object('TestObject');
    obj.set('authData', 'random');
    await obj.save();
    expect(obj.get('authData')).toBe('random');
    const query = new Parse.Query('TestObject');
    const object = await query.get(obj.id, { useMasterKey: true });
    expect(object.get('authData')).toBe('random');
  });

  it('invalid class name', function (done) {
    const item = new Parse.Object('Foo^bar');
    item.save().then(
      function () {
        ok(false, 'The name should have been invalid.');
        done();
      },
      function () {
        // Because the class name is invalid, the router will not be able to route
        // it, so it will actually return a -1 error code.
        // equal(error.code, Parse.Error.INVALID_CLASS_NAME);
        done();
      }
    );
  });

  it('invalid key name', function (done) {
    const item = new Parse.Object('Item');
    ok(!item.set({ 'foo^bar': 'baz' }), 'Item should not be updated with invalid key.');
    item.save({ 'foo^bar': 'baz' }).then(fail, () => done());
  });

  it('invalid __type', function (done) {
    const item = new Parse.Object('Item');
    const types = ['Pointer', 'File', 'Date', 'GeoPoint', 'Bytes', 'Polygon', 'Relation'];
    const tests = types.map(type => {
      const test = new Parse.Object('Item');
      test.set('foo', {
        __type: type,
      });
      return test;
    });
    const next = function (index) {
      if (index < tests.length) {
        tests[index].save().then(fail, error => {
          expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
          next(index + 1);
        });
      } else {
        done();
      }
    };
    item
      .save({
        foo: {
          __type: 'IvalidName',
        },
      })
      .then(fail, () => next(0));
  });

  it('simple field deletion', function (done) {
    const simple = new Parse.Object('SimpleObject');
    simple
      .save({
        foo: 'bar',
      })
      .then(
        function (simple) {
          simple.unset('foo');
          ok(!simple.has('foo'), 'foo should have been unset.');
          ok(simple.dirty('foo'), 'foo should be dirty.');
          ok(simple.dirty(), 'the whole object should be dirty.');
          simple.save().then(
            function (simple) {
              ok(!simple.has('foo'), 'foo should have been unset.');
              ok(!simple.dirty('foo'), 'the whole object was just saved.');
              ok(!simple.dirty(), 'the whole object was just saved.');

              const query = new Parse.Query('SimpleObject');
              query.get(simple.id).then(
                function (simpleAgain) {
                  ok(!simpleAgain.has('foo'), 'foo should have been removed.');
                  done();
                },
                function (simpleAgain, error) {
                  ok(false, 'Error ' + error.code + ': ' + error.message);
                  done();
                }
              );
            },
            function (simple, error) {
              ok(false, 'Error ' + error.code + ': ' + error.message);
              done();
            }
          );
        },
        function (simple, error) {
          ok(false, 'Error ' + error.code + ': ' + error.message);
          done();
        }
      );
  });

  it('field deletion before first save', function (done) {
    const simple = new Parse.Object('SimpleObject');
    simple.set('foo', 'bar');
    simple.unset('foo');

    ok(!simple.has('foo'), 'foo should have been unset.');
    ok(simple.dirty('foo'), 'foo should be dirty.');
    ok(simple.dirty(), 'the whole object should be dirty.');
    simple.save().then(
      function (simple) {
        ok(!simple.has('foo'), 'foo should have been unset.');
        ok(!simple.dirty('foo'), 'the whole object was just saved.');
        ok(!simple.dirty(), 'the whole object was just saved.');

        const query = new Parse.Query('SimpleObject');
        query.get(simple.id).then(
          function (simpleAgain) {
            ok(!simpleAgain.has('foo'), 'foo should have been removed.');
            done();
          },
          function (simpleAgain, error) {
            ok(false, 'Error ' + error.code + ': ' + error.message);
            done();
          }
        );
      },
      function (simple, error) {
        ok(false, 'Error ' + error.code + ': ' + error.message);
        done();
      }
    );
  });

  it('relation deletion', function (done) {
    const simple = new Parse.Object('SimpleObject');
    const child = new Parse.Object('Child');
    simple
      .save({
        child: child,
      })
      .then(
        function (simple) {
          simple.unset('child');
          ok(!simple.has('child'), 'child should have been unset.');
          ok(simple.dirty('child'), 'child should be dirty.');
          ok(simple.dirty(), 'the whole object should be dirty.');
          simple.save().then(
            function (simple) {
              ok(!simple.has('child'), 'child should have been unset.');
              ok(!simple.dirty('child'), 'the whole object was just saved.');
              ok(!simple.dirty(), 'the whole object was just saved.');

              const query = new Parse.Query('SimpleObject');
              query.get(simple.id).then(
                function (simpleAgain) {
                  ok(!simpleAgain.has('child'), 'child should have been removed.');
                  done();
                },
                function (simpleAgain, error) {
                  ok(false, 'Error ' + error.code + ': ' + error.message);
                  done();
                }
              );
            },
            function (simple, error) {
              ok(false, 'Error ' + error.code + ': ' + error.message);
              done();
            }
          );
        },
        function (simple, error) {
          ok(false, 'Error ' + error.code + ': ' + error.message);
          done();
        }
      );
  });

  it('deleted keys get cleared', function (done) {
    const simpleObject = new Parse.Object('SimpleObject');
    simpleObject.set('foo', 'bar');
    simpleObject.unset('foo');
    simpleObject.save().then(function (simpleObject) {
      simpleObject.set('foo', 'baz');
      simpleObject.save().then(function (simpleObject) {
        const query = new Parse.Query('SimpleObject');
        query.get(simpleObject.id).then(function (simpleObjectAgain) {
          equal(simpleObjectAgain.get('foo'), 'baz');
          done();
        }, done.fail);
      }, done.fail);
    }, done.fail);
  });

  it('setting after deleting', function (done) {
    const simpleObject = new Parse.Object('SimpleObject');
    simpleObject.set('foo', 'bar');
    simpleObject.save().then(
      function (simpleObject) {
        simpleObject.unset('foo');
        simpleObject.set('foo', 'baz');
        simpleObject.save().then(
          function (simpleObject) {
            const query = new Parse.Query('SimpleObject');
            query.get(simpleObject.id).then(
              function (simpleObjectAgain) {
                equal(simpleObjectAgain.get('foo'), 'baz');
                done();
              },
              function (error) {
                ok(false, 'Error ' + error.code + ': ' + error.message);
                done();
              }
            );
          },
          function (error) {
            ok(false, 'Error ' + error.code + ': ' + error.message);
            done();
          }
        );
      },
      function (error) {
        ok(false, 'Error ' + error.code + ': ' + error.message);
        done();
      }
    );
  });

  it('increment', function (done) {
    const simple = new Parse.Object('SimpleObject');
    simple
      .save({
        foo: 5,
      })
      .then(function (simple) {
        simple.increment('foo');
        equal(simple.get('foo'), 6);
        ok(simple.dirty('foo'), 'foo should be dirty.');
        ok(simple.dirty(), 'the whole object should be dirty.');
        simple.save().then(function (simple) {
          equal(simple.get('foo'), 6);
          ok(!simple.dirty('foo'), 'the whole object was just saved.');
          ok(!simple.dirty(), 'the whole object was just saved.');

          const query = new Parse.Query('SimpleObject');
          query.get(simple.id).then(function (simpleAgain) {
            equal(simpleAgain.get('foo'), 6);
            done();
          });
        });
      });
  });

  it('addUnique', function (done) {
    const x1 = new Parse.Object('X');
    x1.set('stuff', [1, 2]);
    x1.save()
      .then(() => {
        const objectId = x1.id;
        const x2 = new Parse.Object('X', { objectId: objectId });
        x2.addUnique('stuff', 2);
        x2.addUnique('stuff', 4);
        expect(x2.get('stuff')).toEqual([2, 4]);
        return x2.save();
      })
      .then(() => {
        const query = new Parse.Query('X');
        return query.get(x1.id);
      })
      .then(
        x3 => {
          const stuff = x3.get('stuff');
          const expected = [1, 2, 4];
          expect(stuff.length).toBe(expected.length);
          for (const i of stuff) {
            expect(expected.indexOf(i) >= 0).toBe(true);
          }
          done();
        },
        error => {
          on_db('mongo', () => {
            jfail(error);
          });
          on_db('postgres', () => {
            expect(error.message).toEqual('Postgres does not support AddUnique operator.');
          });
          done();
        }
      );
  });

  it_only_db('mongo')('can increment array nested fields', async () => {
    const obj = new TestObject();
    obj.set('items', [ { value: 'a', count: 5 }, { value: 'b', count: 1 } ]);
    await obj.save();
    obj.increment('items.0.count', 15);
    obj.increment('items.1.count', 4);
    await obj.save();
    expect(obj.toJSON().items[0].value).toBe('a');
    expect(obj.toJSON().items[1].value).toBe('b');
    expect(obj.toJSON().items[0].count).toBe(20);
    expect(obj.toJSON().items[1].count).toBe(5);
    const query = new Parse.Query(TestObject);
    const result = await query.get(obj.id);
    expect(result.get('items')[0].value).toBe('a');
    expect(result.get('items')[1].value).toBe('b');
    expect(result.get('items')[0].count).toBe(20);
    expect(result.get('items')[1].count).toBe(5);
    expect(result.get('items')).toEqual(obj.get('items'));
  });

  it_only_db('mongo')('can increment array nested fields missing index', async () => {
    const obj = new TestObject();
    obj.set('items', []);
    await obj.save();
    obj.increment('items.1.count', 15);
    await obj.save();
    expect(obj.toJSON().items[0]).toBe(null);
    expect(obj.toJSON().items[1].count).toBe(15);
    const query = new Parse.Query(TestObject);
    const result = await query.get(obj.id);
    expect(result.get('items')[0]).toBe(null);
    expect(result.get('items')[1].count).toBe(15);
    expect(result.get('items')).toEqual(obj.get('items'));
  });

  it_id('44097c6f-d0ca-4dc5-aa8a-3dd2d9ac645a')(it)('can query array nested fields', async () => {
    const objects = [];
    for (let i = 0; i < 10; i++) {
      const obj = new TestObject();
      obj.set('items', [i, { value: i }]);
      objects.push(obj);
    }
    await Parse.Object.saveAll(objects);
    let query = new Parse.Query(TestObject);
    query.greaterThan('items.1.value', 5);
    let result = await query.find();
    expect(result.length).toBe(4);

    query = new Parse.Query(TestObject);
    query.lessThan('items.0', 3);
    result = await query.find();
    expect(result.length).toBe(3);

    query = new Parse.Query(TestObject);
    query.equalTo('items.0', 5);
    result = await query.find();
    expect(result.length).toBe(1);

    query = new Parse.Query(TestObject);
    query.notEqualTo('items.0', 5);
    result = await query.find();
    expect(result.length).toBe(9);
  });

  it('addUnique with object', function (done) {
    const x1 = new Parse.Object('X');
    x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]);
    x1.save()
      .then(() => {
        const objectId = x1.id;
        const x2 = new Parse.Object('X', { objectId: objectId });
        x2.addUnique('stuff', { hello: 'world' });
        x2.addUnique('stuff', { bar: 'baz' });
        expect(x2.get('stuff')).toEqual([{ hello: 'world' }, { bar: 'baz' }]);
        return x2.save();
      })
      .then(() => {
        const query = new Parse.Query('X');
        return query.get(x1.id);
      })
      .then(
        x3 => {
          const stuff = x3.get('stuff');
          const target = [1, { hello: 'world' }, { foo: 'bar' }, { bar: 'baz' }];
          expect(stuff.length).toEqual(target.length);
          let found = 0;
          for (const thing in target) {
            for (const st in stuff) {
              if (st == thing) {
                found++;
              }
            }
          }
          expect(found).toBe(target.length);
          done();
        },
        error => {
          jfail(error);
          done();
        }
      );
  });

  it('removes with object', function (done) {
    const x1 = new Parse.Object('X');
    x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]);
    x1.save()
      .then(() => {
        const objectId = x1.id;
        const x2 = new Parse.Object('X', { objectId: objectId });
        x2.remove('stuff', { hello: 'world' });
        expect(x2.get('stuff')).toEqual([]);
        return x2.save();
      })
      .then(() => {
        const query = new Parse.Query('X');
        return query.get(x1.id);
      })
      .then(
        x3 => {
          expect(x3.get('stuff')).toEqual([1, { foo: 'bar' }]);
          done();
        },
        error => {
          jfail(error);
          done();
        }
      );
  });

  it('dirty attributes', function (done) {
    const object = new Parse.Object('TestObject');
    object.set('cat', 'good');
    object.set('dog', 'bad');
    object.save().then(
      function (object) {
        ok(!object.dirty());
        ok(!object.dirty('cat'));
        ok(!object.dirty('dog'));

        object.set('dog', 'okay');

        ok(object.dirty());
        ok(!object.dirty('cat'));
        ok(object.dirty('dog'));

        done();
      },
      function () {
        ok(false, 'This should have saved.');
        done();
      }
    );
  });

  it('dirty keys', function (done) {
    const object = new Parse.Object('TestObject');
    object.set('gogo', 'good');
    object.set('sito', 'sexy');
    ok(object.dirty());
    let dirtyKeys = object.dirtyKeys();
    equal(dirtyKeys.length, 2);
    ok(arrayContains(dirtyKeys, 'gogo'));
    ok(arrayContains(dirtyKeys, 'sito'));

    object
      .save()
      .then(function (obj) {
        ok(!obj.dirty());
        dirtyKeys = obj.dirtyKeys();
        equal(dirtyKeys.length, 0);
        ok(!arrayContains(dirtyKeys, 'gogo'));
        ok(!arrayContains(dirtyKeys, 'sito'));

        // try removing keys
        obj.unset('sito');
        ok(obj.dirty());
        dirtyKeys = obj.dirtyKeys();
        equal(dirtyKeys.length, 1);
        ok(!arrayContains(dirtyKeys, 'gogo'));
        ok(arrayContains(dirtyKeys, 'sito'));

        return obj.save();
      })
      .then(function (obj) {
        ok(!obj.dirty());
        equal(obj.get('gogo'), 'good');
        equal(obj.get('sito'), undefined);
        dirtyKeys = obj.dirtyKeys();
        equal(dirtyKeys.length, 0);
        ok(!arrayContains(dirtyKeys, 'gogo'));
        ok(!arrayContains(dirtyKeys, 'sito'));

        done();
      });
  });

  it('acl attribute', function (done) {
    Parse.User.signUp('bob', 'password').then(function (user) {
      const TestObject = Parse.Object.extend('TestObject');
      const obj = new TestObject({
        ACL: new Parse.ACL(user), // ACLs cause things like validation to run
      });
      ok(obj.get('ACL') instanceof Parse.ACL);

      obj.save().then(function (obj) {
        ok(obj.get('ACL') instanceof Parse.ACL);

        const query = new Parse.Query(TestObject);
        query.get(obj.id).then(function (obj) {
          ok(obj.get('ACL') instanceof Parse.ACL);

          const query = new Parse.Query(TestObject);
          query.find().then(function (results) {
            obj = results[0];
            ok(obj.get('ACL') instanceof Parse.ACL);

            done();
          });
        });
      });
    });
  });

  it('cannot save object with invalid field', async () => {
    const invalidFields = ['className', 'length'];
    const promises = invalidFields.map(async field => {
      const obj = new TestObject();
      obj.set(field, 'bar');
      try {
        await obj.save();
        fail('should not succeed');
      } catch (e) {
        expect(e.message).toBe(`Invalid field name: ${field}.`);
      }
    });
    await Promise.all(promises);
  });

  it('old attribute unset then unset', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.set('x', 3);
    obj.save().then(function () {
      obj.unset('x');
      obj.unset('x');
      obj.save().then(function () {
        equal(obj.has('x'), false);
        equal(obj.get('x'), undefined);
        const query = new Parse.Query(TestObject);
        query.get(obj.id).then(function (objAgain) {
          equal(objAgain.has('x'), false);
          equal(objAgain.get('x'), undefined);
          done();
        });
      });
    });
  });

  it('new attribute unset then unset', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.set('x', 5);
    obj.unset('x');
    obj.unset('x');
    obj.save().then(function () {
      equal(obj.has('x'), false);
      equal(obj.get('x'), undefined);
      const query = new Parse.Query(TestObject);
      query.get(obj.id).then(function (objAgain) {
        equal(objAgain.has('x'), false);
        equal(objAgain.get('x'), undefined);
        done();
      });
    });
  });

  it('unknown attribute unset then unset', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.unset('x');
    obj.unset('x');
    obj.save().then(function () {
      equal(obj.has('x'), false);
      equal(obj.get('x'), undefined);
      const query = new Parse.Query(TestObject);
      query.get(obj.id).then(function (objAgain) {
        equal(objAgain.has('x'), false);
        equal(objAgain.get('x'), undefined);
        done();
      });
    });
  });

  it('old attribute unset then clear', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.set('x', 3);
    obj.save().then(function () {
      obj.unset('x');
      obj.clear();
      obj.save().then(function () {
        equal(obj.has('x'), false);
        equal(obj.get('x'), undefined);
        const query = new Parse.Query(TestObject);
        query.get(obj.id).then(function (objAgain) {
          equal(objAgain.has('x'), false);
          equal(objAgain.get('x'), undefined);
          done();
        });
      });
    });
  });

  it('new attribute unset then clear', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.set('x', 5);
    obj.unset('x');
    obj.clear();
    obj.save().then(function () {
      equal(obj.has('x'), false);
      equal(obj.get('x'), undefined);
      const query = new Parse.Query(TestObject);
      query.get(obj.id).then(function (objAgain) {
        equal(objAgain.has('x'), false);
        equal(objAgain.get('x'), undefined);
        done();
      });
    });
  });

  it('unknown attribute unset then clear', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.unset('x');
    obj.clear();
    obj.save().then(function () {
      equal(obj.has('x'), false);
      equal(obj.get('x'), undefined);
      const query = new Parse.Query(TestObject);
      query.get(obj.id).then(function (objAgain) {
        equal(objAgain.has('x'), false);
        equal(objAgain.get('x'), undefined);
        done();
      });
    });
  });

  it('old attribute clear then unset', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.set('x', 3);
    obj.save().then(function () {
      obj.clear();
      obj.unset('x');
      obj.save().then(function () {
        equal(obj.has('x'), false);
        equal(obj.get('x'), undefined);
        const query = new Parse.Query(TestObject);
        query.get(obj.id).then(function (objAgain) {
          equal(objAgain.has('x'), false);
          equal(objAgain.get('x'), undefined);
          done();
        });
      });
    });
  });

  it('new attribute clear then unset', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.set('x', 5);
    obj.clear();
    obj.unset('x');
    obj.save().then(function () {
      equal(obj.has('x'), false);
      equal(obj.get('x'), undefined);
      const query = new Parse.Query(TestObject);
      query.get(obj.id).then(function (objAgain) {
        equal(objAgain.has('x'), false);
        equal(objAgain.get('x'), undefined);
        done();
      });
    });
  });

  it('unknown attribute clear then unset', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.clear();
    obj.unset('x');
    obj.save().then(function () {
      equal(obj.has('x'), false);
      equal(obj.get('x'), undefined);
      const query = new Parse.Query(TestObject);
      query.get(obj.id).then(function (objAgain) {
        equal(objAgain.has('x'), false);
        equal(objAgain.get('x'), undefined);
        done();
      });
    });
  });

  it('old attribute clear then clear', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.set('x', 3);
    obj.save().then(function () {
      obj.clear();
      obj.clear();
      obj.save().then(function () {
        equal(obj.has('x'), false);
        equal(obj.get('x'), undefined);
        const query = new Parse.Query(TestObject);
        query.get(obj.id).then(function (objAgain) {
          equal(objAgain.has('x'), false);
          equal(objAgain.get('x'), undefined);
          done();
        });
      });
    });
  });

  it('new attribute clear then clear', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.set('x', 5);
    obj.clear();
    obj.clear();
    obj.save().then(function () {
      equal(obj.has('x'), false);
      equal(obj.get('x'), undefined);
      const query = new Parse.Query(TestObject);
      query.get(obj.id).then(function (objAgain) {
        equal(objAgain.has('x'), false);
        equal(objAgain.get('x'), undefined);
        done();
      });
    });
  });

  it('unknown attribute clear then clear', function (done) {
    const TestObject = Parse.Object.extend('TestObject');
    const obj = new TestObject();
    obj.clear();
    obj.clear();
    obj.save().then(function () {
      equal(obj.has('x'), false);
      equal(obj.get('x'), undefined);
      const query = new Parse.Query(TestObject);
      query.get(obj.id).then(function (objAgain) {
        equal(objAgain.has('x'), false);
        equal(objAgain.get('x'), undefined);
        done();
      });
    });
  });

  it('saving children in an array', function (done) {
    const Parent = Parse.Object.extend('Parent');
    const Child = Parse.Object.extend('Child');

    const child1 = new Child();
    const child2 = new Child();
    const parent = new Parent();

    child1.set('name', 'jamie');
    child2.set('name', 'cersei');
    parent.set('children', [child1, child2]);

    parent.save().then(function () {
      const query = new Parse.Query(Child);
      query.ascending('name');
      query.find().then(function (results) {
        equal(results.length, 2);
        equal(results[0].get('name'), 'cersei');
        equal(results[1].get('name'), 'jamie');
        done();
      });
    }, done.fail);
  });

  it('two saves at the same time', function (done) {
    const object = new Parse.Object('TestObject');
    let firstSave = true;

    const success = function () {
      if (firstSave) {
        firstSave = false;
        return;
      }

      const query = new Parse.Query('TestObject');
      query.find().then(function (results) {
        equal(results.length, 1);
        equal(results[0].get('cat'), 'meow');
        equal(results[0].get('dog'), 'bark');
        done();
      });
    };

    object.save({ cat: 'meow' }).then(success, fail);
    object.save({ dog: 'bark' }).then(success, fail);
  });

  // The schema-checking parts of this are working.
  // We dropped the part where number can be reset to a correctly
  // typed field and saved okay, since that appears to be borked in
  // the client.
  // If this fails, it's probably a schema issue.
  it('many saves after a failure', function (done) {
    // Make a class with a number in the schema.
    const o1 = new Parse.Object('TestObject');
    o1.set('number', 1);
    let object = null;
    o1.save()
      .then(() => {
        object = new Parse.Object('TestObject');
        object.set('number', 'two');
        return object.save();
      })
      .then(fail, error => {
        expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);

        object.set('other', 'foo');
        return object.save();
      })
      .then(fail, error => {
        expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);

        object.set('other', 'bar');
        return object.save();
      })
      .then(fail, error => {
        expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);

        done();
      });
  });

  it('is not dirty after save', function (done) {
    const obj = new Parse.Object('TestObject');
    obj.save().then(function () {
      obj.set({ content: 'x' });
      obj.fetch().then(function () {
        equal(false, obj.dirty('content'));
        done();
      });
    });
  });

  it('add with an object', function (done) {
    const child = new Parse.Object('Person');
    const parent = new Parse.Object('Person');

    Promise.resolve()
      .then(function () {
        return child.save();
      })
      .then(function () {
        parent.add('children', child);
        return parent.save();
      })
      .then(function () {
        const query = new Parse.Query('Person');
        return query.get(parent.id);
      })
      .then(function (parentAgain) {
        equal(parentAgain.get('children')[0].id, child.id);
      })
      .then(
        function () {
          done();
        },
        function (error) {
          ok(false, error);
          done();
        }
      );
  });

  it('toJSON saved object', function (done) {
    create({ foo: 'bar' }, function (model) {
      const objJSON = model.toJSON();
      ok(objJSON.foo, "expected json to contain key 'foo'");
      ok(objJSON.objectId, "expected json to contain key 'objectId'");
      ok(objJSON.createdAt, "expected json to contain key 'createdAt'");
      ok(objJSON.updatedAt, "expected json to contain key 'updatedAt'");
      done();
    });
  });

  it('remove object from array', function (done) {
    const obj = new TestObject();
    obj.save().then(function () {
      const container = new TestObject();
      container.add('array', obj);
      equal(container.get('array').length, 1);
      container.save(null).then(function () {
        const objAgain = new TestObject();
        objAgain.id = obj.id;
        container.remove('array', objAgain);
        equal(container.get('array').length, 0);
        done();
      });
    });
  });

  it('async methods', function (done) {
    const obj = new TestObject();
    obj.set('time', 'adventure');

    obj
      .save()
      .then(function (obj) {
        ok(obj.id, 'objectId should not be null.');
        const objAgain = new TestObject();
        objAgain.id = obj.id;
        return objAgain.fetch();
      })
      .then(function (objAgain) {
        equal(objAgain.get('time'), 'adventure');
        return objAgain.destroy();
      })
      .then(function () {
        const query = new Parse.Query(TestObject);
        return query.find();
      })
      .then(function (results) {
        equal(results.length, 0);
      })
      .then(function () {
        done();
      });
  });

  it('fail validation with promise', function (done) {
    const PickyEater = Parse.Object.extend('PickyEater', {
      validate: function (attrs) {
        if (attrs.meal === 'tomatoes') {
          return 'Ew. Tomatoes are gross.';
        }
        return Parse.Object.prototype.validate.apply(this, arguments);
      },
    });

    const bryan = new PickyEater();
    bryan
      .save({
        meal: 'burrito',
      })
      .then(
        function () {
          return bryan.save({
            meal: 'tomatoes',
          });
        },
        function () {
          ok(false, 'Save should have succeeded.');
        }
      )
      .then(
        function () {
          ok(false, 'Save should have failed.');
        },
        function (error) {
          equal(error, 'Ew. Tomatoes are gross.');
          done();
        }
      );
  });

  it("beforeSave doesn't make object dirty with new field", function (done) {
    const restController = Parse.CoreManager.getRESTController();
    const r = restController.request;
    restController.request = function () {
      return r.apply(this, arguments).then(function (result) {
        result.aDate = { __type: 'Date', iso: '2014-06-24T06:06:06.452Z' };
        return result;
      });
    };

    const obj = new Parse.Object('Thing');
    obj
      .save()
      .then(function () {
        ok(!obj.dirty(), 'The object should not be dirty');
        ok(obj.get('aDate'));
      })
      .then(function () {
        restController.request = r;
        done();
      });
  });

  xit("beforeSave doesn't make object dirty with existing field", function (done) {
    const restController = Parse.CoreManager.getRESTController();
    const r = restController.request;
    restController.request = function () {
      return r.apply(restController, arguments).then(function (result) {
        result.aDate = { __type: 'Date', iso: '2014-06-24T06:06:06.452Z' };
        return result;
      });
    };

    const now = new Date();

    const obj = new Parse.Object('Thing');
    const promise = obj.save();
    obj.set('aDate', now);

    promise
      .then(function () {
        ok(obj.dirty(), 'The object should be dirty');
        equal(now, obj.get('aDate'));
      })
      .then(function () {
        restController.request = r;
        done();
      });
  });

  it('bytes work', function (done) {
    Promise.resolve()
      .then(function () {
        const obj = new TestObject();
        obj.set('bytes', { __type: 'Bytes', base64: 'ZnJveW8=' });
        return obj.save();
      })
      .then(function (obj) {
        const query = new Parse.Query(TestObject);
        return query.get(obj.id);
      })
      .then(
        function (obj) {
          equal(obj.get('bytes').__type, 'Bytes');
          equal(obj.get('bytes').base64, 'ZnJveW8=');
          done();
        },
        function (error) {
          ok(false, JSON.stringify(error));
          done();
        }
      );
  });

  it('destroyAll no objects', function (done) {
    Parse.Object.destroyAll([])
      .then(function (success) {
        ok(success, 'Should be able to destroy no objects');
        done();
      })
      .catch(done.fail);
  });

  it('destroyAll new objects only', function (done) {
    const objects = [new TestObject(), new TestObject()];
    Parse.Object.destroyAll(objects)
      .then(function (success) {
        ok(success, 'Should be able to destroy only new objects');
        done();
      })
      .catch(done.fail);
  });

  it('fetchAll', function (done) {
    const numItems = 11;
    const container = new Container();
    const items = [];
    for (let i = 0; i < numItems; i++) {
      const item = new Item();
      item.set('x', i);
      items.push(item);
    }
    Parse.Object.saveAll(items)
      .then(function () {
        container.set('items', items);
        return container.save();
      })
      .then(function () {
        const query = new Parse.Query(Container);
        return query.get(container.id);
      })
      .then(function (containerAgain) {
        const itemsAgain = containerAgain.get('items');
        if (!itemsAgain || !itemsAgain.forEach) {
          fail('no itemsAgain retrieved', itemsAgain);
          done();
          return;
        }
        equal(itemsAgain.length, numItems, 'Should get the array back');
        itemsAgain.forEach(function (item, i) {
          const newValue = i * 2;
          item.set('x', newValue);
        });
        return Parse.Object.saveAll(itemsAgain);
      })
      .then(function () {
        return Parse.Object.fetchAll(items);
      })
      .then(function (fetchedItemsAgain) {
        equal(fetchedItemsAgain.length, numItems, 'Number of items fetched should not change');
        fetchedItemsAgain.forEach(function (item, i) {
          equal(item.get('x'), i * 2);
        });
        done();
      });
  });

  it('fetchAll no objects', function (done) {
    Parse.Object.fetchAll([])
      .then(function (success) {
        ok(Array.isArray(success), 'Should be able to fetchAll no objects');
        done();
      })
      .catch(done.fail);
  });

  it('fetchAll updates dates', function (done) {
    let updatedObject;
    const object = new TestObject();
    object.set('x', 7);
    object
      .save()
      .then(function () {
        const query = new Parse.Query(TestObject);
        return query.find(object.id);
      })
      .then(function (results) {
        updatedObject = results[0];
        updatedObject.set('x', 11);
        return updatedObject.save();
      })
      .then(function () {
        return Parse.Object.fetchAll([object]);
      })
      .then(function () {
        equal(object.createdAt.getTime(), updatedObject.createdAt.getTime());
        equal(object.updatedAt.getTime(), updatedObject.updatedAt.getTime());
        done();
      });
  });

  xit('fetchAll backbone-style callbacks', function (done) {
    const numItems = 11;
    const container = new Container();
    const items = [];
    for (let i = 0; i < numItems; i++) {
      const item = new Item();
      item.set('x', i);
      items.push(item);
    }
    Parse.Object.saveAll(items)
      .then(function () {
        container.set('items', items);
        return container.save();
      })
      .then(function () {
        const query = new Parse.Query(Container);
        return query.get(container.id);
      })
      .then(function (containerAgain) {
        const itemsAgain = containerAgain.get('items');
        if (!itemsAgain || !itemsAgain.forEach) {
          fail('no itemsAgain retrieved', itemsAgain);
          done();
          return;
        }
        equal(itemsAgain.length, numItems, 'Should get the array back');
        itemsAgain.forEach(function (item, i) {
          const newValue = i * 2;
          item.set('x', newValue);
        });
        return Parse.Object.saveAll(itemsAgain);
      })
      .then(function () {
        return Parse.Object.fetchAll(items).then(
          function (fetchedItemsAgain) {
            equal(fetchedItemsAgain.length, numItems, 'Number of items fetched should not change');
            fetchedItemsAgain.forEach(function (item, i) {
              equal(item.get('x'), i * 2);
            });
            done();
          },
          function () {
            ok(false, 'Failed to fetchAll');
            done();
          }
        );
      });
  });

  it('fetchAll error on multiple classes', function (done) {
    const container = new Container();
    container.set('item', new Item());
    container.set('subcontainer', new Container());
    return container
      .save()
      .then(function () {
        const query = new Parse.Query(Container);
        return query.get(container.id);
      })
      .then(function (containerAgain) {
        const subContainerAgain = containerAgain.get('subcontainer');
        const itemAgain = containerAgain.get('item');
        const multiClassArray = [subContainerAgain, itemAgain];
        return Parse.Object.fetchAll(multiClassArray).catch(e => {
          expect(e.code).toBe(Parse.Error.INVALID_CLASS_NAME);
          done();
        });
      });
  });

  it('fetchAll error on unsaved object', async function (done) {
    const unsavedObjectArray = [new TestObject()];
    await Parse.Object.fetchAll(unsavedObjectArray).catch(e => {
      expect(e.code).toBe(Parse.Error.MISSING_OBJECT_ID);
      done();
    });
  });

  it('fetchAll error on deleted object', function (done) {
    const numItems = 11;
    const items = [];
    for (let i = 0; i < numItems; i++) {
      const item = new Item();
      item.set('x', i);
      items.push(item);
    }
    Parse.Object.saveAll(items)
      .then(function () {
        const query = new Parse.Query(Item);
        return query.get(items[0].id);
      })
      .then(function (objectToDelete) {
        return objectToDelete.destroy();
      })
      .then(function (deletedObject) {
        const nonExistentObject = new Item({ objectId: deletedObject.id });
        const nonExistentObjectArray = [nonExistentObject, items[1]];
        return Parse.Object.fetchAll(nonExistentObjectArray).catch(e => {
          expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
          done();
        });
      });
  });

  // TODO: Verify that with Sessions, this test is wrong... A fetch on
  //       user should not bring down a session token.
  xit('fetchAll User attributes get merged', function (done) {
    let sameUser;
    let user = new Parse.User();
    user.set('username', 'asdf');
    user.set('password', 'zxcv');
    user.set('foo', 'bar');
    user
      .signUp()
      .then(function () {
        Parse.User.logOut();
        const query = new Parse.Query(Parse.User);
        return query.get(user.id);
      })
      .then(function (userAgain) {
        user = userAgain;
        sameUser = new Parse.User();
        sameUser.set('username', 'asdf');
        sameUser.set('password', 'zxcv');
        return sameUser.logIn();
      })
      .then(function () {
        ok(!user.getSessionToken(), 'user should not have a sessionToken');
        ok(sameUser.getSessionToken(), 'sameUser should have a sessionToken');
        sameUser.set('baz', 'qux');
        return sameUser.save();
      })
      .then(function () {
        return Parse.Object.fetchAll([user]);
      })
      .then(function () {
        equal(user.getSessionToken(), sameUser.getSessionToken());
        equal(user.createdAt.getTime(), sameUser.createdAt.getTime());
        equal(user.updatedAt.getTime(), sameUser.updatedAt.getTime());
        Parse.User.logOut();
        done();
      });
  });

  it('fetchAllIfNeeded', function (done) {
    const numItems = 11;
    const container = new Container();
    const items = [];
    for (let i = 0; i < numItems; i++) {
      const item = new Item();
      item.set('x', i);
      items.push(item);
    }
    Parse.Object.saveAll(items)
      .then(function () {
        container.set('items', items);
        return container.save();
      })
      .then(function () {
        const query = new Parse.Query(Container);
        return query.get(container.id);
      })
      .then(function (containerAgain) {
        const itemsAgain = containerAgain.get('items');
        if (!itemsAgain || !itemsAgain.forEach) {
          fail('no itemsAgain retrieved', itemsAgain);
          done();
          return;
        }
        itemsAgain.forEach(function (item, i) {
          item.set('x', i * 2);
        });
        return Parse.Object.saveAll(itemsAgain);
      })
      .then(function () {
        return Parse.Object.fetchAllIfNeeded(items);
      })
      .then(function (fetchedItems) {
        equal(fetchedItems.length, numItems, 'Number of items should not change');
        fetchedItems.forEach(function (item, i) {
          equal(item.get('x'), i);
        });
        done();
      });
  });

  xit('fetchAllIfNeeded backbone-style callbacks', function (done) {
    const numItems = 11;
    const container = new Container();
    const items = [];
    for (let i = 0; i < numItems; i++) {
      const item = new Item();
      item.set('x', i);
      items.push(item);
    }
    Parse.Object.saveAll(items)
      .then(function () {
        container.set('items', items);
        return container.save();
      })
      .then(function () {
        const query = new Parse.Query(Container);
        return query.get(container.id);
      })
      .then(function (containerAgain) {
        const itemsAgain = containerAgain.get('items');
        if (!itemsAgain || !itemsAgain.forEach) {
          fail('no itemsAgain retrieved', itemsAgain);
          done();
          return;
        }
        itemsAgain.forEach(function (item, i) {
          item.set('x', i * 2);
        });
        return Parse.Object.saveAll(itemsAgain);
      })
      .then(function () {
        const items = container.get('items');
        return Parse.Object.fetchAllIfNeeded(items).then(
          function (fetchedItems) {
            equal(fetchedItems.length, numItems, 'Number of items should not change');
            fetchedItems.forEach(function (item, j) {
              equal(item.get('x'), j);
            });
            done();
          },
          function () {
            ok(false, 'Failed to fetchAll');
            done();
          }
        );
      });
  });

  it('fetchAllIfNeeded no objects', function (done) {
    Parse.Object.fetchAllIfNeeded([])
      .then(function (success) {
        ok(Array.isArray(success), 'Should be able to fetchAll no objects');
        done();
      })
      .catch(done.fail);
  });

  it('fetchAllIfNeeded unsaved object', async function (done) {
    const unsavedObjectArray = [new TestObject()];
    await Parse.Object.fetchAllIfNeeded(unsavedObjectArray).catch(e => {
      expect(e.code).toBe(Parse.Error.MISSING_OBJECT_ID);
      done();
    });
  });

  it('fetchAllIfNeeded error on multiple classes', function (done) {
    const container = new Container();
    container.set('item', new Item());
    container.set('subcontainer', new Container());
    return container
      .save()
      .then(function () {
        const query = new Parse.Query(Container);
        return query.get(container.id);
      })
      .then(function (containerAgain) {
        const subContainerAgain = containerAgain.get('subcontainer');
        const itemAgain = containerAgain.get('item');
        const multiClassArray = [subContainerAgain, itemAgain];
        return Parse.Object.fetchAllIfNeeded(multiClassArray).catch(e => {
          expect(e.code).toBe(Parse.Error.INVALID_CLASS_NAME);
          done();
        });
      });
  });

  it('Objects with className User', function (done) {
    equal(Parse.CoreManager.get('PERFORM_USER_REWRITE'), true);
    const User1 = Parse.Object.extend({
      className: 'User',
    });

    equal(User1.className, '_User', 'className is rewritten by default');

    Parse.User.allowCustomUserClass(true);
    equal(Parse.CoreManager.get('PERFORM_USER_REWRITE'), false);
    const User2 = Parse.Object.extend({
      className: 'User',
    });

    equal(User2.className, 'User', 'className is not rewritten when allowCustomUserClass(true)');

    // Set back to default so as not to break other tests.
    Parse.User.allowCustomUserClass(false);
    equal(Parse.CoreManager.get('PERFORM_USER_REWRITE'), true, 'PERFORM_USER_REWRITE is reset');

    const user = new User2();
    user.set('name', 'Me');
    user.save({ height: 181 }).then(function (user) {
      equal(user.get('name'), 'Me');
      equal(user.get('height'), 181);

      const query = new Parse.Query(User2);
      query.get(user.id).then(function (user) {
        equal(user.className, 'User');
        equal(user.get('name'), 'Me');
        equal(user.get('height'), 181);
        done();
      });
    });
  });

  it('create without data', function (done) {
    const t1 = new TestObject({ test: 'test' });
    t1.save()
      .then(function (t1) {
        const t2 = TestObject.createWithoutData(t1.id);
        return t2.fetch();
      })
      .then(function (t2) {
        equal(t2.get('test'), 'test', 'Fetch should have grabbed ' + "'test' property.");
        const t3 = TestObject.createWithoutData(t2.id);
        t3.set('test', 'not test');
        return t3.fetch();
      })
      .then(
        function (t3) {
          equal(t3.get('test'), 'test', "Fetch should have grabbed server 'test' property.");
          done();
        },
        function (error) {
          ok(false, error);
          done();
        }
      );
  });

  it('remove from new field creates array key', done => {
    const obj = new TestObject();
    obj.remove('shouldBeArray', 'foo');
    obj
      .save()
      .then(() => {
        const query = new Parse.Query('TestObject');
        return query.get(obj.id);
      })
      .then(objAgain => {
        const arr = objAgain.get('shouldBeArray');
        ok(Array.isArray(arr), 'Should have created array key');
        ok(!arr || arr.length === 0, 'Should have an empty array.');
        done();
      });
  });

  it('increment with type conflict fails', done => {
    const obj = new TestObject();
    obj.set('astring', 'foo');
    obj
      .save()
      .then(() => {
        const obj2 = new TestObject();
        obj2.increment('astring');
        return obj2.save();
      })
      .then(
        () => {
          fail('Should not have saved.');
          done();
        },
        error => {
          expect(error.code).toEqual(111);
          done();
        }
      );
  });

  it('increment with empty field solidifies type', done => {
    const obj = new TestObject();
    obj.increment('aninc');
    obj
      .save()
      .then(() => {
        const obj2 = new TestObject();
        obj2.set('aninc', 'foo');
        return obj2.save();
      })
      .then(
        () => {
          fail('Should not have saved.');
          done();
        },
        error => {
          expect(error.code).toEqual(111);
          done();
        }
      );
  });

  it('increment update with type conflict fails', done => {
    const obj = new TestObject();
    obj.set('someString', 'foo');
    obj
      .save()
      .then(objAgain => {
        const obj2 = new TestObject();
        obj2.id = objAgain.id;
        obj2.increment('someString');
        return obj2.save();
      })
      .then(
        () => {
          fail('Should not have saved.');
          done();
        },
        error => {
          expect(error.code).toEqual(111);
          done();
        }
      );
  });

  it('dictionary fetched pointers do not lose data on fetch', done => {
    const parent = new Parse.Object('Parent');
    const dict = {};
    for (let i = 0; i < 5; i++) {
      const proc = iter => {
        const child = new Parse.Object('Child');
        child.set('name', 'testname' + i);
        dict[iter] = child;
      };
      proc(i);
    }
    parent.set('childDict', dict);
    parent
      .save()
      .then(() => {
        return parent.fetch();
      })
      .then(parentAgain => {
        const dictAgain = parentAgain.get('childDict');
        if (!dictAgain) {
          fail('Should have been a dictionary.');
          return done();
        }
        expect(typeof dictAgain).toEqual('object');
        expect(typeof dictAgain['0']).toEqual('object');
        expect(typeof dictAgain['1']).toEqual('object');
        expect(typeof dictAgain['2']).toEqual('object');
        expect(typeof dictAgain['3']).toEqual('object');
        expect(typeof dictAgain['4']).toEqual('object');
        done();
      });
  });

  it('should create nested keys with _', done => {
    const object = new Parse.Object('AnObject');
    object.set('foo', {
      _bar: '_',
      baz_bar: 1,
      __foo_bar: true,
      _0: 'underscore_zero',
      _more: {
        _nested: 'key',
      },
    });
    object
      .save()
      .then(res => {
        ok(res);
        return res.fetch();
      })
      .then(res => {
        const foo = res.get('foo');
        expect(foo['_bar']).toEqual('_');
        expect(foo['baz_bar']).toEqual(1);
        expect(foo['__foo_bar']).toBe(true);
        expect(foo['_0']).toEqual('underscore_zero');
        expect(foo['_more']['_nested']).toEqual('key');
        done();
      })
      .catch(err => {
        jfail(err);
        fail('should not fail');
        done();
      });
  });

  it('should have undefined includes when object is missing', done => {
    const obj1 = new Parse.Object('AnObject');
    const obj2 = new Parse.Object('AnObject');

    Parse.Object.saveAll([obj1, obj2])
      .then(() => {
        obj1.set('obj', obj2);
        // Save the pointer, delete the pointee
        return obj1.save().then(() => {
          return obj2.destroy();
        });
      })
      .then(() => {
        const query = new Parse.Query('AnObject');
        query.include('obj');
        return query.find();
      })
      .then(res => {
        expect(res.length).toBe(1);
        if (res[0]) {
          expect(res[0].get('obj')).toBe(undefined);
        }
        const query = new Parse.Query('AnObject');
        return query.find();
      })
      .then(res => {
        expect(res.length).toBe(1);
        if (res[0]) {
          expect(res[0].get('obj')).not.toBe(undefined);
          return res[0].get('obj').fetch();
        } else {
          done();
        }
      })
      .then(
        () => {
          fail('Should not fetch a deleted object');
        },
        err => {
          expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND);
          done();
        }
      );
  });

  it('should have undefined includes when object is missing on deeper path', done => {
    const obj1 = new Parse.Object('AnObject');
    const obj2 = new Parse.Object('AnObject');
    const obj3 = new Parse.Object('AnObject');
    Parse.Object.saveAll([obj1, obj2, obj3])
      .then(() => {
        obj1.set('obj', obj2);
        obj2.set('obj', obj3);
        // Save the pointer, delete the pointee
        return Parse.Object.saveAll([obj1, obj2]).then(() => {
          return obj3.destroy();
        });
      })
      .then(() => {
        const query = new Parse.Query('AnObject');
        query.include('obj.obj');
        return query.get(obj1.id);
      })
      .then(res => {
        expect(res.get('obj')).not.toBe(undefined);
        expect(res.get('obj').get('obj')).toBe(undefined);
        done();
      })
      .catch(err => {
        jfail(err);
        done();
      });
  });

  it('should handle includes on null arrays #2752', done => {
    const obj1 = new Parse.Object('AnObject');
    const obj2 = new Parse.Object('AnotherObject');
    const obj3 = new Parse.Object('NestedObject');
    obj3.set({
      foo: 'bar',
    });
    obj2.set({
      key: obj3,
    });

    Parse.Object.saveAll([obj1, obj2])
      .then(() => {
        obj1.set('objects', [null, null, obj2]);
        return obj1.save();
      })
      .then(() => {
        const query = new Parse.Query('AnObject');
        query.include('objects.key');
        return query.find();
      })
      .then(res => {
        const obj = res[0];
        expect(obj.get('objects')).not.toBe(undefined);
        const array = obj.get('objects');
        expect(Array.isArray(array)).toBe(true);
        expect(array[0]).toBe(null);
        expect(array[1]).toBe(null);
        expect(array[2].get('key').get('foo')).toEqual('bar');
        done();
      })
      .catch(err => {
        jfail(err);
        done();
      });
  });

  it('should handle select and include #2786', done => {
    const score = new Parse.Object('GameScore');
    const player = new Parse.Object('Player');
    score.set({
      score: 1234,
    });

    score
      .save()
      .then(() => {
        player.set('gameScore', score);
        player.set('other', 'value');
        return player.save();
      })
      .then(() => {
        const query = new Parse.Query('Player');
        query.include('gameScore');
        query.select('gameScore');
        return query.find();
      })
      .then(res => {
        const obj = res[0];
        const gameScore = obj.get('gameScore');
        const other = obj.get('other');
        expect(other).toBeUndefined();
        expect(gameScore).not.toBeUndefined();
        expect(gameScore.get('score')).toBe(1234);
        done();
      })
      .catch(err => {
        jfail(err);
        done();
      });
  });

  it('should include ACLs with select', done => {
    const score = new Parse.Object('GameScore');
    const player = new Parse.Object('Player');
    score.set({
      score: 1234,
    });
    const acl = new Parse.ACL();
    acl.setPublicReadAccess(true);
    acl.setPublicWriteAccess(false);

    score
      .save()
      .then(() => {
        player.set('gameScore', score);
        player.set('other', 'value');
        player.setACL(acl);
        return player.save();
      })
      .then(() => {
        const query = new Parse.Query('Player');
        query.include('gameScore');
        query.select('gameScore');
        return query.find();
      })
      .then(res => {
        const obj = res[0];
        const gameScore = obj.get('gameScore');
        const other = obj.get('other');
        expect(other).toBeUndefined();
        expect(gameScore).not.toBeUndefined();
        expect(gameScore.get('score')).toBe(1234);
        expect(obj.getACL().getPublicReadAccess()).toBe(true);
        expect(obj.getACL().getPublicWriteAccess()).toBe(false);
      })
      .then(done)
      .catch(done.fail);
  });

  it('Update object field should store exactly same sent object', async done => {
    let object = new TestObject();

    // Set initial data
    object.set('jsonData', { a: 'b' });
    object = await object.save();
    equal(object.get('jsonData'), { a: 'b' });

    // Set empty JSON
    object.set('jsonData', {});
    object = await object.save();
    equal(object.get('jsonData'), {});

    // Set new JSON data
    object.unset('jsonData');
    object.set('jsonData', { c: 'd' });
    object = await object.save();
    equal(object.get('jsonData'), { c: 'd' });

    // Fetch object from server
    object = await object.fetch();
    equal(object.get('jsonData'), { c: 'd' });

    done();
  });

  it('isNew in cloud code', async () => {
    Parse.Cloud.beforeSave('CloudCodeIsNew', req => {
      expect(req.object.isNew()).toBeTruthy();
      expect(req.object.id).toBeUndefined();
    });

    Parse.Cloud.afterSave('CloudCodeIsNew', req => {
      expect(req.object.isNew()).toBeFalsy();
      expect(req.object.id).toBeDefined();
    });

    const object = new Parse.Object('CloudCodeIsNew');
    await object.save();
  });

  it('should not change the json field to array in afterSave', async () => {
    Parse.Cloud.beforeSave('failingJSONTestCase', req => {
      expect(req.object.get('jsonField')).toEqual({ '123': 'test' });
    });

    Parse.Cloud.afterSave('failingJSONTestCase', req => {
      expect(req.object.get('jsonField')).toEqual({ '123': 'test' });
    });

    const object = new Parse.Object('failingJSONTestCase');
    object.set('jsonField', { '123': 'test' });
    await object.save();
  });

  it('returns correct field values', async () => {
    const values = [
      { field: 'string', value: 'string' },
      { field: 'number', value: 1 },
      { field: 'boolean', value: true },
      { field: 'array', value: [0, 1, 2] },
      { field: 'array', value: [1, 2, 3] },
      { field: 'array', value: [{ '0': 'a' }, 2, 3] },
      { field: 'object', value: { key: 'value' } },
      { field: 'object', value: { key1: 'value1', key2: 'value2' } },
      { field: 'object', value: { key1: 1, key2: 2 } },
      { field: 'object', value: { '1x1': 1 } },
      { field: 'object', value: { '1x1': 1, '2': 2 } },
      { field: 'object', value: { '0': 0 } },
      { field: 'object', value: { '1': 1 } },
      { field: 'object', value: { '0': { '0': 'a', '1': 'b' } } },
      { field: 'date', value: new Date() },
      {
        field: 'file',
        value: Parse.File.fromJSON({
          __type: 'File',
          name: 'name',
          url: 'http://localhost:8378/1/files/test/name',
        }),
      },
      { field: 'geoPoint', value: new Parse.GeoPoint(40, -30) },
      { field: 'bytes', value: { __type: 'Bytes', base64: 'ZnJveW8=' } },
    ];
    for (const value of values) {
      const object = new TestObject();
      object.set(value.field, value.value);
      await object.save();
      const query = new Parse.Query(TestObject);
      const objectAgain = await query.get(object.id);
      expect(objectAgain.get(value.field)).toEqual(value.value);
    }
  });
});
