'use strict';

const path = require('path');
const YAML = require('js-yaml');
const _ = require('lodash');
const chai = require('chai');
const sinon = require('sinon');
const Service = require('../../lib/classes/Service');
const Utils = require('../../lib/classes/Utils');
const Serverless = require('../../lib/Serverless');
const { getTmpDirPath } = require('../../tests/utils/fs');

// Configure chai
chai.use(require('chai-as-promised'));
const expect = require('chai').expect;

describe('Service', () => {
  describe('#constructor()', () => {
    const serverless = new Serverless();
    serverless.processedInput = { options: {} };

    it('should attach serverless instance', () => {
      const serviceInstance = new Service(serverless);
      expect(typeof serviceInstance.serverless.version).to.be.equal('string');
    });

    it('should construct with defaults', () => {
      const serviceInstance = new Service(serverless);

      expect(serviceInstance.service).to.be.equal(null);
      expect(serviceInstance.serviceObject).to.be.equal(null);
      expect(serviceInstance.provider).to.deep.equal({
        stage: 'dev',
        variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}',
      });
      expect(serviceInstance.custom).to.deep.equal({});
      expect(serviceInstance.plugins).to.deep.equal([]);
      expect(serviceInstance.functions).to.deep.equal({});
      expect(serviceInstance.resources).to.deep.equal({});
      expect(serviceInstance.package).to.deep.equal({});
    });

    it('should construct with data', () => {
      const data = {
        service: 'testService',
        provider: 'testProvider',
        custom: {
          customProp: 'value',
        },
        plugins: ['testPlugin'],
        functions: {
          functionA: {},
        },
        resources: {
          aws: {
            resourcesProp: 'value',
          },
          azure: {},
          google: {},
        },
        package: {
          exclude: ['exclude-me'],
          include: ['include-me'],
          artifact: 'some/path/foo.zip',
        },
      };

      const serviceInstance = new Service(serverless, data);

      expect(serviceInstance.service).to.be.equal('testService');
      expect(serviceInstance.provider).to.be.equal('testProvider');
      expect(serviceInstance.custom).to.deep.equal({ customProp: 'value' });
      expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
      expect(serviceInstance.functions).to.deep.equal({ functionA: {} });
      expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
      expect(serviceInstance.resources.azure).to.deep.equal({});
      expect(serviceInstance.resources.google).to.deep.equal({});
      expect(serviceInstance.package.exclude.length).to.equal(1);
      expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
      expect(serviceInstance.package.include.length).to.equal(1);
      expect(serviceInstance.package.include[0]).to.equal('include-me');
      expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
      expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
    });

    it('should support string based provider config', () => {
      const data = {
        provider: 'testProvider',
      };

      const serviceInstance = new Service(serverless, data);

      expect(serviceInstance.provider).to.be.equal('testProvider');
    });

    it('should support object based provider config', () => {
      const data = {
        provider: {
          name: 'testProvider',
          runtime: 'nodejs12.x',
        },
      };

      const serviceInstance = new Service(serverless, data);

      expect(serviceInstance.provider.name).to.be.equal('testProvider');
      expect(serviceInstance.provider.runtime).to.be.equal('nodejs12.x');
    });
  });

  describe('#load()', () => {
    let serviceInstance;
    let tmpDirPath;

    beforeEach(() => {
      tmpDirPath = getTmpDirPath();
    });

    it('should resolve if no servicePath is found', () => {
      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      const noService = new Service(serverless);

      return expect(noService.load()).to.be.fulfilled;
    });

    it('should load serverless.yml from filesystem', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'new-service',
        provider: {
          name: 'aws',
          stage: 'dev',
          region: 'us-east-1',
          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
        },
        plugins: ['testPlugin'],
        functions: {
          functionA: {},
        },
        resources: {
          aws: {
            resourcesProp: 'value',
          },
          azure: {},
          google: {},
        },
        package: {
          exclude: ['exclude-me'],
          include: ['include-me'],
          artifact: 'some/path/foo.zip',
          excludeDevDependencies: false,
        },
      };

      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      serverless.config.update({ servicePath: tmpDirPath });
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        expect(serviceInstance.service).to.be.equal('new-service');
        expect(serviceInstance.provider.name).to.deep.equal('aws');
        expect(serviceInstance.provider.variableSyntax).to.equal(
          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
        );
        expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
        expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
        expect(serviceInstance.resources.azure).to.deep.equal({});
        expect(serviceInstance.resources.google).to.deep.equal({});
        expect(serviceInstance.package.exclude.length).to.equal(1);
        expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
        expect(serviceInstance.package.include.length).to.equal(1);
        expect(serviceInstance.package.include[0]).to.equal('include-me');
        expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
        expect(serviceInstance.package.excludeDevDependencies).to.equal(false);
      });
    });

    it('should load serverless.yaml from filesystem', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'new-service',
        provider: {
          name: 'aws',
          stage: 'dev',
          region: 'us-east-1',
          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
        },
        plugins: ['testPlugin'],
        functions: {
          functionA: {},
        },
        resources: {
          aws: {
            resourcesProp: 'value',
          },
          azure: {},
          google: {},
        },
        package: {
          exclude: ['exclude-me'],
          include: ['include-me'],
          artifact: 'some/path/foo.zip',
        },
      };

      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYml));

      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      serverless.config.update({ servicePath: tmpDirPath });
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        expect(serviceInstance.service).to.be.equal('new-service');
        expect(serviceInstance.provider.name).to.deep.equal('aws');
        expect(serviceInstance.provider.variableSyntax).to.equal(
          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
        );
        expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
        expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
        expect(serviceInstance.resources.azure).to.deep.equal({});
        expect(serviceInstance.resources.google).to.deep.equal({});
        expect(serviceInstance.package.exclude.length).to.equal(1);
        expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
        expect(serviceInstance.package.include.length).to.equal(1);
        expect(serviceInstance.package.include[0]).to.equal('include-me');
        expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
        expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
      });
    });

    it('should load serverless.json from filesystem', () => {
      const SUtils = new Utils();
      const serverlessJSON = {
        service: 'new-service',
        provider: {
          name: 'aws',
          stage: 'dev',
          region: 'us-east-1',
          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
        },
        plugins: ['testPlugin'],
        functions: {
          functionA: {},
        },
        resources: {
          aws: {
            resourcesProp: 'value',
          },
          azure: {},
          google: {},
        },
        package: {
          exclude: ['exclude-me'],
          include: ['include-me'],
          artifact: 'some/path/foo.zip',
        },
      };

      SUtils.writeFileSync(
        path.join(tmpDirPath, 'serverless.json'),
        JSON.stringify(serverlessJSON)
      );

      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      serverless.config.update({ servicePath: tmpDirPath });
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        expect(serviceInstance.service).to.be.equal('new-service');
        expect(serviceInstance.provider.name).to.deep.equal('aws');
        expect(serviceInstance.provider.variableSyntax).to.equal(
          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
        );
        expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
        expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
        expect(serviceInstance.resources.azure).to.deep.equal({});
        expect(serviceInstance.resources.google).to.deep.equal({});
        expect(serviceInstance.package.exclude.length).to.equal(1);
        expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
        expect(serviceInstance.package.include.length).to.equal(1);
        expect(serviceInstance.package.include[0]).to.equal('include-me');
        expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
        expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
      });
    });

    it('should load serverless.js from filesystem', () => {
      const SUtils = new Utils();
      const serverlessJSON = {
        service: 'new-service',
        provider: {
          name: 'aws',
          stage: 'dev',
          region: 'us-east-1',
          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
        },
        plugins: ['testPlugin'],
        functions: {
          functionA: {},
        },
        resources: {
          aws: {
            resourcesProp: 'value',
          },
          azure: {},
          google: {},
        },
        package: {
          exclude: ['exclude-me'],
          include: ['include-me'],
          artifact: 'some/path/foo.zip',
        },
      };

      SUtils.writeFileSync(
        path.join(tmpDirPath, 'serverless.js'),
        `module.exports = ${JSON.stringify(serverlessJSON)};`
      );

      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      serverless.config.update({ servicePath: tmpDirPath });
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        expect(serviceInstance.service).to.be.equal('new-service');
        expect(serviceInstance.provider.name).to.deep.equal('aws');
        expect(serviceInstance.provider.variableSyntax).to.equal(
          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
        );
        expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
        expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
        expect(serviceInstance.resources.azure).to.deep.equal({});
        expect(serviceInstance.resources.google).to.deep.equal({});
        expect(serviceInstance.package.exclude.length).to.equal(1);
        expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
        expect(serviceInstance.package.include.length).to.equal(1);
        expect(serviceInstance.package.include[0]).to.equal('include-me');
        expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
        expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
      });
    });

    it('should load serverless.js from filesystem', () => {
      const SUtils = new Utils();
      const serverlessJSON = {
        service: 'new-service',
        provider: {
          name: 'aws',
          stage: 'dev',
          region: 'us-east-1',
          variableSyntax: '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)*?]+?)}}',
        },
        plugins: ['testPlugin'],
        functions: {
          functionA: {},
        },
        resources: {
          aws: {
            resourcesProp: 'value',
          },
          azure: {},
          google: {},
        },
        package: {
          exclude: ['exclude-me'],
          include: ['include-me'],
          artifact: 'some/path/foo.zip',
        },
      };

      SUtils.writeFileSync(
        path.join(tmpDirPath, 'serverless.js'),
        `module.exports = new Promise(resolve => { resolve(${JSON.stringify(serverlessJSON)}) });`
      );

      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      serverless.config.update({ servicePath: tmpDirPath });
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        expect(serviceInstance.service).to.be.equal('new-service');
        expect(serviceInstance.provider.name).to.deep.equal('aws');
        expect(serviceInstance.provider.variableSyntax).to.equal(
          '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)*?]+?)}}'
        );
        expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
        expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
        expect(serviceInstance.resources.azure).to.deep.equal({});
        expect(serviceInstance.resources.google).to.deep.equal({});
        expect(serviceInstance.package.exclude.length).to.equal(1);
        expect(serviceInstance.package.exclude[0]).to.equal('exclude-me');
        expect(serviceInstance.package.include.length).to.equal(1);
        expect(serviceInstance.package.include[0]).to.equal('include-me');
        expect(serviceInstance.package.artifact).to.equal('some/path/foo.zip');
        expect(serviceInstance.package.excludeDevDependencies).to.equal(undefined);
      });
    });

    it('should throw error if serverless.js exports invalid config', () => {
      const SUtils = new Utils();

      SUtils.writeFileSync(
        path.join(tmpDirPath, 'serverless.js'),
        'module.exports = function config() {};'
      );

      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      serverless.config.update({ servicePath: tmpDirPath });
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.be.rejectedWith(
        'serverless.js must export plain object'
      );
    });

    it('should load YAML in favor of JSON', () => {
      const SUtils = new Utils();
      const serverlessJSON = {
        provider: {
          name: 'aws',
          stage: 'dev',
          region: 'us-east-1',
          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
        },
        plugins: ['testPlugin'],
        functions: {
          functionA: {},
        },
        resources: {
          aws: {
            resourcesProp: 'value',
          },
          azure: {},
          google: {},
        },
        package: {
          exclude: ['exclude-me'],
          include: ['include-me'],
          artifact: 'some/path/foo.zip',
        },
      };

      serverlessJSON.service = 'JSON service';
      SUtils.writeFileSync(
        path.join(tmpDirPath, 'serverless.json'),
        JSON.stringify(serverlessJSON)
      );

      serverlessJSON.service = 'YAML service';
      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessJSON));

      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      serverless.config.update({ servicePath: tmpDirPath });
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        // YAML should have been loaded instead of JSON
        expect(serviceInstance.service).to.be.equal('YAML service');
      });
    });

    it('should reject when the service name is missing', () => {
      const SUtils = new Utils();
      const serverlessYaml = {
        service: {},
        provider: 'aws',
      };

      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.rejectedWith(
        '"service" is missing the "name" property in'
      );
    });

    it('should support service objects', () => {
      const SUtils = new Utils();
      const serverlessYaml = {
        service: {
          name: 'my-service',
          foo: 'bar',
        },
        provider: 'aws',
      };

      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        expect(serviceInstance.service).to.equal('my-service');
        expect(serviceInstance.serviceObject).to.deep.equal(serverlessYaml.service);
      });
    });

    it('should support Serverless file with a non-aws provider', () => {
      const SUtils = new Utils();
      const serverlessYaml = {
        service: 'my-service',
        provider: 'openwhisk',
        functions: {
          functionA: {
            name: 'customFunctionName',
          },
        },
      };

      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        serviceInstance.setFunctionNames();
        const expectedFunc = {
          functionA: {
            name: 'customFunctionName',
            events: [],
          },
        };
        expect(serviceInstance.service).to.be.equal('my-service');
        expect(serviceInstance.provider.name).to.deep.equal('openwhisk');
        expect(serviceInstance.functions).to.deep.equal(expectedFunc);
      });
    });

    it('should support Serverless file with a .yaml extension', () => {
      const SUtils = new Utils();
      const serverlessYaml = {
        service: 'my-service',
        provider: 'aws',
        functions: {
          functionA: {
            name: 'customFunctionName',
          },
        },
      };

      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yaml'), YAML.dump(serverlessYaml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        serviceInstance.setFunctionNames();
        const expectedFunc = {
          functionA: {
            name: 'customFunctionName',
            events: [],
          },
        };
        expect(serviceInstance.service).to.be.equal('my-service');
        expect(serviceInstance.provider.name).to.deep.equal('aws');
        expect(serviceInstance.functions).to.deep.equal(expectedFunc);
      });
    });

    it('should support Serverless file with a .yml extension', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'my-service',
        provider: 'aws',
        functions: {
          functionA: {},
        },
      };

      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load({ stage: 'dev' })).to.eventually.be.fulfilled.then(() => {
        serviceInstance.setFunctionNames();
        const expectedFunc = {
          functionA: {
            name: 'my-service-dev-functionA',
            events: [],
          },
        };
        expect(serviceInstance.service).to.be.equal('my-service');
        expect(serviceInstance.provider.name).to.deep.equal('aws');
        expect(serviceInstance.functions).to.deep.equal(expectedFunc);
      });
    });

    it('should reject if service property is missing', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        provider: 'aws',
        functions: {},
      };
      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.rejectedWith(
        '"service" property is missing in serverless.yml'
      );
    });

    it('should reject if provider property is missing', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'service-name',
        functions: {},
      };
      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.rejectedWith(
        '"provider" property is missing in serverless.yml'
      );
    });

    it('should reject if frameworkVersion is not satisfied', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'service-name',
        frameworkVersion: '=1.0.0',
        provider: 'aws',
        functions: {},
      };
      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      const getVersion = sinon.stub(serverless.utils, 'getVersion');
      getVersion.returns('1.0.2');
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.rejectedWith(
        /version \(1\.0\.2\).*"frameworkVersion" \(=1\.0\.0\)/
      );
    });

    it('should pass if frameworkVersion is satisfied', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'service-name',
        frameworkVersion: '>=1.0.0',
        provider: 'aws',
        functions: {},
      };
      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      const getVersion = sinon.stub(serverless.utils, 'getVersion');
      getVersion.returns('1.2.2');
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled;
    });

    it('should fulfill if functions property is missing', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'service-name',
        provider: 'aws',
      };
      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serverless.variables.service = serverless.service;
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        // populate variables in service configuration
        serverless.variables.populateService();

        // validate the service configuration, now that variables are loaded
        serviceInstance.validate();

        expect(serviceInstance.functions).to.deep.equal({});
      });
    });
  });

  describe('#validate()', () => {
    let tmpDirPath;

    beforeEach(() => {
      tmpDirPath = getTmpDirPath();
    });

    it("should throw if a function's event is not an array or a variable", () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'service-name',
        provider: 'aws',
        functions: {
          functionA: {
            events: 'not an array or a variable',
          },
        },
      };
      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless({ servicePath: tmpDirPath });
      serverless.processedInput = { options: {} };
      serverless.service = new Service(serverless);

      return expect(serverless.service.load()).to.eventually.be.fulfilled.then(() => {
        // validate the service configuration, now that variables are loaded
        expect(() => serverless.service.validate()).to.throw(
          'Events for "functionA" must be an array, not an string'
        );
      });
    });

    describe('stage name validation', () => {
      function simulateRun(serverless) {
        return serverless.init().then(() =>
          serverless.variables.populateService(serverless.pluginManager.cliOptions).then(() => {
            serverless.service.mergeArrays();
            serverless.service.setFunctionNames(serverless.processedInput.options);
          })
        );
      }

      it(`should not throw an error if http event is absent and
          stage contains only alphanumeric, underscore and hyphen`, function() {
        this.timeout(10000); // Occasionally times out with default settings
        const SUtils = new Utils();
        const serverlessYml = {
          service: 'new-service',
          provider: {
            name: 'aws',
            stage: 'xyz-101_abc-123',
          },
          functions: {
            first: {
              events: [],
            },
          },
        };
        SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

        const serverless = new Serverless({ servicePath: tmpDirPath });
        serverless.processedInput = { options: {} };
        return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
          expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error);
        });
      });

      it(`should not throw an error after variable population if http event is present and
          the populated stage contains only alphanumeric, underscore and hyphen`, () => {
        const SUtils = new Utils();
        const serverlessYml = {
          service: 'new-service',
          provider: {
            name: 'aws',
            stage: '${opt:stage, "default-stage"}',
          },
          functions: {
            first: {
              events: [
                {
                  http: {
                    path: 'foo',
                    method: 'GET',
                  },
                },
              ],
            },
          },
        };
        SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

        const serverless = new Serverless({ servicePath: tmpDirPath });
        return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
          expect(() => serverless.service.validate()).to.not.throw(serverless.classes.Error);
        });
      });

      it('should throw an error if http event is present and stage contains invalid chars', () => {
        const SUtils = new Utils();
        const serverlessYml = {
          service: 'new-service',
          provider: {
            name: 'aws',
            stage: 'my@stage',
          },
          functions: {
            first: {
              events: [
                {
                  http: {
                    path: 'foo',
                    method: 'GET',
                  },
                },
              ],
            },
          },
        };
        SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

        const serverless = new Serverless({ servicePath: tmpDirPath });
        return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
          expect(() => serverless.service.validate()).to.throw(
            serverless.classes.Error,
            [
              'Invalid stage name my@stage: it should contains only [-_a-zA-Z0-9]',
              'for AWS provider if http event are present',
              'according to API Gateway limitation.',
            ].join(' ')
          );
        });
      });

      it(`should throw an error after variable population
            if http event is present and stage contains hyphen`, () => {
        const SUtils = new Utils();
        const serverlessYml = {
          service: 'new-service',
          provider: {
            name: 'aws',
            stage: '${opt:stage, "default:stage"}',
          },
          functions: {
            first: {
              events: [
                {
                  http: {
                    path: 'foo',
                    method: 'GET',
                  },
                },
              ],
            },
          },
        };
        SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

        const serverless = new Serverless({ servicePath: tmpDirPath });
        return expect(simulateRun(serverless)).to.eventually.be.fulfilled.then(() => {
          expect(() => serverless.service.validate()).to.throw(
            serverless.classes.Error,
            [
              'Invalid stage name default:stage: it should contains only [-_a-zA-Z0-9]',
              'for AWS provider if http event are present',
              'according to API Gateway limitation.',
            ].join(' ')
          );
        });
      });
    });
  });

  describe('#mergeArrays', () => {
    it('should merge resources given as an array', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);

      serviceInstance.resources = [
        {
          Resources: {
            aws: {
              resourcesProp: 'value',
            },
          },
        },
        {
          Resources: {
            azure: {},
          },
        },
        {
          foo: 'bar',
        },
      ];

      serviceInstance.mergeArrays();

      expect(serviceInstance.resources).to.be.an('object');
      expect(serviceInstance.resources.Resources).to.be.an('object');
      expect(serviceInstance.resources.Resources.aws).to.deep.equal({ resourcesProp: 'value' });
      expect(serviceInstance.resources.Resources.azure).to.deep.equal({});
      expect(serviceInstance.resources.foo).to.deep.equal('bar');
    });

    it('should ignore an object', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);

      serviceInstance.resources = {
        Resources: 'foo',
      };

      serviceInstance.mergeArrays();

      expect(serviceInstance.resources).to.deep.eql({
        Resources: 'foo',
      });
    });

    it('should tolerate an empty string', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);

      serviceInstance.resources = [
        '',
        {
          aws: {
            resourcesProp: 'value',
          },
        },
      ];

      serviceInstance.mergeArrays();
      expect(serviceInstance.resources).to.deep.eql({
        aws: {
          resourcesProp: 'value',
        },
      });
    });

    it('should throw when given a number', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);

      serviceInstance.resources = [42];

      expect(() => serviceInstance.mergeArrays()).to.throw(Error);
    });

    it('should throw when given a string', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);

      serviceInstance.resources = ['string'];

      expect(() => serviceInstance.mergeArrays()).to.throw(Error);
    });

    it('should merge functions given as an array', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);

      serviceInstance.functions = [
        {
          a: {},
        },
        {
          b: {},
        },
      ];

      serviceInstance.mergeArrays();

      expect(serviceInstance.functions).to.be.an('object');
      expect(serviceInstance.functions.a).to.be.an('object');
      expect(serviceInstance.functions.b).to.be.an('object');
    });
  });

  describe('#setFunctionNames()', () => {
    let serviceInstance;
    let tmpDirPath;

    beforeEach(() => {
      tmpDirPath = getTmpDirPath();
    });

    it('should make sure function name contains the default stage', () => {
      const SUtils = new Utils();
      const serverlessYml = {
        service: 'new-service',
        provider: {
          name: 'aws',
          stage: 'dev',
          region: 'us-east-1',
          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
        },
        plugins: ['testPlugin'],
        functions: {
          functionA: {},
        },
        resources: {
          aws: {
            resourcesProp: 'value',
          },
          azure: {},
          google: {},
        },
        package: {
          exclude: ['exclude-me'],
          include: ['include-me'],
          artifact: 'some/path/foo.zip',
        },
      };

      SUtils.writeFileSync(path.join(tmpDirPath, 'serverless.yml'), YAML.dump(serverlessYml));

      const serverless = new Serverless();
      serverless.processedInput = { options: {} };
      serverless.config.update({ servicePath: tmpDirPath });
      serviceInstance = new Service(serverless);

      return expect(serviceInstance.load()).to.eventually.be.fulfilled.then(() => {
        serviceInstance.setFunctionNames();
        expect(serviceInstance.functions.functionA.name).to.be.equal('new-service-dev-functionA');
      });
    });
  });

  describe('#update()', () => {
    it('should update service instance data', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);
      const newData = { service: 'newName' };
      const updatedInstance = serviceInstance.update(newData);
      expect(updatedInstance.service).to.be.equal('newName');
    });
  });

  describe('#getServiceName()', () => {
    it('should return the service name', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);
      serviceInstance.serviceObject = {
        name: 'my-service',
      };

      const serviceName = serviceInstance.getServiceName();

      expect(serviceName).to.equal('my-service');
    });
  });

  describe('#getServiceObject()', () => {
    it('should return the service object with all properties', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);
      const testObject = {
        name: 'my-service',
        foo: 'bar',
      };
      // Use a clone here to check for implicit reference errors
      serviceInstance.serviceObject = _.cloneDeep(testObject);

      expect(serviceInstance.getServiceObject()).to.deep.equal(testObject);
    });
  });

  describe('#getFunction()', () => {
    let serviceInstance;
    before(() => {
      const serverless = new Serverless();
      serviceInstance = new Service(serverless);
      serviceInstance.functions = {
        create: {
          handler: 'users.create',
        },
      };
    });

    it('should return function object', () => {
      expect(serviceInstance.getFunction('create')).to.deep.equal({ handler: 'users.create' });
    });

    it('should throw error if function does not exist', () => {
      expect(() => {
        serviceInstance.getFunction('random');
      }).to.throw(Error);
    });
  });

  describe('#getAllFunctionsNames', () => {
    it('should return an empty array if there are no functions in Service', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);
      serviceInstance.functions = {};

      const functionsNames = serviceInstance.getAllFunctionsNames();

      expect(functionsNames).to.deep.equal([]);
    });

    it('should return array of lambda function names in Service', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);
      serviceInstance.functions = {
        create: {
          name: 'createUser',
        },
        list: {
          name: 'listUsers',
        },
      };

      const functionsNames = serviceInstance.getAllFunctionsNames();

      expect(functionsNames).to.deep.equal(['createUser', 'listUsers']);
    });
  });

  describe('#getAllFunctions()', () => {
    it('should return an empty array if there are no functions in Service', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);
      serviceInstance.functions = {};
      expect(serviceInstance.getAllFunctions()).to.deep.equal([]);
    });

    it('should return an array of function names in Service', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);
      serviceInstance.functions = {
        create: {
          handler: 'users.create',
        },
        list: {
          handler: 'users.list',
        },
      };
      expect(serviceInstance.getAllFunctions()).to.deep.equal(['create', 'list']);
    });
  });

  describe('#getEventInFunction()', () => {
    let serviceInstance;
    before(() => {
      const serverless = new Serverless();
      serviceInstance = new Service(serverless);
      serviceInstance.functions = {
        create: {
          events: [
            {
              schedule: 'rate(5 minutes)',
            },
          ],
        },
      };
    });

    it('should return an event object based on provided function', () => {
      expect(serviceInstance.getEventInFunction('schedule', 'create')).to.deep.equal({
        schedule: 'rate(5 minutes)',
      });
    });

    it('should throw error if function does not exist in service', () => {
      expect(() => {
        serviceInstance.getEventInFunction(null, 'list');
      }).to.throw(Error);
    });

    it('should throw error if event doesnt exist in function', () => {
      expect(() => {
        serviceInstance.getEventInFunction('randomEvent', 'create');
      }).to.throw(Error);
    });
  });

  describe('#getAllEventsInFunction()', () => {
    it('should return an array of events in a specified function', () => {
      const serverless = new Serverless();
      const serviceInstance = new Service(serverless);
      serviceInstance.functions = {
        create: {
          events: [
            {
              schedule: 'rate(5 minutes)',
            },
            {
              bucket: 'my_bucket',
            },
          ],
        },
      };

      expect(serviceInstance.getAllEventsInFunction('create')).to.deep.equal([
        {
          schedule: 'rate(5 minutes)',
        },
        {
          bucket: 'my_bucket',
        },
      ]);
    });
  });
});
