'use strict';

const chai = require('chai');
const sinon = require('sinon');
const proxyquire = require('proxyquire');
const overrideEnv = require('process-utils/override-env');
const overrideStdoutWrite = require('process-utils/override-stdout-write');
const requireUncached = require('ncjsm/require-uncached');
const chalk = require('chalk');
const { StepHistory } = require('@serverless/utils/telemetry');

const { expect } = chai;

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

const { join, resolve } = require('path');
const { remove: rmDir, outputFile: writeFile } = require('fs-extra');
const { resolveFileProfiles } = require('../../../../../lib/plugins/aws/utils/credentials');

const mockedSdk = {
  organizations: {
    get: () => {
      return {
        orgUid: 'org-uid',
      };
    },
  },
  getProviders: async () => {
    return { result: [] };
  },
};

const step = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
  '../../utils/openBrowser': async (url) => {
    openBrowserUrls.push(url);
  },
  '@serverless/dashboard-plugin/lib/clientUtils': {
    getPlatformClientWithAccessKey: async () => mockedSdk,
  },
});

const inquirer = require('@serverless/utils/inquirer');
const configureInquirerStub = require('@serverless/test/configure-inquirer-stub');

const openBrowserUrls = [];

describe('test/unit/lib/cli/interactive-setup/aws-credentials.test.js', () => {
  const accessKeyId = 'AKIAIOSFODNN7EXAMPLE';
  const secretAccessKey = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';

  afterEach(() => {
    openBrowserUrls.length = 0;
    sinon.restore();
  });

  it('Should be ineffective, when not at service path', async () => {
    const context = {};
    expect(await step.isApplicable(context)).to.equal(false);
    expect(context.inapplicabilityReasonCode).to.equal('NOT_IN_SERVICE_DIRECTORY');
  });

  it('Should be ineffective, when not at AWS service', async () => {
    const context = {
      serviceDir: process.cwd(),
      configuration: {},
      configurationFilename: 'serverless.yml',
    };
    expect(await step.isApplicable(context)).to.equal(false);
    expect(context.inapplicabilityReasonCode).to.equal('NON_AWS_PROVIDER');
  });

  it('Should be ineffective, when user has default provider set', async () => {
    const internalMockedSdk = {
      ...mockedSdk,
      getProviders: async () => {
        return {
          result: [
            {
              alias: 'someprovider',
              providerName: 'aws',
              providerType: 'roleArn',
              providerUid: 'provideruid',
              isDefault: true,
              providerDetails: {
                roleArn: 'arn:xxx',
              },
            },
          ],
        };
      },
    };
    const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
      '@serverless/dashboard-plugin/lib/clientUtils': {
        getPlatformClientWithAccessKey: async () => internalMockedSdk,
      },
      '@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
    });

    const context = {
      serviceDir: process.cwd(),
      configuration: { provider: { name: 'aws' }, org: 'someorg' },
      configurationFilename: 'serverless.yml',
    };
    expect(await mockedStep.isApplicable(context)).to.be.false;
    expect(context.inapplicabilityReasonCode).to.equal('DEFAULT_PROVIDER_CONFIGURED');
  });

  it('Should be ineffective, when existing service already has a provider set', async () => {
    const internalMockedSdk = {
      ...mockedSdk,
      getProviders: async () => {
        return {
          result: [
            {
              alias: 'someprovider',
              providerName: 'aws',
              providerType: 'roleArn',
              providerUid: 'provideruid',
              isDefault: false,
              providerDetails: {
                roleArn: 'arn:xxx',
              },
            },
          ],
        };
      },
    };
    const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
      '@serverless/dashboard-plugin/lib/clientUtils': {
        getPlatformClientWithAccessKey: async () => internalMockedSdk,
      },
      '@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
      './utils': {
        doesServiceInstanceHaveLinkedProvider: () => true,
      },
    });

    const context = {
      history: new Set(),
      serviceDir: process.cwd(),
      configuration: {
        provider: { name: 'aws' },
        org: 'someorg',
        app: 'someapp',
        service: 'service',
      },
      options: {},
      configurationFilename: 'serverless.yml',
    };
    expect(await mockedStep.isApplicable(context)).to.be.false;
    expect(context.inapplicabilityReasonCode).to.equal('LINKED_PROVIDER_CONFIGURED');
  });

  it('Should be effective, when existing service instance does not have a provider set', async () => {
    const internalMockedSdk = {
      ...mockedSdk,
      getProviders: async () => {
        return {
          result: [
            {
              alias: 'someprovider',
              providerName: 'aws',
              providerType: 'roleArn',
              providerUid: 'provideruid',
              isDefault: false,
              providerDetails: {
                roleArn: 'arn:xxx',
              },
            },
          ],
        };
      },
    };
    const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
      '@serverless/dashboard-plugin/lib/clientUtils': {
        getPlatformClientWithAccessKey: async () => internalMockedSdk,
      },
      '@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
      './utils': {
        doesServiceInstanceHaveLinkedProvider: () => false,
      },
    });

    expect(
      await mockedStep.isApplicable({
        history: new Set(),
        serviceDir: process.cwd(),
        configuration: {
          provider: { name: 'aws' },
          org: 'someorg',
          app: 'someapp',
          service: 'service',
        },
        options: {},
        configurationFilename: 'serverless.yml',
      })
    ).to.be.true;
  });

  it('Should emit warning when dashboard is not available when fetching providers', async () => {
    const internalMockedSdk = {
      ...mockedSdk,
      getProviders: async () => {
        const err = new Error('unavailable');
        err.statusCode = 500;
        throw err;
      },
    };
    const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
      '@serverless/dashboard-plugin/lib/clientUtils': {
        getPlatformClientWithAccessKey: async () => internalMockedSdk,
      },
      '@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
    });

    let stdoutData = '';
    await overrideStdoutWrite(
      (data) => (stdoutData += data),
      async () =>
        expect(
          await mockedStep.isApplicable({
            serviceDir: process.cwd(),
            configuration: { provider: { name: 'aws' }, org: 'someorg' },
            configurationFilename: 'serverless.yml',
          })
        ).to.be.false
    );

    expect(stdoutData).to.include('Dashboard service is currently unavailable');
  });

  it('Should be effective, at AWS service and no credentials are set', async () =>
    expect(
      await step.isApplicable({
        serviceDir: process.cwd(),
        configuration: { provider: { name: 'aws' } },
        configurationFilename: 'serverless.yml',
      })
    ).to.equal(true));

  it('Should emit a message when user decides to skip credentials setup', async () => {
    configureInquirerStub(inquirer, {
      list: { credentialsSetupChoice: '_skip_' },
    });

    const context = {
      serviceDir: process.cwd(),
      configuration: { provider: { name: 'aws' } },
      configurationFilename: 'serverless.yml',
      stepHistory: new StepHistory(),
      options: {},
    };
    let stdoutData = '';
    await overrideStdoutWrite(
      (data) => (stdoutData += data),
      async () => await step.run(context)
    );

    expect(stdoutData).to.include('You can setup your AWS account later');
    expect(context.stepHistory.valuesMap()).to.deep.equal(
      new Map([['credentialsSetupChoice', '_skip_']])
    );
  });

  describe('In environment credentials', () => {
    let restoreEnv;
    let uncachedStep;

    before(() => {
      ({ restoreEnv } = overrideEnv({ asCopy: true }));
      process.env.AWS_ACCESS_KEY_ID = accessKeyId;
      process.env.AWS_SECRET_ACCESS_KEY = secretAccessKey;
      uncachedStep = requireUncached(() =>
        require('../../../../../lib/cli/interactive-setup/aws-credentials')
      );
    });

    after(() => restoreEnv);

    it('Should be ineffective, when credentials are set in environment', async () => {
      expect(
        await uncachedStep.isApplicable({
          serviceDir: process.cwd(),
          configuration: { provider: { name: 'aws' } },
          configurationFilename: 'serverless.yml',
        })
      ).to.equal(false);
    });
  });

  describe('AWS config handling', () => {
    let credentialsDirPath;
    let credentialsFilePath;

    before(() => {
      credentialsDirPath = resolve('.aws');
      credentialsFilePath = join(credentialsDirPath, 'credentials');
    });

    afterEach(() => rmDir(credentialsDirPath));

    describe('Existing credentials case', () => {
      before(() =>
        writeFile(
          credentialsFilePath,
          [
            '[some-profile]',
            `aws_access_key_id = ${accessKeyId}`,
            `aws_secret_access_key = ${secretAccessKey}`,
          ].join('\n')
        )
      );

      it('Should be ineffective, When credentials are set in AWS config', async () =>
        expect(
          await step.isApplicable({
            serviceDir: process.cwd(),
            configuration: { provider: { name: 'aws' } },
            configurationFilename: 'serverless.yml',
          })
        ).to.equal(false));
    });

    it('Should setup credentials for users not having an AWS account', async () => {
      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: '_local_' },
        confirm: { hasAwsAccount: false },
        input: {
          createAwsAccountPrompt: '',
          generateAwsCredsPrompt: '',
          accessKeyId,
          secretAccessKey,
        },
      });
      const context = {
        configuration: { provider: {} },
        options: {},
        stepHistory: new StepHistory(),
      };
      await step.run(context);
      expect(openBrowserUrls.length).to.equal(2);
      expect(openBrowserUrls[0].includes('signup')).to.be.true;
      expect(openBrowserUrls[1].includes('console.aws.amazon.com')).to.be.true;
      resolveFileProfiles().then((profiles) => {
        expect(profiles).to.deep.equal(new Map([['default', { accessKeyId, secretAccessKey }]]));
      });
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([
          ['credentialsSetupChoice', '_local_'],
          ['createAwsAccountPrompt', '_continuation_'],
          ['generateAwsCredsPrompt', '_continuation_'],
          ['accessKeyId', '_user_input_'],
          ['secretAccessKey', '_user_input_'],
        ])
      );
    });

    it('Should setup credentials for users having an AWS account', async () => {
      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: '_local_' },
        confirm: { hasAwsAccount: true },
        input: { generateAwsCredsPrompt: '', accessKeyId, secretAccessKey },
      });
      const context = {
        configuration: { provider: {} },
        options: {},
        stepHistory: new StepHistory(),
      };
      await step.run(context);
      expect(openBrowserUrls.length).to.equal(1);
      expect(openBrowserUrls[0].includes('console.aws.amazon.com')).to.be.true;
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([
          ['credentialsSetupChoice', '_local_'],
          ['generateAwsCredsPrompt', '_continuation_'],
          ['accessKeyId', '_user_input_'],
          ['secretAccessKey', '_user_input_'],
        ])
      );
      return resolveFileProfiles().then((profiles) => {
        expect(profiles).to.deep.equal(new Map([['default', { accessKeyId, secretAccessKey }]]));
      });
    });

    it('Should not accept invalid access key id', async () => {
      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: '_local_' },
        confirm: { hasAwsAccount: true },
        input: { generateAwsCredsPrompt: '', accessKeyId: 'foo', secretAccessKey },
      });
      const context = {
        configuration: { provider: {} },
        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([
          ['credentialsSetupChoice', '_local_'],
          ['generateAwsCredsPrompt', '_continuation_'],
          ['accessKeyId', undefined],
        ])
      );
    });

    it('Should not accept invalid secret access key', async () => {
      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: '_local_' },
        confirm: { hasAwsAccount: true },
        input: { generateAwsCredsPrompt: '', accessKeyId, secretAccessKey: 'foo' },
      });
      const context = {
        configuration: { provider: {} },
        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([
          ['credentialsSetupChoice', '_local_'],
          ['generateAwsCredsPrompt', '_continuation_'],
          ['accessKeyId', '_user_input_'],
          ['secretAccessKey', undefined],
        ])
      );
    });
  });

  describe('Provider config handling', () => {
    it('Should correctly setup with newly created provider when no previous providers exist', async () => {
      const mockedOpenBrowser = sinon.stub().returns();
      const mockedDisconnect = sinon.stub().returns();
      const mockedCreateProviderLink = sinon.stub().resolves();
      const providerUid = 'provideruid';
      const internalMockedSdk = {
        ...mockedSdk,
        connect: ({ onEvent }) => {
          onEvent({
            data: {
              object: {
                provider_uid: providerUid,
              },
            },
          });
        },
        disconnect: mockedDisconnect,
        createProviderLink: mockedCreateProviderLink,
      };
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
        '@serverless/dashboard-plugin/lib/clientUtils': {
          getPlatformClientWithAccessKey: async () => internalMockedSdk,
        },
        '../../utils/openBrowser': mockedOpenBrowser,
        '@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
      });

      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: '_create_provider_' },
      });

      let stdoutData = '';
      const context = {
        serviceDir: process.cwd(),
        configuration: {
          service: 'someservice',
          provider: { name: 'aws' },
          org: 'someorg',
          app: 'someapp',
        },
        options: {},
        configurationFilename: 'serverless.yml',
        stepHistory: new StepHistory(),
      };
      await overrideStdoutWrite(
        (data) => (stdoutData += data),
        async () => await mockedStep.run(context)
      );

      expect(stdoutData).to.include('AWS Access Role provider was successfully created');
      expect(mockedOpenBrowser).to.have.been.calledWith(
        chalk.bold.white(
          'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws'
        )
      );
      expect(mockedDisconnect).to.have.been.called;
      expect(mockedCreateProviderLink).not.to.have.been.called;
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['credentialsSetupChoice', '_create_provider_']])
      );
    });

    it('Should correctly setup with newly created provider when previous providers exist', async () => {
      const mockedOpenBrowser = sinon.stub().returns();
      const mockedDisconnect = sinon.stub().returns();
      const mockedCreateProviderLink = sinon.stub().resolves();
      const providerUid = 'provideruid';
      const internalMockedSdk = {
        ...mockedSdk,
        connect: ({ onEvent }) => {
          onEvent({
            data: {
              object: {
                provider_uid: providerUid,
              },
            },
          });
        },
        getProviders: async () => {
          return {
            result: [
              {
                alias: 'someprovider',
                providerName: 'aws',
                providerType: 'roleArn',
                providerUid,
                providerDetails: {
                  roleArn: 'arn:xxx',
                },
              },
            ],
          };
        },
        disconnect: mockedDisconnect,
        createProviderLink: mockedCreateProviderLink,
      };
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
        '@serverless/dashboard-plugin/lib/clientUtils': {
          getPlatformClientWithAccessKey: async () => internalMockedSdk,
        },
        '../../utils/openBrowser': mockedOpenBrowser,
        '@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
      });

      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: '_create_provider_' },
      });

      const context = {
        serviceDir: process.cwd(),
        configuration: {
          service: 'someservice',
          provider: { name: 'aws' },
          org: 'someorg',
          app: 'someapp',
        },
        options: {},
        configurationFilename: 'serverless.yml',
        stepHistory: new StepHistory(),
      };
      let stdoutData = '';
      await overrideStdoutWrite(
        (data) => (stdoutData += data),
        async () => await mockedStep.run(context)
      );

      expect(stdoutData).to.include('AWS Access Role provider was successfully created');
      expect(mockedOpenBrowser).to.have.been.calledWith(
        chalk.bold.white(
          'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws'
        )
      );
      expect(mockedDisconnect).to.have.been.called;
      expect(mockedCreateProviderLink).to.have.been.calledWith(
        'org-uid',
        'instance',
        'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1',
        providerUid
      );
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['credentialsSetupChoice', '_create_provider_']])
      );
    });

    it('Should emit warning when dashboard unavailable when connecting to it', async () => {
      const mockedOpenBrowser = sinon.stub().returns();
      const internalMockedSdk = {
        ...mockedSdk,
        connect: () => {
          const err = new Error('error');
          err.statusCode = 500;
          throw err;
        },
      };
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
        '@serverless/dashboard-plugin/lib/clientUtils': {
          getPlatformClientWithAccessKey: async () => internalMockedSdk,
        },
        '../../utils/openBrowser': mockedOpenBrowser,
      });

      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: '_create_provider_' },
      });

      const context = {
        serviceDir: process.cwd(),
        configuration: {
          service: 'someservice',
          provider: { name: 'aws' },
          org: 'someorg',
          app: 'someapp',
        },
        configurationFilename: 'serverless.yml',
        stepHistory: new StepHistory(),
      };
      let stdoutData = '';
      await overrideStdoutWrite(
        (data) => (stdoutData += data),
        async () => await mockedStep.run(context)
      );

      expect(stdoutData).to.include('Dashboard service is currently unavailable');
      expect(mockedOpenBrowser).to.have.been.calledWith(
        chalk.bold.white(
          'https://app.serverless.com/someorg/settings/providers?source=cli&providerId=new&provider=aws'
        )
      );
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['credentialsSetupChoice', '_create_provider_']])
      );
    });

    it('Should correctly setup with existing provider', async () => {
      const providerUid = 'provideruid';
      const mockedCreateProviderLink = sinon.stub().resolves();
      const internalMockedSdk = {
        ...mockedSdk,
        getProviders: async () => {
          return {
            result: [
              {
                alias: 'someprovider',
                providerName: 'aws',
                providerType: 'accessKey',
                providerUid,
                providerDetails: {
                  accessKeyId: 'axx',
                },
              },
            ],
          };
        },
        createProviderLink: mockedCreateProviderLink,
      };
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
        '@serverless/dashboard-plugin/lib/clientUtils': {
          getPlatformClientWithAccessKey: async () => internalMockedSdk,
        },
      });

      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: providerUid },
      });

      const context = {
        serviceDir: process.cwd(),
        configuration: {
          service: 'someservice',
          provider: { name: 'aws' },
          org: 'someorg',
          app: 'someapp',
        },
        options: {},
        configurationFilename: 'serverless.yml',
        stepHistory: new StepHistory(),
      };
      let stdoutData = '';
      await overrideStdoutWrite(
        (data) => (stdoutData += data),
        async () => await mockedStep.run(context)
      );

      expect(mockedCreateProviderLink).to.have.been.calledWith(
        'org-uid',
        'instance',
        'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1',
        'provideruid'
      );
      expect(stdoutData).to.include('Selected provider was successfully linked');
      expect(context.stepHistory.valuesMap()).to.deep.equal(
        new Map([['credentialsSetupChoice', '_user_choice_']])
      );
    });

    it('Should emit a warning when dashboard is not available and link cannot be created', async () => {
      const providerUid = 'provideruid';
      const mockedCreateProviderLink = sinon.stub().callsFake(async () => {
        const err = new Error('error');
        err.statusCode = 500;
        throw err;
      });
      const internalMockedSdk = {
        ...mockedSdk,
        getProviders: async () => {
          return {
            result: [
              {
                alias: 'someprovider',
                providerName: 'aws',
                providerType: 'roleArn',
                providerUid,
                providerDetails: {
                  roleArn: 'arn:xxx',
                },
              },
            ],
          };
        },
        createProviderLink: mockedCreateProviderLink,
      };
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
        '@serverless/dashboard-plugin/lib/clientUtils': {
          getPlatformClientWithAccessKey: async () => internalMockedSdk,
        },
      });

      configureInquirerStub(inquirer, {
        list: { credentialsSetupChoice: providerUid },
      });

      const context = {
        serviceDir: process.cwd(),
        configuration: {
          service: 'someservice',
          provider: { name: 'aws' },
          org: 'someorg',
          app: 'someapp',
        },
        options: {},
        stepHistory: new StepHistory(),
        configurationFilename: 'serverless.yml',
      };
      let stdoutData = '';
      await overrideStdoutWrite(
        (data) => (stdoutData += data),
        async () => await mockedStep.run(context)
      );

      expect(stdoutData).to.include(
        'Dashboard service is currently unavailable, please try again later'
      );
      expect(stdoutData).not.to.include('Selected provider was successfully linked');
      expect(mockedCreateProviderLink).to.have.been.calledWith(
        'org-uid',
        'instance',
        'appName|someapp|serviceName|someservice|stage|dev|region|us-east-1',
        'provideruid'
      );

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

    it('Should emit a warning when dashboard is not available when fetching providers', async () => {
      const internalMockedSdk = {
        ...mockedSdk,
        getProviders: async () => {
          const err = new Error('unavailable');
          err.statusCode = 500;
          throw err;
        },
      };
      const mockedStep = proxyquire('../../../../../lib/cli/interactive-setup/aws-credentials', {
        '@serverless/dashboard-plugin/lib/clientUtils': {
          getPlatformClientWithAccessKey: async () => internalMockedSdk,
        },
        '@serverless/dashboard-plugin/lib/isAuthenticated': () => true,
      });

      let stdoutData = '';
      await overrideStdoutWrite(
        (data) => (stdoutData += data),
        async () =>
          await mockedStep.run({
            serviceDir: process.cwd(),
            configuration: { provider: { name: 'aws' }, org: 'someorg' },
            options: {},
            configurationFilename: 'serverless.yml',
          })
      );

      expect(stdoutData).to.include('Dashboard service is currently unavailable');
    });
  });
});
