const ParseServerRESTController = require('../lib/ParseServerRESTController')
  .ParseServerRESTController;
const ParseServer = require('../lib/ParseServer').default;
const Parse = require('parse/node').Parse;

let RESTController;

describe('ParseServerRESTController', () => {
  let createSpy;
  beforeEach(() => {
    RESTController = ParseServerRESTController(
      Parse.applicationId,
      ParseServer.promiseRouter({ appId: Parse.applicationId })
    );
    createSpy = spyOn(databaseAdapter, 'createObject').and.callThrough();
  });

  it('should handle a get request', async () => {
    const res = await RESTController.request('GET', '/classes/MyObject');
    expect(res.results.length).toBe(0);
  });

  it('should handle a get request with full serverURL mount path', async () => {
    const res = await RESTController.request('GET', '/1/classes/MyObject');
    expect(res.results.length).toBe(0);
  });

  it('should handle a POST batch without transaction', async () => {
    const res = await RESTController.request('POST', 'batch', {
      requests: [
        {
          method: 'GET',
          path: '/classes/MyObject',
        },
        {
          method: 'POST',
          path: '/classes/MyObject',
          body: { key: 'value' },
        },
        {
          method: 'GET',
          path: '/classes/MyObject',
        },
      ],
    });
    expect(res.length).toBe(3);
  });

  it('should handle a POST batch with transaction=false', async () => {
    const res = await RESTController.request('POST', 'batch', {
      requests: [
        {
          method: 'GET',
          path: '/classes/MyObject',
        },
        {
          method: 'POST',
          path: '/classes/MyObject',
          body: { key: 'value' },
        },
        {
          method: 'GET',
          path: '/classes/MyObject',
        },
      ],
      transaction: false,
    });
    expect(res.length).toBe(3);
  });

  it('should handle response status', async () => {
    const router = ParseServer.promiseRouter({ appId: Parse.applicationId });
    spyOn(router, 'tryRouteRequest').and.callThrough();
    RESTController = ParseServerRESTController(Parse.applicationId, router);
    const resp = await RESTController.request('POST', '/classes/MyObject');
    const { status, response, location } = await router.tryRouteRequest.calls.all()[0].returnValue;

    expect(status).toBe(201);
    expect(response).toEqual(resp);
    expect(location).toBe(`http://localhost:8378/1/classes/MyObject/${resp.objectId}`);
  });

  it('should handle response status in batch', async () => {
    const router = ParseServer.promiseRouter({ appId: Parse.applicationId });
    spyOn(router, 'tryRouteRequest').and.callThrough();
    RESTController = ParseServerRESTController(Parse.applicationId, router);
    const resp = await RESTController.request(
      'POST',
      'batch',
      {
        requests: [
          {
            method: 'POST',
            path: '/classes/MyObject',
          },
          {
            method: 'POST',
            path: '/classes/MyObject',
          },
        ],
      },
      {
        returnStatus: true,
      }
    );
    expect(resp.length).toBe(2);
    expect(resp[0]._status).toBe(201);
    expect(resp[1]._status).toBe(201);
    expect(resp[0].success).toBeDefined();
    expect(resp[1].success).toBeDefined();
    expect(router.tryRouteRequest.calls.all().length).toBe(2);
  });

  it('properly handle existed', async done => {
    const restController = Parse.CoreManager.getRESTController();
    Parse.CoreManager.setRESTController(RESTController);
    Parse.Cloud.define('handleStatus', async () => {
      const obj = new Parse.Object('TestObject');
      expect(obj.existed()).toBe(false);
      await obj.save();
      expect(obj.existed()).toBe(false);

      const query = new Parse.Query('TestObject');
      const result = await query.get(obj.id);
      expect(result.existed()).toBe(true);
      Parse.CoreManager.setRESTController(restController);
      done();
    });
    await Parse.Cloud.run('handleStatus');
  });

  if (
    process.env.MONGODB_TOPOLOGY === 'replicaset' ||
    process.env.PARSE_SERVER_TEST_DB === 'postgres'
  ) {
    describe('transactions', () => {
      it('should handle a batch request with transaction = true', async () => {
        const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
        await myObject.save();
        await myObject.destroy();
        createSpy.calls.reset();
        const response = await RESTController.request('POST', 'batch', {
          requests: [
            {
              method: 'POST',
              path: '/1/classes/MyObject',
              body: { key: 'value1' },
            },
            {
              method: 'POST',
              path: '/1/classes/MyObject',
              body: { key: 'value2' },
            },
          ],
          transaction: true,
        });
        expect(response.length).toEqual(2);
        expect(response[0].success.objectId).toBeDefined();
        expect(response[0].success.createdAt).toBeDefined();
        expect(response[1].success.objectId).toBeDefined();
        expect(response[1].success.createdAt).toBeDefined();
        const query = new Parse.Query('MyObject');
        const results = await query.find();
        expect(createSpy.calls.count()).toBe(2);
        for (let i = 0; i + 1 < createSpy.calls.length; i = i + 2) {
          expect(createSpy.calls.argsFor(i)[3]).toBe(
            createSpy.calls.argsFor(i + 1)[3]
          );
        }
        expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
      });

      it('should not save anything when one operation fails in a transaction', async () => {
        const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
        await myObject.save({ key: 'stringField' });
        await myObject.destroy();
        createSpy.calls.reset();
        try {
          // Saving a number to a string field should fail
          await RESTController.request('POST', 'batch', {
            requests: [
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 'value1' },
              },
              {
                method: 'POST',
                path: '/1/classes/MyObject',
                body: { key: 10 },
              },
            ],
            transaction: true,
          });
          fail();
        } catch (error) {
          expect(error).toBeDefined();
          const query = new Parse.Query('MyObject');
          const results = await query.find();
          expect(results.length).toBe(0);
        }
      });

      it('should generate separate session for each call', async () => {
        await reconfigureServer();
        const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
        await myObject.save({ key: 'stringField' });
        await myObject.destroy();

        const myObject2 = new Parse.Object('MyObject2'); // This is important because transaction only works on pre-existing collections
        await myObject2.save({ key: 'stringField' });
        await myObject2.destroy();

        createSpy.calls.reset();

        let myObjectCalls = 0;
        Parse.Cloud.beforeSave('MyObject', async () => {
          myObjectCalls++;
          if (myObjectCalls === 2) {
            try {
              // Saving a number to a string field should fail
              await RESTController.request('POST', 'batch', {
                requests: [
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 'value1' },
                  },
                  {
                    method: 'POST',
                    path: '/1/classes/MyObject2',
                    body: { key: 10 },
                  },
                ],
                transaction: true,
              });
              fail('should fail');
            } catch (e) {
              expect(e).toBeDefined();
            }
          }
        });

        const response = await RESTController.request('POST', 'batch', {
          requests: [
            {
              method: 'POST',
              path: '/1/classes/MyObject',
              body: { key: 'value1' },
            },
            {
              method: 'POST',
              path: '/1/classes/MyObject',
              body: { key: 'value2' },
            },
          ],
          transaction: true,
        });

        expect(response.length).toEqual(2);
        expect(response[0].success.objectId).toBeDefined();
        expect(response[0].success.createdAt).toBeDefined();
        expect(response[1].success.objectId).toBeDefined();
        expect(response[1].success.createdAt).toBeDefined();

        await RESTController.request('POST', 'batch', {
          requests: [
            {
              method: 'POST',
              path: '/1/classes/MyObject3',
              body: { key: 'value1' },
            },
            {
              method: 'POST',
              path: '/1/classes/MyObject3',
              body: { key: 'value2' },
            },
          ],
        });

        const query = new Parse.Query('MyObject');
        const results = await query.find();
        expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);

        const query2 = new Parse.Query('MyObject2');
        const results2 = await query2.find();
        expect(results2.length).toEqual(0);

        const query3 = new Parse.Query('MyObject3');
        const results3 = await query3.find();
        expect(results3.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);

        expect(createSpy.calls.count() >= 13).toEqual(true);
        let transactionalSession;
        let transactionalSession2;
        let myObjectDBCalls = 0;
        let myObject2DBCalls = 0;
        let myObject3DBCalls = 0;
        for (let i = 0; i < createSpy.calls.count(); i++) {
          const args = createSpy.calls.argsFor(i);
          switch (args[0]) {
            case 'MyObject':
              myObjectDBCalls++;
              if (!transactionalSession || (myObjectDBCalls - 1) % 2 === 0) {
                transactionalSession = args[3];
              } else {
                expect(transactionalSession).toBe(args[3]);
              }
              if (transactionalSession2) {
                expect(transactionalSession2).not.toBe(args[3]);
              }
              break;
            case 'MyObject2':
              myObject2DBCalls++;
              if (!transactionalSession2 || (myObject2DBCalls - 1) % 9 === 0) {
                transactionalSession2 = args[3];
              } else {
                expect(transactionalSession2).toBe(args[3]);
              }
              if (transactionalSession) {
                expect(transactionalSession).not.toBe(args[3]);
              }
              break;
            case 'MyObject3':
              myObject3DBCalls++;
              expect(args[3]).toEqual(null);
              break;
          }
        }
        expect(myObjectDBCalls % 2).toEqual(0);
        expect(myObjectDBCalls > 0).toEqual(true);
        expect(myObject2DBCalls % 9).toEqual(0);
        expect(myObject2DBCalls > 0).toEqual(true);
        expect(myObject3DBCalls % 2).toEqual(0);
        expect(myObject3DBCalls > 0).toEqual(true);
      });
    });
  }

  it('should handle a POST request', async () => {
    await RESTController.request('POST', '/classes/MyObject', { key: 'value' });
    const res = await RESTController.request('GET', '/classes/MyObject');
    expect(res.results.length).toBe(1);
    expect(res.results[0].key).toEqual('value');
  });

  it('should handle a POST request with context', async () => {
    Parse.Cloud.beforeSave('MyObject', req => {
      expect(req.context.a).toEqual('a');
    });
    Parse.Cloud.afterSave('MyObject', req => {
      expect(req.context.a).toEqual('a');
    });

    await RESTController.request(
      'POST',
      '/classes/MyObject',
      { key: 'value' },
      { context: { a: 'a' } }
    );
  });

  it('ensures sessionTokens are properly handled', async () => {
    const user = await Parse.User.signUp('user', 'pass');
    const sessionToken = user.getSessionToken();
    const res = await RESTController.request('GET', '/users/me', undefined, {
      sessionToken,
    });
    // Result is in JSON format
    expect(res.objectId).toEqual(user.id);
  });

  it('ensures masterKey is properly handled', async () => {
    const user = await Parse.User.signUp('user', 'pass');
    const userId = user.id;
    await Parse.User.logOut();
    const res = await RESTController.request('GET', '/classes/_User', undefined, {
      useMasterKey: true,
    });
    expect(res.results.length).toBe(1);
    expect(res.results[0].objectId).toEqual(userId);
  });

  it('ensures no user is created when passing an empty username', async () => {
    try {
      await RESTController.request('POST', '/classes/_User', {
        username: '',
        password: 'world',
      });
      fail('Success callback should not be called when passing an empty username.');
    } catch (err) {
      expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
      expect(err.message).toBe('bad or missing username');
    }
  });

  it('ensures no user is created when passing an empty password', async () => {
    try {
      await RESTController.request('POST', '/classes/_User', {
        username: 'hello',
        password: '',
      });
      fail('Success callback should not be called when passing an empty password.');
    } catch (err) {
      expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
      expect(err.message).toBe('password is required');
    }
  });

  it('ensures no session token is created on creating users', async () => {
    const user = await RESTController.request('POST', '/classes/_User', {
      username: 'hello',
      password: 'world',
    });
    expect(user.sessionToken).toBeUndefined();
    const query = new Parse.Query('_Session');
    const sessions = await query.find({ useMasterKey: true });
    expect(sessions.length).toBe(0);
  });

  it('ensures a session token is created when passing installationId != cloud', async () => {
    const user = await RESTController.request(
      'POST',
      '/classes/_User',
      { username: 'hello', password: 'world' },
      { installationId: 'my-installation' }
    );
    expect(user.sessionToken).not.toBeUndefined();
    const query = new Parse.Query('_Session');
    const sessions = await query.find({ useMasterKey: true });
    expect(sessions.length).toBe(1);
    expect(sessions[0].get('installationId')).toBe('my-installation');
  });

  it('ensures logIn is saved with installationId', async () => {
    const installationId = 'installation123';
    const user = await RESTController.request(
      'POST',
      '/classes/_User',
      { username: 'hello', password: 'world' },
      { installationId }
    );
    expect(user.sessionToken).not.toBeUndefined();
    const query = new Parse.Query('_Session');
    let sessions = await query.find({ useMasterKey: true });

    expect(sessions.length).toBe(1);
    expect(sessions[0].get('installationId')).toBe(installationId);
    expect(sessions[0].get('sessionToken')).toBe(user.sessionToken);

    const loggedUser = await RESTController.request(
      'POST',
      '/login',
      { username: 'hello', password: 'world' },
      { installationId }
    );
    expect(loggedUser.sessionToken).not.toBeUndefined();
    sessions = await query.find({ useMasterKey: true });

    // Should clean up old sessions with this installationId
    expect(sessions.length).toBe(1);
    expect(sessions[0].get('installationId')).toBe(installationId);
    expect(sessions[0].get('sessionToken')).toBe(loggedUser.sessionToken);
  });

  it('returns a statusId when running jobs', async () => {
    Parse.Cloud.job('CloudJob', () => {
      return 'Cloud job completed';
    });
    const res = await RESTController.request(
      'POST',
      '/jobs/CloudJob',
      {},
      { useMasterKey: true, returnStatus: true }
    );
    const jobStatusId = res._headers['X-Parse-Job-Status-Id'];
    expect(jobStatusId).toBeDefined();
    const result = await Parse.Cloud.getJobStatus(jobStatusId);
    expect(result.id).toBe(jobStatusId);
  });

  it('returns a statusId when running push notifications', async () => {
    const payload = {
      data: { alert: 'We return status!' },
      where: { deviceType: 'ios' },
    };
    const res = await RESTController.request('POST', '/push', payload, {
      useMasterKey: true,
      returnStatus: true,
    });
    const pushStatusId = res._headers['X-Parse-Push-Status-Id'];
    expect(pushStatusId).toBeDefined();

    const result = await Parse.Push.getPushStatus(pushStatusId);
    expect(result.id).toBe(pushStatusId);
  });

  it('returns a statusId when running batch push notifications', async () => {
    const payload = {
      data: { alert: 'We return status!' },
      where: { deviceType: 'ios' },
    };
    const res = await RESTController.request('POST', 'batch', {
      requests: [{
        method: 'POST',
        path: '/push',
        body: payload,
      }],
    }, {
      useMasterKey: true,
      returnStatus: true,
    });
    const pushStatusId = res[0]._headers['X-Parse-Push-Status-Id'];
    expect(pushStatusId).toBeDefined();

    const result = await Parse.Push.getPushStatus(pushStatusId);
    expect(result.id).toBe(pushStatusId);
  });
});
