'use strict';

const chai = require('chai');
const path = require('path');
const sinon = require('sinon');
const configureInquirerStub = require('@serverless/test/configure-inquirer-stub');
const step = require('../../../../../lib/cli/interactive-setup/service');
const proxyquire = require('proxyquire');
const overrideStdoutWrite = require('process-utils/override-stdout-write');
const ServerlessError = require('../../../../../lib/serverless-error');
const { StepHistory } = require('@serverless/utils/telemetry');

const templatesPath = path.resolve(__dirname, '../../../../../lib/plugins/create/templates');

const { expect } = chai;

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

const fsp = require('fs').promises;
const inquirer = require('@serverless/utils/inquirer');

const confirmEmptyWorkingDir = async () =>
  expect(await fsp.readdir(process.cwd())).to.deep.equal([]);

describe('test/unit/lib/cli/interactive-setup/service.test.js', () => {
  afterEach(() => {
    sinon.restore();
  });

  it('Should be not applied, when at service path', () => {
    const context = {
      serviceDir: '/foo',
      options: {},
    };
    expect(step.isApplicable(context)).to.equal(false);
    expect(context.inapplicabilityReasonCode).to.equal('IN_SERVICE_DIRECTORY');
  });

  it('Should be applied, when not at service path', () => {
    const context = { options: {} };
    expect(step.isApplicable(context)).to.equal(true);
    expect(context.inapplicabilityReasonCode).to.be.undefined;
  });

  it('Should result in an error when at service path with `template-path` options provided', () => {
    expect(() =>
      step.isApplicable({ serviceDir: '/foo', options: { 'template-path': 'path/to/template' } })
    )
      .to.throw()
      .and.have.property('code', 'NOT_APPLICABLE_SERVICE_OPTIONS');
  });

  it('Should result in an error when at service path with `template` option provided', () => {
    expect(() => step.isApplicable({ serviceDir: '/foo', options: { template: 'test-template' } }))
      .to.throw()
      .and.have.property('code', 'NOT_APPLICABLE_SERVICE_OPTIONS');
  });

  it('Should result in an error when at service path with `template-url` option provided', () => {
    expect(() =>
      step.isApplicable({ serviceDir: '/foo', options: { 'template-url': 'test-template' } })
    )
      .to.throw()
      .and.have.property('code', 'NOT_APPLICABLE_SERVICE_OPTIONS');
  });

  it("Should abort if user choses 'other' template", async () => {
    configureInquirerStub(inquirer, {
      list: { projectType: 'other' },
    });
    const context = { options: {}, stepHistory: new StepHistory() };
    await step.run(context);
    expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['projectType', 'other']]));
    return confirmEmptyWorkingDir();
  });

  describe('Create new project', () => {
    it('Should create project at not existing directory', async () => {
      const downloadTemplateFromRepoStub = sinon.stub();
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: downloadTemplateFromRepoStub.callsFake(
            async (templateUrl, projectType, projectName) => {
              await fsp.mkdir(projectName);
              const serverlessYmlContent = `
            service: service
            provider:
              name: aws
           `;

              await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent);
            }
          ),
        },
      });

      configureInquirerStub(inquirer, {
        list: { projectType: 'aws-nodejs' },
        input: { projectName: 'test-project' },
      });
      const context = { options: {}, stepHistory: new StepHistory() };
      await mockedStep.run(context);
      const stats = await fsp.lstat('test-project/serverless.yml');
      expect(stats.isFile()).to.be.true;
      expect(downloadTemplateFromRepoStub).to.have.been.calledWith(
        'https://github.com/serverless/examples/tree/master/aws-nodejs',
        'aws-nodejs',
        'test-project'
      );
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([
          ['projectType', 'aws-nodejs'],
          ['projectName', '_user_input_'],
        ])
      );
    });

    it('Should remove `serverless.template.yml` if its a part of the template', async () => {
      const downloadTemplateFromRepoStub = sinon.stub();
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: downloadTemplateFromRepoStub.callsFake(
            async (templateUrl, projectType, projectName) => {
              await fsp.mkdir(projectName);
              const serverlessYmlContent = `
            service: service
            provider:
              name: aws
           `;

              await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent);
              await fsp.writeFile(path.join(projectName, 'serverless.template.yml'), '');
            }
          ),
        },
      });

      configureInquirerStub(inquirer, {
        list: { projectType: 'aws-nodejs' },
        input: { projectName: 'test-project-template' },
      });
      const context = { options: {}, stepHistory: new StepHistory() };
      await mockedStep.run(context);
      const stats = await fsp.lstat('test-project-template/serverless.yml');
      expect(stats.isFile()).to.be.true;
      expect(downloadTemplateFromRepoStub).to.have.been.calledWith(
        'https://github.com/serverless/examples/tree/master/aws-nodejs',
        'aws-nodejs',
        'test-project-template'
      );
      await expect(
        fsp.lstat('test-proejct-template/serverless.template.yml')
      ).to.eventually.be.rejected.and.have.property('code', 'ENOENT');

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([
          ['projectType', 'aws-nodejs'],
          ['projectName', '_user_input_'],
        ])
      );
    });

    it('Should run `npm install` if `package.json` present', async () => {
      const downloadTemplateFromRepoStub = sinon.stub();
      const spawnStub = sinon.stub();
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        'child-process-ext/spawn': spawnStub,
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: downloadTemplateFromRepoStub.callsFake(
            async (templateUrl, projectType, projectName) => {
              await fsp.mkdir(projectName);
              const serverlessYmlContent = `
            service: service
            provider:
              name: aws
           `;

              await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent);
              await fsp.writeFile(path.join(projectName, 'package.json'), '{}');
            }
          ),
        },
      });

      configureInquirerStub(inquirer, {
        list: { projectType: 'aws-nodejs' },
        input: { projectName: 'test-project-package-json' },
      });
      const context = { options: {}, stepHistory: new StepHistory() };
      await mockedStep.run(context);
      const stats = await fsp.lstat('test-project-package-json/serverless.yml');
      expect(stats.isFile()).to.be.true;
      expect(downloadTemplateFromRepoStub).to.have.been.calledWith(
        'https://github.com/serverless/examples/tree/master/aws-nodejs',
        'aws-nodejs',
        'test-project-package-json'
      );
      expect(spawnStub).to.have.been.calledWith('npm', ['install'], {
        cwd: path.join(process.cwd(), 'test-project-package-json'),
      });

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([
          ['projectType', 'aws-nodejs'],
          ['projectName', '_user_input_'],
        ])
      );
    });

    it('Should emit warning if npm installation not found', async () => {
      const downloadTemplateFromRepoStub = sinon.stub();
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        'child-process-ext/spawn': sinon.stub().rejects({ code: 'ENOENT' }),
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: downloadTemplateFromRepoStub.callsFake(
            async (templateUrl, projectType, projectName) => {
              await fsp.mkdir(projectName);
              const serverlessYmlContent = `
            service: service
            provider:
              name: aws
           `;

              await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent);
              await fsp.writeFile(path.join(projectName, 'package.json'), '{}');
            }
          ),
        },
      });

      configureInquirerStub(inquirer, {
        list: { projectType: 'aws-nodejs' },
        input: { projectName: 'test-project-missing-npm' },
      });

      const context = { options: {}, stepHistory: new StepHistory() };
      let stdoutData = '';
      await overrideStdoutWrite(
        (data) => (stdoutData += data),
        async () => mockedStep.run(context)
      );

      const stats = await fsp.lstat('test-project-missing-npm/serverless.yml');
      expect(stats.isFile()).to.be.true;
      expect(stdoutData).to.include('Cannot install dependencies');

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([
          ['projectType', 'aws-nodejs'],
          ['projectName', '_user_input_'],
        ])
      );
    });

    it('Should emit warning if npm installation not found', async () => {
      const downloadTemplateFromRepoStub = sinon.stub();
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        'child-process-ext/spawn': sinon.stub().rejects({ message: 'Error message' }),
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: downloadTemplateFromRepoStub.callsFake(
            async (templateUrl, projectType, projectName) => {
              await fsp.mkdir(projectName);
              const serverlessYmlContent = `
            service: service
            provider:
              name: aws
           `;

              await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent);
              await fsp.writeFile(path.join(projectName, 'package.json'), '{}');
            }
          ),
        },
      });

      configureInquirerStub(inquirer, {
        list: { projectType: 'aws-nodejs' },
        input: { projectName: 'test-project-failed-install' },
      });

      const context = { options: {}, stepHistory: new StepHistory() };
      await expect(mockedStep.run(context)).to.be.eventually.rejected.and.have.property(
        'code',
        'DEPENDENCIES_INSTALL_FAILED'
      );

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([
          ['projectType', 'aws-nodejs'],
          ['projectName', '_user_input_'],
        ])
      );
    });

    it('Should create project at not existing directory from a provided `template-path`', async () => {
      configureInquirerStub(inquirer, {
        input: { projectName: 'test-project-from-local-template' },
      });
      const context = {
        options: { 'template-path': path.join(templatesPath, 'aws-nodejs') },
        stepHistory: new StepHistory(),
      };
      await step.run(context);
      const stats = await fsp.lstat('test-project-from-local-template/serverless.yml');
      expect(stats.isFile()).to.be.true;

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['projectName', '_user_input_']])
      );
    });

    it('Should create project at not existing directory with provided `name`', async () => {
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: sinon
            .stub()
            .callsFake(async (templateUrl, projectType, projectName) => {
              await fsp.mkdir(projectName);
              const serverlessYmlContent = `
            service: service
            provider:
              name: aws
           `;

              await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent);
            }),
        },
      });
      configureInquirerStub(inquirer, {
        list: { projectType: 'aws-nodejs' },
      });
      const context = {
        options: { name: 'test-project-from-cli-option' },
        stepHistory: new StepHistory(),
      };
      await mockedStep.run(context);
      const stats = await fsp.lstat('test-project-from-cli-option/serverless.yml');
      expect(stats.isFile()).to.be.true;
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['projectType', 'aws-nodejs']])
      );
    });

    it('Should create project at not existing directory with provided template', async () => {
      const downloadTemplateFromRepoStub = sinon.stub();
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: downloadTemplateFromRepoStub.callsFake(
            async (templateUrl, projectType, projectName) => {
              const serverlessYmlContent = `
            service: service
            provider:
              name: aws
           `;

              await fsp.mkdir(projectName);
              await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent);
            }
          ),
        },
      });
      configureInquirerStub(inquirer, {
        input: { projectName: 'test-project-from-provided-template' },
      });
      const context = { options: { template: 'test-template' }, stepHistory: new StepHistory() };
      await mockedStep.run(context);
      const stats = await fsp.lstat('test-project-from-provided-template/serverless.yml');
      expect(stats.isFile()).to.be.true;
      expect(downloadTemplateFromRepoStub).to.have.been.calledWith(
        'https://github.com/serverless/examples/tree/master/test-template',
        'test-template',
        'test-project-from-provided-template'
      );

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['projectName', '_user_input_']])
      );
    });

    it('Should create project at not existing directory with provided `template-url`', async () => {
      const providedTemplateUrl =
        'https://github.com/serverless/examples/tree/master/test-template';
      const downloadTemplateFromRepoStub = sinon.stub();
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: downloadTemplateFromRepoStub.callsFake(
            async (templateUrl, projectType, projectName) => {
              const serverlessYmlContent = `
            service: service
            provider:
              name: aws
           `;

              await fsp.mkdir(projectName);
              await fsp.writeFile(path.join(projectName, 'serverless.yml'), serverlessYmlContent);
            }
          ),
        },
      });
      configureInquirerStub(inquirer, {
        input: { projectName: 'test-project-from-provided-template-url' },
      });
      const context = {
        options: { 'template-url': providedTemplateUrl },
        stepHistory: new StepHistory(),
      };
      await mockedStep.run(context);
      const stats = await fsp.lstat('test-project-from-provided-template-url/serverless.yml');
      expect(stats.isFile()).to.be.true;
      expect(downloadTemplateFromRepoStub).to.have.been.calledWith(
        providedTemplateUrl,
        null,
        'test-project-from-provided-template-url'
      );

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['projectName', '_user_input_']])
      );
    });

    it('Should throw an error when template cannot be downloaded', async () => {
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: sinon.stub().callsFake(async () => {
            throw new ServerlessError();
          }),
        },
      });
      configureInquirerStub(inquirer, {
        list: { projectType: 'aws-nodejs' },
        input: { projectName: 'test-error-during-download' },
      });
      const context = { options: {}, stepHistory: new StepHistory() };
      await expect(mockedStep.run(context)).to.be.eventually.rejected.and.have.property(
        'code',
        'TEMPLATE_DOWNLOAD_FAILED'
      );

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([
          ['projectType', 'aws-nodejs'],
          ['projectName', '_user_input_'],
        ])
      );
    });

    it('Should throw an error when provided template cannot be found', async () => {
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: sinon.stub().rejects({ code: 'ENOENT' }),
        },
      });
      configureInquirerStub(inquirer, {
        input: { projectName: 'test-error-during-download' },
      });
      const context = { options: { template: 'test-template' }, stepHistory: new StepHistory() };
      await expect(mockedStep.run(context)).to.be.eventually.rejected.and.have.property(
        'code',
        'INVALID_TEMPLATE'
      );
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['projectName', '_user_input_']])
      );
    });

    it('Should throw an error when template provided with url cannot be found', async () => {
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/service', {
        '../../utils/downloadTemplateFromRepo': {
          downloadTemplateFromRepo: sinon.stub().callsFake(async () => {
            throw new ServerlessError();
          }),
        },
      });
      configureInquirerStub(inquirer, {
        input: { projectName: 'test-error-during-download-custom-template' },
      });
      const context = {
        options: { 'template-url': 'test-template-url' },
        stepHistory: new StepHistory(),
      };
      await expect(mockedStep.run(context)).to.be.eventually.rejected.and.have.property(
        'code',
        'INVALID_TEMPLATE_URL'
      );

      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['projectName', '_user_input_']])
      );
    });
  });

  it('Should not allow project creation in a directory in which already service is configured', async () => {
    configureInquirerStub(inquirer, {
      list: { projectType: 'aws-nodejs' },
      input: { projectName: 'existing' },
    });

    await fsp.mkdir('existing');

    const context = { options: {}, stepHistory: new StepHistory() };
    await expect(step.run(context)).to.eventually.be.rejected.and.have.property(
      'code',
      'INVALID_ANSWER'
    );

    expect(context.stepHistory.valuesMap()).to.deep.equal(
      new Map([
        ['projectType', 'aws-nodejs'],
        ['projectName', undefined],
      ])
    );
  });

  it('Should not allow project creation in a directory in which already service is configured when `name` flag provided', async () => {
    configureInquirerStub(inquirer, {
      list: { projectType: 'aws-nodejs' },
    });

    await fsp.mkdir('anotherexisting');

    const context = { options: { name: 'anotherexisting' }, stepHistory: new StepHistory() };
    await expect(step.run(context)).to.eventually.be.rejected.and.have.property(
      'code',
      'TARGET_FOLDER_ALREADY_EXISTS'
    );

    expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['projectType', 'aws-nodejs']]));
  });

  it('Should not allow project creation using an invalid project name', async () => {
    configureInquirerStub(inquirer, {
      list: { projectType: 'aws-nodejs' },
      input: { projectName: 'elo grzegżółka' },
    });
    const context = { options: {}, stepHistory: new StepHistory() };
    await expect(step.run(context)).to.eventually.be.rejected.and.have.property(
      'code',
      'INVALID_ANSWER'
    );

    expect(context.stepHistory.valuesMap()).to.deep.equal(
      new Map([
        ['projectType', 'aws-nodejs'],
        ['projectName', undefined],
      ])
    );
  });

  it('Should not allow project creation using an invalid project name when `name` flag provided', async () => {
    configureInquirerStub(inquirer, {
      list: { projectType: 'aws-nodejs' },
    });
    const context = { options: { name: 'elo grzegżółka' }, stepHistory: new StepHistory() };
    await expect(step.run(context)).to.eventually.be.rejected.and.have.property(
      'code',
      'INVALID_PROJECT_NAME'
    );

    expect(context.stepHistory.valuesMap()).to.deep.equal(new Map([['projectType', 'aws-nodejs']]));
  });

  it('Should not allow project creation if multiple template-related options are provided', async () => {
    await expect(
      step.run({ options: { 'template': 'some-template', 'template-url': 'https://template.com' } })
    ).to.eventually.be.rejected.and.have.property('code', 'MULTIPLE_TEMPLATE_OPTIONS_PROVIDED');
  });
});
