import Parse from 'parse';
import ParseNode from 'parse/node';
import ParseRN from 'parse/react-native';

class GameScore extends Parse.Object {
  constructor() {
    super('GameScore');
  }
}

class Game extends Parse.Object {
  constructor() {
    super('Game');
  }
}

async function test_config() {
  await Parse.Config.save({ foo: 'bar' }, { foo: true });
  await Parse.Config.get({ useMasterKey: true });
}

async function test_object() {
  const game = new Game();
  game
    .save(null, {
      useMasterKey: true,
      sessionToken: 'sometoken',
      cascadeSave: false,
    })
    .then(result => result)
    .catch(error => error);

  // $ExpectType boolean
  game.isNew();
  // $ExpectType Pointer
  game.toPointer();
  // $ExpectType string
  game.toPointer().className;

  await game.fetch({});

  // Create a new instance of that class.
  const gameScore = new GameScore();

  // Expect that `createWithoutData` returns the types for the same classes (not generic ParseObjects)
  // $ExpectType Game
  Game.createWithoutData('someid');
  // $ExpectType GameScore
  GameScore.createWithoutData('scoreid');
  // $ExpectType ParseUser<Attributes>
  Parse.User.createWithoutData('someuser');

  gameScore.set('score', 1337);
  gameScore.set('playerName', 'Sean Plott');
  gameScore.set('cheatMode', false);

  // Setting attrs using object
  gameScore.set({
    level: '10',
    difficult: 15,
  });
  gameScore.get('score');
  gameScore.get('playerName');
  gameScore.get('cheatMode');
  gameScore.increment('score');
  gameScore.addUnique('skills', 'flying');
  gameScore.addUnique('skills', 'kungfu');
  gameScore.addAll('skills', ['kungfu']);
  gameScore.addAllUnique('skills', ['kungfu']);
  gameScore.remove('skills', 'flying');
  gameScore.removeAll('skills', ['kungFu']);
  game.set('gameScore', gameScore);
  // $ExpectType ParseObject<Attributes>
  Game.fromJSON(JSON.parse(JSON.stringify(game)), true);

  const object = new Parse.Object('TestObject');
  object.equals(gameScore);
  await object.fetchWithInclude(['key1', 'key2']);
}

function test_errors() {
  const error = new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'sdfds');
  // $ExpectType number
  error.code;
  // $ExpectType string
  error.message;
}

async function test_query() {
  const gameScore = new GameScore();

  const query = new Parse.Query(GameScore);
  query.equalTo('playerName', 'Dan Stemkoski');
  query.notEqualTo('playerName', 'Michael Yabuti');
  query.fullText('playerName', 'dan', {
    language: 'en',
    caseSensitive: false,
    diacriticSensitive: true,
  });
  query.greaterThan('playerAge', 18);
  await query.eachBatch(objs => {}, { batchSize: 10 });
  await query.each(score => {});
  query.hint('_id_');
  query.explain(true);
  query.limit(10);
  query.skip(10);

  // Sorts the results in ascending order by the score field
  query.ascending('score');

  // Sorts the results in descending order by the score field
  query.descending('score');

  // Restricts to wins < 50
  query.lessThan('wins', 50);

  // Restricts to wins <= 50
  query.lessThanOrEqualTo('wins', 50);

  // Restricts to wins > 50
  query.greaterThan('wins', 50);

  // Restricts to wins >= 50
  query.greaterThanOrEqualTo('wins', 50);

  query.containedBy('place', ['1', '2']);
  // Finds scores from any of Jonathan, Dario, or Shawn
  query.containedIn('playerName', ['Jonathan Walsh', 'Dario Wunsch', 'Shawn Simon']);

  // Finds scores from anyone who is neither Jonathan, Dario, nor Shawn
  query.notContainedIn('playerName', ['Jonathan Walsh', 'Dario Wunsch', 'Shawn Simon']);

  // Finds objects that have the score set
  query.exists('score');

  // Finds objects that don't have the score set
  query.doesNotExist('score');
  query.matchesKeyInQuery('hometown', 'city', query);
  query.doesNotMatchKeyInQuery('hometown', 'city', query);
  query.select('score', 'playerName');

  // Find objects where the array in arrayKey contains 2.
  query.equalTo('arrayKey', 2);

  // Find objects where the array in arrayKey contains all of the elements 2, 3, and 4.
  query.containsAll('arrayKey', [2, 3, 4]);
  query.containsAllStartingWith('arrayKey', ['2', '3', '4']);

  query.startsWith('name', "Big Daddy's");
  query.equalTo('score', gameScore);
  query.exists('score');
  query.include('score');
  query.include('score', 'team');
  query.include(['score.team']);
  query.include('*');
  query.includeAll();
  query.sortByTextScore();
  // Find objects that match the aggregation pipeline
  await query.aggregate({
    group: {
      objectId: '$name',
    },
  });

  await query.aggregate({
    count: 'total',
  });

  await query.aggregate({
    lookup: {
      from: 'Collection',
      foreignField: 'id',
      localField: 'id',
      as: 'result',
    },
  });
  await query.aggregate({
    lookup: {
      from: 'Target',
      let: { foo: 'bar', baz: 123 },
      pipeline: [],
      as: 'result',
    },
  });

  await query.aggregate({
    graphLookup: {
      from: 'Target',
      connectFromField: 'objectId',
      connectToField: 'newId',
      as: 'result',
    },
  });

  await query.aggregate({
    facet: {
      foo: [
        {
          count: 'total',
        },
      ],
      bar: [
        {
          group: {
            objectId: '$name',
          },
        },
      ],
    },
  });

  await query.aggregate({
    unwind: '$field',
  });

  await query.aggregate({
    unwind: {
      path: '$field',
      includeArrayIndex: 'newIndex',
      preserveNullAndEmptyArrays: true,
    },
  });

  // Find objects with distinct key
  await query.distinct('name');

  const testQuery = Parse.Query.or(query, query);

  // Test that query can do `or`/`and` joins while preserving class types
  // This was the behaviour with the old DefinitelyTyped typings.
  class MyClass extends Parse.Object<{ a: number }> {};
  const q1 = new Parse.Query(MyClass).equalTo('a', 2);
  const q2 = new Parse.Query(MyClass).equalTo('a', 3);
  // $ExpectType ParseQuery<MyClass>
  const orQ = Parse.Query.or(q1, q2);
  // $ExpectType ParseQuery<MyClass>
  const andQ = Parse.Query.and(q1, q2);
  // $ExpectType ParseQuery<MyClass>
  const norQ = Parse.Query.nor(q1, q2);
}

function test_query_exclude() {
  const gameScore = new GameScore();

  const query = new Parse.Query(GameScore);

  // Show all keys, except the specified key.
  query.exclude('place');

  const testQuery = Parse.Query.or(query, query);
}

async function test_query_promise() {
  // Test promise with a query
  const findQuery = new Parse.Query('Test');
  findQuery
    .find()
    .then(() => {
      // success
    })
    .catch(() => {
      // error
    });

  const getQuery = new Parse.Query('Test');
  try {
    await getQuery.get('objectId');
  } catch (error) {
    // noop
  }

  await getQuery.map((score, index) => score.increment('score', index));
  await getQuery.reduce((accum, score, index) => (accum += score.get('score')), 0);
  await getQuery.reduce((accum, score, index) => (accum += score.get('score')), 0, {
    batchSize: 200,
  });
  await getQuery.filter(scores => scores.get('score') > 0);
  await getQuery.filter(scores => scores.get('score') > 0, { batchSize: 10 });
}

async function test_live_query() {
  const subscription = await new Parse.Query('Test').subscribe();
  subscription.on('close', (object: Parse.Object) => {
    // $ExpectType ParseObject<Attributes>
    object;
  });
  subscription.on('create', (object: Parse.Object) => {
    // $ExpectType ParseObject<Attributes>
    object;
  });
  subscription.on('delete', (object: Parse.Object) => {
    // $ExpectType ParseObject<Attributes>
    object;
  });
  subscription.on('enter', (object: Parse.Object) => {
    // $ExpectType ParseObject<Attributes>
    object;
  });
  subscription.on('leave', (object: Parse.Object) => {
    // $ExpectType ParseObject<Attributes>
    object;
  });
  subscription.on('open', (object: Parse.Object) => {
    // $ExpectType ParseObject<Attributes>
    object;
  });
  subscription.on('update', (object: Parse.Object) => {
    // $ExpectType ParseObject<Attributes>
    object;
  });
}

async function test_anonymous_utils() {
  // $ExpectType boolean
  Parse.AnonymousUtils.isLinked(new Parse.User());
  // $ExpectType ParseUser<Attributes>
  await Parse.AnonymousUtils.link(new Parse.User(), { useMasterKey: true, sessionToken: '' });
  // $ExpectType ParseUser<Attributes>
  await Parse.AnonymousUtils.logIn({ useMasterKey: true, sessionToken: '' });
}

function return_a_query(): Parse.Query {
  return new Parse.Query(Game);
}

async function test_each() {
  await new Parse.Query(Game).each(game => {
    // $ExpectType Game
    game;
  });
}

async function test_file() {
  const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
  let file = new Parse.File('myfile.txt', { base64 });

  file = new Parse.File('nana', { uri: 'http://example.com/image.jps' });

  const bytes = [0xbe, 0xef, 0xca, 0xfe];
  file = new Parse.File('myfile.txt', bytes);

  file = new Parse.File('myfile.zzz', new Blob(), 'image/png');

  const src = file.url();
  const secure = file.url({ forceSecure: true });

  await file.save();
  file.cancel();
  await file.destroy();
}

function test_file_tags_and_metadata() {
  const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
  const file = new Parse.File('myfile.txt', { base64 });
  file.setTags({ ownerId: 42, status: 'okay' });
  file.addTag('labes', 'one');
  file.setMetadata({ contentType: 'plain/text', contentLength: 579 });
  file.addMetadata('author', 'John Doe');
  // $ExpectType Record<string, any>
  file.tags();
  // $ExpectType Record<string, any>
  file.metadata();
}

