const Parse = require('parse/node');

const Id = require('../lib/LiveQuery/Id');
const QueryTools = require('../lib/LiveQuery/QueryTools');
const queryHash = QueryTools.queryHash;
const matchesQuery = QueryTools.matchesQuery;

const Item = Parse.Object.extend('Item');

describe('queryHash', function () {
  it('should always hash a query to the same string', function () {
    const q = new Parse.Query(Item);
    q.equalTo('field', 'value');
    q.exists('name');
    q.ascending('createdAt');
    q.limit(10);
    const firstHash = queryHash(q);
    const secondHash = queryHash(q);
    expect(firstHash).toBe(secondHash);
  });

  it('should return equivalent hashes for equivalent queries', function () {
    let q1 = new Parse.Query(Item);
    q1.equalTo('field', 'value');
    q1.exists('name');
    q1.lessThan('age', 30);
    q1.greaterThan('age', 3);
    q1.ascending('createdAt');
    q1.include(['name', 'age']);
    q1.limit(10);

    let q2 = new Parse.Query(Item);
    q2.limit(10);
    q2.greaterThan('age', 3);
    q2.lessThan('age', 30);
    q2.include(['name', 'age']);
    q2.ascending('createdAt');
    q2.exists('name');
    q2.equalTo('field', 'value');

    let firstHash = queryHash(q1);
    let secondHash = queryHash(q2);
    expect(firstHash).toBe(secondHash);

    q1.containedIn('fruit', ['apple', 'banana', 'cherry']);
    firstHash = queryHash(q1);
    expect(firstHash).not.toBe(secondHash);

    q2.containedIn('fruit', ['banana', 'cherry', 'apple']);
    secondHash = queryHash(q2);
    expect(secondHash).toBe(firstHash);

    q1.containedIn('fruit', ['coconut']);
    firstHash = queryHash(q1);
    expect(firstHash).not.toBe(secondHash);

    q1 = new Parse.Query(Item);
    q1.equalTo('field', 'value');
    q1.lessThan('age', 30);
    q1.exists('name');

    q2 = new Parse.Query(Item);
    q2.equalTo('name', 'person');
    q2.equalTo('field', 'other');

    firstHash = queryHash(Parse.Query.or(q1, q2));
    secondHash = queryHash(Parse.Query.or(q2, q1));
    expect(firstHash).toBe(secondHash);
  });

  it('should not let fields of different types appear similar', function () {
    let q1 = new Parse.Query(Item);
    q1.lessThan('age', 30);

    const q2 = new Parse.Query(Item);
    q2.equalTo('age', '{$lt:30}');

    expect(queryHash(q1)).not.toBe(queryHash(q2));

    q1 = new Parse.Query(Item);
    q1.equalTo('age', 15);

    q2.equalTo('age', '15');

    expect(queryHash(q1)).not.toBe(queryHash(q2));
  });
});

