'use strict';
/* eslint-disable no-unused-expressions */
const chai = require('chai');
const sinon = require('sinon');
const path = require('path');
const fs = require('fs');
const proxyquire = require('proxyquire');
const AwsProvider = require('../provider/awsProvider');
const Serverless = require('../../../Serverless');
const { getTmpDirPath } = require('../../../../tests/utils/fs');

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

const expect = chai.expect;

describe('AwsDeployFunction', () => {
  let AwsDeployFunction;
  let serverless;
  let awsDeployFunction;
  let cryptoStub;

  beforeEach(() => {
    serverless = new Serverless();
    serverless.servicePath = true;
    serverless.service.environment = {
      vars: {},
      stages: {
        dev: {
          vars: {},
          regions: {
            'us-east-1': {
              vars: {},
            },
          },
        },
      },
    };
    serverless.service.functions = {
      first: {
        handler: true,
      },
    };
    const options = {
      stage: 'dev',
      region: 'us-east-1',
      function: 'first',
      functionObj: {
        name: 'first',
      },
    };
    return serverless.init().then(() => {
      serverless.setProvider('aws', new AwsProvider(serverless, options));
      cryptoStub = {
        createHash() {
          return this;
        }, // eslint-disable-line
        update() {
          return this;
        }, // eslint-disable-line
        digest: sinon.stub(),
      };
      AwsDeployFunction = proxyquire('./index.js', {
        crypto: cryptoStub,
      });
      awsDeployFunction = new AwsDeployFunction(serverless, options);
    });
  });

  describe('#constructor()', () => {
    it('should have hooks', () => expect(awsDeployFunction.hooks).to.be.not.empty);

    it('should set the provider variable to an instance of AwsProvider', () =>
      expect(awsDeployFunction.provider).to.be.instanceof(AwsProvider));

    it('should set an empty options object if no options are given', () => {
      const awsDeployFunctionWithEmptyOptions = new AwsDeployFunction(serverless);

      expect(awsDeployFunctionWithEmptyOptions.options).to.deep.equal({});
    });
  });

  describe('#checkIfFunctionExists()', () => {
    let getFunctionStub;

    beforeEach(() => {
      getFunctionStub = sinon
        .stub(awsDeployFunction.provider, 'request')
        .resolves({ func: { name: 'first' } });
    });

    afterEach(() => {
      awsDeployFunction.provider.request.restore();
    });

    it('it should throw error if function is not provided', () => {
      serverless.service.functions = {};
      expect(() => awsDeployFunction.checkIfFunctionExists()).to.throw(Error);
    });

    it('should check if the function is deployed and save the result', () => {
      awsDeployFunction.serverless.service.functions = {
        first: {
          name: 'first',
          handler: 'handler.first',
        },
      };

      return awsDeployFunction.checkIfFunctionExists().then(() => {
        expect(getFunctionStub.calledOnce).to.be.equal(true);
        expect(
          getFunctionStub.calledWithExactly('Lambda', 'getFunction', {
            FunctionName: 'first',
          })
        ).to.be.equal(true);
        expect(awsDeployFunction.serverless.service.provider.remoteFunctionData).to.deep.equal({
          func: {
            name: 'first',
          },
        });
      });
    });
  });

  describe('#normalizeArnRole', () => {
    let getAccountInfoStub;
    let getRoleStub;

    beforeEach(() => {
      getAccountInfoStub = sinon
        .stub(awsDeployFunction.provider, 'getAccountInfo')
        .resolves({ accountId: '123456789012', partition: 'aws' });
      getRoleStub = sinon
        .stub(awsDeployFunction.provider, 'request')
        .resolves({ Arn: 'arn:aws:iam::123456789012:role/role_2' });

      serverless.service.resources = {
        Resources: {
          MyCustomRole: {
            Type: 'AWS::IAM::Role',
            Properties: {
              RoleName: 'role_123',
            },
          },
        },
      };
    });

    afterEach(() => {
      awsDeployFunction.provider.getAccountInfo.restore();
      awsDeployFunction.provider.request.restore();
      serverless.service.resources = undefined;
    });

    it('should return unmodified ARN if ARN was provided', () => {
      const arn = 'arn:aws:iam::123456789012:role/role';

      return awsDeployFunction.normalizeArnRole(arn).then(result => {
        expect(getAccountInfoStub).to.not.have.been.called;
        expect(result).to.be.equal(arn);
      });
    });

    it('should return compiled ARN if role name was provided', () => {
      const roleName = 'MyCustomRole';

      return awsDeployFunction.normalizeArnRole(roleName).then(result => {
        expect(getAccountInfoStub).to.have.been.called;
        expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_123');
      });
    });

    it('should return compiled ARN if object role was provided', () => {
      const roleObj = {
        'Fn::GetAtt': ['role_2', 'Arn'],
      };

      return awsDeployFunction.normalizeArnRole(roleObj).then(result => {
        expect(getRoleStub.calledOnce).to.be.equal(true);
        expect(getAccountInfoStub).to.not.have.been.called;
        expect(result).to.be.equal('arn:aws:iam::123456789012:role/role_2');
      });
    });
  });

  describe('#updateFunctionConfiguration', () => {
    let updateFunctionConfigurationStub;
    let normalizeArnRoleStub;
    const options = {
      stage: 'dev',
      region: 'us-east-1',
      function: 'first',
      functionObj: {
        name: 'first',
      },
    };

    beforeEach(() => {
      updateFunctionConfigurationStub = sinon
        .stub(awsDeployFunction.provider, 'request')
        .resolves();

      normalizeArnRoleStub = sinon
        .stub(awsDeployFunction, 'normalizeArnRole')
        .resolves('arn:aws:us-east-1:123456789012:role/role');
    });

    afterEach(() => {
      awsDeployFunction.provider.request.restore();
      awsDeployFunction.normalizeArnRole.restore();
      awsDeployFunction.serverless.service.provider.timeout = undefined;
      awsDeployFunction.serverless.service.provider.memorySize = undefined;
      awsDeployFunction.serverless.service.provider.role = undefined;
      awsDeployFunction.serverless.service.provider.vpc = undefined;
    });

    it("should update function's configuration", () => {
      options.functionObj = {
        awsKmsKeyArn: 'arn:aws:kms:us-east-1:123456789012',
        description: 'desc',
        handler: 'my_handler',
        environment: {
          VARIABLE: 'value',
        },
        name: 'first',
        memorySize: 128,
        onError: 'arn:aws:sqs:us-east-1:123456789012:dlq',
        role: 'arn:aws:iam::123456789012:role/Admin',
        timeout: 3,
        vpc: {
          securityGroupIds: ['1'],
          subnetIds: ['2'],
        },
        layers: ['arn:aws:lambda:us-east-1:123456789012:layer:layer:1'],
      };

      awsDeployFunction.options = options;

      return awsDeployFunction.updateFunctionConfiguration().then(() => {
        expect(normalizeArnRoleStub.calledOnce).to.be.equal(true);
        expect(normalizeArnRoleStub.calledWithExactly('arn:aws:iam::123456789012:role/Admin'));
        expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true);
        expect(
          updateFunctionConfigurationStub.calledWithExactly(
            'Lambda',
            'updateFunctionConfiguration',
            {
              DeadLetterConfig: {
                TargetArn: 'arn:aws:sqs:us-east-1:123456789012:dlq',
              },
              Handler: 'my_handler',
              Description: 'desc',
              Environment: {
                Variables: {
                  VARIABLE: 'value',
                },
              },
              FunctionName: 'first',
              KMSKeyArn: 'arn:aws:kms:us-east-1:123456789012',
              MemorySize: 128,
              Role: 'arn:aws:us-east-1:123456789012:role/role',
              Timeout: 3,
              VpcConfig: {
                SecurityGroupIds: ['1'],
                SubnetIds: ['2'],
              },
              Layers: ['arn:aws:lambda:us-east-1:123456789012:layer:layer:1'],
            }
          )
        ).to.be.equal(true);
      });
    });

    it('should update only specified params', () => {
      options.functionObj = {
        name: 'first',
        description: 'change',
        vpc: undefined,
      };

      awsDeployFunction.options = options;

      return awsDeployFunction.updateFunctionConfiguration().then(() => {
        expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true);
        expect(
          updateFunctionConfigurationStub.calledWithExactly(
            'Lambda',
            'updateFunctionConfiguration',
            {
              FunctionName: 'first',
              Description: 'change',
            }
          )
        ).to.be.equal(true);
      });
    });

    it('should skip elements that contain references', () => {
      options.functionObj = {
        name: 'first',
        description: 'change',
        handler: 'my_handler',
        vpc: {
          securityGroupIds: [
            'xxxxx',
            {
              ref: 'myVPCRef',
            },
          ],
          subnetIds: [
            'xxxxx',
            {
              ref: 'myVPCRef',
            },
          ],
        },
        environment: {
          myvar: 'this is my var',
          myref: {
            ref: 'aCFReference',
          },
        },
      };

      awsDeployFunction.options = options;

      return awsDeployFunction.updateFunctionConfiguration().then(() => {
        expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true);
        expect(
          updateFunctionConfigurationStub.calledWithExactly(
            'Lambda',
            'updateFunctionConfiguration',
            {
              FunctionName: 'first',
              Handler: 'my_handler',
              Description: 'change',
            }
          )
        ).to.be.equal(true);
      });
    });

    it('should do nothing if only references are in', () => {
      options.functionObj = {
        name: 'first',
        environment: {
          myvar: 'this is my var',
          myref: {
            ref: 'aCFReference',
          },
        },
      };

      awsDeployFunction.options = options;

      return expect(awsDeployFunction.updateFunctionConfiguration()).to.be.fulfilled.then(
        () => expect(updateFunctionConfigurationStub).to.not.be.called
      );
    });

    it('should fail when using invalid characters in environment variable', () => {
      options.functionObj = {
        name: 'first',
        description: 'change',
        environment: {
          '!llegal_v@riable': 1,
        },
      };

      awsDeployFunction.options = options;

      expect(() => awsDeployFunction.updateFunctionConfiguration()).to.throw(Error);
    });

    it('should transform to string values when using non-string values as environment variables', () => {
      options.functionObj = {
        name: 'first',
        handler: 'my_handler',
        description: 'change',
        environment: {
          COUNTER: 6,
        },
      };

      awsDeployFunction.options = options;
      return expect(awsDeployFunction.updateFunctionConfiguration()).to.be.fulfilled.then(() => {
        expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true);
        expect(
          updateFunctionConfigurationStub.calledWithExactly(
            'Lambda',
            'updateFunctionConfiguration',
            {
              FunctionName: 'first',
              Handler: 'my_handler',
              Description: 'change',
              Environment: {
                Variables: {
                  COUNTER: '6',
                },
              },
            }
          )
        ).to.be.equal(true);
      });
    });

    it('should inherit provider-level config', () => {
      options.functionObj = {
        name: 'first',
        handler: 'my_handler',
        description: 'change',
      };

      awsDeployFunction.serverless.service.provider.timeout = 12;
      awsDeployFunction.serverless.service.provider.memorySize = 512;
      awsDeployFunction.serverless.service.provider.role = 'role';
      awsDeployFunction.serverless.service.provider.vpc = {
        securityGroupIds: [123, 12],
        subnetIds: [1234, 12345],
      };

      awsDeployFunction.options = options;

      return awsDeployFunction.updateFunctionConfiguration().then(() => {
        expect(normalizeArnRoleStub.calledOnce).to.be.equal(true);
        expect(normalizeArnRoleStub.calledWithExactly('role'));
        expect(updateFunctionConfigurationStub.calledOnce).to.be.equal(true);
        expect(
          updateFunctionConfigurationStub.calledWithExactly(
            'Lambda',
            'updateFunctionConfiguration',
            {
              FunctionName: 'first',
              Handler: 'my_handler',
              Description: 'change',
              VpcConfig: {
                SubnetIds: [1234, 12345],
                SecurityGroupIds: [123, 12],
              },
              Timeout: 12,
              MemorySize: 512,
              Role: 'arn:aws:us-east-1:123456789012:role/role',
            }
          )
        ).to.be.equal(true);
      });
    });
  });

  describe('#deployFunction()', () => {
    let artifactFilePath;
    let updateFunctionCodeStub;
    let statSyncStub;
    let readFileSyncStub;

    beforeEach(() => {
      // write a file to disc to simulate that the deployment artifact exists
      awsDeployFunction.packagePath = getTmpDirPath();
      artifactFilePath = path.join(awsDeployFunction.packagePath, 'first.zip');
      serverless.utils.writeFileSync(artifactFilePath, 'first.zip file content');
      updateFunctionCodeStub = sinon.stub(awsDeployFunction.provider, 'request').resolves();
      statSyncStub = sinon.stub(fs, 'statSync').returns({ size: 1024 });
      sinon.spy(awsDeployFunction.serverless.cli, 'log');
      readFileSyncStub = sinon.stub(fs, 'readFileSync').returns();
      awsDeployFunction.serverless.service.provider.remoteFunctionData = {
        Configuration: {
          CodeSha256: 'remote-hash-zip-file',
        },
      };
    });

    afterEach(() => {
      awsDeployFunction.provider.request.restore();
      fs.statSync.restore();
      fs.readFileSync.restore();
    });

    it('should deploy the function if the hashes are different', () => {
      cryptoStub
        .createHash()
        .update()
        .digest.onCall(0)
        .returns('local-hash-zip-file');

      return awsDeployFunction.deployFunction().then(() => {
        const data = fs.readFileSync(artifactFilePath);

        expect(updateFunctionCodeStub.calledOnce).to.be.equal(true);
        expect(readFileSyncStub.called).to.equal(true);
        expect(
          updateFunctionCodeStub.calledWithExactly('Lambda', 'updateFunctionCode', {
            FunctionName: 'first',
            ZipFile: data,
          })
        ).to.be.equal(true);
        expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
      });
    });

    it('should deploy the function if the hashes are same but the "force" option is used', () => {
      awsDeployFunction.options.force = true;
      cryptoStub
        .createHash()
        .update()
        .digest.onCall(0)
        .returns('remote-hash-zip-file');

      return awsDeployFunction.deployFunction().then(() => {
        const data = fs.readFileSync(artifactFilePath);

        expect(updateFunctionCodeStub.calledOnce).to.be.equal(true);
        expect(readFileSyncStub.called).to.equal(true);
        expect(
          updateFunctionCodeStub.calledWithExactly('Lambda', 'updateFunctionCode', {
            FunctionName: 'first',
            ZipFile: data,
          })
        ).to.be.equal(true);
        expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
      });
    });

    it('should resolve if the hashes are the same', () => {
      cryptoStub
        .createHash()
        .update()
        .digest.onCall(0)
        .returns('remote-hash-zip-file');

      return awsDeployFunction.deployFunction().then(() => {
        const expected = 'Code not changed. Skipping function deployment.';

        expect(updateFunctionCodeStub.calledOnce).to.be.equal(false);
        expect(readFileSyncStub.calledOnce).to.equal(true);
        expect(awsDeployFunction.serverless.cli.log.calledWithExactly(expected)).to.equal(true);
        expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
      });
    });

    it('should log artifact size', () => {
      // awnY7Oi280gp5kTCloXzsqJCO4J766x6hATWqQsN/uM= <-- hash of the local zip file
      readFileSyncStub.returns(Buffer.from('my-service.zip content'));

      return awsDeployFunction.deployFunction().then(() => {
        const expected = 'Uploading function: first (1 KB)...';

        expect(readFileSyncStub.calledOnce).to.equal(true);
        expect(statSyncStub.calledOnce).to.equal(true);
        expect(awsDeployFunction.serverless.cli.log.calledWithExactly(expected)).to.be.equal(true);
        expect(readFileSyncStub.calledWithExactly(artifactFilePath)).to.equal(true);
      });
    });

    describe('when artifact is provided', () => {
      let getFunctionStub;
      const artifactZipFile = 'artifact.zip';

      beforeEach(() => {
        getFunctionStub = sinon.stub(serverless.service, 'getFunction').returns({
          handler: true,
          package: {
            artifact: artifactZipFile,
          },
        });
      });

      afterEach(() => {
        serverless.service.getFunction.restore();
      });

      it('should read the provided artifact', () =>
        awsDeployFunction.deployFunction().then(() => {
          const data = fs.readFileSync(artifactZipFile);

          expect(readFileSyncStub).to.have.been.calledWithExactly(artifactZipFile);
          expect(statSyncStub).to.have.been.calledWithExactly(artifactZipFile);
          expect(getFunctionStub).to.have.been.calledWithExactly('first');
          expect(updateFunctionCodeStub.calledOnce).to.equal(true);
          expect(
            updateFunctionCodeStub.calledWithExactly('Lambda', 'updateFunctionCode', {
              FunctionName: 'first',
              ZipFile: data,
            })
          ).to.be.equal(true);
        }));
    });
  });
});