async function test_analytics() {
  const dimensions = {
    // Define  ranges to bucket data points into meaningful segments
    priceRange: '1000-1500',
    // Did the user filter the query?
    source: 'craigslist',
    // Do searches happen more often on weekdays or weekends?
    dayType: 'weekday',
  };
  // Send the dimensions to Parse along with the 'search' event
  await Parse.Analytics.track('search', dimensions);

  const codeString = '404';
  await Parse.Analytics.track('error', { code: codeString });
}

function test_relation() {
  const game1 = new Game();
  const game2 = new Game();

  new Parse.User()
    .relation<Game>('games')
    .query()
    .find()
    .then((g: Game[]) => {})
    .catch(error => error);
  new Parse.User().relation('games').add(game1);
  new Parse.User().relation('games').add([game1, game2]);

  new Parse.User().relation('games').remove(game1);
  new Parse.User().relation('games').remove([game1, game2]);
}

async function test_user() {
  const user = new Parse.User();
  user.set('username', 'my name');
  user.set('password', 'my pass');
  user.set('email', 'email@example.com');
  await user.signUp(null, { useMasterKey: true });

  const anotherUser: Parse.User = Parse.User.fromJSON({});
  anotherUser.set('email', 'email@example.com');
}

async function test_user_currentAsync() {
  const asyncUser = await Parse.User.currentAsync();
  if (asyncUser) {
    asyncUser.set('email', 'email@example.com');
  } else if (asyncUser === null) {
    await Parse.User.logIn('email@example.com', 'my pass');
  }
}

async function test_user_acl_roles() {
  const user = new Parse.User();
  user.set('username', 'my name');
  user.set('password', 'my pass');
  user.set('email', 'email@example.com');

  // other fields can be set just like with Parse.Object
  user.set('phone', '415-392-0202');

  const currentUser = Parse.User.current();
  if (currentUser) {
    // do stuff with the user
  } else {
    // show the signup or login page
  }

  Parse.User.become('session-token-here')
    .then(
      user => {
        // The current user is now set to user.
      },
      error => {
        // The token could not be validated.
      }
    )
    .catch(error => error);

  Parse.User.hydrate({})
    .then(
      user => {
        // The current user is now set to user.
      },
      error => {
        // The token could not be validated.
      }
    )
    .catch(error => error);

  const game = new Game();
  game.set('gameScore', new GameScore());
  game.setACL(new Parse.ACL(Parse.User.current()));
  game
    .save()
    .then((game: Game) => {})
    .catch(error => error);
  await game.save(null, { useMasterKey: true });
  game
    .save({ score: '10' }, { useMasterKey: true })
    .then(
      game => {
        // Update game then revert it to the last saved state.
        game.set('score', '20');
        game.revert('score');
        game.revert('score', 'ACL');
        game.revert();
      },
      error => {
        // The save failed
      }
    )
    .catch(error => error);

  const groupACL = new Parse.ACL();

  const userList: Parse.User[] = [Parse.User.current()!];
  // userList is an array with the users we are sending this message to.
  for (const userListItem of userList) {
    groupACL.setReadAccess(userListItem, true);
    groupACL.setWriteAccess(userListItem, true);
  }

  groupACL.setPublicReadAccess(true);

  game.setACL(groupACL);

  Parse.User.requestPasswordReset('email@example.com')
    .then(
      data => {
        // The current user is now set to user.
      },
      error => {
        // The token could not be validated.
      }
    )
    .catch(error => error);

  Parse.User.requestEmailVerification('email@example.com')
    .then(
      data => {
        // The current user is now set to user.
      },
      error => {
        // The token could not be validated.
      }
    )
    .catch(error => error);

  // By specifying no write privileges for the ACL, we can ensure the role cannot be altered.
  const role = new Parse.Role('Administrator', groupACL);
  role.getUsers().add(userList[0]);
  role.getRoles().add(role);
  await role.save();

  await Parse.User.logOut()
    .then(data => {
      // logged out
    })
    .catch(error => error);
}

async function test_facebook_util() {
  Parse.FacebookUtils.init({
    appId: 'YOUR_APP_ID', // Facebook App ID
    channelUrl: '//WWW.YOUR_DOMAIN.COM/channel.html', // Channel File
    cookie: true, // enable cookies to allow Parse to access the session
    xfbml: true, // parse XFBML
  });

  await Parse.FacebookUtils.logIn(null, {
    success: (user: Parse.User) => {
      if (!user.existed()) {
        alert('User signed up and logged in through Facebook!');
      } else {
        alert('User logged in through Facebook!');
      }
    },
    error: (user: Parse.User, error: any) => {
      alert('User cancelled the Facebook login or did not fully authorize.');
    },
  });

  const user = Parse.User.current()!;

  if (!Parse.FacebookUtils.isLinked(user)) {
    await Parse.FacebookUtils.link(user, null, {
      success: (user: any) => {
        alert('Woohoo, user logged in with Facebook!');
      },
      error: (user: any, error: any) => {
        alert('User cancelled the Facebook login or did not fully authorize.');
      },
    });
  }

  await Parse.FacebookUtils.unlink(user, {
    success: (user: Parse.User) => {
      alert('The user is no longer associated with their Facebook account.');
    },
  });
}

async function test_cloud_functions() {
  // $ExpectType any
  await Parse.Cloud.run('SomeFunction');

  // $ExpectType any
  await Parse.Cloud.run('SomeFunction', { something: 'whatever' });

  // $ExpectType any
  await Parse.Cloud.run('SomeFunction', null, { useMasterKey: true });

  // $ExpectType boolean
  await Parse.Cloud.run<() => boolean>('SomeFunction');

  // $ExpectType boolean
  await Parse.Cloud.run<() => boolean>('SomeFunction', null);

  // $ExpectType boolean
  await Parse.Cloud.run<() => boolean>('SomeFunction', null, { useMasterKey: true });

  // $ExpectType number
  await Parse.Cloud.run<(params: { paramA: string }) => number>('SomeFunction', { paramA: 'hello' });

  // @ts-expect-error - params are required
  await Parse.Cloud.run<(params: { paramA: string }) => number>('SomeFunction');

  // @ts-expect-error - wrong param name
  await Parse.Cloud.run<(params: { paramA: string }) => number>('SomeFunction', { paramZ: 'hello' });

  // @ts-expect-error - params are required, null not allowed
  await Parse.Cloud.run<(params: { paramA: string }) => number>('SomeFunction', null, { useMasterKey: true });

  // @ts-expect-error - params must be object not string
  await Parse.Cloud.run<(params: string) => any>('SomeFunction', 'hello');

  ParseNode.Cloud.afterDelete('MyCustomClass', request => {
    request.object;
    request.user;
  });

  ParseNode.Cloud.afterSave('MyCustomClass', request => {
    if (!request.context) {
      throw new Error('Request context should be defined');
    }
    request.object;
  });

  ParseNode.Cloud.beforeDelete('MyCustomClass', request => {
    request.object;
  });

  ParseNode.Cloud.beforeSave('MyCustomClass', request => {
    if (request.object.isNew()) {
      if (!request.object.has('immutable')) throw new Error('Field immutable is required');
    } else {
      const original = request.original;
      if (original == null) {
        throw new Error('Original must be defined for an existing object');
      }
      if (original.get('immutable') !== request.object.get('immutable')) {
        throw new Error('This field cannot be changed');
      }
    }
    if (!request.context) {
      throw new Error('Request context should be defined');
    }
  });

  ParseNode.Cloud.beforeSave('MyCustomClass', (request): void => {
    request.object;
  });

  // Tests to allow for Parse.Object subclasses with non-optional constructor params.
  class ArgObject extends Parse.Object<{ a: string }> {
    constructor(arg: { a: string }) {
      super('ArgObject', arg);
    }
  }
  ParseNode.Cloud.beforeSave(ArgObject, request => {
    // $ExpectType ArgObject
    request.object;
  });

  ParseNode.Cloud.beforeFind('MyCustomClass', request => {
    request.query;
    request.user;
    request.master;
    request.count;
    request.isGet;

    request.readPreference = ParseNode.Cloud.ReadPreferenceOption.Primary;
    request.readPreference = ParseNode.Cloud.ReadPreferenceOption.PrimaryPreferred;
    request.readPreference = ParseNode.Cloud.ReadPreferenceOption.Secondary;
    request.readPreference = ParseNode.Cloud.ReadPreferenceOption.SecondaryPreferred;
    request.readPreference = ParseNode.Cloud.ReadPreferenceOption.Nearest;
  });

  ParseNode.Cloud.beforeFind('MyCustomClass', request => {
    request.query;
    return new Parse.Query('QueryMe!');
  });

  ParseNode.Cloud.afterFind('MyCustomClass', request => {
    request.results;
    return request.results;
  });

  ParseNode.Cloud.beforeLogin(request => {
    request.object;
    request.user;
  });

  ParseNode.Cloud.afterLogin(request => {
    request.object;
  });

  ParseNode.Cloud.afterLogout(request => {
    request.object;
  });

  ParseNode.Cloud.beforeSaveFile(request => {
    request.file;
    request.fileSize;
    return new Parse.File('myFile.txt', { base64: '' });
  });

  ParseNode.Cloud.beforeSaveFile(request => {
    request.file;
  });

  ParseNode.Cloud.beforeDeleteFile(request => {
    request.file;
  });

  ParseNode.Cloud.afterDeleteFile(request => {
    request.file;
  });

  ParseNode.Cloud.beforeConnect(request => {
    request.user;
    request.installationId;
    request.useMasterKey;
    request.clients;
    request.subscriptions;
    request.sessionToken;
  });

  ParseNode.Cloud.beforeSubscribe('MyCustomClass', request => {
    request.object;
    request.user;
    request.original;
    request.installationId;
  });

  ParseNode.Cloud.afterLiveQueryEvent('MyCustomClass', request => {
    request.event;
    request.object;
    request.original;
    request.user;
    request.sendEvent;
    request.clients;
    request.subscriptions;
    request.sessionToken;
  });

  ParseNode.Cloud.beforePasswordResetRequest(request => {
    request.object;
    request.user;
    request.master;
    request.ip;
    request.headers;
  });

  void ParseNode.Cloud.httpRequest({
    url: 'https://example.com',
    method: 'GET',
    headers: { 'Content-Type': 'application/json' },
    body: { key: 'value' },
    followRedirects: true,
  }).then(response => {
    response.status;
    response.data;
    response.text;
    response.headers;
    response.buffer;
    response.cookies;
  });

  void ParseNode.Cloud.sendEmail({
    to: 'test@example.com',
    from: 'sender@example.com',
    subject: 'Test Subject',
    text: 'Plain text content',
    html: '<p>HTML content</p>',
  });

  ParseNode.Cloud.define('AFunc', request => {
    request.params;
    return 'Some result';
  });

  ParseNode.Cloud.define(
    'AFunc',
    request => {
      request.params;
      return 'Some result';
    },
    {
      requireUser: true,
      requireMaster: true,
      validateMasterKey: true,
      skipWithMasterKey: true,
      requireAnyUserRoles: ['a'],
      requireAllUserRoles: ['a'],
      fields: {
        name: {
          type: String,
          constant: true,
          default: true,
          options: [],
          error: 'invalid field.',
        },
      },
      requireUserKeys: {
        name: {
          type: String,
          constant: true,
          default: true,
          options: [],
          error: 'invalid field.',
        },
      },
    },
  );

  ParseNode.Cloud.define('AFunc', request => {
    request.params;
    request.params.anything;
  });

  ParseNode.Cloud.job('AJob', request => {
    request.params;
    request.message('Message to associate with this job run');
  });

  void Parse.Cloud.startJob('AJob', {}).then(v => v);

  void Parse.Cloud.getJobStatus('AJob').then(v => v);

  void Parse.Cloud.getJobsData().then(v => v);

  // @ts-expect-error - define should not exist on browser Parse.Cloud
  Parse.Cloud.define('test', () => {});
  // @ts-expect-error - beforeSave should not exist on browser Parse.Cloud
  Parse.Cloud.beforeSave('Test', () => {});
  // @ts-expect-error - job should not exist on browser Parse.Cloud
  Parse.Cloud.job('test', () => {});
  // @ts-expect-error - httpRequest should not exist on browser Parse.Cloud
  void Parse.Cloud.httpRequest({ url: '' });
  // @ts-expect-error - beforeConnect should not exist on browser Parse.Cloud
  Parse.Cloud.beforeConnect(() => {});
  // @ts-expect-error - beforeSubscribe should not exist on browser Parse.Cloud
  Parse.Cloud.beforeSubscribe('Test', () => {});
  // @ts-expect-error - afterLiveQueryEvent should not exist on browser Parse.Cloud
  Parse.Cloud.afterLiveQueryEvent('Test', () => {});
  // @ts-expect-error - beforePasswordResetRequest should not exist on browser Parse.Cloud
  Parse.Cloud.beforePasswordResetRequest(() => {});
  // @ts-expect-error - sendEmail should not exist on browser Parse.Cloud
  void Parse.Cloud.sendEmail({ to: '' });
}