describe('matchesQuery', function () {
  it('matches blanket queries', function () {
    const obj = {
      id: new Id('Klass', 'O1'),
      value: 12,
    };
    const q = new Parse.Query('Klass');
    expect(matchesQuery(obj, q)).toBe(true);

    obj.id = new Id('Other', 'O1');
    expect(matchesQuery(obj, q)).toBe(false);
  });

  it('matches existence queries', function () {
    const obj = {
      id: new Id('Item', 'O1'),
      count: 15,
    };
    const q = new Parse.Query('Item');
    q.exists('count');
    expect(matchesQuery(obj, q)).toBe(true);
    q.exists('name');
    expect(matchesQuery(obj, q)).toBe(false);
  });

  it('matches queries with doesNotExist constraint', function () {
    const obj = {
      id: new Id('Item', 'O1'),
      count: 15,
    };
    let q = new Parse.Query('Item');
    q.doesNotExist('name');
    expect(matchesQuery(obj, q)).toBe(true);

    q = new Parse.Query('Item');
    q.doesNotExist('count');
    expect(matchesQuery(obj, q)).toBe(false);
  });

  it('matches queries with eq constraint', function () {
    const obj = {
      objectId: 'Person2',
      score: 12,
      name: 'Tom',
    };

    const q1 = {
      objectId: {
        $eq: 'Person2',
      },
    };

    const q2 = {
      score: {
        $eq: 12,
      },
    };

    const q3 = {
      name: {
        $eq: 'Tom',
      },
    };
    expect(matchesQuery(obj, q1)).toBe(true);
    expect(matchesQuery(obj, q2)).toBe(true);
    expect(matchesQuery(obj, q3)).toBe(true);
  });

  it('matches on equality queries', function () {
    const day = new Date();
    const location = new Parse.GeoPoint({
      latitude: 37.484815,
      longitude: -122.148377,
    });
    const obj = {
      id: new Id('Person', 'O1'),
      score: 12,
      name: 'Bill',
      birthday: day,
      lastLocation: location,
    };

    let q = new Parse.Query('Person');
    q.equalTo('score', 12);
    expect(matchesQuery(obj, q)).toBe(true);

    q = new Parse.Query('Person');
    q.equalTo('name', 'Bill');
    expect(matchesQuery(obj, q)).toBe(true);
    q.equalTo('name', 'Jeff');
    expect(matchesQuery(obj, q)).toBe(false);

    q = new Parse.Query('Person');
    q.containedIn('name', ['Adam', 'Ben', 'Charles']);
    expect(matchesQuery(obj, q)).toBe(false);
    q.containedIn('name', ['Adam', 'Bill', 'Charles']);
    expect(matchesQuery(obj, q)).toBe(true);

    q = new Parse.Query('Person');
    q.notContainedIn('name', ['Adam', 'Bill', 'Charles']);
    expect(matchesQuery(obj, q)).toBe(false);
    q.notContainedIn('name', ['Adam', 'Ben', 'Charles']);
    expect(matchesQuery(obj, q)).toBe(true);

    q = new Parse.Query('Person');
    q.equalTo('birthday', day);
    expect(matchesQuery(obj, q)).toBe(true);
    q.equalTo('birthday', new Date(1990, 1));
    expect(matchesQuery(obj, q)).toBe(false);

    q = new Parse.Query('Person');
    q.equalTo(
      'lastLocation',
      new Parse.GeoPoint({
        latitude: 37.484815,
        longitude: -122.148377,
      })
    );
    expect(matchesQuery(obj, q)).toBe(true);
    q.equalTo(
      'lastLocation',
      new Parse.GeoPoint({
        latitude: 37.4848,
        longitude: -122.1483,
      })
    );
    expect(matchesQuery(obj, q)).toBe(false);

    q.equalTo(
      'lastLocation',
      new Parse.GeoPoint({
        latitude: 37.484815,
        longitude: -122.148377,
      })
    );
    q.equalTo('score', 12);
    q.equalTo('name', 'Bill');
    q.equalTo('birthday', day);
    expect(matchesQuery(obj, q)).toBe(true);

    q.equalTo('name', 'bill');
    expect(matchesQuery(obj, q)).toBe(false);

    let img = {
      id: new Id('Image', 'I1'),
      tags: ['nofilter', 'latergram', 'tbt'],
    };

    q = new Parse.Query('Image');
    q.equalTo('tags', 'selfie');
    expect(matchesQuery(img, q)).toBe(false);
    q.equalTo('tags', 'tbt');
    expect(matchesQuery(img, q)).toBe(true);

    const q2 = new Parse.Query('Image');
    q2.containsAll('tags', ['latergram', 'nofilter']);
    expect(matchesQuery(img, q2)).toBe(true);
    q2.containsAll('tags', ['latergram', 'selfie']);
    expect(matchesQuery(img, q2)).toBe(false);

    const u = new Parse.User();
    u.id = 'U2';
    q = new Parse.Query('Image');
    q.equalTo('owner', u);

    img = {
      className: 'Image',
      objectId: 'I1',
      owner: {
        className: '_User',
        objectId: 'U2',
      },
    };
    expect(matchesQuery(img, q)).toBe(true);

    img.owner.objectId = 'U3';
    expect(matchesQuery(img, q)).toBe(false);

    // pointers in arrays
    q = new Parse.Query('Image');
    q.equalTo('owners', u);

    img = {
      className: 'Image',
      objectId: 'I1',
      owners: [
        {
          className: '_User',
          objectId: 'U2',
        },
      ],
    };
    expect(matchesQuery(img, q)).toBe(true);

    img.owners[0].objectId = 'U3';
    expect(matchesQuery(img, q)).toBe(false);
  });

  it('matches on inequalities', function () {
    const player = {
      id: new Id('Person', 'O1'),
      score: 12,
      name: 'Bill',
      birthday: new Date(1980, 2, 4),
    };
    let q = new Parse.Query('Person');
    q.lessThan('score', 15);
    expect(matchesQuery(player, q)).toBe(true);
    q.lessThan('score', 10);
    expect(matchesQuery(player, q)).toBe(false);

    q = new Parse.Query('Person');
    q.lessThanOrEqualTo('score', 15);
    expect(matchesQuery(player, q)).toBe(true);
    q.lessThanOrEqualTo('score', 12);
    expect(matchesQuery(player, q)).toBe(true);
    q.lessThanOrEqualTo('score', 10);
    expect(matchesQuery(player, q)).toBe(false);

    q = new Parse.Query('Person');
    q.greaterThan('score', 15);
    expect(matchesQuery(player, q)).toBe(false);
    q.greaterThan('score', 10);
    expect(matchesQuery(player, q)).toBe(true);

    q = new Parse.Query('Person');
    q.greaterThanOrEqualTo('score', 15);
    expect(matchesQuery(player, q)).toBe(false);
    q.greaterThanOrEqualTo('score', 12);
    expect(matchesQuery(player, q)).toBe(true);
    q.greaterThanOrEqualTo('score', 10);
    expect(matchesQuery(player, q)).toBe(true);

    q = new Parse.Query('Person');
    q.notEqualTo('score', 12);
    expect(matchesQuery(player, q)).toBe(false);
    q.notEqualTo('score', 40);
    expect(matchesQuery(player, q)).toBe(true);
  });

  it('matches an $or query', function () {
    const player = {
      id: new Id('Player', 'P1'),
      name: 'Player 1',
      score: 12,
    };
    const q = new Parse.Query('Player');
    q.equalTo('name', 'Player 1');
    const q2 = new Parse.Query('Player');
    q2.equalTo('name', 'Player 2');
    const orQuery = Parse.Query.or(q, q2);
    expect(matchesQuery(player, q)).toBe(true);
    expect(matchesQuery(player, q2)).toBe(false);
    expect(matchesQuery(player, orQuery)).toBe(true);
  });

  it('does not match $all query when value is missing', () => {
    const player = {
      id: new Id('Player', 'P1'),
      name: 'Player 1',
      score: 12,
    };
    const q = { missing: { $all: [1, 2, 3] } };
    expect(matchesQuery(player, q)).toBe(false);
  });

  it('matches an $and query', () => {
    const player = {
      id: new Id('Player', 'P1'),
      name: 'Player 1',
      score: 12,
    };

    const q = new Parse.Query('Player');
    q.equalTo('name', 'Player 1');
    const q2 = new Parse.Query('Player');
    q2.equalTo('score', 12);
    const q3 = new Parse.Query('Player');
    q3.equalTo('score', 100);
    const andQuery1 = Parse.Query.and(q, q2);
    const andQuery2 = Parse.Query.and(q, q3);
    expect(matchesQuery(player, q)).toBe(true);
    expect(matchesQuery(player, q2)).toBe(true);
    expect(matchesQuery(player, andQuery1)).toBe(true);
    expect(matchesQuery(player, andQuery2)).toBe(false);
  });

  it('matches an $nor query', () => {
    const player = {
      id: new Id('Player', 'P1'),
      name: 'Player 1',
      score: 12,
    };

    const q = new Parse.Query('Player');
    q.equalTo('name', 'Player 1');
    const q2 = new Parse.Query('Player');
    q2.equalTo('name', 'Player 2');
    const q3 = new Parse.Query('Player');
    q3.equalTo('name', 'Player 3');

    const norQuery1 = Parse.Query.nor(q, q2);
    const norQuery2 = Parse.Query.nor(q2, q3);
    expect(matchesQuery(player, q)).toBe(true);
    expect(matchesQuery(player, q2)).toBe(false);
    expect(matchesQuery(player, q3)).toBe(false);
    expect(matchesQuery(player, norQuery1)).toBe(false);
    expect(matchesQuery(player, norQuery2)).toBe(true);
  });

  it('matches $regex queries', function () {
    const player = {
      id: new Id('Player', 'P1'),
      name: 'Player 1',
      score: 12,
    };

    let q = new Parse.Query('Player');
    q.startsWith('name', 'Play');
    expect(matchesQuery(player, q)).toBe(true);
    q.startsWith('name', 'Ploy');
    expect(matchesQuery(player, q)).toBe(false);

    q = new Parse.Query('Player');
    q.endsWith('name', ' 1');
    expect(matchesQuery(player, q)).toBe(true);
    q.endsWith('name', ' 2');
    expect(matchesQuery(player, q)).toBe(false);

    // Check that special characters are escaped
    player.name = 'Android-7';
    q = new Parse.Query('Player');
    q.contains('name', 'd-7');
    expect(matchesQuery(player, q)).toBe(true);

    q = new Parse.Query('Player');
    q.matches('name', /A.d/);
    expect(matchesQuery(player, q)).toBe(true);

    q.matches('name', /A[^n]d/);
    expect(matchesQuery(player, q)).toBe(false);

    // Check that the string \\E is returned to normal
    player.name = 'Slash \\E';
    q = new Parse.Query('Player');
    q.endsWith('name', 'h \\E');
    expect(matchesQuery(player, q)).toBe(true);

    q.endsWith('name', 'h \\Ee');
    expect(matchesQuery(player, q)).toBe(false);

    player.name = 'Slash \\Q and more';
    q = new Parse.Query('Player');
    q.contains('name', 'h \\Q and');
    expect(matchesQuery(player, q)).toBe(true);
    q.contains('name', 'h \\Q or');
    expect(matchesQuery(player, q)).toBe(false);
  });

  it('matches $nearSphere queries', function () {
    let q = new Parse.Query('Checkin');
    q.near('location', new Parse.GeoPoint(20, 20));
    // With no max distance, any GeoPoint is 'near'
    const pt = {
      id: new Id('Checkin', 'C1'),
      location: new Parse.GeoPoint(40, 40),
    };
    const ptUndefined = {
      id: new Id('Checkin', 'C1'),
    };
    const ptNull = {
      id: new Id('Checkin', 'C1'),
      location: null,
    };
    expect(matchesQuery(pt, q)).toBe(true);
    expect(matchesQuery(ptUndefined, q)).toBe(false);
    expect(matchesQuery(ptNull, q)).toBe(false);

    q = new Parse.Query('Checkin');
    pt.location = new Parse.GeoPoint(40, 40);
    q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.3);
    expect(matchesQuery(pt, q)).toBe(true);

    q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.2);
    expect(matchesQuery(pt, q)).toBe(false);
  });

  it('matches $within queries', function () {
    const caltrainStation = {
      id: new Id('Checkin', 'C1'),
      location: new Parse.GeoPoint(37.776346, -122.394218),
      name: 'Caltrain',
    };

    const santaClara = {
      id: new Id('Checkin', 'C2'),
      location: new Parse.GeoPoint(37.325635, -121.945753),
      name: 'Santa Clara',
    };

    const noLocation = {
      id: new Id('Checkin', 'C2'),
      name: 'Santa Clara',
    };

    const nullLocation = {
      id: new Id('Checkin', 'C2'),
      location: null,
      name: 'Santa Clara',
    };

    let q = new Parse.Query('Checkin').withinGeoBox(
      'location',
      new Parse.GeoPoint(37.708813, -122.526398),
      new Parse.GeoPoint(37.822802, -122.373962)
    );

    expect(matchesQuery(caltrainStation, q)).toBe(true);
    expect(matchesQuery(santaClara, q)).toBe(false);
    expect(matchesQuery(noLocation, q)).toBe(false);
    expect(matchesQuery(nullLocation, q)).toBe(false);
    // Invalid rectangles
    q = new Parse.Query('Checkin').withinGeoBox(
      'location',
      new Parse.GeoPoint(37.822802, -122.373962),
      new Parse.GeoPoint(37.708813, -122.526398)
    );

    expect(matchesQuery(caltrainStation, q)).toBe(false);
    expect(matchesQuery(santaClara, q)).toBe(false);

    q = new Parse.Query('Checkin').withinGeoBox(
      'location',
      new Parse.GeoPoint(37.708813, -122.373962),
      new Parse.GeoPoint(37.822802, -122.526398)
    );

    expect(matchesQuery(caltrainStation, q)).toBe(false);
    expect(matchesQuery(santaClara, q)).toBe(false);
  });

  it('matches on subobjects with dot notation', function () {
    const message = {
      id: new Id('Message', 'O1'),
      text: 'content',
      status: { x: 'read', y: 'delivered' },
    };

    let q = new Parse.Query('Message');
    q.equalTo('status.x', 'read');
    expect(matchesQuery(message, q)).toBe(true);

    q = new Parse.Query('Message');
    q.equalTo('status.z', 'read');
    expect(matchesQuery(message, q)).toBe(false);

    q = new Parse.Query('Message');
    q.equalTo('status.x', 'delivered');
    expect(matchesQuery(message, q)).toBe(false);

    q = new Parse.Query('Message');
    q.notEqualTo('status.x', 'read');
    expect(matchesQuery(message, q)).toBe(false);

    q = new Parse.Query('Message');
    q.notEqualTo('status.z', 'read');
    expect(matchesQuery(message, q)).toBe(true);

    q = new Parse.Query('Message');
    q.notEqualTo('status.x', 'delivered');
    expect(matchesQuery(message, q)).toBe(true);

    q = new Parse.Query('Message');
    q.exists('status.x');
    expect(matchesQuery(message, q)).toBe(true);

    q = new Parse.Query('Message');
    q.exists('status.z');
    expect(matchesQuery(message, q)).toBe(false);

    q = new Parse.Query('Message');
    q.exists('nonexistent.x');
    expect(matchesQuery(message, q)).toBe(false);

    q = new Parse.Query('Message');
    q.doesNotExist('status.x');
    expect(matchesQuery(message, q)).toBe(false);

    q = new Parse.Query('Message');
    q.doesNotExist('status.z');
    expect(matchesQuery(message, q)).toBe(true);

    q = new Parse.Query('Message');
    q.doesNotExist('nonexistent.z');
    expect(matchesQuery(message, q)).toBe(true);

    q = new Parse.Query('Message');
    q.equalTo('status.x', 'read');
    q.doesNotExist('status.y');
    expect(matchesQuery(message, q)).toBe(false);
  });

  function pointer(className, objectId) {
    return { __type: 'Pointer', className, objectId };
  }

  it('should support containedIn with pointers', () => {
    const message = {
      id: new Id('Message', 'O1'),
      profile: pointer('Profile', 'abc'),
    };
    let q = new Parse.Query('Message');
    q.containedIn('profile', [
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'abc' }),
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }),
    ]);
    expect(matchesQuery(message, q)).toBe(true);

    q = new Parse.Query('Message');
    q.containedIn('profile', [
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' }),
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }),
    ]);
    expect(matchesQuery(message, q)).toBe(false);
  });

  it('should support containedIn with array of pointers', () => {
    const message = {
      id: new Id('Message', 'O2'),
      profiles: [pointer('Profile', 'yeahaw'), pointer('Profile', 'yes')],
    };

    let q = new Parse.Query('Message');
    q.containedIn('profiles', [
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'no' }),
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'yes' }),
    ]);

    expect(matchesQuery(message, q)).toBe(true);

    q = new Parse.Query('Message');
    q.containedIn('profiles', [
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'no' }),
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'nope' }),
    ]);

    expect(matchesQuery(message, q)).toBe(false);
  });

  it('should support notContainedIn with pointers', () => {
    let message = {
      id: new Id('Message', 'O1'),
      profile: pointer('Profile', 'abc'),
    };
    let q = new Parse.Query('Message');
    q.notContainedIn('profile', [
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }),
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' }),
    ]);
    expect(matchesQuery(message, q)).toBe(true);

    message = {
      id: new Id('Message', 'O1'),
      profile: pointer('Profile', 'def'),
    };
    q = new Parse.Query('Message');
    q.notContainedIn('profile', [
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' }),
      Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }),
    ]);
    expect(matchesQuery(message, q)).toBe(false);
  });

  it('should support containedIn queries with [objectId]', () => {
    let message = {
      id: new Id('Message', 'O1'),
      profile: pointer('Profile', 'abc'),
    };
    let q = new Parse.Query('Message');
    q.containedIn('profile', ['abc', 'def']);
    expect(matchesQuery(message, q)).toBe(true);

    message = {
      id: new Id('Message', 'O1'),
      profile: pointer('Profile', 'ghi'),
    };
    q = new Parse.Query('Message');
    q.containedIn('profile', ['abc', 'def']);
    expect(matchesQuery(message, q)).toBe(false);
  });

  it('should support notContainedIn queries with [objectId]', () => {
    let message = {
      id: new Id('Message', 'O1'),
      profile: pointer('Profile', 'ghi'),
    };
    let q = new Parse.Query('Message');
    q.notContainedIn('profile', ['abc', 'def']);
    expect(matchesQuery(message, q)).toBe(true);
    message = {
      id: new Id('Message', 'O1'),
      profile: pointer('Profile', 'ghi'),
    };
    q = new Parse.Query('Message');
    q.notContainedIn('profile', ['abc', 'def', 'ghi']);
    expect(matchesQuery(message, q)).toBe(false);
  });

  it('matches on Date', () => {
    // given
    const now = new Date();
    const obj = {
      id: new Id('Person', '01'),
      dateObject: now,
      dateJSON: {
        __type: 'Date',
        iso: now.toISOString(),
      },
    };

    // when, then: Equal
    let q = new Parse.Query('Person');
    q.equalTo('dateObject', now);
    q.equalTo('dateJSON', now);
    expect(matchesQuery(Object.assign({}, obj), q)).toBe(true);

    // when, then: lessThan
    const future = Date(now.getTime() + 1000);
    q = new Parse.Query('Person');
    q.lessThan('dateObject', future);
    q.lessThan('dateJSON', future);
    expect(matchesQuery(Object.assign({}, obj), q)).toBe(true);

    // when, then: lessThanOrEqualTo
    q = new Parse.Query('Person');
    q.lessThanOrEqualTo('dateObject', now);
    q.lessThanOrEqualTo('dateJSON', now);
    expect(matchesQuery(Object.assign({}, obj), q)).toBe(true);

    // when, then: greaterThan
    const past = Date(now.getTime() - 1000);
    q = new Parse.Query('Person');
    q.greaterThan('dateObject', past);
    q.greaterThan('dateJSON', past);
    expect(matchesQuery(Object.assign({}, obj), q)).toBe(true);

    // when, then: greaterThanOrEqualTo
    q = new Parse.Query('Person');
    q.greaterThanOrEqualTo('dateObject', now);
    q.greaterThanOrEqualTo('dateJSON', now);
    expect(matchesQuery(Object.assign({}, obj), q)).toBe(true);
  });

  it('should support containedBy query', () => {
    const obj1 = {
      id: new Id('Numbers', 'N1'),
      numbers: [0, 1, 2],
    };
    const obj2 = {
      id: new Id('Numbers', 'N2'),
      numbers: [2, 0],
    };
    const obj3 = {
      id: new Id('Numbers', 'N3'),
      numbers: [1, 2, 3, 4],
    };

    const q = new Parse.Query('Numbers');
    q.containedBy('numbers', [1, 2, 3, 4, 5]);
    expect(matchesQuery(obj1, q)).toBe(false);
    expect(matchesQuery(obj2, q)).toBe(false);
    expect(matchesQuery(obj3, q)).toBe(true);
  });

  it('should support withinPolygon query', () => {
    const sacramento = {
      id: new Id('Location', 'L1'),
      location: new Parse.GeoPoint(38.52, -121.5),
      name: 'Sacramento',
    };
    const honolulu = {
      id: new Id('Location', 'L2'),
      location: new Parse.GeoPoint(21.35, -157.93),
      name: 'Honolulu',
    };
    const sf = {
      id: new Id('Location', 'L3'),
      location: new Parse.GeoPoint(37.75, -122.68),
      name: 'San Francisco',
    };

    const points = [
      new Parse.GeoPoint(37.85, -122.33),
      new Parse.GeoPoint(37.85, -122.9),
      new Parse.GeoPoint(37.68, -122.9),
      new Parse.GeoPoint(37.68, -122.33),
    ];
    const q = new Parse.Query('Location');
    q.withinPolygon('location', points);

    expect(matchesQuery(sacramento, q)).toBe(false);
    expect(matchesQuery(honolulu, q)).toBe(false);
    expect(matchesQuery(sf, q)).toBe(true);
  });

  it('should support polygonContains query', () => {
    const p1 = [
      [0, 0],
      [0, 1],
      [1, 1],
      [1, 0],
    ];
    const p2 = [
      [0, 0],
      [0, 2],
      [2, 2],
      [2, 0],
    ];
    const p3 = [
      [10, 10],
      [10, 15],
      [15, 15],
      [15, 10],
      [10, 10],
    ];

    const obj1 = {
      id: new Id('Bounds', 'B1'),
      polygon: new Parse.Polygon(p1),
    };
    const obj2 = {
      id: new Id('Bounds', 'B2'),
      polygon: new Parse.Polygon(p2),
    };
    const obj3 = {
      id: new Id('Bounds', 'B3'),
      polygon: new Parse.Polygon(p3),
    };

    const point = new Parse.GeoPoint(0.5, 0.5);
    const q = new Parse.Query('Bounds');
    q.polygonContains('polygon', point);

    expect(matchesQuery(obj1, q)).toBe(true);
    expect(matchesQuery(obj2, q)).toBe(true);
    expect(matchesQuery(obj3, q)).toBe(false);
  });
});
