text: 'use strict';
  
  const chai = require('chai');
  const sinon = require('sinon');
  const BbPromise = require('bluebird');
  const YAML = require('js-yaml');
  const path = require('path');
  const childProcess = BbPromise.promisifyAll(require('child_process'));
  const fs = require('fs');
  const fse = require('fs-extra');
  const PluginInstall = require('./install');
  const Serverless = require('../../../Serverless');
  const CLI = require('../../../classes/CLI');
  const _ = require('lodash');
  const userStats = require('../../../utils/userStats');
  const { expect } = require('chai');
  const { getTmpDirPath } = require('../../../../tests/utils/fs');
  
  chai.use(require('chai-as-promised'));
  
  describe('PluginInstall', () => {
    let pluginInstall;
    let serverless;
    let consoleLogStub;
    let serverlessErrorStub;
    const plugins = [
      {
        name: '@scope/serverless-plugin-1',
        description: 'Scoped Serverless Plugin 1',
        githubUrl: 'https://github.com/serverless/serverless-plugin-1',
      },
      {
        name: 'serverless-plugin-1',
        description: 'Serverless Plugin 1',
        githubUrl: 'https://github.com/serverless/serverless-plugin-1',
      },
      {
        name: 'serverless-plugin-2',
        description: 'Serverless Plugin 2',
        githubUrl: 'https://github.com/serverless/serverless-plugin-2',
      },
      {
        name: 'serverless-existing-plugin',
        description: 'Serverless Existing plugin',
        githubUrl: 'https://github.com/serverless/serverless-existing-plugin',
      },
    ];
  
    beforeEach(() => {
      serverless = new Serverless();
      serverless.cli = new CLI(serverless);
      serverless.processedInput = serverless.cli.processInput();
      const options = {};
      pluginInstall = new PluginInstall(serverless, options);
      consoleLogStub = sinon.stub(serverless.cli, 'consoleLog').returns();
      serverlessErrorStub = sinon.stub(serverless.classes, 'Error').throws();
    });
  
    afterEach(() => {
      serverless.cli.consoleLog.restore();
      serverless.classes.Error.restore();
    });
  
    describe('#constructor()', () => {
      let installStub;
  
      beforeEach(() => {
        installStub = sinon.stub(pluginInstall, 'install').returns(BbPromise.resolve());
        sinon.stub(userStats, 'track').resolves();
      });
  
      afterEach(() => {
        pluginInstall.install.restore();
        userStats.track.restore();
      });
  
      it('should have the sub-command "install"', () => {
        expect(pluginInstall.commands.plugin.commands.install).to.not.equal(undefined);
      });
  
      it('should have the lifecycle event "install" for the "install" sub-command', () => {
        expect(pluginInstall.commands.plugin.commands.install.lifecycleEvents).to.deep.equal([
          'install',
        ]);
      });
  
      it('should have a required option "name" for the "install" sub-command', () => {
        // eslint-disable-next-line no-unused-expressions
        expect(pluginInstall.commands.plugin.commands.install.options.name.required).to.be.true;
      });
  
      it('should have a "plugin:install:install" hook', () => {
        expect(pluginInstall.hooks['plugin:install:install']).to.not.equal(undefined);
      });
  
      it('should run promise chain in order for "plugin:install:install" hook', () =>
        pluginInstall.hooks['plugin:install:install']().then(() => {
          expect(installStub.calledOnce).to.equal(true);
        }));
    });
  
    describe('#install()', () => {
      let servicePath;
      let serverlessYmlFilePath;
      let pluginInstallStub;
      let validateStub;
      let getPluginsStub;
      let savedCwd;
      let addPluginToServerlessFileStub;
      let installPeerDependenciesStub;
  
      beforeEach(() => {
        servicePath = getTmpDirPath();
        pluginInstall.serverless.config.servicePath = servicePath;
        fse.ensureDirSync(servicePath);
        serverlessYmlFilePath = path.join(servicePath, 'serverless.yml');
        validateStub = sinon.stub(pluginInstall, 'validate').returns(BbPromise.resolve());
        pluginInstallStub = sinon.stub(pluginInstall, 'pluginInstall').returns(BbPromise.resolve());
        addPluginToServerlessFileStub = sinon
          .stub(pluginInstall, 'addPluginToServerlessFile')
          .returns(BbPromise.resolve());
        installPeerDependenciesStub = sinon
          .stub(pluginInstall, 'installPeerDependencies')
          .returns(BbPromise.resolve());
        getPluginsStub = sinon.stub(pluginInstall, 'getPlugins').returns(BbPromise.resolve(plugins));
        // save the cwd so that we can restore it later
        savedCwd = process.cwd();
        process.chdir(servicePath);
      });
  
      afterEach(() => {
        pluginInstall.validate.restore();
        pluginInstall.getPlugins.restore();
        pluginInstall.pluginInstall.restore();
        pluginInstall.addPluginToServerlessFile.restore();
        pluginInstall.installPeerDependencies.restore();
        process.chdir(savedCwd);
      });
  
      it('should install the plugin if it can be found in the registry', () => {
        // serverless.yml
        const serverlessYml = {
          service: 'plugin-service',
          provider: 'aws',
        };
        serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
  
        pluginInstall.options.name = 'serverless-plugin-1';
  
        return expect(pluginInstall.install()).to.be.fulfilled.then(() => {
          expect(validateStub.calledOnce).to.equal(true);
          expect(getPluginsStub.calledOnce).to.equal(true);
          expect(pluginInstallStub.calledOnce).to.equal(true);
          expect(consoleLogStub.called).to.equal(true);
          expect(serverlessErrorStub.calledOnce).to.equal(false);
          expect(addPluginToServerlessFileStub.calledOnce).to.equal(true);
          expect(installPeerDependenciesStub.calledOnce).to.equal(true);
        });
      });
  
      it('should install a scoped plugin if it can be found in the registry', () => {
        // serverless.yml
        const serverlessYml = {
          service: 'plugin-service',
          provider: 'aws',
        };
        serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
  
        pluginInstall.options.name = '@scope/serverless-plugin-1';
  
        return expect(pluginInstall.install()).to.be.fulfilled.then(() => {
          expect(validateStub.calledOnce).to.equal(true);
          expect(getPluginsStub.calledOnce).to.equal(true);
          expect(pluginInstallStub.calledOnce).to.equal(true);
          expect(consoleLogStub.called).to.equal(true);
          expect(serverlessErrorStub.calledOnce).to.equal(false);
          expect(addPluginToServerlessFileStub.calledOnce).to.equal(true);
          expect(installPeerDependenciesStub.calledOnce).to.equal(true);
        });
      });
  
      it('should install the plugin even if it can not be found in the registry', () => {
        // serverless.yml
        const serverlessYml = {
          service: 'plugin-service',
          provider: 'aws',
        };
        serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
  
        pluginInstall.options.name = 'serverless-not-in-registry-plugin';
        return expect(pluginInstall.install()).to.be.fulfilled.then(() => {
          expect(validateStub.calledOnce).to.equal(true);
          expect(getPluginsStub.calledOnce).to.equal(true);
          expect(pluginInstallStub.calledOnce).to.equal(true);
          expect(consoleLogStub.called).to.equal(true);
          expect(serverlessErrorStub.calledOnce).to.equal(false);
          expect(addPluginToServerlessFileStub.calledOnce).to.equal(true);
          expect(installPeerDependenciesStub.calledOnce).to.equal(true);
        });
      });
  
      it('should apply the latest version if you can not get the version from name option', () => {
        const serverlessYml = {
          service: 'plugin-service',
          provider: 'aws',
        };
        serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
        pluginInstall.options.name = 'serverless-plugin-1';
        return expect(pluginInstall.install()).to.be.fulfilled.then(() => {
          expect(pluginInstall.options.pluginName).to.be.equal('serverless-plugin-1');
          expect(pluginInstall.options.pluginVersion).to.be.equal('latest');
        });
      });
  
      it(
        'should apply the latest version if you can not get the ' +
          'version from name option even if scoped',
        () => {
          const serverlessYml = {
            service: 'plugin-service',
            provider: 'aws',
          };
          serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
          pluginInstall.options.name = '@scope/serverless-plugin-1';
          return expect(pluginInstall.install()).to.be.fulfilled.then(() => {
            expect(pluginInstall.options.pluginName).to.be.equal('@scope/serverless-plugin-1');
            expect(pluginInstall.options.pluginVersion).to.be.equal('latest');
          });
        }
      );
  
      it('should apply the specified version if you can get the version from name option', () => {
        const serverlessYml = {
          service: 'plugin-service',
          provider: 'aws',
        };
        serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
        pluginInstall.options.name = 'serverless-plugin-1@1.0.0';
        return expect(pluginInstall.install()).to.be.fulfilled.then(() => {
          expect(pluginInstall.options.pluginName).to.be.equal('serverless-plugin-1');
          expect(pluginInstall.options.pluginVersion).to.be.equal('1.0.0');
        });
      });
    });
  
    describe('#pluginInstall()', () => {
      let servicePath;
      let packageJsonFilePath;
      let npmInstallStub;
      let savedCwd;
  
      beforeEach(() => {
        pluginInstall.options.pluginName = 'serverless-plugin-1';
        pluginInstall.options.pluginVersion = 'latest';
        servicePath = getTmpDirPath();
        pluginInstall.serverless.config.servicePath = servicePath;
        fse.ensureDirSync(servicePath);
        packageJsonFilePath = path.join(servicePath, 'package.json');
        npmInstallStub = sinon.stub(childProcess, 'execAsync').callsFake(() => {
          const packageJson = serverless.utils.readFileSync(packageJsonFilePath, 'utf8');
          packageJson.devDependencies = {
            'serverless-plugin-1': 'latest',
          };
          serverless.utils.writeFileSync(packageJsonFilePath, packageJson);
          return BbPromise.resolve();
        });
  
        // save the cwd so that we can restore it later
        savedCwd = process.cwd();
        process.chdir(servicePath);
      });
  
      afterEach(() => {
        childProcess.execAsync.restore();
        process.chdir(savedCwd);
      });
  
      it('should install the plugin if it has not been installed yet', () => {
        const packageJson = {
          name: 'test-service',
          description: '',
          version: '0.1.0',
          dependencies: {},
          devDependencies: {},
        };
  
        serverless.utils.writeFileSync(packageJsonFilePath, packageJson);
  
        return expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() =>
          Promise.all([
            expect(consoleLogStub.called).to.equal(true),
            expect(
              npmInstallStub.calledWithExactly('npm install --save-dev serverless-plugin-1@latest', {
                stdio: 'ignore',
              })
            ).to.equal(true),
            expect(serverlessErrorStub.calledOnce).to.equal(false),
          ])
        );
      });
  
      it('should generate a package.json file in the service directory if not present', () =>
        expect(pluginInstall.pluginInstall()).to.be.fulfilled.then(() => {
          expect(consoleLogStub.called).to.equal(true);
          expect(
            npmInstallStub.calledWithExactly('npm install --save-dev serverless-plugin-1@latest', {
              stdio: 'ignore',
            })
          ).to.equal(true);
          expect(fs.existsSync(packageJsonFilePath)).to.equal(true);
        }));
    });
  
    describe('#addPluginToServerlessFile()', () => {
      let servicePath;
      let serverlessYmlFilePath;
  
      beforeEach(() => {
        servicePath = getTmpDirPath();
        pluginInstall.serverless.config.servicePath = servicePath;
        serverlessYmlFilePath = path.join(servicePath, 'serverless.yml');
      });
  
      it('should add the plugin to the service file if plugins array is not present', () => {
        // serverless.yml
        const serverlessYml = {
          service: 'plugin-service',
          provider: 'aws',
          // no plugins array here
        };
        serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
  
        pluginInstall.options.pluginName = 'serverless-plugin-1';
  
        return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
          expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal(
            _.assign({}, serverlessYml, {
              plugins: ['serverless-plugin-1'],
            })
          );
        });
      });
  
      it('should push the plugin to the service files plugin array if present', () => {
        // serverless.yml
        const serverlessYml = {
          service: 'plugin-service',
          provider: 'aws',
          plugins: ['serverless-existing-plugin'], // one plugin was already added
        };
        serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
  
        pluginInstall.options.pluginName = 'serverless-plugin-1';
  
        return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
          expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal(
            _.assign({}, serverlessYml, {
              plugins: ['serverless-existing-plugin', 'serverless-plugin-1'],
            })
          );
        });
      });
  
      it('should add the plugin to serverless file path for a .yaml file', () => {
        const serverlessYamlFilePath = path.join(servicePath, 'serverless.yaml');
        const serverlessYml = {
          service: 'plugin-service',
          provider: 'aws',
        };
        serverless.utils.writeFileSync(serverlessYamlFilePath, YAML.dump(serverlessYml));
        pluginInstall.options.pluginName = 'serverless-plugin-1';
        return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
          expect(serverless.utils.readFileSync(serverlessYamlFilePath, 'utf8')).to.deep.equal(
            _.assign({}, serverlessYml, { plugins: ['serverless-plugin-1'] })
          );
        });
      });
  
      it('should add the plugin to serverless file path for a .json file', () => {
        const serverlessJsonFilePath = path.join(servicePath, 'serverless.json');
        const serverlessJson = {
          service: 'plugin-service',
          provider: 'aws',
        };
        serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson);
        pluginInstall.options.pluginName = 'serverless-plugin-1';
        return expect(pluginInstall.addPluginToServerlessFile())
          .to.be.fulfilled.then(() => {
            expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal(
              _.assign({}, serverlessJson, { plugins: ['serverless-plugin-1'] })
            );
          })
          .then(() => {
            pluginInstall.options.pluginName = 'serverless-plugin-2';
            return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
              expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal(
                _.assign({}, serverlessJson, {
                  plugins: ['serverless-plugin-1', 'serverless-plugin-2'],
                })
              );
            });
          });
      });
  
      it('should not modify serverless .js file', () => {
        const serverlessJsFilePath = path.join(servicePath, 'serverless.js');
        const serverlessJson = {
          service: 'plugin-service',
          provider: 'aws',
          plugins: [],
        };
        serverless.utils.writeFileSync(
          serverlessJsFilePath,
          `module.exports = ${JSON.stringify(serverlessJson)};`
        );
        pluginInstall.options.pluginName = 'serverless-plugin-1';
        return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
          // use require to load serverless.js
          // eslint-disable-next-line global-require
          expect(require(serverlessJsFilePath).plugins).to.be.deep.equal([]);
        });
      });
  
      describe('if plugins object is not array', () => {
        it('should add the plugin to the service file', () => {
          // serverless.yml
          const serverlessYml = {
            service: 'plugin-service',
            provider: 'aws',
            plugins: {
              localPath: 'test',
              modules: [],
            },
          };
          serverless.utils.writeFileSync(serverlessYmlFilePath, YAML.dump(serverlessYml));
  
          pluginInstall.options.pluginName = 'serverless-plugin-1';
  
          return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
            expect(serverless.utils.readFileSync(serverlessYmlFilePath, 'utf8')).to.deep.equal(
              _.assign({}, serverlessYml, {
                plugins: {
                  localPath: 'test',
                  modules: [pluginInstall.options.pluginName],
                },
              })
            );
          });
        });
  
        it('should add the plugin to serverless file path for a .json file', () => {
          const serverlessJsonFilePath = path.join(servicePath, 'serverless.json');
          const serverlessJson = {
            service: 'plugin-service',
            provider: 'aws',
            plugins: {
              localPath: 'test',
              modules: [],
            },
          };
          serverless.utils.writeFileSync(serverlessJsonFilePath, serverlessJson);
          pluginInstall.options.pluginName = 'serverless-plugin-1';
          return expect(pluginInstall.addPluginToServerlessFile())
            .to.be.fulfilled.then(() => {
              expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal(
                _.assign({}, serverlessJson, {
                  plugins: {
                    localPath: 'test',
                    modules: [pluginInstall.options.pluginName],
                  },
                })
              );
            })
            .then(() => {
              pluginInstall.options.pluginName = 'serverless-plugin-2';
              return expect(pluginInstall.addPluginToServerlessFile()).to.be.fulfilled.then(() => {
                expect(serverless.utils.readFileSync(serverlessJsonFilePath, 'utf8')).to.deep.equal(
                  _.assign({}, serverlessJson, {
                    plugins: {
                      localPath: 'test',
                      modules: ['serverless-plugin-1', 'serverless-plugin-2'],
                    },
                  })
                );
              });
            });
        });
      });
    });
  
    describe('#installPeerDependencies()', () => {
      let servicePath;
      let servicePackageJsonFilePath;
      let pluginPath;
      let pluginPackageJsonFilePath;
      let pluginName;
      let npmInstallStub;
      let savedCwd;
  
      beforeEach(() => {
        pluginName = 'some-plugin';
        pluginInstall.options.pluginName = pluginName;
        servicePath = getTmpDirPath();
        fse.ensureDirSync(servicePath);
        pluginInstall.serverless.config.servicePath = servicePath;
        servicePackageJsonFilePath = path.join(servicePath, 'package.json');
        fse.writeJsonSync(servicePackageJsonFilePath, {
          devDependencies: {},
        });
        pluginPath = path.join(servicePath, 'node_modules', pluginName);
        fse.ensureDirSync(pluginPath);
        pluginPackageJsonFilePath = path.join(pluginPath, 'package.json');
        npmInstallStub = sinon.stub(childProcess, 'execAsync').returns(BbPromise.resolve());
        savedCwd = process.cwd();
        process.chdir(servicePath);
      });
  
      afterEach(() => {
        childProcess.execAsync.restore();
        process.chdir(savedCwd);
      });
  
      it('should install peerDependencies if an installed plugin has ones', () => {
        fse.writeJsonSync(pluginPackageJsonFilePath, {
          peerDependencies: {
            'some-package': '*',
          },
        });
        return expect(pluginInstall.installPeerDependencies()).to.be.fulfilled.then(() => {
          expect(
            npmInstallStub.calledWithExactly('npm install --save-dev some-package@"*"', {
              stdio: 'ignore',
            })
          ).to.equal(true);
        });
      });
  
      it('should not install peerDependencies if an installed plugin does not have ones', () => {
        fse.writeJsonSync(pluginPackageJsonFilePath, {});
        return expect(pluginInstall.installPeerDependencies()).to.be.fulfilled.then(() => {
          expect(fse.readJsonSync(servicePackageJsonFilePath)).to.be.deep.equal({
            devDependencies: {},
          });
          expect(npmInstallStub.calledWithExactly('npm install', { stdio: 'ignore' })).to.equal(
            false
          );
        });
      });
    });
  });
  