class PlaceObject extends Parse.Object {}

function test_geo_points() {
  let point = new Parse.GeoPoint();
  // $ExpectError
  point = new Parse.GeoPoint('40.0');
  // $ExpectType ParseGeoPoint
  point = new Parse.GeoPoint(40.0);
  // $ExpectError
  point = new Parse.GeoPoint([40.0, -30.0, 20.0]);
  point = new Parse.GeoPoint([40.0, -30.0]);
  point = new Parse.GeoPoint(40.0, -30.0);
  point = new Parse.GeoPoint({ latitude: 40.0, longitude: -30.0 });

  const userObject = Parse.User.current<Parse.User<{ location: Parse.GeoPoint }>>()!;

  // User's location
  const userGeoPoint = userObject.get('location');

  // Create a query for places
  const query = new Parse.Query(Parse.User);
  // Interested in locations near user.
  query.near('location', userGeoPoint);
  // Limit what could be a lot of points.
  query.limit(10);

  const southwestOfSF = new Parse.GeoPoint(37.708813, -122.526398);
  const northeastOfSF = new Parse.GeoPoint(37.822802, -122.373962);

  const query2 = new Parse.Query(PlaceObject);
  query2.withinGeoBox('location', southwestOfSF, northeastOfSF);

  const query3 = new Parse.Query('PlaceObject')
    .find()
    .then((o: Parse.Object[]) => {})
    .catch(error => error);
}

async function test_push() {
  await Parse.Push.send(
    {
      channels: ['Gia nts', 'Mets'],
      data: {
        alert: 'The Giants won against the Mets 2-3.',
      },
    },
    {
      success: () => {
        // Push was successful
      },
      error: (error: any) => {
        // Handle error
      },
    }
  );

  const query = new Parse.Query(Parse.Installation);
  query.equalTo('injuryReports', true);

  await Parse.Push.send(
    {
      where: query, // Set our Installation query
      data: {
        alert: 'Willie Hayes injured by own pop fly.',
      },
    },
    {
      success() {
        // Push was successful
      },
      error(error: any) {
        // Handle error
      },
    }
  );
}

async function test_batch_operations() {
  const game1 = new Game();
  const game2 = new Game();
  const games = [game1, game2];

  // Master key
  await Parse.Object.saveAll(games, { useMasterKey: true });
  await Parse.Object.destroyAll(games, { useMasterKey: true });
  await Parse.Object.fetchAll(games, { useMasterKey: true });
  await Parse.Object.fetchAllIfNeeded(games, { useMasterKey: true });

  // Session token
  await Parse.Object.saveAll(games, { sessionToken: '' });
  await Parse.Object.destroyAll(games, { sessionToken: '' });
  await Parse.Object.fetchAll(games, { sessionToken: '' });
  await Parse.Object.fetchAllIfNeeded(games, { sessionToken: '' });
}

async function test_query_subscribe() {
  // create new query from Game object type
  const query = new Parse.Query(Game);

  // create subscription to Game object
  // Without a token
  // $ExpectType LiveQuerySubscription
  let subscription = await query.subscribe();

  // With a session token
  // $ExpectType LiveQuerySubscription
  subscription = await query.subscribe(new Parse.User().getSessionToken());

  // listen for new Game objects created on Parse server
  subscription.on('create', (game: any) => {
    console.log(game);
  });

  // unsubscribe
  await subscription.unsubscribe();
}

function test_serverURL() {
  Parse.serverURL = 'http://localhost:1337/parse';
}
function test_polygon() {
  const point = new Parse.GeoPoint(1, 2);
  const polygon1 = new Parse.Polygon([
    [0, 0],
    [1, 0],
    [1, 1],
    [0, 1],
  ]);
  const polygon2 = new Parse.Polygon([point, point, point]);
  polygon1.equals(polygon2);
  polygon1.containsPoint(point);

  const query = new Parse.Query('TestObject');
  query.polygonContains('key', point);
  query.withinPolygon('key', [
    [0, 0],
    [1, 0],
    [1, 1],
    [0, 1],
  ]);
}

async function test_local_datastore() {
  Parse.enableLocalDatastore();
  const name = 'test_pin';
  const obj = new Parse.Object('TestObject');
  await obj.pin();
  await obj.unPin();
  await obj.isPinned();
  await obj.pinWithName(name);
  await obj.unPinWithName(name);
  await obj.fetchFromLocalDatastore();

  await Parse.Object.pinAll([obj]);
  await Parse.Object.unPinAll([obj]);
  await Parse.Object.pinAllWithName(name, [obj]);
  await Parse.Object.unPinAllWithName(name, [obj]);
  await Parse.Object.unPinAllObjects();
  await Parse.Object.unPinAllObjectsWithName(name);
  // $ExpectType boolean
  Parse.isLocalDatastoreEnabled();
  // $ExpectType any
  await Parse.dumpLocalDatastore();

  const query = new Parse.Query('TestObject');
  query.fromPin();
  query.fromPinWithName(name);
  query.fromLocalDatastore();

  Parse.setLocalDatastoreController({});
}

async function test_from_network() {
  const obj = new Parse.Object('TestObject');
  await obj.save();

  const query = new Parse.Query('TestObject');
  query.fromNetwork();
}

async function test_cancel_query() {
  const obj = new Parse.Object('TestObject');
  await obj.save();

  const query = new Parse.Query('TestObject');
  await query.fromNetwork().find();
  query.cancel();
}

/* eslint-disable @typescript-eslint/no-redundant-type-constituents -- object is intentionally included for testing Exclude<FieldType, object>. */
type FieldType =
  | string
  | number
  | boolean
  | Date
  | Parse.File
  | Parse.GeoPoint
  | any[]
  | object
  | Parse.Pointer
  | Parse.Polygon
  | Parse.Relation;
