'use strict';

const Config = require('../lib/Config');
const SchemaController = require('../lib/Controllers/SchemaController');
const dd = require('deep-diff');

let config;

const hasAllPODobject = () => {
  const obj = new Parse.Object('HasAllPOD');
  obj.set('aNumber', 5);
  obj.set('aString', 'string');
  obj.set('aBool', true);
  obj.set('aDate', new Date());
  obj.set('aObject', { k1: 'value', k2: true, k3: 5 });
  obj.set('aArray', ['contents', true, 5]);
  obj.set('aGeoPoint', new Parse.GeoPoint({ latitude: 0, longitude: 0 }));
  obj.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' }));
  return obj;
};

describe('SchemaController', () => {
  beforeEach(() => {
    config = Config.get('test');
  });

  it('can validate one object', done => {
    config.database
      .loadSchema()
      .then(schema => {
        return schema.validateObject('TestObject', { a: 1, b: 'yo', c: false });
      })
      .then(
        () => {
          done();
        },
        error => {
          jfail(error);
          done();
        }
      );
  });

  it('can validate one object with dot notation', done => {
    config.database
      .loadSchema()
      .then(schema => {
        return schema.validateObject('TestObjectWithSubDoc', {
          x: false,
          y: 'YY',
          z: 1,
          'aObject.k1': 'newValue',
        });
      })
      .then(
        () => {
          done();
        },
        error => {
          jfail(error);
          done();
        }
      );
  });

  it('can validate two objects in a row', done => {
    config.database
      .loadSchema()
      .then(schema => {
        return schema.validateObject('Foo', { x: true, y: 'yyy', z: 0 });
      })
      .then(schema => {
        return schema.validateObject('Foo', { x: false, y: 'YY', z: 1 });
      })
      .then(() => {
        done();
      });
  });

  it('can validate Relation object', done => {
    config.database
      .loadSchema()
      .then(schema => {
        return schema.validateObject('Stuff', {
          aRelation: { __type: 'Relation', className: 'Stuff' },
        });
      })
      .then(schema => {
        return schema
          .validateObject('Stuff', {
            aRelation: { __type: 'Pointer', className: 'Stuff' },
          })
          .then(
            () => {
              done.fail('expected invalidity');
            },
            () => done()
          );
      }, done.fail);
  });

  it('rejects inconsistent types', done => {
    config.database
      .loadSchema()
      .then(schema => {
        return schema.validateObject('Stuff', { bacon: 7 });
      })
      .then(schema => {
        return schema.validateObject('Stuff', { bacon: 'z' });
      })
      .then(
        () => {
          fail('expected invalidity');
          done();
        },
        () => done()
      );
  });

  it('updates when new fields are added', done => {
    config.database
      .loadSchema()
      .then(schema => {
        return schema.validateObject('Stuff', { bacon: 7 });
      })
      .then(schema => {
        return schema.validateObject('Stuff', { sausage: 8 });
      })
      .then(schema => {
        return schema.validateObject('Stuff', { sausage: 'ate' });
      })
      .then(
        () => {
          fail('expected invalidity');
          done();
        },
        () => done()
      );
  });

  it('class-level permissions test find', done => {
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          find: {},
        });
      })
      .then(() => {
        const query = new Parse.Query('Stuff');
        return query.find();
      })
      .then(
        () => {
          fail('Class permissions should have rejected this query.');
          done();
        },
        () => {
          done();
        }
      );
  });

  it('class-level permissions test user', done => {
    let user;
    createTestUser()
      .then(u => {
        user = u;
        return config.database.loadSchema();
      })
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        const find = {};
        find[user.id] = true;
        return schema.setPermissions('Stuff', {
          find: find,
        });
      })
      .then(() => {
        const query = new Parse.Query('Stuff');
        return query.find();
      })
      .then(
        () => {
          done();
        },
        () => {
          fail('Class permissions should have allowed this query.');
          done();
        }
      );
  });

  it('class-level permissions test get', done => {
    let obj;
    createTestUser().then(user => {
      return (
        config.database
          .loadSchema()
          // Create a valid class
          .then(schema => schema.validateObject('Stuff', { foo: 'bar' }))
          .then(schema => {
            const find = {};
            const get = {};
            get[user.id] = true;
            return schema.setPermissions('Stuff', {
              create: { '*': true },
              find: find,
              get: get,
            });
          })
          .then(() => {
            obj = new Parse.Object('Stuff');
            obj.set('foo', 'bar');
            return obj.save();
          })
          .then(o => {
            obj = o;
            const query = new Parse.Query('Stuff');
            return query.find();
          })
          .then(
            () => {
              fail('Class permissions should have rejected this query.');
              done();
            },
            () => {
              const query = new Parse.Query('Stuff');
              return query.get(obj.id).then(
                () => {
                  done();
                },
                () => {
                  fail('Class permissions should have allowed this get query');
                  done();
                }
              );
            }
          )
      );
    });
  });

  it('class-level permissions test count', done => {
    let obj;
    return (
      config.database
        .loadSchema()
        // Create a valid class
        .then(schema => schema.validateObject('Stuff', { foo: 'bar' }))
        .then(schema => {
          const count = {};
          return schema.setPermissions('Stuff', {
            create: { '*': true },
            find: { '*': true },
            count: count,
          });
        })
        .then(() => {
          obj = new Parse.Object('Stuff');
          obj.set('foo', 'bar');
          return obj.save();
        })
        .then(o => {
          obj = o;
          const query = new Parse.Query('Stuff');
          return query.find();
        })
        .then(results => {
          expect(results.length).toBe(1);
          const query = new Parse.Query('Stuff');
          return query.count();
        })
        .then(
          () => {
            fail('Class permissions should have rejected this query.');
          },
          err => {
            expect(err.message).toEqual('Permission denied for action count on class Stuff.');
            done();
          }
        )
    );
  });

  it('can add classes without needing an object', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 'String' },
        })
      )
      .then(actualSchema => {
        const expectedSchema = {
          className: 'NewClass',
          fields: {
            objectId: { type: 'String' },
            updatedAt: { type: 'Date' },
            createdAt: { type: 'Date' },
            ACL: { type: 'ACL' },
            foo: { type: 'String' },
          },
          classLevelPermissions: {
            find: { '*': true },
            get: { '*': true },
            count: { '*': true },
            create: { '*': true },
            update: { '*': true },
            delete: { '*': true },
            addField: { '*': true },
            protectedFields: { '*': [] },
          },
        };
        expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
        done();
      })
      .catch(error => {
        fail('Error creating class: ' + JSON.stringify(error));
      });
  });

  it('can update classes without needing an object', done => {
    const levelPermissions = {
      find: { '*': true },
      get: { '*': true },
      count: { '*': true },
      create: { '*': true },
      update: { '*': true },
      delete: { '*': true },
      addField: { '*': true },
      protectedFields: { '*': [] },
    };
    config.database.loadSchema().then(schema => {
      schema
        .validateObject('NewClass', { foo: 2 })
        .then(() => schema.reloadData())
        .then(() =>
          schema.updateClass(
            'NewClass',
            {
              fooOne: { type: 'Number' },
              fooTwo: { type: 'Array' },
              fooThree: { type: 'Date' },
              fooFour: { type: 'Object' },
              fooFive: { type: 'Relation', targetClass: '_User' },
              fooSix: { type: 'String' },
              fooSeven: { type: 'Object' },
              fooEight: { type: 'String' },
              fooNine: { type: 'String' },
              fooTeen: { type: 'Number' },
              fooEleven: { type: 'String' },
              fooTwelve: { type: 'String' },
              fooThirteen: { type: 'String' },
              fooFourteen: { type: 'String' },
              fooFifteen: { type: 'String' },
              fooSixteen: { type: 'String' },
              fooEighteen: { type: 'String' },
              fooNineteen: { type: 'String' },
            },
            levelPermissions,
            {},
            config.database
          )
        )
        .then(actualSchema => {
          const expectedSchema = {
            className: 'NewClass',
            fields: {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              ACL: { type: 'ACL' },
              foo: { type: 'Number' },
              fooOne: { type: 'Number' },
              fooTwo: { type: 'Array' },
              fooThree: { type: 'Date' },
              fooFour: { type: 'Object' },
              fooFive: { type: 'Relation', targetClass: '_User' },
              fooSix: { type: 'String' },
              fooSeven: { type: 'Object' },
              fooEight: { type: 'String' },
              fooNine: { type: 'String' },
              fooTeen: { type: 'Number' },
              fooEleven: { type: 'String' },
              fooTwelve: { type: 'String' },
              fooThirteen: { type: 'String' },
              fooFourteen: { type: 'String' },
              fooFifteen: { type: 'String' },
              fooSixteen: { type: 'String' },
              fooEighteen: { type: 'String' },
              fooNineteen: { type: 'String' },
            },
            classLevelPermissions: { ...levelPermissions },
            indexes: {
              _id_: { _id: 1 },
            },
          };

          expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
          done();
        })
        .catch(error => {
          console.trace(error);
          done();
          fail('Error creating class: ' + JSON.stringify(error));
        });
    });
  });

  it('can update class level permission', done => {
    const newLevelPermissions = {
      find: {},
      get: { '*': true },
      count: {},
      create: { '*': true },
      update: {},
      delete: { '*': true },
      addField: {},
      protectedFields: { '*': [] },
    };
    config.database.loadSchema().then(schema => {
      schema
        .validateObject('NewClass', { foo: 2 })
        .then(() => schema.reloadData())
        .then(() => schema.updateClass('NewClass', {}, newLevelPermissions, {}, config.database))
        .then(actualSchema => {
          expect(dd(actualSchema.classLevelPermissions, newLevelPermissions)).toEqual(undefined);
          done();
        })
        .catch(error => {
          console.trace(error);
          done();
          fail('Error creating class: ' + JSON.stringify(error));
        });
    });
  });

  it('will fail to create a class if that class was already created by an object', done => {
    config.database.loadSchema().then(schema => {
      schema
        .validateObject('NewClass', { foo: 7 })
        .then(() => schema.reloadData())
        .then(() =>
          schema.addClassIfNotExists('NewClass', {
            foo: { type: 'String' },
          })
        )
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
          expect(error.message).toEqual('Class NewClass already exists.');
          done();
        });
    });
  });

  it('will resolve class creation races appropriately', done => {
    // If two callers race to create the same schema, the response to the
    // race loser should be the same as if they hadn't been racing.
    config.database.loadSchema().then(schema => {
      const p1 = schema
        .addClassIfNotExists('NewClass', {
          foo: { type: 'String' },
        })
        .then(validateSchema)
        .catch(validateError);
      const p2 = schema
        .addClassIfNotExists('NewClass', {
          foo: { type: 'String' },
        })
        .then(validateSchema)
        .catch(validateError);
      let schemaValidated = false;
      function validateSchema(actualSchema) {
        const expectedSchema = {
          className: 'NewClass',
          fields: {
            objectId: { type: 'String' },
            updatedAt: { type: 'Date' },
            createdAt: { type: 'Date' },
            ACL: { type: 'ACL' },
            foo: { type: 'String' },
          },
          classLevelPermissions: {
            find: { '*': true },
            get: { '*': true },
            count: { '*': true },
            create: { '*': true },
            update: { '*': true },
            delete: { '*': true },
            addField: { '*': true },
            protectedFields: { '*': [] },
          },
        };
        expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
        schemaValidated = true;
      }
      let errorValidated = false;
      function validateError(error) {
        expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
        expect(error.message).toEqual('Class NewClass already exists.');
        errorValidated = true;
      }
      Promise.all([p1, p2]).then(() => {
        expect(schemaValidated).toEqual(true);
        expect(errorValidated).toEqual(true);
        done();
      });
    });
  });

  it('refuses to create classes with invalid names', done => {
    config.database.loadSchema().then(schema => {
      schema.addClassIfNotExists('_InvalidName', { foo: { type: 'String' } }).catch(error => {
        expect(error.message).toEqual(
          'Invalid classname: _InvalidName, classnames can only have alphanumeric characters and _, and must start with an alpha character '
        );
        done();
      });
    });
  });

  it('refuses to add fields with invalid names', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          '0InvalidName': { type: 'String' },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME);
        expect(error.message).toEqual('invalid field name: 0InvalidName');
        done();
      });
  });

  it('refuses to explicitly create the default fields for custom classes', done => {
    config.database
      .loadSchema()
      .then(schema => schema.addClassIfNotExists('NewClass', { objectId: { type: 'String' } }))
      .catch(error => {
        expect(error.code).toEqual(136);
        expect(error.message).toEqual('field objectId cannot be added');
        done();
      });
  });

  it('refuses to explicitly create the default fields for non-custom classes', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('_Installation', {
          localeIdentifier: { type: 'String' },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(136);
        expect(error.message).toEqual('field localeIdentifier cannot be added');
        done();
      });
  });

  it('refuses to add fields with invalid types', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 7 },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_JSON);
        expect(error.message).toEqual('invalid JSON');
        done();
      });
  });

  it('refuses to add fields with invalid pointer types', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 'Pointer' },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(135);
        expect(error.message).toEqual('type Pointer needs a class name');
        done();
      });
  });

  it('refuses to add fields with invalid pointer target', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 'Pointer', targetClass: 7 },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_JSON);
        expect(error.message).toEqual('invalid JSON');
        done();
      });
  });

  it('refuses to add fields with invalid Relation type', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 'Relation', uselessKey: 7 },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(135);
        expect(error.message).toEqual('type Relation needs a class name');
        done();
      });
  });

  it('refuses to add fields with invalid relation target', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 'Relation', targetClass: 7 },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_JSON);
        expect(error.message).toEqual('invalid JSON');
        done();
      });
  });

  it('refuses to add fields with uncreatable pointer target class', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 'Pointer', targetClass: 'not a valid class name' },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
        expect(error.message).toEqual(
          'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character '
        );
        done();
      });
  });

  it('refuses to add fields with uncreatable relation target class', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 'Relation', targetClass: 'not a valid class name' },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
        expect(error.message).toEqual(
          'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character '
        );
        done();
      });
  });

  it('refuses to add fields with unknown types', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          foo: { type: 'Unknown' },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
        expect(error.message).toEqual('invalid field type: Unknown');
        done();
      });
  });

  it('refuses to add CLP with incorrect find', done => {
    const levelPermissions = {
      find: { '*': false },
      get: { '*': true },
      create: { '*': true },
      update: { '*': true },
      delete: { '*': true },
      addField: { '*': true },
      protectedFields: { '*': ['email'] },
    };
    config.database.loadSchema().then(schema => {
      schema
        .validateObject('NewClass', {})
        .then(() => schema.reloadData())
        .then(() => schema.updateClass('NewClass', {}, levelPermissions, {}, config.database))
        .then(done.fail)
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_JSON);
          done();
        });
    });
  });

  it('refuses to add CLP when incorrectly sending a string to protectedFields object value instead of an array', done => {
    const levelPermissions = {
      find: { '*': true },
      get: { '*': true },
      create: { '*': true },
      update: { '*': true },
      delete: { '*': true },
      addField: { '*': true },
      protectedFields: { '*': 'email' },
    };
    config.database.loadSchema().then(schema => {
      schema
        .validateObject('NewClass', {})
        .then(() => schema.reloadData())
        .then(() => schema.updateClass('NewClass', {}, levelPermissions, {}, config.database))
        .then(done.fail)
        .catch(error => {
          expect(error.code).toEqual(Parse.Error.INVALID_JSON);
          done();
        });
    });
  });

  it('will create classes', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          aNumber: { type: 'Number' },
          aString: { type: 'String' },
          aBool: { type: 'Boolean' },
          aDate: { type: 'Date' },
          aObject: { type: 'Object' },
          aArray: { type: 'Array' },
          aGeoPoint: { type: 'GeoPoint' },
          aFile: { type: 'File' },
          aPointer: {
            type: 'Pointer',
            targetClass: 'ThisClassDoesNotExistYet',
          },
          aRelation: { type: 'Relation', targetClass: 'NewClass' },
          aBytes: { type: 'Bytes' },
          aPolygon: { type: 'Polygon' },
        })
      )
      .then(actualSchema => {
        const expectedSchema = {
          className: 'NewClass',
          fields: {
            objectId: { type: 'String' },
            updatedAt: { type: 'Date' },
            createdAt: { type: 'Date' },
            ACL: { type: 'ACL' },
            aString: { type: 'String' },
            aNumber: { type: 'Number' },
            aBool: { type: 'Boolean' },
            aDate: { type: 'Date' },
            aObject: { type: 'Object' },
            aArray: { type: 'Array' },
            aGeoPoint: { type: 'GeoPoint' },
            aFile: { type: 'File' },
            aPointer: {
              type: 'Pointer',
              targetClass: 'ThisClassDoesNotExistYet',
            },
            aRelation: { type: 'Relation', targetClass: 'NewClass' },
            aBytes: { type: 'Bytes' },
            aPolygon: { type: 'Polygon' },
          },
          classLevelPermissions: {
            find: { '*': true },
            get: { '*': true },
            count: { '*': true },
            create: { '*': true },
            update: { '*': true },
            delete: { '*': true },
            addField: { '*': true },
            protectedFields: { '*': [] },
          },
        };
        expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
        done();
      });
  });

  it('creates the default fields for non-custom classes', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('_Installation', {
          foo: { type: 'Number' },
        })
      )
      .then(actualSchema => {
        const expectedSchema = {
          className: '_Installation',
          fields: {
            objectId: { type: 'String' },
            updatedAt: { type: 'Date' },
            createdAt: { type: 'Date' },
            ACL: { type: 'ACL' },
            foo: { type: 'Number' },
            installationId: { type: 'String' },
            deviceToken: { type: 'String' },
            channels: { type: 'Array' },
            deviceType: { type: 'String' },
            pushType: { type: 'String' },
            GCMSenderId: { type: 'String' },
            timeZone: { type: 'String' },
            localeIdentifier: { type: 'String' },
            badge: { type: 'Number' },
            appVersion: { type: 'String' },
            appName: { type: 'String' },
            appIdentifier: { type: 'String' },
            parseVersion: { type: 'String' },
          },
          classLevelPermissions: {
            find: { '*': true },
            get: { '*': true },
            count: { '*': true },
            create: { '*': true },
            update: { '*': true },
            delete: { '*': true },
            addField: { '*': true },
            protectedFields: { '*': [] },
          },
        };
        expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
        done();
      });
  });

  it('creates non-custom classes which include relation field', async done => {
    await reconfigureServer();
    config.database
      .loadSchema()
      //as `_Role` is always created by default, we only get it here
      .then(schema => schema.getOneSchema('_Role'))
      .then(actualSchema => {
        const expectedSchema = {
          className: '_Role',
          fields: {
            objectId: { type: 'String' },
            updatedAt: { type: 'Date' },
            createdAt: { type: 'Date' },
            ACL: { type: 'ACL' },
            name: { type: 'String' },
            users: { type: 'Relation', targetClass: '_User' },
            roles: { type: 'Relation', targetClass: '_Role' },
          },
          classLevelPermissions: {
            find: { '*': true },
            get: { '*': true },
            count: { '*': true },
            create: { '*': true },
            update: { '*': true },
            delete: { '*': true },
            addField: { '*': true },
            protectedFields: { '*': [] },
          },
        };
        expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
        done();
      });
  });

  it('creates non-custom classes which include pointer field', done => {
    config.database
      .loadSchema()
      .then(schema => schema.addClassIfNotExists('_Session', {}))
      .then(actualSchema => {
        const expectedSchema = {
          className: '_Session',
          fields: {
            objectId: { type: 'String' },
            updatedAt: { type: 'Date' },
            createdAt: { type: 'Date' },
            user: { type: 'Pointer', targetClass: '_User' },
            installationId: { type: 'String' },
            sessionToken: { type: 'String' },
            expiresAt: { type: 'Date' },
            createdWith: { type: 'Object' },
            ACL: { type: 'ACL' },
          },
          classLevelPermissions: {
            find: { '*': true },
            get: { '*': true },
            count: { '*': true },
            create: { '*': true },
            update: { '*': true },
            delete: { '*': true },
            addField: { '*': true },
            protectedFields: { '*': [] },
          },
        };
        expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
        done();
      });
  });

  it('refuses to create two geopoints', done => {
    config.database
      .loadSchema()
      .then(schema =>
        schema.addClassIfNotExists('NewClass', {
          geo1: { type: 'GeoPoint' },
          geo2: { type: 'GeoPoint' },
        })
      )
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);
        expect(error.message).toEqual(
          'currently, only one GeoPoint field may exist in an object. Adding geo2 when geo1 already exists.'
        );
        done();
      });
  });

  it('can check if a class exists', done => {
    config.database
      .loadSchema()
      .then(schema => {
        return schema
          .addClassIfNotExists('NewClass', {})
          .then(() => schema.reloadData({ clearCache: true }))
          .then(() => {
            schema
              .hasClass('NewClass')
              .then(hasClass => {
                expect(hasClass).toEqual(true);
                done();
              })
              .catch(fail);

            schema
              .hasClass('NonexistantClass')
              .then(hasClass => {
                expect(hasClass).toEqual(false);
                done();
              })
              .catch(fail);
          })
          .catch(error => {
            fail("Couldn't create class");
            jfail(error);
          });
      })
      .catch(() => fail("Couldn't load schema"));
  });

  it('refuses to delete fields from invalid class names', done => {
    config.database
      .loadSchema()
      .then(schema => schema.deleteField('fieldName', 'invalid class name'))
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
        done();
      });
  });

  it('refuses to delete invalid fields', done => {
    config.database
      .loadSchema()
      .then(schema => schema.deleteField('invalid field name', 'ValidClassName'))
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME);
        done();
      });
  });

  it('refuses to delete the default fields', done => {
    config.database
      .loadSchema()
      .then(schema => schema.deleteField('installationId', '_Installation'))
      .catch(error => {
        expect(error.code).toEqual(136);
        expect(error.message).toEqual('field installationId cannot be changed');
        done();
      });
  });

  it('refuses to delete fields from nonexistant classes', done => {
    config.database
      .loadSchema()
      .then(schema => schema.deleteField('field', 'NoClass'))
      .catch(error => {
        expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME);
        expect(error.message).toEqual('Class NoClass does not exist.');
        done();
      });
  });

  it('refuses to delete fields that dont exist', done => {
    hasAllPODobject()
      .save()
      .then(() => config.database.loadSchema())
      .then(schema => schema.deleteField('missingField', 'HasAllPOD'))
      .catch(error => {
        expect(error.code).toEqual(255);
        expect(error.message).toEqual('Field missingField does not exist, cannot delete.');
        done();
      });
  });

  it('drops related collection when deleting relation field', done => {
    const obj1 = hasAllPODobject();
    obj1
      .save()
      .then(savedObj1 => {
        const obj2 = new Parse.Object('HasPointersAndRelations');
        obj2.set('aPointer', savedObj1);
        const relation = obj2.relation('aRelation');
        relation.add(obj1);
        return obj2.save();
      })
      .then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations'))
      .then(exists => {
        if (!exists) {
          fail('Relation collection ' + 'should exist after save.');
        }
      })
      .then(() => config.database.loadSchema())
      .then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database))
      .then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations'))
      .then(
        exists => {
          if (exists) {
            fail('Relation collection should not exist after deleting relation field.');
          }
          done();
        },
        error => {
          jfail(error);
          done();
        }
      );
  });

  it('can delete relation field when related _Join collection not exist', done => {
    config.database.loadSchema().then(schema => {
      schema
        .addClassIfNotExists('NewClass', {
          relationField: { type: 'Relation', targetClass: '_User' },
        })
        .then(actualSchema => {
          const expectedSchema = {
            className: 'NewClass',
            fields: {
              objectId: { type: 'String' },
              updatedAt: { type: 'Date' },
              createdAt: { type: 'Date' },
              ACL: { type: 'ACL' },
              relationField: { type: 'Relation', targetClass: '_User' },
            },
            classLevelPermissions: {
              find: { '*': true },
              get: { '*': true },
              count: { '*': true },
              create: { '*': true },
              update: { '*': true },
              delete: { '*': true },
              addField: { '*': true },
              protectedFields: { '*': [] },
            },
          };
          expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
        })
        .then(() => config.database.collectionExists('_Join:relationField:NewClass'))
        .then(exist => {
          on_db(
            'postgres',
            () => {
              // We create the table when creating the column
              expect(exist).toEqual(true);
            },
            () => {
              expect(exist).toEqual(false);
            }
          );
        })
        .then(() => schema.deleteField('relationField', 'NewClass', config.database))
        .then(() => schema.reloadData())
        .then(() => {
          const expectedSchema = {
            objectId: { type: 'String' },
            updatedAt: { type: 'Date' },
            createdAt: { type: 'Date' },
            ACL: { type: 'ACL' },
          };
          expect(dd(schema.schemaData.NewClass.fields, expectedSchema)).toEqual(undefined);
        })
        .then(done)
        .catch(done.fail);
    });
  });

  it('can delete string fields and resave as number field', done => {
    Parse.Object.disableSingleInstance();
    const obj1 = hasAllPODobject();
    const obj2 = hasAllPODobject();
    Parse.Object.saveAll([obj1, obj2])
      .then(() => config.database.loadSchema())
      .then(schema => schema.deleteField('aString', 'HasAllPOD', config.database))
      .then(() => new Parse.Query('HasAllPOD').get(obj1.id))
      .then(obj1Reloaded => {
        expect(obj1Reloaded.get('aString')).toEqual(undefined);
        obj1Reloaded.set('aString', ['not a string', 'this time']);
        obj1Reloaded
          .save()
          .then(obj1reloadedAgain => {
            expect(obj1reloadedAgain.get('aString')).toEqual(['not a string', 'this time']);
            return new Parse.Query('HasAllPOD').get(obj2.id);
          })
          .then(obj2reloaded => {
            expect(obj2reloaded.get('aString')).toEqual(undefined);
            done();
          });
      })
      .catch(error => {
        jfail(error);
        done();
      });
  });

  it('can delete pointer fields and resave as string', done => {
    Parse.Object.disableSingleInstance();
    const obj1 = new Parse.Object('NewClass');
    obj1
      .save()
      .then(() => {
        obj1.set('aPointer', obj1);
        return obj1.save();
      })
      .then(obj1 => {
        expect(obj1.get('aPointer').id).toEqual(obj1.id);
      })
      .then(() => config.database.loadSchema())
      .then(schema => schema.deleteField('aPointer', 'NewClass', config.database))
      .then(() => new Parse.Query('NewClass').get(obj1.id))
      .then(obj1 => {
        expect(obj1.get('aPointer')).toEqual(undefined);
        obj1.set('aPointer', 'Now a string');
        return obj1.save();
      })
      .then(obj1 => {
        expect(obj1.get('aPointer')).toEqual('Now a string');
        done();
      });
  });

  it('can merge schemas', done => {
    expect(
      SchemaController.buildMergedSchemaObject(
        {
          _id: 'SomeClass',
          someType: { type: 'Number' },
        },
        {
          newType: { type: 'Number' },
        }
      )
    ).toEqual({
      someType: { type: 'Number' },
      newType: { type: 'Number' },
    });
    done();
  });

  it('can merge deletions', done => {
    expect(
      SchemaController.buildMergedSchemaObject(
        {
          _id: 'SomeClass',
          someType: { type: 'Number' },
          outDatedType: { type: 'String' },
        },
        {
          newType: { type: 'GeoPoint' },
          outDatedType: { __op: 'Delete' },
        }
      )
    ).toEqual({
      someType: { type: 'Number' },
      newType: { type: 'GeoPoint' },
    });
    done();
  });

  it('ignore default field when merge with system class', done => {
    expect(
      SchemaController.buildMergedSchemaObject(
        {
          _id: '_User',
          username: { type: 'String' },
          password: { type: 'String' },
          email: { type: 'String' },
          emailVerified: { type: 'Boolean' },
        },
        {
          emailVerified: { type: 'String' },
          customField: { type: 'String' },
        }
      )
    ).toEqual({
      customField: { type: 'String' },
    });
    done();
  });

  it('yields a proper schema mismatch error (#2661)', done => {
    const anObject = new Parse.Object('AnObject');
    const anotherObject = new Parse.Object('AnotherObject');
    const someObject = new Parse.Object('SomeObject');
    Parse.Object.saveAll([anObject, anotherObject, someObject])
      .then(() => {
        anObject.set('pointer', anotherObject);
        return anObject.save();
      })
      .then(() => {
        anObject.set('pointer', someObject);
        return anObject.save();
      })
      .then(
        () => {
          fail('shoud not save correctly');
          done();
        },
        err => {
          expect(err instanceof Parse.Error).toBeTruthy();
          expect(err.message).toEqual(
            'schema mismatch for AnObject.pointer; expected Pointer<AnotherObject> but got Pointer<SomeObject>'
          );
          done();
        }
      );
  });

  it('yields a proper schema mismatch error bis (#2661)', done => {
    const anObject = new Parse.Object('AnObject');
    const someObject = new Parse.Object('SomeObject');
    Parse.Object.saveAll([anObject, someObject])
      .then(() => {
        anObject.set('number', 1);
        return anObject.save();
      })
      .then(() => {
        anObject.set('number', someObject);
        return anObject.save();
      })
      .then(
        () => {
          fail('shoud not save correctly');
          done();
        },
        err => {
          expect(err instanceof Parse.Error).toBeTruthy();
          expect(err.message).toEqual(
            'schema mismatch for AnObject.number; expected Number but got Pointer<SomeObject>'
          );
          done();
        }
      );
  });

  it('yields a proper schema mismatch error ter (#2661)', done => {
    const anObject = new Parse.Object('AnObject');
    const someObject = new Parse.Object('SomeObject');
    Parse.Object.saveAll([anObject, someObject])
      .then(() => {
        anObject.set('pointer', someObject);
        return anObject.save();
      })
      .then(() => {
        anObject.set('pointer', 1);
        return anObject.save();
      })
      .then(
        () => {
          fail('shoud not save correctly');
          done();
        },
        err => {
          expect(err instanceof Parse.Error).toBeTruthy();
          expect(err.message).toEqual(
            'schema mismatch for AnObject.pointer; expected Pointer<SomeObject> but got Number'
          );
          done();
        }
      );
  });

  it('properly handles volatile _Schemas', async done => {
    await reconfigureServer();
    function validateSchemaStructure(schema) {
      expect(Object.prototype.hasOwnProperty.call(schema, 'className')).toBe(true);
      expect(Object.prototype.hasOwnProperty.call(schema, 'fields')).toBe(true);
      expect(Object.prototype.hasOwnProperty.call(schema, 'classLevelPermissions')).toBe(true);
    }
    function validateSchemaDataStructure(schemaData) {
      Object.keys(schemaData).forEach(className => {
        const schema = schemaData[className];
        // Hooks has className...
        if (className != '_Hooks') {
          expect(Object.prototype.hasOwnProperty.call(schema, 'className')).toBe(false);
        }
        expect(Object.prototype.hasOwnProperty.call(schema, 'fields')).toBe(false);
        expect(Object.prototype.hasOwnProperty.call(schema, 'classLevelPermissions')).toBe(false);
      });
    }
    let schema;
    config.database
      .loadSchema()
      .then(s => {
        schema = s;
        return schema.getOneSchema('_User', false);
      })
      .then(userSchema => {
        validateSchemaStructure(userSchema);
        validateSchemaDataStructure(schema.schemaData);
        return schema.getOneSchema('_PushStatus', true);
      })
      .then(pushStatusSchema => {
        validateSchemaStructure(pushStatusSchema);
        validateSchemaDataStructure(schema.schemaData);
      })
      .then(done)
      .catch(done.fail);
  });

  it('should not throw on null field types', async () => {
    const schema = await config.database.loadSchema();
    const result = await schema.enforceFieldExists('NewClass', 'fieldName', null);
    expect(result).toBeUndefined();
  });

  it('ensureFields should throw when schema is not set', async () => {
    const schema = await config.database.loadSchema();
    try {
      schema.ensureFields([
        {
          className: 'NewClass',
          fieldName: 'fieldName',
          type: 'String',
        },
      ]);
    } catch (e) {
      expect(e.message).toBe('Could not add field fieldName');
    }
  });
});

