require 'spec_helper'
require 'aws-sdk'
require 'dpl/error'
require 'dpl/provider'
require 'dpl/provider/lambda'

describe DPL::Provider::Lambda do

  subject :provider do
    described_class.new(DummyContext.new, :access_key_id => 'qwertyuiopasdfghjklz', :secret_access_key => 'qwertyuiopasdfghjklzqwertyuiopasdfghjklz')
  end

  describe '#lambda_options' do
    context 'without region' do
      example do
        options = provider.lambda_options
        expect(options[:region]).to eq('us-east-1')
      end
    end

    context 'with region' do
      example do
        region = 'us-west-1'
        provider.options.update(:region => region)
        options = provider.lambda_options
        expect(options[:region]).to eq(region)
      end
    end
  end
end

describe DPL::Provider::Lambda do
  access_key_id = 'someaccesskey'
  secret_access_key = 'somesecretaccesskey'
  region = 'us-east-1'

  client_options = {
    stub_responses: true,
    region: region,
    credentials: Aws::Credentials.new(access_key_id, secret_access_key)
  }

  subject :provider do
    described_class.new(DummyContext.new, {
      access_key_id: access_key_id,
      secret_access_key: secret_access_key
    })
  end

  before :each do
    FileUtils.touch provider.output_file_path
    allow(provider).to receive(:lambda_options).and_return(client_options)
    allow(provider).to receive(:create_zip).and_return(provider.output_file_path)
  end

  describe '#lambda' do
    example do
      expect(Aws::Lambda::Client).to receive(:new).with(client_options).once
      provider.lambda
    end
  end

  describe '#push_app' do
    lambda_options = {
      function_name: 'test-function',
      role: 'some-role',
      module_name: 'index',
      handler_name: 'handler'
    }

    example_get_function_response = {
      code: {
        location: 'location',
        repository_type: 's3',
      },
      configuration: {
        function_name: 'test-function'
      }
    }

    example_response = {
      function_name: 'test-function',
      function_arn: 'arn:lambda:region:account-id:function:test-function',
      role: 'some-role',
      handler: 'index.handler'
    }

    before(:each) do
      old_options = provider.options
      allow(provider).to receive(:options) { old_options.merge(lambda_options) }
    end

    context 'by creating a new function' do
      before do
        provider.lambda.stub_responses(:get_function, 'ResourceNotFoundException')
        provider.lambda.stub_responses(:create_function, example_response)
      end

      example do
        expect(provider).to receive(:log).with(/Function #{lambda_options[:function_name]} does not exist, creating\./)
        expect(provider).to receive(:log).with(/Created lambda: #{lambda_options[:function_name]}\./)
        provider.push_app
      end
    end

    context 'by updating an existing function' do
      before do
        provider.lambda.stub_responses(:get_function, example_get_function_response)
        provider.lambda.stub_responses(:update_function_configuration, example_response)
        provider.lambda.stub_responses(:update_function_code, example_response)
      end

      example do
        expect(provider).to receive(:log).with(/Function #{lambda_options[:function_name]} already exists, updating\./)
        expect(provider).to receive(:log).with(/Updated configuration of function: #{lambda_options[:function_name]}\./)
        expect(provider).to receive(:log).with(/Updated code of function: #{lambda_options[:function_name]}\./)
        provider.push_app
      end
    end

    context 'by updating an existing function with new tags' do
      before do
        lambda_options[:function_tags] = [ 'TAG_KEY=some-value' ]
        provider.lambda.stub_responses(:get_function, example_get_function_response)
        provider.lambda.stub_responses(:update_function_configuration, example_response)
        provider.lambda.stub_responses(:tag_resource)
        provider.lambda.stub_responses(:update_function_code, example_response)
      end

      example do
        expect(provider).to receive(:log).with(/Function #{lambda_options[:function_name]} already exists, updating\./)
        expect(provider).to receive(:log).with(/Updated configuration of function: #{lambda_options[:function_name]}\./)
        expect(provider).to receive(:log).with(/Add tags to function #{lambda_options[:function_name]}\./)
        expect(provider).to receive(:log).with(/Updated code of function: #{lambda_options[:function_name]}\./)
        provider.push_app
      end
    end

    context 'with a ServiceException response' do
      before do
        provider.lambda.stub_responses(:get_function, 'ResourceNotFoundException')
        provider.lambda.stub_responses(:create_function, 'ServiceException')
      end

      example do
        expect(provider).to receive(:error).once
        provider.push_app
      end
    end

    context 'with a InvalidParameterValueException response' do
      before do
        provider.lambda.stub_responses(:get_function, 'InvalidParameterValueException')
      end

      example do
        expect(provider).to receive(:error).once
        provider.push_app
      end
    end

    context 'with a ResourceNotFoundException response' do
      before do
        provider.lambda.stub_responses(:get_function, 'ResourceNotFoundException')
        provider.lambda.stub_responses(:create_function, 'ResourceNotFoundException')
      end

      example do
        expect(provider).to receive(:error).once
        provider.push_app
      end
    end
  end

  describe "#handler" do
    context "without a module name" do
      module_name = 'index'
      handler_name = 'HandlerName'
      expected_handler = "#{module_name}.#{handler_name}"

      before do
        expect(provider.options).to receive(:[]).with(:module_name).and_return(nil)
        expect(provider.options).to receive(:fetch).with(:handler_name).and_return(handler_name)
      end

      example do
        expect(provider.handler).to eq(expected_handler)
      end
    end

    context "with a module name" do
      module_name = 'ModuleName'
      handler_name = 'HandlerName'
      expected_handler = "#{module_name}.#{handler_name}"

      before do
        expect(provider.options).to receive(:[]).with(:module_name).and_return(module_name)
        expect(provider.options).to receive(:fetch).with(:handler_name).and_return(handler_name)
      end

      example do
        expect(provider.handler).to eq(expected_handler)
      end
    end
  end

  describe '#function_zip' do
    context 'when zip is not specified' do
      path = Dir.pwd
      output_file_path = '/some/path.zip'

      before do
        expect(provider.options).to receive(:[]).with(:zip).and_return(nil)
        expect(provider).to receive(:output_file_path).and_return(output_file_path)
        expect(File).to receive(:directory?).with(path).and_return(true)
        expect(provider).to receive(:zip_directory).with(output_file_path, path)
        expect(File).to receive(:new).with(output_file_path)
      end

      example do
        provider.function_zip
      end
    end

    context 'when zip is a file path' do
      path = '/some/file/path.zip'
      output_file_path = '/some/path.zip'

      before do
        expect(provider.options).to receive(:[]).with(:zip).and_return(path)
        expect(provider).to receive(:output_file_path).and_return(output_file_path)
        expect(File).to receive(:directory?).with(path).and_return(false)
        expect(File).to receive(:file?).with(path).and_return(true)
        expect(provider).to receive(:zip_file).with(output_file_path, path)
        expect(File).to receive(:new).with(output_file_path)
      end

      example do
        provider.function_zip
      end
    end

    context 'when zip is a directory' do
      path = '/some/dir/path'
      output_file_path = '/some/path.zip'

      before do
        expect(provider.options).to receive(:[]).with(:zip).and_return(path)
        expect(provider).to receive(:output_file_path).and_return(output_file_path)
        expect(File).to receive(:directory?).with(path).and_return(true)
        expect(provider).to receive(:zip_directory).with(output_file_path, path)
        expect(File).to receive(:new).with(output_file_path)
      end

      example do
        provider.function_zip
      end
    end

    context 'with an invalid zip option' do
      path = '/some/file/path.zip'
      output_file_path = '/some/path.zip'
      error = 'Invalid zip option. If set, must be path to directory, js file, or a zip file.'

      before do
        expect(provider.options).to receive(:[]).with(:zip).and_return(path)
        expect(provider).to receive(:output_file_path).and_return(output_file_path)
        expect(File).to receive(:directory?).with(path).and_return(false)
        expect(File).to receive(:file?).with(path).and_return(false)
      end

      example do
        expect { provider.function_zip }.to raise_error(DPL::Error, error)
      end
    end
  end

  describe '#zip_file' do
    dest = '/some/path/to/write.zip'

    context 'when zip is a file path' do
      dir = '/some/target'
      target = File.join(dir, 'file.js')

      before do
        expect(File).to receive(:extname).with(target).and_return('.js')
        expect(provider).to receive(:create_zip).with(dest, dir, [ target ])
      end

      example do
        provider.zip_file(dest, target)
      end
    end

    context 'when zip is an existing zip file' do
      dir = '/some/target'
      target = File.join(dir, 'file.js')

      before do
        expect(File).to receive(:extname).with(target).and_return('.zip')
        expect(FileUtils).to receive(:cp).with(target, dest)
      end

      example do
        provider.zip_file(dest, target)
      end
    end

    context 'when zip is an existing jar file' do
      dir = '/some/target'
      target = File.join(dir, 'file.js')

      before do
        expect(File).to receive(:extname).with(target).and_return('.jar')
        expect(FileUtils).to receive(:cp).with(target, dest)
      end

      example do
        provider.zip_file(dest, target)
      end
    end
  end

  describe '#zip_directory' do
    dest = '/some/path/to/write.zip'
    target = '/some/dir'
    glob = File.join(target, '**', '**')
    files = %w[ 'one' 'two' ]

    before do
      expect(Dir).to receive(:glob).with(*glob).and_return(files)
      expect(provider).to receive(:create_zip).with(dest, target, files)
    end

    example do
      provider.zip_directory(dest, target)
    end
  end

  describe '#create_zip' do
    dest = '/some/dest.zip'
    src =  '/some/src/dir'
    file_one = 'one.js'
    file_two = 'two.js'
    files = [
      File.join(src, file_one),
      File.join(src, file_two)
    ]

    before do
      expect(provider).to receive(:create_zip).and_call_original
      zip_file = double(Zip::File)
      expect(Zip::File).to receive(:open).with(dest, Zip::File::CREATE).and_yield(zip_file)
      expect(zip_file).to receive(:add).once.with(file_one, File.join(src, file_one))
      expect(zip_file).to receive(:add).once.with(file_two, File.join(src, file_two))
    end

    example do
      provider.create_zip(dest, src, files)
    end
  end

  describe '#needs_key?' do
    example do
      expect(provider.needs_key?).to eq(false)
    end
  end

  describe '#check_auth' do
    example do
      expect(provider).to receive(:log).with("Using Access Key: #{access_key_id[-4..-1].rjust(20, '*')}")
      provider.check_auth
    end
  end

  describe '#output_file_path' do
    example do
      expect(provider.output_file_path).to match(/tmp\/\w{8}\-lambda\.zip/)
    end
  end

  describe '#default_runtime' do
    example do
      expect(provider.default_runtime).to eq('nodejs')
    end
  end

  describe '#default_timeout' do
    example do
      expect(provider.default_timeout).to eq(3)
    end
  end

  describe '#default_description' do
    build_number = 2

    before do
      allow(provider.context.env).to receive(:[]).with('TRAVIS_BUILD_NUMBER').and_return(build_number)
    end

    let(:build_number) { provider.context.env['TRAVIS_BUILD_NUMBER'] }

    example do
      expect(provider.default_description).to eq(
        "Deploy build #{build_number} to AWS Lambda via Travis CI"
      )
    end
  end

  describe '#default_memory_size' do
    example do
      expect(provider.default_memory_size).to eq(128)
    end
  end

  describe '#publish' do
    context 'is default turned off' do
      example do
        expect(provider.publish).to eq(false)
      end
    end
    context 'can be turned on' do
      before do
        expect(provider.options).to receive(:[]).with(:publish).and_return(true)
      end

      example do
        expect(provider.publish).to eq(true)
      end
    end
  end

  describe '#random_chars' do
    context 'without specifying count' do
      example do
        expect(provider.random_chars.length).to eq(8)
      end
    end

    context 'with specified count' do
      count = 4
      example do
        expect(provider.random_chars(count).length).to eq(count)
      end
    end
  end

end