/* eslint-enable @typescript-eslint/no-redundant-type-constituents */
async function test_schema(
  anyField: FieldType,
  notString: Exclude<FieldType, string>,
  notNumber: Exclude<FieldType, number>,
  notboolean: Exclude<FieldType, boolean>,
  notDate: Exclude<FieldType, Date>,
  notFile: Exclude<FieldType, Parse.File>,
  notGeopoint: Exclude<FieldType, Parse.GeoPoint[]>,
  notArray: Exclude<FieldType, any[]>,
  notObject: Exclude<FieldType, object>,
  notPointer: Exclude<FieldType, Parse.Pointer>,
  notPolygon: Exclude<FieldType, Parse.Polygon>
) {
  // $ExpectType RestSchema[]
  await Parse.Schema.all();

  const schema = new Parse.Schema('TestSchema');

  schema.addArray('arrayField');
  schema.addArray('arrayField', { defaultValue: [1, 2, 3, 4] });
  // $ExpectError
  schema.addArray('arrayField', { defaultValue: notArray });

  /**
   * @todo Enable type check for default value
   */
  schema.addField('defaultFieldString');
  schema.addField('defaultFieldString', 'String', { defaultValue: anyField });
  schema.addField('defaultFieldString', 'Number');
  schema.addField('defaultFieldString', 'Relation');
  // $ExpectError
  schema.addField('defaultFieldString', 'String', 'Invalid Options');

  schema.addString('field');
  schema.addString('field', { defaultValue: 'some string', required: true });
  // $ExpectError
  schema.addString('field', { defaultValue: notString });

  schema.addNumber('field');
  schema.addNumber('field', { defaultValue: 0, required: true });
  // $ExpectError
  schema.addNumber('field', { defaultValue: notNumber });

  schema.addBoolean('field');
  schema.addBoolean('field', { defaultValue: true, required: true });
  // $ExpectError
  schema.addBoolean('field', { defaultValue: notboolean });

  schema.addDate('field');
  schema.addDate('field', { defaultValue: new Date(), required: true });
  // $ExpectError
  schema.addDate('field', { defaultValue: notDate });

  schema.addFile('field');
  schema.addFile('field', { defaultValue: new Parse.File('myfile', []), required: true });
  // $ExpectError
  schema.addFile('field', { defaultValue: notFile });

  schema.addGeoPoint('field');
  schema.addGeoPoint('field', { defaultValue: new Parse.GeoPoint(), required: true });
  // $ExpectError
  schema.addGeoPoint('field', { defaultValue: notGeopoint });

  schema.addPolygon('field');
  schema.addPolygon('field', { defaultValue: new Parse.Polygon([]), required: true });
  // $ExpectError
  schema.addPolygon('field', { defaultValue: notPolygon });

  schema.addObject('field');
  schema.addObject('field', { defaultValue: {}, required: true });
  schema.addObject('field', { defaultValue: { abc: 'def' } });
  // $ExpectError
  schema.addObject('field', { defaultValue: notObject });

  schema.addPointer('field', 'SomeClass');
  // $ExpectError
  schema.addPointer('field');
  /**
   * @todo Infer defaultValue type from targetClass
   */
  schema.addPointer('field', '_User', {
    defaultValue: new Parse.User().toPointer(),
    required: true,
  });
  // $ExpectError
  schema.addPointer('field', { defaultValue: notPointer });

  schema.addRelation('field', 'SomeClass');
  // $ExpectError
  schema.addRelation('field');
  // $ExpectError
  schema.addRelation('field', 'SomeClass', 'anything');

  schema.addIndex('testIndex', { stringField: 'text' });
  schema.addIndex('testIndex', { stringField: 1 });
  schema.addIndex('testIndex', { stringField: -1 });
  // $ExpectError
  schema.addIndex('testIndex', { stringField: true });

  schema.deleteField('defaultFieldString');
  schema.deleteIndex('testIndex');
  await schema.delete();
  // $ExpectType RestSchema
  await schema.get();
  await schema.purge();
  await schema.save();
  await schema.update();

  function testGenericType() {
    interface iTestAttributes {
      arrField: any[];
      boolField: boolean;
      stringField: string;
      numField: number;
      dateField: Date;
      fileField: Parse.File;
      geoPointField: Parse.GeoPoint;
      polygonField: Parse.Polygon;
      objectField: object;
      relationField: Parse.Relation;
      pointerField: Parse.Pointer | Parse.Object;
    }
    class TestObject extends Parse.Object<iTestAttributes> {}

    const schema = new Parse.Schema<TestObject>('TestObject');
    schema.addArray('arrField');
    schema.addBoolean('boolField');
    schema.addDate('dateField');
    schema.addFile('fileField');
    schema.addGeoPoint('geoPointField');
    schema.addNumber('numField');
    schema.addObject('objectField');
    schema.addPointer('pointerField', 'FooClass');
    schema.addPolygon('polygonField');
    schema.addRelation('relationField', 'FooClass');
    schema.addString('stringField');

    // $ExpectError
    schema.addArray('wrong');
    // $ExpectError
    schema.addBoolean('wrong');
    // $ExpectError
    schema.addDate('wrong');
    // $ExpectError
    schema.addFile('wrong');
    // $ExpectError
    schema.addGeoPoint('wrong');
    // $ExpectError
    schema.addNumber('wrong');
    // $ExpectError
    schema.addObject('wrong');
    // $ExpectError
    schema.addPointer('wrong', 'FooClass');
    // $ExpectError
    schema.addPolygon('wrong');
    // $ExpectError
    schema.addRelation('wrong', 'FooClass');
    // $ExpectError
    schema.addString('wrong');
  }
}

