const { GraphQLObjectType } = require('graphql');
const defaultLogger = require('../lib/logger').default;
const { ParseGraphQLSchema } = require('../lib/GraphQL/ParseGraphQLSchema');

describe('ParseGraphQLSchema', () => {
  let parseServer;
  let databaseController;
  let parseGraphQLController;
  let parseGraphQLSchema;
  const appId = 'test';

  beforeEach(async () => {
    parseServer = await global.reconfigureServer();
    databaseController = parseServer.config.databaseController;
    parseGraphQLController = parseServer.config.parseGraphQLController;
    parseGraphQLSchema = new ParseGraphQLSchema({
      databaseController,
      parseGraphQLController,
      log: defaultLogger,
      appId,
    });
  });

  describe('constructor', () => {
    it('should require a parseGraphQLController, databaseController, a log instance, and the appId', () => {
      expect(() => new ParseGraphQLSchema()).toThrow(
        'You must provide a parseGraphQLController instance!'
      );
      expect(() => new ParseGraphQLSchema({ parseGraphQLController: {} })).toThrow(
        'You must provide a databaseController instance!'
      );
      expect(
        () =>
          new ParseGraphQLSchema({
            parseGraphQLController: {},
            databaseController: {},
          })
      ).toThrow('You must provide a log instance!');
      expect(
        () =>
          new ParseGraphQLSchema({
            parseGraphQLController: {},
            databaseController: {},
            log: {},
          })
      ).toThrow('You must provide the appId!');
    });
  });

  describe('load', () => {
    it('should cache schema', async () => {
      const graphQLSchema = await parseGraphQLSchema.load();
      const updatedGraphQLSchema = await parseGraphQLSchema.load();
      expect(graphQLSchema).toBe(updatedGraphQLSchema);
    });

    it('should load a brand new GraphQL Schema if Parse Schema changes', async () => {
      await parseGraphQLSchema.load();
      const parseClasses = parseGraphQLSchema.parseClasses;
      const parseClassTypes = parseGraphQLSchema.parseClassTypes;
      const graphQLSchema = parseGraphQLSchema.graphQLSchema;
      const graphQLTypes = parseGraphQLSchema.graphQLTypes;
      const graphQLQueries = parseGraphQLSchema.graphQLQueries;
      const graphQLMutations = parseGraphQLSchema.graphQLMutations;
      const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions;
      const newClassObject = new Parse.Object('NewClass');
      await newClassObject.save();
      await parseServer.config.schemaCache.clear();
      await new Promise(resolve => setTimeout(resolve, 200));
      await parseGraphQLSchema.load();
      expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
      expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
      expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
      expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
      expect(graphQLQueries).not.toBe(parseGraphQLSchema.graphQLQueries);
      expect(graphQLMutations).not.toBe(parseGraphQLSchema.graphQLMutations);
      expect(graphQLSubscriptions).not.toBe(parseGraphQLSchema.graphQLSubscriptions);
    });

    it('should load a brand new GraphQL Schema if graphQLConfig changes', async () => {
      const parseGraphQLController = {
        graphQLConfig: { enabledForClasses: [] },
        getGraphQLConfig() {
          return this.graphQLConfig;
        },
      };
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: defaultLogger,
        appId,
      });
      await parseGraphQLSchema.load();
      const parseClasses = parseGraphQLSchema.parseClasses;
      const parseClassTypes = parseGraphQLSchema.parseClassTypes;
      const graphQLSchema = parseGraphQLSchema.graphQLSchema;
      const graphQLTypes = parseGraphQLSchema.graphQLTypes;
      const graphQLQueries = parseGraphQLSchema.graphQLQueries;
      const graphQLMutations = parseGraphQLSchema.graphQLMutations;
      const graphQLSubscriptions = parseGraphQLSchema.graphQLSubscriptions;

      parseGraphQLController.graphQLConfig = {
        enabledForClasses: ['_User'],
      };

      await new Promise(resolve => setTimeout(resolve, 200));
      await parseGraphQLSchema.load();
      expect(parseClasses).not.toBe(parseGraphQLSchema.parseClasses);
      expect(parseClassTypes).not.toBe(parseGraphQLSchema.parseClassTypes);
      expect(graphQLSchema).not.toBe(parseGraphQLSchema.graphQLSchema);
      expect(graphQLTypes).not.toBe(parseGraphQLSchema.graphQLTypes);
      expect(graphQLQueries).not.toBe(parseGraphQLSchema.graphQLQueries);
      expect(graphQLMutations).not.toBe(parseGraphQLSchema.graphQLMutations);
      expect(graphQLSubscriptions).not.toBe(parseGraphQLSchema.graphQLSubscriptions);
    });
  });

  describe('addGraphQLType', () => {
    it('should not load and warn duplicated types', async () => {
      let logged = false;
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: message => {
            logged = true;
            expect(message).toEqual(
              'Type SomeClass could not be added to the auto schema because it collided with an existing type.'
            );
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      const type = new GraphQLObjectType({ name: 'SomeClass' });
      expect(parseGraphQLSchema.addGraphQLType(type)).toBe(type);
      expect(parseGraphQLSchema.graphQLTypes).toContain(type);
      expect(
        parseGraphQLSchema.addGraphQLType(new GraphQLObjectType({ name: 'SomeClass' }))
      ).toBeUndefined();
      expect(logged).toBeTruthy();
    });

    it('should throw error when required', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: () => {
            fail('Should not warn');
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      const type = new GraphQLObjectType({ name: 'SomeClass' });
      expect(parseGraphQLSchema.addGraphQLType(type, true)).toBe(type);
      expect(parseGraphQLSchema.graphQLTypes).toContain(type);
      expect(() =>
        parseGraphQLSchema.addGraphQLType(new GraphQLObjectType({ name: 'SomeClass' }), true)
      ).toThrowError(
        'Type SomeClass could not be added to the auto schema because it collided with an existing type.'
      );
    });

    it('should warn reserved name collision', async () => {
      let logged = false;
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: message => {
            logged = true;
            expect(message).toEqual(
              'Type String could not be added to the auto schema because it collided with an existing type.'
            );
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      expect(
        parseGraphQLSchema.addGraphQLType(new GraphQLObjectType({ name: 'String' }))
      ).toBeUndefined();
      expect(logged).toBeTruthy();
    });

    it('should ignore collision when necessary', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: () => {
            fail('Should not warn');
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      const type = new GraphQLObjectType({ name: 'String' });
      expect(parseGraphQLSchema.addGraphQLType(type, true, true)).toBe(type);
      expect(parseGraphQLSchema.graphQLTypes).toContain(type);
    });
  });

  describe('addGraphQLQuery', () => {
    it('should not load and warn duplicated queries', async () => {
      let logged = false;
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: message => {
            logged = true;
            expect(message).toEqual(
              'Query someClasses could not be added to the auto schema because it collided with an existing field.'
            );
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      const field = {};
      expect(parseGraphQLSchema.addGraphQLQuery('someClasses', field)).toBe(field);
      expect(parseGraphQLSchema.graphQLQueries['someClasses']).toBe(field);
      expect(parseGraphQLSchema.addGraphQLQuery('someClasses', {})).toBeUndefined();
      expect(logged).toBeTruthy();
    });

    it('should throw error when required', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: () => {
            fail('Should not warn');
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      const field = {};
      expect(parseGraphQLSchema.addGraphQLQuery('someClasses', field)).toBe(field);
      expect(parseGraphQLSchema.graphQLQueries['someClasses']).toBe(field);
      expect(() => parseGraphQLSchema.addGraphQLQuery('someClasses', {}, true)).toThrowError(
        'Query someClasses could not be added to the auto schema because it collided with an existing field.'
      );
    });

    it('should warn reserved name collision', async () => {
      let logged = false;
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: message => {
            logged = true;
            expect(message).toEqual(
              'Query viewer could not be added to the auto schema because it collided with an existing field.'
            );
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      expect(parseGraphQLSchema.addGraphQLQuery('viewer', {})).toBeUndefined();
      expect(logged).toBeTruthy();
    });

    it('should ignore collision when necessary', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: () => {
            fail('Should not warn');
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      delete parseGraphQLSchema.graphQLQueries.viewer;
      const field = {};
      expect(parseGraphQLSchema.addGraphQLQuery('viewer', field, true, true)).toBe(field);
      expect(parseGraphQLSchema.graphQLQueries['viewer']).toBe(field);
    });
  });

  describe('addGraphQLMutation', () => {
    it('should not load and warn duplicated mutations', async () => {
      let logged = false;
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: message => {
            logged = true;
            expect(message).toEqual(
              'Mutation createSomeClass could not be added to the auto schema because it collided with an existing field.'
            );
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      const field = {};
      expect(parseGraphQLSchema.addGraphQLMutation('createSomeClass', field)).toBe(field);
      expect(parseGraphQLSchema.graphQLMutations['createSomeClass']).toBe(field);
      expect(parseGraphQLSchema.addGraphQLMutation('createSomeClass', {})).toBeUndefined();
      expect(logged).toBeTruthy();
    });

    it('should throw error when required', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: () => {
            fail('Should not warn');
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      const field = {};
      expect(parseGraphQLSchema.addGraphQLMutation('createSomeClass', field)).toBe(field);
      expect(parseGraphQLSchema.graphQLMutations['createSomeClass']).toBe(field);
      expect(() => parseGraphQLSchema.addGraphQLMutation('createSomeClass', {}, true)).toThrowError(
        'Mutation createSomeClass could not be added to the auto schema because it collided with an existing field.'
      );
    });

    it('should warn reserved name collision', async () => {
      let logged = false;
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: message => {
            logged = true;
            expect(message).toEqual(
              'Mutation signUp could not be added to the auto schema because it collided with an existing field.'
            );
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      expect(parseGraphQLSchema.addGraphQLMutation('signUp', {})).toBeUndefined();
      expect(logged).toBeTruthy();
    });

    it('should ignore collision when necessary', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: () => {
            fail('Should not warn');
          },
        },
        appId,
      });
      await parseGraphQLSchema.load();
      delete parseGraphQLSchema.graphQLMutations.signUp;
      const field = {};
      expect(parseGraphQLSchema.addGraphQLMutation('signUp', field, true, true)).toBe(field);
      expect(parseGraphQLSchema.graphQLMutations['signUp']).toBe(field);
    });
  });

  describe('_getParseClassesWithConfig', () => {
    it('should sort classes', () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: {
          warn: () => {
            fail('Should not warn');
          },
        },
        appId,
      });
      expect(
        parseGraphQLSchema
          ._getParseClassesWithConfig(
            [
              { className: 'b' },
              { className: '_b' },
              { className: 'B' },
              { className: '_B' },
              { className: 'a' },
              { className: '_a' },
              { className: 'A' },
              { className: '_A' },
            ],
            {
              classConfigs: [],
            }
          )
          .map(item => item[0])
      ).toEqual([
        { className: '_A' },
        { className: '_B' },
        { className: '_a' },
        { className: '_b' },
        { className: 'A' },
        { className: 'B' },
        { className: 'a' },
        { className: 'b' },
      ]);
    });
  });

  describe('name collision', () => {
    it('should not generate duplicate types when colliding to default classes', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: defaultLogger,
        appId,
      });
      await parseGraphQLSchema.schemaCache.clear();
      const schema1 = await parseGraphQLSchema.load();
      const types1 = parseGraphQLSchema.graphQLTypes;
      const queries1 = parseGraphQLSchema.graphQLQueries;
      const mutations1 = parseGraphQLSchema.graphQLMutations;
      const user = new Parse.Object('User');
      await user.save();
      await parseGraphQLSchema.schemaCache.clear();
      const schema2 = await parseGraphQLSchema.load();
      const types2 = parseGraphQLSchema.graphQLTypes;
      const queries2 = parseGraphQLSchema.graphQLQueries;
      const mutations2 = parseGraphQLSchema.graphQLMutations;
      expect(schema1).not.toBe(schema2);
      expect(types1).not.toBe(types2);
      expect(types1.map(type => type.name).sort()).toEqual(types2.map(type => type.name).sort());
      expect(queries1).not.toBe(queries2);
      expect(Object.keys(queries1).sort()).toEqual(Object.keys(queries2).sort());
      expect(mutations1).not.toBe(mutations2);
      expect(Object.keys(mutations1).sort()).toEqual(Object.keys(mutations2).sort());
    });

    it('should not generate duplicate types when colliding the same name', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: defaultLogger,
        appId,
      });
      const car1 = new Parse.Object('Car');
      await car1.save();
      await parseGraphQLSchema.schemaCache.clear();
      const schema1 = await parseGraphQLSchema.load();
      const types1 = parseGraphQLSchema.graphQLTypes;
      const queries1 = parseGraphQLSchema.graphQLQueries;
      const mutations1 = parseGraphQLSchema.graphQLMutations;
      const car2 = new Parse.Object('car');
      await car2.save();
      await parseGraphQLSchema.schemaCache.clear();
      const schema2 = await parseGraphQLSchema.load();
      const types2 = parseGraphQLSchema.graphQLTypes;
      const queries2 = parseGraphQLSchema.graphQLQueries;
      const mutations2 = parseGraphQLSchema.graphQLMutations;
      expect(schema1).not.toBe(schema2);
      expect(types1).not.toBe(types2);
      expect(types1.map(type => type.name).sort()).toEqual(types2.map(type => type.name).sort());
      expect(queries1).not.toBe(queries2);
      expect(Object.keys(queries1).sort()).toEqual(Object.keys(queries2).sort());
      expect(mutations1).not.toBe(mutations2);
      expect(Object.keys(mutations1).sort()).toEqual(Object.keys(mutations2).sort());
    });

    it('should not generate duplicate queries when query name collide', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: defaultLogger,
        appId,
      });
      const car = new Parse.Object('Car');
      await car.save();
      await parseGraphQLSchema.schemaCache.clear();
      const schema1 = await parseGraphQLSchema.load();
      const queries1 = parseGraphQLSchema.graphQLQueries;
      const mutations1 = parseGraphQLSchema.graphQLMutations;
      const cars = new Parse.Object('cars');
      await cars.save();
      await parseGraphQLSchema.schemaCache.clear();
      const schema2 = await parseGraphQLSchema.load();
      const queries2 = parseGraphQLSchema.graphQLQueries;
      const mutations2 = parseGraphQLSchema.graphQLMutations;
      expect(schema1).not.toBe(schema2);
      expect(queries1).not.toBe(queries2);
      expect(Object.keys(queries1).sort()).toEqual(Object.keys(queries2).sort());
      expect(mutations1).not.toBe(mutations2);
      expect(
        Object.keys(mutations1).concat('createCars', 'updateCars', 'deleteCars').sort()
      ).toEqual(Object.keys(mutations2).sort());
    });
  });
  describe('alias', () => {
    it_id('45282d26-f4c7-4d2d-a7b6-cd8741d5322f')(it)('Should be able to define alias for get and find query', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: defaultLogger,
        appId,
      });

      await parseGraphQLSchema.parseGraphQLController.updateGraphQLConfig({
        classConfigs: [
          {
            className: 'Data',
            query: {
              get: true,
              getAlias: 'precious_data',
              find: true,
              findAlias: 'data_results',
            },
          },
        ],
      });

      const data = new Parse.Object('Data');

      await data.save();

      await parseGraphQLSchema.schemaCache.clear();
      await parseGraphQLSchema.load();

      const queries1 = parseGraphQLSchema.graphQLQueries;

      expect(Object.keys(queries1)).toContain('data_results');
      expect(Object.keys(queries1)).toContain('precious_data');
    });

    it_id('f04b46e3-a25d-401d-a315-3298cfee1df8')(it)('Should be able to define alias for mutation', async () => {
      const parseGraphQLSchema = new ParseGraphQLSchema({
        databaseController,
        parseGraphQLController,
        log: defaultLogger,
        appId,
      });

      await parseGraphQLSchema.parseGraphQLController.updateGraphQLConfig({
        classConfigs: [
          {
            className: 'Track',
            mutation: {
              create: true,
              createAlias: 'addTrack',
              update: true,
              updateAlias: 'modifyTrack',
              destroy: true,
              destroyAlias: 'eraseTrack',
            },
          },
        ],
      });

      const data = new Parse.Object('Track');

      await data.save();

      await parseGraphQLSchema.schemaCache.clear();
      await parseGraphQLSchema.load();

      const mutations = parseGraphQLSchema.graphQLMutations;

      expect(Object.keys(mutations)).toContain('addTrack');
      expect(Object.keys(mutations)).toContain('modifyTrack');
      expect(Object.keys(mutations)).toContain('eraseTrack');
    });
  });
});