describe('Class Level Permissions for requiredAuth', () => {
  beforeEach(() => {
    config = Config.get('test');
  });

  function createUser() {
    const user = new Parse.User();
    user.set('username', 'hello');
    user.set('password', 'world');
    return user.signUp(null);
  }

  it('required auth test find', done => {
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          find: {
            requiresAuthentication: true,
          },
        });
      })
      .then(() => {
        const query = new Parse.Query('Stuff');
        return query.find();
      })
      .then(
        () => {
          fail('Class permissions should have rejected this query.');
          done();
        },
        e => {
          expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
          done();
        }
      );
  });

  it('required auth test find authenticated', done => {
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          find: {
            requiresAuthentication: true,
          },
        });
      })
      .then(() => {
        return createUser();
      })
      .then(() => {
        const query = new Parse.Query('Stuff');
        return query.find();
      })
      .then(
        results => {
          expect(results.length).toEqual(0);
          done();
        },
        e => {
          console.error(e);
          fail('Should not have failed');
          done();
        }
      );
  });

  it('required auth should allow create authenticated', done => {
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          create: {
            requiresAuthentication: true,
          },
        });
      })
      .then(() => {
        return createUser();
      })
      .then(() => {
        const stuff = new Parse.Object('Stuff');
        stuff.set('foo', 'bar');
        return stuff.save();
      })
      .then(
        () => {
          done();
        },
        e => {
          console.error(e);
          fail('Should not have failed');
          done();
        }
      );
  });

  it('required auth should reject create when not authenticated', done => {
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          create: {
            requiresAuthentication: true,
          },
        });
      })
      .then(() => {
        const stuff = new Parse.Object('Stuff');
        stuff.set('foo', 'bar');
        return stuff.save();
      })
      .then(
        () => {
          fail('Class permissions should have rejected this query.');
          done();
        },
        e => {
          expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
          done();
        }
      );
  });

  it('required auth test create/get/update/delete authenticated', done => {
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          create: {
            requiresAuthentication: true,
          },
          get: {
            requiresAuthentication: true,
          },
          delete: {
            requiresAuthentication: true,
          },
          update: {
            requiresAuthentication: true,
          },
        });
      })
      .then(() => {
        return createUser();
      })
      .then(() => {
        const stuff = new Parse.Object('Stuff');
        stuff.set('foo', 'bar');
        return stuff.save().then(() => {
          const query = new Parse.Query('Stuff');
          return query.get(stuff.id);
        });
      })
      .then(gotStuff => {
        return gotStuff.save({ foo: 'baz' }).then(() => {
          return gotStuff.destroy();
        });
      })
      .then(
        () => {
          done();
        },
        e => {
          console.error(e);
          fail('Should not have failed');
          done();
        }
      );
  });

  it('required auth test get not authenticated', done => {
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          get: {
            requiresAuthentication: true,
          },
          create: {
            '*': true,
          },
        });
      })
      .then(() => {
        const stuff = new Parse.Object('Stuff');
        stuff.set('foo', 'bar');
        return stuff.save().then(() => {
          const query = new Parse.Query('Stuff');
          return query.get(stuff.id);
        });
      })
      .then(
        () => {
          fail('Should not succeed!');
          done();
        },
        e => {
          expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
          done();
        }
      );
  });

  it('required auth test find not authenticated', done => {
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          find: {
            requiresAuthentication: true,
          },
          create: {
            '*': true,
          },
          get: {
            '*': true,
          },
        });
      })
      .then(() => {
        const stuff = new Parse.Object('Stuff');
        stuff.set('foo', 'bar');
        return stuff.save().then(() => {
          const query = new Parse.Query('Stuff');
          return query.get(stuff.id);
        });
      })
      .then(result => {
        expect(result.get('foo')).toEqual('bar');
        const query = new Parse.Query('Stuff');
        return query.find();
      })
      .then(
        () => {
          fail('Should not succeed!');
          done();
        },
        e => {
          expect(e.message).toEqual('Permission denied, user needs to be authenticated.');
          done();
        }
      );
  });

  it('required auth test create/get/update/delete with roles (#3753)', done => {
    let user;
    config.database
      .loadSchema()
      .then(schema => {
        // Just to create a valid class
        return schema.validateObject('Stuff', { foo: 'bar' });
      })
      .then(schema => {
        return schema.setPermissions('Stuff', {
          find: {
            requiresAuthentication: true,
            'role:admin': true,
          },
          create: { 'role:admin': true },
          update: { 'role:admin': true },
          delete: { 'role:admin': true },
          get: {
            requiresAuthentication: true,
            'role:admin': true,
          },
        });
      })
      .then(() => {
        const stuff = new Parse.Object('Stuff');
        stuff.set('foo', 'bar');
        return stuff
          .save(null, { useMasterKey: true })
          .then(() => {
            const query = new Parse.Query('Stuff');
            return query
              .get(stuff.id)
              .then(
                () => {
                  done.fail('should not succeed');
                },
                () => {
                  return new Parse.Query('Stuff').find();
                }
              )
              .then(
                () => {
                  done.fail('should not succeed');
                },
                () => {
                  return Promise.resolve();
                }
              );
          })
          .then(() => {
            return Parse.User.signUp('user', 'password').then(signedUpUser => {
              user = signedUpUser;
              const query = new Parse.Query('Stuff');
              return query.get(stuff.id, {
                sessionToken: user.getSessionToken(),
              });
            });
          });
      })
      .then(result => {
        expect(result.get('foo')).toEqual('bar');
        const query = new Parse.Query('Stuff');
        return query.find({ sessionToken: user.getSessionToken() });
      })
      .then(
        results => {
          expect(results.length).toBe(1);
          done();
        },
        e => {
          console.error(e);
          done.fail(e);
        }
      );
  });
});