function testObject() {
  function testConstructor() {
    // $ExpectType ParseObject<Attributes>
    new Parse.Object();

    // $ExpectType ParseObject<Attributes>
    new Parse.Object('TestObject');

    // $ExpectType ParseObject<{ example: number; }>
    new Parse.Object('TestObject', { example: 100 });

    // $ExpectType ParseObject<{ example: boolean; }>
    new Parse.Object<{ example: boolean }>('TestObject', { example: true });

    // $ExpectType ParseObject<{ example: string; }>
    new Parse.Object('TestObject', { example: 'hello' }, { ignoreValidation: true });

    // $ExpectType ParseObject<{ example: string; }>
    new Parse.Object<{ example: string }>('TestObject');

    // $ExpectType ParseObject<{ example: number; }>
    new Parse.Object<{ example: number }>('TestObject', { example: 100 });

    // $ExpectError
    new Parse.Object<{ example: boolean }>('TestObject', { example: 'hello' });
  }

  function testStaticMethods() {
    async function testSaveAll(
      objUntyped: Parse.Object,
      objTyped: Parse.Object<{ example: string }>
    ) {
      // $ExpectType ParseObject<Attributes>[]
      await Parse.Object.saveAll([objUntyped]);

      // $ExpectType ParseObject<{ example: string; }>[]
      await Parse.Object.saveAll([objTyped]);

      // $ExpectType [ParseObject<Attributes>, ParseObject<{ example: string; }>]
      await Parse.Object.saveAll<[typeof objUntyped, typeof objTyped]>([objUntyped, objTyped]);

      // $ExpectError
      await Parse.Object.saveAll([123]);
    }
  }

  function testAttributes(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: string }>) {
    // $ExpectType any
    objUntyped.attributes.whatever;

    // $ExpectType string
    objTyped.attributes.example;

    // $ExpectError
    objTyped.attributes.other;
  }

  function testAdd(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ stringList: string[]; thing: boolean }>
  ) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.add('whatever', 'hello');

    // $ExpectType ParseObject<{ stringList: string[]; thing: boolean; }>
    objTyped.add('stringList', 'hello');

    // $ExpectError
    objTyped.add('stringList', 100);

    // $ExpectError
    objTyped.add('thing', true);

    // $ExpectError
    objTyped.add('whatever', 'hello');
  }

  function testAddAll(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ stringList: string[]; thing: boolean }>
  ) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.addAll('whatever', ['hello', 100]);

    // $ExpectType ParseObject<{ stringList: string[]; thing: boolean; }>
    objTyped.addAll('stringList', ['hello']);

    // $ExpectError
    objTyped.addAll('stringList', [100]);

    // $ExpectError
    objTyped.addAll('thing', [true]);

    // $ExpectError
    objTyped.addAll('whatever', ['hello']);
  }

  function testAddAllUnique(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ stringList: string[]; thing: boolean }>
  ) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.addAllUnique('whatever', ['hello', 100]);

    // $ExpectType ParseObject<{ stringList: string[]; thing: boolean; }>
    objTyped.addAllUnique('stringList', ['hello']);

    // $ExpectError
    objTyped.addAllUnique('stringList', [100]);

    // $ExpectError
    objTyped.addAllUnique('thing', [true]);

    // $ExpectError
    objTyped.addAllUnique('whatever', ['hello']);
  }

  function testAddUnique(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ stringList: string[]; thing: boolean }>
  ) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.addUnique('whatever', 'hello');

    // $ExpectType ParseObject<{ stringList: string[]; thing: boolean; }>
    objTyped.addUnique('stringList', 'hello');

    // $ExpectError
    objTyped.addUnique('stringList', 100);

    // $ExpectError
    objTyped.addUnique('thing', true);

    // $ExpectError
    objTyped.addUnique('whatever', 'hello');
  }

  function testDirty(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: string }>) {
    // $ExpectType boolean
    objUntyped.dirty();

    // $ExpectType boolean
    objUntyped.dirty('whatever');

    // $ExpectType boolean
    objTyped.dirty();

    // $ExpectType boolean
    objTyped.dirty('example');

    // $ExpectError
    objTyped.dirty('other');
  }

  function testEquals(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: string }>) {
    // $ExpectType boolean
    objUntyped.equals(objTyped);

    // $ExpectType boolean
    objTyped.equals(objUntyped);

    // $ExpectError
    objUntyped.equals('blah');
  }

  function testEscape(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: string }>) {
    // $ExpectType string
    objUntyped.escape('whatever');

    // $ExpectType string
    objTyped.escape('example');

    // $ExpectError
    objTyped.escape('other');
  }

  async function testFetchWithInclude(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ example: string }>
  ) {
    // $ExpectType ParseObject<Attributes>
    await objUntyped.fetchWithInclude('whatever');

    // $ExpectType ParseObject<Attributes>
    await objUntyped.fetchWithInclude(['whatever']);

    // $ExpectType ParseObject<Attributes>
    await objUntyped.fetchWithInclude([['whatever']]);

    // $ExpectError
    await objUntyped.fetchWithInclude([[['whatever']]]);

    // $ExpectType ParseObject<{ example: string; }>
    await objTyped.fetchWithInclude('example');

    // $ExpectType ParseObject<{ example: string; }>
    await objTyped.fetchWithInclude(['example']);

    // $ExpectType ParseObject<{ example: string; }>
    await objTyped.fetchWithInclude([['example']]);

    // $ExpectError
    await objTyped.fetchWithInclude([[['example']]]);

    // $ExpectType ParseObject<{ example: string; }>[]
    await Parse.Object.fetchAllIfNeededWithInclude([objTyped], 'example');

    // $ExpectError
    await Parse.Object.fetchAllIfNeededWithInclude([objTyped], 'notAnAttribute');

    // $ExpectType ParseObject<{ example: string; }>[]
    await Parse.Object.fetchAllWithInclude([objTyped], 'example');

    // $ExpectError
    await Parse.Object.fetchAllWithInclude([objTyped], 'notAnAttribute');
  }

  function testGet(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: number }>) {
    // $ExpectType any
    objUntyped.get('whatever');

    // $ExpectType number
    objTyped.get('example');

    // $ExpectError
    objTyped.get('other');
  }

  function testHas(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: number }>) {
    // $ExpectType boolean
    objUntyped.has('whatever');

    // $ExpectType boolean
    objTyped.has('example');

    // $ExpectError
    objTyped.has('other');
  }

  function testIncrement(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: number }>) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.increment('whatever');

    // $ExpectType ParseObject<Attributes>
    objUntyped.increment('whatever', 10);

    // $ExpectType ParseObject<{ example: number; }>
    objTyped.increment('example');

    // $ExpectType ParseObject<{ example: number; }>
    objTyped.increment('example', 20);

    // $ExpectError
    objTyped.increment('example', true);

    // $ExpectError
    objTyped.increment('other');
  }

  function testDecrement(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: number }>) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.decrement('whatever');

    // $ExpectType ParseObject<Attributes>
    objUntyped.decrement('whatever', 10);

    // $ExpectType ParseObject<{ example: number; }>
    objTyped.decrement('example');

    // $ExpectType ParseObject<{ example: number; }>
    objTyped.decrement('example', 20);

    // $ExpectError
    objTyped.decrement('example', true);

    // $ExpectError
    objTyped.decrement('other');
  }

  function testNewInstance(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: number }>) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.newInstance();

    // $ExpectType ParseObject<{ example: number; }>
    objTyped.newInstance();
  }

  function testOp(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: number }>) {
    // $ExpectType Op | undefined
    objUntyped.op('whatever');

    // $ExpectType Op | undefined
    objTyped.op('example');

    // $ExpectError
    objTyped.op('other');
  }

  function testRelation(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ example: number; rel: Parse.Relation }>
  ) {
    // $ExpectType ParseRelation<ParseObject<Attributes>, ParseObject<Attributes>>
    objUntyped.relation('whatever');

    // $ExpectType ParseRelation<ParseObject<{ example: number; rel: ParseRelation<ParseObject<Attributes>, ParseObject<Attributes>>; }>, ParseObject<Attributes>>
    objTyped.relation('rel');

    // $ExpectError
    objTyped.relation('example');

    // $ExpectError
    objTyped.relation('other');
  }

  function testRemove(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ stringList: string[]; thing: boolean }>
  ) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.remove('whatever', 'hello');

    // $ExpectType ParseObject<{ stringList: string[]; thing: boolean; }>
    objTyped.remove('stringList', 'hello');

    // $ExpectError
    objTyped.remove('stringList', 100);

    // $ExpectError
    objTyped.remove('thing', true);

    // $ExpectError
    objTyped.remove('whatever', 'hello');
  }

  function testRemoveAll(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ stringList: string[]; thing: boolean }>
  ) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.removeAll('whatever', ['hello', 100]);

    // $ExpectType ParseObject<{ stringList: string[]; thing: boolean; }>
    objTyped.removeAll('stringList', ['hello']);

    // $ExpectError
    objTyped.removeAll('stringList', [100]);

    // $ExpectError
    objTyped.removeAll('thing', [true]);

    // $ExpectError
    objTyped.removeAll('whatever', ['hello']);
  }

  function testRevert(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<{ thingOne: number; thingTwo: boolean }>
  ) {
    // $ExpectType void
    objUntyped.revert();

    // $ExpectType void
    objUntyped.revert('whatever', 'more whatever');

    // $ExpectType void
    objTyped.revert();

    // $ExpectType void
    objTyped.revert('thingOne', 'thingTwo');

    // $ExpectError
    objTyped.revert('other');
  }
  interface ObjectAttributes {
    example: boolean;
    someString: string;
  }
  interface OptionalObjectAttributes {
    example?: boolean | undefined;
    another?: string | undefined;
  }

  async function testSave(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<ObjectAttributes>,
    objTypedOptional: Parse.Object<OptionalObjectAttributes>
  ) {
    // $ExpectType ParseObject<Attributes>
    await objUntyped.save({ whatever: 100 });

    // $ExpectType ParseObject<Attributes>
    await objUntyped.save('whatever', 100);

    // $ExpectType ParseObject<ObjectAttributes>
    await objTyped.save({ example: true });

    // $ExpectType ParseObject<ObjectAttributes>
    await objTyped.save({ example: true, someString: 'hello' });

    // $ExpectError
    await objTyped.save({ example: 'hello', someString: true });

    // $ExpectType ParseObject<ObjectAttributes>
    await objTyped.save('example', true);

    // $ExpectError
    await objTyped.save({ example: 'hello' });

    // $ExpectError
    await objTyped.save({ wrongProp: 5 });

    // $ExpectError
    await objTyped.save('example', 10);

    // $ExpectError
    await objTyped.save('wrongProp', true);

    // $ExpectError
    await objTyped.save({ example: undefined });

    // $ExpectError
    await objTyped.save('example', undefined);

    // $ExpectType ParseObject<ObjectAttributes>
    await objTyped.save({});

    // $ExpectType ParseObject<ObjectAttributes>
    await objTyped.save();

    // $ExpectType ParseObject<OptionalObjectAttributes>
    await objTypedOptional.save({ example: undefined });

    // $ExpectType ParseObject<OptionalObjectAttributes>
    await objTypedOptional.save('example', undefined);

    // $ExpectType ParseObject<OptionalObjectAttributes>
    await objTypedOptional.save({});

    // $ExpectType ParseObject<OptionalObjectAttributes>
    await objTypedOptional.saveEventually({});

    // $ExpectType ParseObject<OptionalObjectAttributes>
    await objTypedOptional.destroyEventually({});

    // $ExpectType ParseObject<OptionalObjectAttributes>
    await objTypedOptional.saveEventually();

    // $ExpectType ParseObject<OptionalObjectAttributes>
    await objTypedOptional.destroyEventually();
  }

  function testSet(
    objUntyped: Parse.Object,
    objTyped: Parse.Object<ObjectAttributes>,
    objTypedOptional: Parse.Object<OptionalObjectAttributes>
  ) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.set('propA', 'some value');

    // $ExpectType ParseObject<Attributes>
    objUntyped.set({ propA: undefined });

    // $ExpectError
    objTyped.set({ example: false });

    // $ExpectType ParseObject<ObjectAttributes>
    objTyped.set({ example: true, someString: 'abc' });

    // $ExpectError
    objTyped.set({ example: 123, someString: 'abc' });

    // $ExpectType ParseObject<ObjectAttributes>
    objTyped.set('example', true);

    // $ExpectError
    objTyped.set({ example: 100 });

    // $ExpectError
    objTyped.set({ other: 'something' });

    // $ExpectError
    objTyped.set('example', 100);

    // $ExpectError
    objTyped.set('other', 100);

    // $ExpectError
    objTyped.set({ example: undefined });

    // $ExpectType ParseObject<ObjectAttributes>
    objTyped.set({}); // Should this error?

    // $ExpectType ParseObject<ObjectAttributes>
    objTyped.set('example', undefined); // Should this error?

    // $ExpectType ParseObject<OptionalObjectAttributes>
    objTypedOptional.set({ example: undefined });

    // $ExpectType ParseObject<OptionalObjectAttributes>
    objTypedOptional.set('example', undefined);

    // $ExpectType ParseObject<OptionalObjectAttributes>
    objTypedOptional.set({});
  }

  interface AttributesAllTypes {
    someString: string;
    someNumber: number;
    someBoolean: boolean;
    someDate: Date;
    someJSONObject: AttributesAllTypes;
    someJSONArray: AttributesAllTypes[];
    someRegExp: RegExp;
    someUndefined: undefined;
    someNull: null;
    someParseObjectUntyped: Parse.Object;
    someParseObjectTyped: Parse.Object<AttributesAllTypes>;
    someParseACL: Parse.ACL;
    someParseGeoPoint: Parse.GeoPoint;
    someParsePolygon: Parse.Polygon;
    someParseRelation: Parse.Relation;
    someParseFile: Parse.File;
  }

  function testToJSON(objUntyped: Parse.Object, objTyped: Parse.Object<AttributesAllTypes>) {
    // $ExpectType ToJSON<Attributes> & JSONBaseAttributes
    const JSONUntyped = objUntyped.toJSON();
    // $ExpectType string
    JSONUntyped.objectId;
    // $ExpectType string
    JSONUntyped.createdAt;
    // $ExpectType string
    JSONUntyped.updatedAt;
    // $ExpectType any
    JSONUntyped.anything;

    // $ExpectType ToJSON<AttributesAllTypes> & JSONBaseAttributes
    const JSONTyped = objTyped.toJSON();
    // $ExpectType string
    JSONTyped.objectId;
    // $ExpectType string
    JSONTyped.createdAt;
    // $ExpectType string
    JSONTyped.updatedAt;
    // $ExpectType string
    JSONTyped.someString;
    // $ExpectType number
    JSONTyped.someNumber;
    // $ExpectType boolean
    JSONTyped.someBoolean;
    // $ExpectType { __type: "Date"; iso: string; }
    JSONTyped.someDate;
    // $ExpectType ToJSON<AttributesAllTypes>
    JSONTyped.someJSONObject;
    // $ExpectType ToJSON<AttributesAllTypes>[]
    JSONTyped.someJSONArray;
    // $ExpectType string
    JSONTyped.someRegExp;
    // $ExpectType undefined
    JSONTyped.someUndefined;
    // $ExpectType null
    JSONTyped.someNull;
    // $ExpectType Pointer | (ToJSON<Attributes> & JSONBaseAttributes)
    JSONTyped.someParseObjectUntyped;
    // $ExpectType Pointer | (ToJSON<AttributesAllTypes> & JSONBaseAttributes)
    JSONTyped.someParseObjectTyped;
    // $ExpectType ByIdMap
    JSONTyped.someParseACL;
    // $ExpectType { __type: string; latitude: number; longitude: number; }
    JSONTyped.someParseGeoPoint;
    // $ExpectType { __type: string; coordinates: Coordinates; }
    JSONTyped.someParsePolygon;
    // $ExpectType { __type: "Relation"; className: string | null; }
    JSONTyped.someParseRelation;
    // $ExpectType { __type: "File"; name?: string | undefined; url?: string | undefined; }
    JSONTyped.someParseFile;
  }

  function testUnset(objUntyped: Parse.Object, objTyped: Parse.Object<{ example: string }>) {
    // $ExpectType ParseObject<Attributes>
    objUntyped.unset('whatever');

    // $ExpectType ParseObject<{ example: string; }>
    objTyped.unset('example');

    // $ExpectError
    objTyped.unset('other');
  }

  function testValidate(obj: Parse.Object) {
    // Note: The attributes being validated don't necessarily have to match the current object's attributes

    // $ExpectType ParseError | boolean
    obj.validate({ someAttrToValidate: 'hello' });
  }

  function testNullableArrays(objTyped: Parse.Object<{ stringList?: string[] | null }>) {
    // $ExpectType ParseObject<{ stringList?: string[] | null | undefined; }>
    objTyped.add('stringList', 'foo');

    // $ExpectError
    objTyped.add('stringList', 4);

    // $ExpectType ParseObject<{ stringList?: string[] | null | undefined; }>
    objTyped.addAll('stringList', ['foo']);

    // $ExpectError
    objTyped.addAll('stringList', [4]);

    // $ExpectType ParseObject<{ stringList?: string[] | null | undefined; }>
    objTyped.addAllUnique('stringList', ['foo', 'bar']);

    // $ExpectError
    objTyped.addAllUnique('stringList', [4]);

    // $ExpectType ParseObject<{ stringList?: string[] | null | undefined; }>
    objTyped.addUnique('stringList', 'foo');

    // $ExpectError
    objTyped.addUnique('stringList', 4);

    // $ExpectType ParseObject<{ stringList?: string[] | null | undefined; }>
    objTyped.remove('stringList', 'bar');

    // $ExpectError
    objTyped.remove('stringList', 4);

    // $ExpectType ParseObject<{ stringList?: string[] | null | undefined; }>
    objTyped.removeAll('stringList', ['bar']);

    // $ExpectError
    objTyped.removeAll('stringList', [4]);
  }
}

