'use strict';

const chai = require('chai');
const runServerless = require('../../../../utils/run-serverless');
const {
  getConfigurationValidationResult,
} = require('../../../../../lib/classes/ConfigSchemaHandler');

chai.use(require('chai-as-promised'));

const expect = chai.expect;
const FUNCTION_NAME_PATTERN = '^[a-zA-Z0-9-_]+$';

describe('test/unit/lib/classes/ConfigSchemaHandler/index.test.js', () => {
  describe('#constructor', () => {
    it('should freeze parts of schema for service', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        expect(() => {
          serverless.configSchemaHandler.schema.properties.service.name = 'changed';
        }).to.throw(Error);
      });
    });

    it('should freeze parts of schema for plugins', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        expect(() => {
          serverless.configSchemaHandler.schema.properties.plugins.properties = 'changed';
        }).to.throw(Error);
      });
    });

    it('should freeze parts of schema for resources', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        expect(() => {
          serverless.configSchemaHandler.schema.properties.resources.something = 'changed';
        }).to.throw(Error);
      });
    });

    it('should freeze parts of schema for package', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        expect(() => {
          serverless.configSchemaHandler.schema.properties.package.properties.oneMore = {
            type: 'string',
          };
        }).to.throw(Error);
      });
    });

    it('should freeze parts of schema for layers', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        expect(() => {
          serverless.configSchemaHandler.schema.properties.layers.properties = 'changed';
        }).to.throw(Error);
      });
    });
  });

  describe('#validateConfig', () => {
    it('should run without errors for valid config', async () => {
      const { serverless } = await runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      });
      expect(getConfigurationValidationResult(serverless.configurationInput)).to.be.true;
    });
  });

  describe('#defineFunctionEvent', () => {
    it('should extend schema with defineFunctionEvent method', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        const expectedPieceOfSchema = {
          type: 'object',
          properties: {
            someEvent: {
              type: 'object',
              properties: {
                someRequiredStringProp: { type: 'string' },
                someNumberProp: { type: 'number' },
              },
              required: ['someRequiredStringProp'],
              additionalProperties: false,
            },
          },
          required: ['someEvent'],
          additionalProperties: false,
        };

        expect(
          serverless.serverless.configSchemaHandler.schema.properties.functions.patternProperties[
            FUNCTION_NAME_PATTERN
          ].properties.events.items.anyOf.find(
            (definition) => definition.required[0] === 'someEvent'
          )
        ).to.deep.equal(expectedPieceOfSchema);
        return;
      });
    });

    it('should throw when defineFunctionEvent is used with an already defined event', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-colliding-function-event'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'SCHEMA_COLLISION');
    });
  });

  describe('#defineFunctionEventProperties', () => {
    it('should extend schema with defineFunctionEventProperties method', async () => {
      const serverless = await runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      });

      const existingEventDefinition =
        serverless.serverless.configSchemaHandler.schema.properties.functions.patternProperties[
          FUNCTION_NAME_PATTERN
        ].properties.events.items.anyOf.find(
          (definition) => definition.required[0] === 'existingEvent'
        ).properties.existingEvent;

      expect(existingEventDefinition.properties).to.have.deep.property(
        'somePluginAdditionalEventProp',
        {
          type: 'string',
        }
      );
      expect(existingEventDefinition.required).to.include('somePluginAdditionalEventProp');
    });

    it('should extend schema with defineFunctionEventProperties method on complex event schema', async () => {
      const serverless = await runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      });

      const existingEventDefinition =
        serverless.serverless.configSchemaHandler.schema.properties.functions.patternProperties[
          FUNCTION_NAME_PATTERN
        ].properties.events.items.anyOf.find(
          (definition) => definition.required[0] === 'existingComplexEvent'
        ).properties.existingComplexEvent;

      expect(existingEventDefinition).to.deep.equal({
        anyOf: [
          { type: 'string' },
          {
            type: 'object',
            properties: {
              existingPropForObjectEventDefinition: { type: 'string' },
              somePluginAdditionalComplexEventProp: { type: 'string' },
            },
            required: ['somePluginAdditionalComplexEventProp'],
          },
        ],
      });
    });

    it('should throw when defineFunctionEventProperties is used on non existing event name', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-non-existing-event-error'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'UNRECOGNIZED_FUNCTION_EVENT_SCHEMA');
    });

    it('should throw when defineFunctionEventProperties is used on event without object definition', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-complex-event-without-object-definition'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'FUNCTION_EVENT_SCHEMA_NOT_OBJECT');
    });

    it('should throw when defineFunctionEventProperties is used with an already defined property', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-colliding-function-event-property'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'SCHEMA_COLLISION');
    });
  });

  describe('#defineFunctionProperties', () => {
    it('should extend schema with defineFunctionProperties method', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        const actualFunctionProperties =
          serverless.serverless.configSchemaHandler.schema.properties.functions.patternProperties[
            FUNCTION_NAME_PATTERN
          ].properties;

        expect(actualFunctionProperties).to.have.deep.property('someFunctionStringProp', {
          type: 'string',
        });

        expect(actualFunctionProperties).to.have.deep.property('someRequiredFunctionNumberProp', {
          type: 'number',
        });

        return;
      });
    });

    it('should throw when defineFunctionProperties is used with an already defined property', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-colliding-function-property'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'SCHEMA_COLLISION');
    });
  });

  describe('#defineCustomProperties', () => {
    it('should extend schema with defineCustomProperties method', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        const someCustomStringProp = {
          type: 'string',
        };
        expect(
          serverless.serverless.configSchemaHandler.schema.properties.custom.properties
            .someCustomStringProp
        ).to.deep.equal(someCustomStringProp);
        return;
      });
    });

    it('should throw when defineCustomProperties is used with an already defined property', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-colliding-custom-property'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'SCHEMA_COLLISION');
    });
  });

  describe('#defineTopLevelProperty', () => {
    it('should extend schema with defineTopLevelProperty method', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        const expectedAppPropSchema = {
          type: 'string',
        };
        expect(serverless.serverless.configSchemaHandler.schema.properties.top).to.deep.equal(
          expectedAppPropSchema
        );
        return;
      });
    });

    it('should throw when defineTopLevelProperty is used with an already defined property', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-colliding-top-level-property'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'SCHEMA_COLLISION');
    });
  });

  describe('#defineProvider', () => {
    it('should extend schema with defineProvider method', () => {
      return runServerless({
        fixture: 'configSchemaExtensions',
        command: 'info',
      }).then((serverless) => {
        const providerPieceOfSchema = {
          type: 'object',
          properties: {
            name: { const: 'someProvider' },
            stage: { type: 'string' },
          },
          required: ['name'],
          additionalProperties: false,
        };
        expect(serverless.serverless.configSchemaHandler.schema.properties.provider).to.deep.equal(
          providerPieceOfSchema
        );

        const expectedHandlerPieceOfSchema = { type: 'string' };
        expect(
          serverless.serverless.configSchemaHandler.schema.properties.functions.patternProperties[
            FUNCTION_NAME_PATTERN
          ].properties.handler
        ).to.deep.equal(expectedHandlerPieceOfSchema);
        return;
      });
    });

    it('should throw when defineProvider is used with an already defined property in provider', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-colliding-provider-property-in-provider'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'SCHEMA_COLLISION');
    });

    it('should throw when defineProvider is used with an already defined property in function', async () => {
      await expect(
        runServerless({
          fixture: 'configSchemaExtensionsError',
          command: 'info',
          configExt: {
            plugins: ['./test-plugin-with-colliding-provider-property-in-function'],
          },
        })
      ).to.eventually.be.rejected.and.have.property('code', 'SCHEMA_COLLISION');
    });
  });
});