function testInstallation() {
  function testConstructor() {
    // $ExpectType ParseInstallation<Attributes>
    new Parse.Installation();

    // $ExpectType ParseInstallation<{ example: number; }>
    new Parse.Installation({ example: 100 });

    // $ExpectType ParseInstallation<{ example: number; }>
    new Parse.Installation<{ example: number }>();

    // $ExpectType ParseInstallation<{ example: number; }>
    new Parse.Installation<{ example: number }>({ example: 100 });

    // $ExpectError
    new Parse.Installation<{ example: number }>({ example: 'hello' });
  }
}

function testQuery() {
  function testConstructor() {
    // $ExpectType ParseQuery<ParseObject<Attributes>>
    new Parse.Query('TestObject');

    // $ExpectType ParseQuery<ParseRole<Attributes>>
    new Parse.Query(Parse.Role);

    // $ExpectType ParseQuery<ParseUser<Attributes>>
    new Parse.Query(Parse.User);

    // $ExpectType ParseQuery<ParseObject<{ example: string; }>>
    new Parse.Query<Parse.Object<{ example: string }>>('TestObject');

    // $ExpectType ParseQuery<ParseRole<{ name: string; }>>
    new Parse.Query<Parse.Role<{ name: string }>>(Parse.Role);

    // $ExpectType ParseQuery<ParseUser<{ example: string; }>>
    new Parse.Query<Parse.User<{ example: string }>>(Parse.User);
  }

  function testQueryMethodTypes() {
    class AnotherSubClass extends Parse.Object<{ x: any }> {
      constructor() {
        super('Another', { x: 'example' });
      }
    }
    class MySubClass extends Parse.Object<{
      attribute1: string;
      attribute2: number;
      attribute3: AnotherSubClass;
      attribute4: string[];
      attribute5?: AnotherSubClass[];
      attribute6?: string[];
    }> {}
    const query = new Parse.Query(MySubClass);

    // $ExpectType ParseQuery<MySubClass>
    query.addAscending(['attribute1', 'attribute2', 'updatedAt']);

    // $ExpectType ParseQuery<MySubClass>
    query.addAscending('attribute1', 'attribute2', 'updatedAt');

    // $ExpectError
    query.addAscending(['attribute1', 'unexistenProp']);

    // $ExpectType ParseQuery<MySubClass>
    query.addDescending(['attribute1', 'attribute2', 'createdAt']);
    // $ExpectError
    query.addDescending(['attribute1', 'unexistenProp']);

    // $ExpectType ParseQuery<MySubClass>
    query.ascending(['attribute1', 'attribute2', 'objectId']);
    // $ExpectError
    query.ascending(['attribute1', 'nonexistentProp']);

    // $ExpectType ParseQuery<MySubClass>
    query.containedBy('attribute1', ['a', 'b', 'c']);
    // $ExpectType ParseQuery<MySubClass>
    query.containedBy('attribute3', ['objectId1', 'objectId2', 'objectId3']);
    // $ExpectError
    query.containedBy('attribute2', ['a', 'b', 'c']);
    // $ExpectError
    query.containedBy('attribute1', [1, 2, 3]);
    // $ExpectError
    query.containedBy('nonexistentProp', ['a', 'b', 'c']);

    // $ExpectType ParseQuery<MySubClass>
    query.containedIn('attribute1', ['a', 'b', 'c']);
    // $ExpectType ParseQuery<MySubClass>
    query.containedIn('attribute3', ['objectId1', 'objectId2', 'objectId3']);
    // $ExpectError
    query.containedIn('attribute2', ['a', 'b', 'c']);
    // $ExpectError
    query.containedIn('attribute1', [1, 2, 3]);
    // $ExpectError
    query.containedIn('nonexistentProp', ['a', 'b', 'c']);

    // $ExpectType ParseQuery<MySubClass>
    query.contains('attribute1', 'a substring');
    // $ExpectError
    query.contains('nonexistentProp', 'a substring');

    // $ExpectType ParseQuery<MySubClass>
    query.containsAll('attribute1', ['a', 'b', 'c']);
    // $ExpectError
    query.containsAll('nonexistentProp', ['a', 'b', 'c']);

    // $ExpectType ParseQuery<MySubClass>
    query.containsAllStartingWith('attribute1', ['a', 'b', 'c']);
    // $ExpectError
    query.containsAllStartingWith('nonexistentProp', ['a', 'b', 'c']);

    // $ExpectType ParseQuery<MySubClass>
    query.descending(['attribute1', 'attribute2', 'objectId']);
    // $ExpectError
    query.descending(['attribute1', 'nonexistentProp']);

    // $ExpectType ParseQuery<MySubClass>
    query.doesNotExist('attribute1');
    // $ExpectError
    query.doesNotExist('nonexistentProp');

    // $ExpectType ParseQuery<MySubClass>
    query.doesNotMatchKeyInQuery('attribute1', 'x', new Parse.Query(AnotherSubClass));
    // $ExpectError
    query.doesNotMatchKeyInQuery('unexistenProp', 'x', new Parse.Query(AnotherSubClass));
    // $ExpectError
    query.doesNotMatchKeyInQuery('attribute1', 'unknownKey', new Parse.Query(AnotherSubClass));
    // $ExpectType ParseQuery<MySubClass>
    query.doesNotMatchKeyInQuery('objectId', 'x', new Parse.Query(AnotherSubClass));
    // $ExpectType ParseQuery<MySubClass>
    query.doesNotMatchKeyInQuery('updatedAt', 'x', new Parse.Query(AnotherSubClass));

    // $ExpectType ParseQuery<MySubClass>
    query.doesNotMatchQuery('attribute1', new Parse.Query('Example'));
    // $ExpectError
    query.doesNotMatchQuery('nonexistentProp', new Parse.Query('Example'));

    // $ExpectType ParseQuery<MySubClass>
    query.endsWith('attribute1', 'asuffixstring');
    // $ExpectError
    query.endsWith('nonexistentProp', 'asuffixstring');

    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute2', 0);
    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute3', new AnotherSubClass());
    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute3', new AnotherSubClass().toPointer());
    // $ExpectError
    query.equalTo('attribute1', new AnotherSubClass().toPointer());
    // $ExpectError
    query.equalTo('attribute2', 'a string value');
    // $ExpectError
    query.equalTo('nonexistentProp', 'any value');

    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute4', 'a_string_value'); // Can query contents of array
    // Can query array itself if equal too (mongodb $eq matches the array exactly or the <field> contains an element that matches the array exactly)
    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute4', ['a_string_value']);

    
    // Optional object[] (e.g. prop?: ClassName[] ): allow matching a single element (array contains object), and allow matching the full array
    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute5', new AnotherSubClass());
    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute5', [new AnotherSubClass()]);
    // Since there is no `$ExpectNotError` thing, this line is instead used to at least prove that there is no regression for type safety for `Array of object` fields
    // $ExpectError
    query.equalTo('attribute5', new MySubClass());

    // Optional string[] (e.g. prop?: string[] ): allow matching a single element (array contains string), and allow matching the full array
    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute6', 'a_string_value');
    // $ExpectType ParseQuery<MySubClass>
    query.equalTo('attribute6', ['a_string_value']);

    // $ExpectType ParseQuery<MySubClass>
    query.notEqualTo('attribute4', 'a_string_value');
    // $ExpectType ParseQuery<MySubClass>
    query.notEqualTo('attribute4', ['a_string_value']);

    // $ExpectError
    query.equalTo('attribute4', 5);
    // $ExpectError
    query.notEqualTo('attribute4', 5);
    // $ExpectError
    query.equalTo('attribute4', [5]);
    // $ExpectError
    query.notEqualTo('attribute4', [5]);

    // Optional string[]: reject invalid element types
    // $ExpectError
    query.equalTo('attribute6', 5);
    // $ExpectError
    query.notEqualTo('attribute6', 5);

    // $ExpectType ParseQuery<MySubClass>
    query.exists('attribute1');
    // $ExpectError
    query.exists('nonexistentProp');

    // $ExpectType ParseQuery<MySubClass>
    query.fullText('attribute1', 'full text');
    // $ExpectError
    query.fullText('nonexistentProp', 'full text');

    // $ExpectType ParseQuery<MySubClass>
    query.greaterThan('attribute2', 1000);
    // $ExpectError
    query.greaterThan('attribute2', '1000');
    // $ExpectError
    query.greaterThan('nonexistentProp', 1000);

    // $ExpectType ParseQuery<MySubClass>
    query.greaterThanOrEqualTo('attribute2', 1000);
    // $ExpectError
    query.greaterThanOrEqualTo('attribute2', '1000');
    // $ExpectError
    query.greaterThanOrEqualTo('nonexistentProp', 1000);

    // $ExpectType ParseQuery<MySubClass>
    query.include(['attribute1', 'attribute2']);
    // $ExpectType ParseQuery<MySubClass>
    query.include('attribute1', 'attribute2');
    // $ExpectType ParseQuery<MySubClass>
    query.include<any>('attribute3.someProp');
    // $ExpectError
    query.include(['attribute1', 'nonexistentProp']);

    // $ExpectType ParseQuery<MySubClass>
    query.exclude('attribute1', 'attribute2');
    // $ExpectType ParseQuery<MySubClass>
    query.exclude(['attribute1', 'attribute2']);
    // $ExpectError
    query.exclude('attribute1', 'nonexistentProp');

    // $ExpectType ParseQuery<MySubClass>
    query.lessThan('attribute2', 1000);
    // $ExpectError
    query.lessThan('attribute2', '1000');
    // $ExpectError
    query.lessThan('nonexistentProp', 1000);

    // $ExpectType ParseQuery<MySubClass>
    query.lessThanOrEqualTo('attribute2', 1000);
    // $ExpectError
    query.lessThanOrEqualTo('attribute2', '1000');
    // $ExpectError
    query.lessThanOrEqualTo('nonexistentProp', 1000);

    // $ExpectType ParseQuery<MySubClass>
    query.matches('attribute1', /a regex/);
    // $ExpectError
    query.matches('nonexistentProp', /a regex/);

    // $ExpectType ParseQuery<MySubClass>
    query.matchesKeyInQuery('attribute1', 'x', new Parse.Query(AnotherSubClass));
    // $ExpectError
    query.matchesKeyInQuery('nonexistentProp', 'x', new Parse.Query(AnotherSubClass));
    // $ExpectError
    query.matchesKeyInQuery('attribute1', 'unknownKey', new Parse.Query(AnotherSubClass));

    // $ExpectType ParseQuery<MySubClass>
    query.matchesQuery('attribute1', new Parse.Query('Example'));
    // $ExpectError
    query.matchesQuery('nonexistentProp', new Parse.Query('Example'));

    // $ExpectType ParseQuery<MySubClass>
    query.near('attribute1', new Parse.GeoPoint());
    // $ExpectError
    query.near('nonexistentProp', new Parse.GeoPoint());

    // $ExpectType ParseQuery<MySubClass>
    query.notContainedIn('attribute2', [1, 2, 3]);
    // $ExpectError
    query.notContainedIn('attribute2', ['1', '2', '3']);
    // $ExpectError
    query.notContainedIn('nonexistentProp', [1, 2, 3]);

    // $ExpectType ParseQuery<MySubClass>
    query.notEqualTo('attribute1', '1');
    // $ExpectError
    query.notEqualTo('attribute1', 1);
    // $ExpectError
    query.notEqualTo('nonexistentProp', 1);

    // $ExpectType ParseQuery<MySubClass>
    query.polygonContains('attribute1', new Parse.GeoPoint());
    // $ExpectError
    query.polygonContains('nonexistentProp', new Parse.GeoPoint());

    // $ExpectType ParseQuery<MySubClass>
    query.select('attribute1', 'attribute2');
    // $ExpectType ParseQuery<MySubClass>
    query.select(['attribute1', 'attribute2']);
    // $ExpectError
    query.select('attribute1', 'nonexistentProp');

    // $ExpectType ParseQuery<MySubClass>
    query.startsWith('attribute1', 'prefix string');
    // $ExpectError
    query.startsWith('nonexistentProp', 'prefix string');

    // $ExpectType ParseQuery<MySubClass>
    query.watch('attribute1', 'attribute2');
    // $ExpectType ParseQuery<MySubClass>
    query.watch(['attribute1', 'attribute2']);
    // $ExpectError
    query.watch('attribute1', 'nonexistentProp');

    // $ExpectType ParseQuery<MySubClass>
    query.withCount(true);

    // $ExpectType ParseQuery<MySubClass>
    query.withinGeoBox('attribute1', new Parse.GeoPoint(), new Parse.GeoPoint());
    // $ExpectError
    query.withinGeoBox('nonexistentProp', new Parse.GeoPoint(), new Parse.GeoPoint());

    // $ExpectType ParseQuery<MySubClass>
    query.withinKilometers('attribute1', new Parse.GeoPoint(), 100);
    // $ExpectError
    query.withinKilometers('nonexistentProp', new Parse.GeoPoint(), 100);

    // $ExpectType ParseQuery<MySubClass>
    query.withinMiles('attribute1', new Parse.GeoPoint(), 100);
    // $ExpectError
    query.withinMiles('nonexistentProp', new Parse.GeoPoint(), 100);

    // $ExpectType ParseQuery<MySubClass>
    query.withinPolygon('attribute1', [
      [12.3, 45.6],
      [-78.9, 10.1],
    ]);
    // $ExpectError
    query.withinPolygon('nonexistentProp', [
      [12.3, 45.6],
      [-78.9, 10.1],
    ]);

    // $ExpectType ParseQuery<MySubClass>
    query.withinRadians('attribute1', new Parse.GeoPoint(), 100);
    // $ExpectError
    query.withinRadians('nonexistentProp', new Parse.GeoPoint(), 100);
  }

  async function testQueryMethods(
    queryUntyped: Parse.Query,
    queryTyped: Parse.Query<Parse.Object<{ example: string }>>
  ) {
    // $ExpectType ParseObject<Attributes>
    await queryUntyped.get('objectId');

    // $ExpectType ParseObject<Attributes>[]
    await queryUntyped.find();

    // $ExpectType string[]
    await queryTyped.distinct('example');

    // $ExpectType ParseObject<Attributes> | undefined
    await queryUntyped.first();

    // $ExpectType ParseObject<{ example: string; }>
    await queryTyped.get('objectId');

    // $ExpectType ParseObject<{ example: string; }>[]
    await queryTyped.find();

    // $ExpectType ParseObject<{ example: string; }> | undefined
    await queryTyped.first();
  }
}

function testRole() {
  function testConstructor(acl: Parse.ACL) {
    // $ExpectType ParseRole<{ example: string; }>
    new Parse.Role<{ example: string }>('TestRole', acl);
  }

  function testAttributes(roleUntyped: Parse.Role, roleTyped: Parse.Role<{ example: number }>) {
    // $ExpectType Attributes
    roleUntyped.attributes;

    // $ExpectType { example: number; }
    roleTyped.attributes;
  }
}

function testSession() {
  async function testConstructor() {
    // $ExpectType ParseSession<Attributes>
    const session = new Parse.Session();

    // $ExpectType ParseSession<{ example: number; }>
    new Parse.Session({ example: 100 });

    // $ExpectType ParseSession<{ example: number; }>
    new Parse.Session<{ example: number }>();

    // $ExpectType ParseSession<{ example: number; }>
    new Parse.Session<{ example: number }>({ example: 100 });

    // $ExpectError
    new Parse.Session<{ example: number }>({ example: 'hello' });

    // $ExpectType boolean
    Parse.Session.isCurrentSessionRevocable();

    // $ExpectType string[]
    Parse.Session.readOnlyAttributes();

    // $ExpectType string
    session.getSessionToken();

    // $ExpectType ParseSession<Attributes>
    await Parse.Session.current();

    // $ExpectType ParseSession<{ example: string; }>
    await Parse.Session.current<Parse.Session<{ example: string }>>();
  }
}

function testUser() {
  function testConstructor() {
    // $ExpectType ParseUser<Attributes>
    new Parse.User();

    // $ExpectType ParseUser<{ example: number; }>
    new Parse.User({ example: 100 });

    // $ExpectType ParseUser<{ example: number; }>
    new Parse.User<{ example: number }>();

    // $ExpectType ParseUser<{ example: number; }>
    new Parse.User<{ example: number }>({ example: 100 });

    // $ExpectError
    new Parse.User<{ example: number }>({ example: 'hello' });
  }

  async function testStatic() {
    const user = new Parse.User<{ field: 'hello' }>();

    // $ExpectType ParseUser<{ field: "hello"; }> | null
    Parse.User.current<Parse.User<{ field: 'hello' }>>();

    // $ExpectType ParseUser<{ field: "hello"; }> | null
    await Parse.User.currentAsync<Parse.User<{ field: 'hello' }>>();

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.signUp<Parse.User<{ field: 'hello' }>>('username', 'password', {});

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.become<Parse.User<{ field: 'hello' }>>('session-token');

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.logIn<Parse.User<{ field: 'hello' }>>('username', 'password');

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.logInWith<Parse.User<{ field: 'hello' }>>('provider', {});

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.logInWithAdditionalAuth<Parse.User<{ field: 'hello' }>>(
      'username',
      'password',
      {}
    );

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.loginAs<Parse.User<{ field: 'hello' }>>(user.id!);

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.verifyPassword<Parse.User<{ field: 'hello' }>>('username', 'password');

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.hydrate<Parse.User<{ field: 'hello' }>>(user);

    // $ExpectType ParseUser<{ field: "hello"; }>
    await Parse.User.me<Parse.User<{ field: 'hello' }>>('session-token');
  }

  async function testAuthenticationProvider() {
    const authProvider: Parse.AuthProvider = {
      authenticate: () => {},
      getAuthType: () => 'customAuthorizationProvider',
      restoreAuthentication: () => false,
      deauthenticate: () => {},
    };
    const authData: Parse.AuthData = {
      id: 'some-user-authentication-id',
      access_token: 'some-access-token',
      expiration_date: new Date().toISOString(),
    };
    Parse.User._registerAuthenticationProvider(authProvider);

    const user = await Parse.User.logInWith(
      authProvider,
      { authData },
      { sessionToken: 'some-session-token', useMasterKey: true }
    );
    const isLinked = user._isLinked(authProvider);
    const unlinkedUser = await user._unlinkFrom(authProvider);
    const linkedUser = await user.linkWith(authProvider, { authData });
  }
}

function testEncryptingUser() {
  function testSecretKey() {
    Parse.secret = 'secret!';
  }

  function testEnableEncryptedUserKey() {
    Parse.encryptedUser = true;
  }

  function testEnablingEncryptedUser() {
    Parse.enableEncryptedUser();
  }

  function testIsEncryptedUserEnabled() {
    Parse.isEncryptedUserEnabled();
  }
}

function testEventuallyQueue() {
  async function test() {
    const obj = new Parse.Object('TestObject');
    // $ExpectType void
    await Parse.EventuallyQueue.clear();
    // $ExpectType Queue
    await Parse.EventuallyQueue.getQueue();
    // $ExpectType boolean
    Parse.EventuallyQueue.isPolling();
    // $ExpectType void
    await Parse.EventuallyQueue.save(obj);
    // $ExpectType void
    await Parse.EventuallyQueue.save(obj, {});
    // $ExpectType void
    await Parse.EventuallyQueue.destroy(obj);
    // $ExpectType void
    await Parse.EventuallyQueue.destroy(obj, {});
    // $ExpectType number
    await Parse.EventuallyQueue.length();
    // $ExpectType boolean
    await Parse.EventuallyQueue.sendQueue();
    // $ExpectType void
    Parse.EventuallyQueue.stopPoll();
    // $ExpectType void
    Parse.EventuallyQueue.poll();
    // $ExpectType void
    Parse.EventuallyQueue.poll(300);
    // $ExpectError
    Parse.EventuallyQueue.poll('300');
  }
}

// TODO: Add missing LiveQuery types (LiveQuerySubscription, etc.)
function LiveQueryEvents() {
  function testLiveQueryEvents() {
    Parse.LiveQuery.on('open', () => {});
    Parse.LiveQuery.on('close', () => {});
    Parse.LiveQuery.on('error', (error: any) => {});
  }
}

function testInitialize() {
  // Browser - 2 params (strict)
  Parse.initialize('appId', 'jsKey');

  // Browser - should error when trying to pass masterKey
  // @ts-expect-error - Browser build should not accept masterKey
  Parse.initialize('appId', 'jsKey', 'masterKey');

  // Node - permissive 4 params (should work)
  ParseNode.initialize('appId', 'jsKey', 'masterKey', 'maintenanceKey');

  // Node - 3 params (should also work)
  ParseNode.initialize('appId', 'jsKey', 'masterKey');

  // Node - 2 params (should also work)
  ParseNode.initialize('appId', 'jsKey');

  // Node - 1 param (should also work since javaScriptKey is optional in node)
  ParseNode.initialize('appId');
}
// Test Parse.* and namespace type exports
async function test_type_exports() {
  // Parse.Object.* namespace
  const objDestroyOpts: Parse.Object.DestroyOptions = { useMasterKey: true, sessionToken: 'token' };
  const objDestroyAllOpts: Parse.Object.DestroyAllOptions = { batchSize: 100, useMasterKey: true };
  const objFetchOpts: Parse.Object.FetchOptions = { useMasterKey: true, include: ['rel'] };
  const objFetchAllOpts: Parse.Object.FetchAllOptions = { useMasterKey: true };
  const objSaveOpts: Parse.Object.SaveOptions = { useMasterKey: true, cascadeSave: false, context: {} };
  const objSaveAllOpts: Parse.Object.SaveAllOptions = { batchSize: 50, useMasterKey: true };
  const objSetOpts: Parse.Object.SetOptions = { ignoreValidation: true };
  type ObjEncode = Parse.Object.Encode<Date>;
  type ObjToJSON = Parse.Object.ToJSON<{ name: string }>;

  // Parse.Query.* namespace
  const queryOpts: Parse.Query.QueryOptions = { useMasterKey: true, sessionToken: 'token', json: true };
  const queryFindOpts: Parse.Query.FindOptions = { useMasterKey: true, context: {} };
  const queryBatchOpts: Parse.Query.BatchOptions = { batchSize: 100, useMasterKey: true };
  const queryFullTextOpts: Parse.Query.FullTextOptions = { language: 'en', caseSensitive: false };

  // Parse.Schema.* namespace
  const schemaTypes: Parse.Schema.TYPE[] = ['String', 'Number', 'Boolean', 'Date', 'File', 'GeoPoint', 'Polygon', 'Array', 'Object', 'Pointer', 'Relation', 'Bytes'];

  // Direct Parse.* exports
  const destroyOpts: Parse.DestroyOptions = { useMasterKey: true };
  const fetchOpts: Parse.FetchOptions = { useMasterKey: true, include: ['rel'] };
  const saveOpts: Parse.SaveOptions = { useMasterKey: true, cascadeSave: true };
  const setOpts: Parse.SetOptions = { ignoreValidation: false };
  const destroyAllOpts: Parse.DestroyAllOptions = { batchSize: 100 };
  const saveAllOpts: Parse.SaveAllOptions = { batchSize: 50 };
  const fetchAllOpts: Parse.FetchAllOptions = { useMasterKey: true };
  const findOpts: Parse.FindOptions = { useMasterKey: true, json: true };
  const batchOpts: Parse.BatchOptions = { batchSize: 100 };
  const fullTextOpts: Parse.FullTextOptions = { language: 'en' };
  const typeVal: Parse.TYPE = 'String';
  type DirectEncode = Parse.Encode<Date>;
  type DirectToJSON = Parse.ToJSON<{ x: number }>;

  // Core types
  const pointer: Parse.Pointer = { __type: 'Pointer', className: 'Test', objectId: 'abc' };
  const attrs: Parse.Attributes = { score: 100 };
  const baseAttrs: Parse.BaseAttributes = { createdAt: new Date(), updatedAt: new Date(), objectId: 'abc' };
  const jsonBaseAttrs: Parse.JSONBaseAttributes = { createdAt: '2023-01-01T00:00:00Z', updatedAt: '2023-01-01T00:00:00Z', objectId: 'abc' };
  const whereClause: Parse.WhereClause = { score: { $gt: 100 } };
  const queryJson: Parse.QueryJSON = { where: {}, limit: 10 };
  const fullOpts: Parse.FullOptions = { useMasterKey: true };
  const reqOpts: Parse.RequestOptions = { useMasterKey: true, batchSize: 50 };
  const authProvider: Parse.AuthProvider = { authenticate: () => {}, getAuthType: () => 'custom', restoreAuthentication: () => true };
  const authData: Parse.AuthData = { id: 'user-id' };
  const signUpOpts: Parse.SignUpOptions = { useMasterKey: true };
  const restSchema: Parse.RestSchema = { className: 'Test', fields: {}, classLevelPermissions: {} };
  const pushData: Parse.PushData = { channels: ['news'], data: { alert: 'Hello' } };
  const sendOpts: Parse.SendOptions = { useMasterKey: true };
  const cloudRunOpts: Parse.Cloud.RunOptions = { useMasterKey: true, context: {} };
  const subscription: Parse.LiveQuerySubscription = await new Parse.Query('Test').subscribe();

  class MyClass extends Parse.Object {
    constructor() {
      super('MyClass');
    }
  }
  // ObjectStatic with generic subclasses
  const objectStatic: Parse.ObjectStatic = MyClass;
  function doCreateWithoutData<T extends Parse.Object>(clz: Parse.ObjectStatic<T>, id: string): T {
    return clz.createWithoutData(id);
  }
  // $ExpectType MyClass
  const myClsObj = doCreateWithoutData(MyClass, '1');

  // Verify types work with actual methods
  const obj = new Parse.Object('Test');
  const query = new Parse.Query('Test');

  await obj.save(null, saveOpts);
  await obj.fetch(fetchOpts);
  await obj.destroy(destroyOpts);
  obj.set('key', 'value', setOpts);
  await Parse.Object.saveAll([obj], saveAllOpts);
  await Parse.Object.destroyAll([obj], destroyAllOpts);
  await Parse.Object.fetchAll([obj], fetchAllOpts);
  await query.find(findOpts);
  await query.first(findOpts);
  await query.count(findOpts);
  await query.each(() => {}, batchOpts);
  await query.eachBatch(() => {}, batchOpts);
  query.fullText('field', 'term', fullTextOpts);
}
