require 'spec_helper'

require 'aws/codedeploy/local/deployer'
require 'instance_agent'
require 'instance_agent/config'
require 'instance_agent/plugins/codedeploy/onpremise_config'

describe AWS::CodeDeploy::Local::Deployer do
  SAMPLE_DIRECTORY_BASENAME = 'sample'
  SAMPLE_FILE_BASENAME = "#{SAMPLE_DIRECTORY_BASENAME}.tgz"
  SAMPLE_FILE_BUNDLE = "#{Dir.pwd}/spec/resource/#{SAMPLE_FILE_BASENAME}"
  SAMPLE_DIRECTORY_BUNDLE = "#{Dir.pwd}/spec/resource/#{SAMPLE_DIRECTORY_BASENAME}"
  GIT_OWNER = 'owner'
  GIT_REPO = 'repo'
  GIT_BRANCH_OR_TAG = 'branchOrTag'
  SAMPLE_GIT_LOCATION_TARBALL = "https://api.github.com/repos/#{GIT_OWNER}/#{GIT_REPO}/tarball/#{GIT_BRANCH_OR_TAG}"
  SAMPLE_GIT_LOCATION_ZIPBALL = "https://api.github.com/repos/#{GIT_OWNER}/#{GIT_REPO}/zipball/#{GIT_BRANCH_OR_TAG}"
  SAMPLE_GIT_LOCATION_DEFAULT_GIT_FORMAT = "https://github.com/#{GIT_OWNER}/#{GIT_REPO}"
  TEST_DEPLOYMENT_ID = 123
  S3_BUCKET = 'bucket'
  S3_KEY = 'key'
  S3_VERSION = 'version'
  S3_ETAG = 'etag'
  SAMPLE_S3_LOCATION = "s3://#{S3_BUCKET}/#{S3_KEY}"
  SAMPLE_S3_LOCATION_WITH_VERSION_AND_ETAG = "s3://#{S3_BUCKET}/#{S3_KEY}?versionId=#{S3_VERSION}&etag=#{S3_ETAG}"
  EXPECTED_HOOK_MAPPING = { "BeforeBlockTraffic"=>["BeforeBlockTraffic"],
                            "AfterBlockTraffic"=>["AfterBlockTraffic"],
                            "ApplicationStop"=>["ApplicationStop"],
                            "BeforeInstall"=>["BeforeInstall"],
                            "AfterInstall"=>["AfterInstall"],
                            "ApplicationStart"=>["ApplicationStart"],
                            "ValidateService"=>["ValidateService"],
                            "BeforeAllowTraffic"=>["BeforeAllowTraffic"],
                            "AfterAllowTraffic"=>["AfterAllowTraffic"]}
  DEPLOYMENT_GROUP_ID = 'deployment-group-id'
  NON_DEFAULT_FILE_EXISTS_BEHAVIOR = 'OVERWRITE'
  SAMPLE_APPLICATION_NAME = 'myapp'
  SAMPLE_DEPLOYMENT_GROUP_NAME = 'mydeploymentgroup'

  let(:test_working_directory) { Dir.mktmpdir }

  before do
    FileUtils.mkdir "#{test_working_directory}/conf"
    @config_file_location = create_config_file(test_working_directory)
    allow(Dir).to receive(:pwd).and_return test_working_directory
    ProcessManager::Config.config[:root_dir] = test_working_directory
    allow(AWS::CodeDeploy::Local::Deployer).to receive(:random_deployment_id).and_return(TEST_DEPLOYMENT_ID)
    allow(File).to receive(:exist?).with(@config_file_location).and_return(true)
    allow(File).to receive(:readable?).with(@config_file_location).and_return(true)
    allow(File).to receive(:exist?).with(AWS::CodeDeploy::Local::Deployer::CONF_DEFAULT_LOCATION).and_return(false)
    allow(File).to receive(:readable?).with(AWS::CodeDeploy::Local::Deployer::CONF_DEFAULT_LOCATION).and_return(false)
    allow(File).to receive(:readable?).with(InstanceAgent::Config.config[:on_premises_config_file]).and_return(false)
    allow(File).to receive(:exist?)
  end

  def create_config_file(working_directory, log_dir = nil)
    configuration_contents = <<-CONFIG
---
:log_aws_wire: false
:log_dir: #{log_dir || working_directory}
:pid_dir: #{working_directory}
:program_name: codedeploy-agent
:root_dir: #{working_directory}/deployment-root
:on_premises_config_file: #{InstanceAgent::Config.config[:on_premises_config_file]}
:verbose: true
:wait_between_runs: 1
:proxy_uri:
:max_revisions: 5
    CONFIG

    InstanceAgent::Config.config[:log_dir] = log_dir || working_directory
    InstanceAgent::Config.config[:config_file] = "#{working_directory}/codedeployagent.yml"
    File.open(InstanceAgent::Config.config[:config_file], 'w') { |file| file.write(configuration_contents) }
    InstanceAgent::Config.config[:config_file]
  end

  after do
    FileUtils.rm_rf(test_working_directory)
  end

  describe 'initialize' do
    it 'tries to load configuration' do
      expect(InstanceAgent::Config).to receive(:load_config)
      AWS::CodeDeploy::Local::Deployer.new(@config_file_location)
    end

    it 'tries to load configuration if the configuration file location provided is nil' do
      expect(File).to receive(:file?).with(AWS::CodeDeploy::Local::Deployer::CONF_DEFAULT_LOCATION).and_return(true)
      expect(File).to receive(:readable?).with(AWS::CodeDeploy::Local::Deployer::CONF_DEFAULT_LOCATION).and_return(true)
      expect(File).to receive(:file?).with(InstanceAgent::Config.config[:on_premises_config_file]).and_return(false)
      expect(InstanceAgent::Config).to receive(:load_config)
      AWS::CodeDeploy::Local::Deployer.new(nil)
    end

    it 'tries to load on-premise-configuration from on_premises_config_file if it exists' do
      expect(File).to receive(:file?).with(AWS::CodeDeploy::Local::Deployer::CONF_DEFAULT_LOCATION).and_return(true)
      expect(File).to receive(:readable?).with(AWS::CodeDeploy::Local::Deployer::CONF_DEFAULT_LOCATION).and_return(true)
      expect(File).to receive(:file?).with(InstanceAgent::Config.config[:on_premises_config_file]).and_return(true)
      expect(File).to receive(:readable?).with(InstanceAgent::Config.config[:on_premises_config_file]).and_return(true)
      expect(InstanceAgent::Plugins::CodeDeployPlugin::OnPremisesConfig).to receive(:configure)
      expect(InstanceAgent::Config).to receive(:load_config)
      AWS::CodeDeploy::Local::Deployer.new
    end

    it 'throws ValidationError if configuration file location does not exist' do
      invalid_config_file_location = '/does/not/exist/path'
      expect(File).to receive(:file?).with(invalid_config_file_location).and_return(false)
      expect{AWS::CodeDeploy::Local::Deployer.new(invalid_config_file_location)}.to raise_error(AWS::CodeDeploy::Local::CLIValidator::ValidationError, "configuration file #{invalid_config_file_location} does not exist or is not readable")
    end

    it 'creates the log directory if it does not exist' do
      new_test_working_directory = "#{test_working_directory}/new_dir"
      FileUtils.mkdir_p new_test_working_directory
      not_yet_existing_directory = "#{new_test_working_directory}/notyetexistsdirectory"
      config_file_location = create_config_file(new_test_working_directory, not_yet_existing_directory)
      expect(FileUtils).to receive(:mkdir_p).with(not_yet_existing_directory).and_call_original
      allow(File).to receive(:readable?).with(config_file_location).and_return(true)
      allow(File).to receive(:exist?).with(config_file_location).and_return(true)
      AWS::CodeDeploy::Local::Deployer.new(config_file_location)
    end
  end

  describe 'execute_events' do
    context 'when local file is specified' do
      let(:args) do
        {"deploy"=>true,
         '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--location"=>true,
         "--bundle-location"=>SAMPLE_FILE_BUNDLE,
         "--type"=>'tgz',
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         "--help"=>false,
         "--version"=>false}
      end

      it 'deploys the local file and calls the executor to execute all commands' do
        allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => EXPECTED_HOOK_MAPPING).
          and_return(executor)

        AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tgz',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end

      context 'when script fails with script error' do
        it 'prints the correct error message to the screen and exits' do
          allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
          executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

          expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
            with(:hook_mapping => EXPECTED_HOOK_MAPPING).
            and_return(executor)

          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.first),
            deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tgz',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).and_raise(
                              InstanceAgent::Plugins::CodeDeployPlugin::ScriptError.new(InstanceAgent::Plugins::CodeDeployPlugin::ScriptError::SCRIPT_FAILED_CODE, 'script-location', nil), 'scripterror')

          deployment_folder = "#{test_working_directory}/deployment-root/deployment-group-id/123"
          expect(STDOUT).to receive(:puts).with("Starting to execute deployment from within folder #{deployment_folder}").once.ordered
          expect(STDOUT).to receive(:puts).with("Your local deployment failed while trying to execute your script at #{deployment_folder}/deployment-archive/script-location").once.ordered
          expect(STDOUT).to receive(:puts).with("See the deployment log at #{deployment_folder}/logs/scripts.log for more details").once.ordered
          expect{AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)}.to raise_error(InstanceAgent::Plugins::CodeDeployPlugin::ScriptError)
        end
      end
    end

    context 'when non-default events specified' do
      NON_DEFAULT_LIFECYCLE_EVENTS = ['Stop','Start','HealthCheck']
      NON_DEFAULT_LIFECYCLE_EVENTS_AFTER_DOWNLOAD_BUNDLE_AND_INSTALL = ['DownloadBundle', 'Install', 'Stop','Start','HealthCheck']
      DOWNLOAD_BUNDLE_AND_APPLICATION_START = ['DownloadBundle','ApplicationStart']
      DOWNLOAD_BUNDLE_AND_INSTALL_AND_APPLICATION_START = ['DownloadBundle','Install','ApplicationStart']

      let(:args) do
        {"deploy"=>true,
         '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--location"=>true,
         "--bundle-location"=>SAMPLE_FILE_BUNDLE,
         "--type"=>'tar',
         "--events"=>NON_DEFAULT_LIFECYCLE_EVENTS.join(','),
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         "--help"=>false,
         "--version"=>false}
      end

      it 'deploys the local file and calls the executor to execute all specified commands after DownloadBundle and Install commands' do
        allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        all_possible_lifecycle_events = AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.to_set.merge(NON_DEFAULT_LIFECYCLE_EVENTS).to_a
        all_expected_hooks = all_possible_lifecycle_events - AWS::CodeDeploy::Local::Deployer::REQUIRED_LIFECYCLE_EVENTS
        expected_hook_mapping = Hash[all_expected_hooks.map{|h|[h,[h]]}]
        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => expected_hook_mapping).
          and_return(executor)

        NON_DEFAULT_LIFECYCLE_EVENTS_AFTER_DOWNLOAD_BUNDLE_AND_INSTALL.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tar',
                            all_possible_lifecycle_events)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end

      let(:wrong_event_order_args) do
        {'--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--bundle-location"=>SAMPLE_FILE_BUNDLE,
         "--type"=>'tar',
         "--events"=>DOWNLOAD_BUNDLE_AND_APPLICATION_START.join(','),
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         }
      end

      it 'corrects the order of download bundle followed by install' do
        allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        all_possible_lifecycle_events = AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS
        all_expected_hooks = all_possible_lifecycle_events - AWS::CodeDeploy::Local::Deployer::REQUIRED_LIFECYCLE_EVENTS
        expected_hook_mapping = Hash[all_expected_hooks.map{|h|[h,[h]]}]
        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => expected_hook_mapping).
          and_return(executor)

        DOWNLOAD_BUNDLE_AND_INSTALL_AND_APPLICATION_START.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tar',
                            all_possible_lifecycle_events)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(wrong_event_order_args)
      end
    end

    context 'when local directory is specified' do
      let(:args) do
        {"deploy"=>true,
         '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--location"=>true,
         "--bundle-location"=>SAMPLE_DIRECTORY_BUNDLE,
         "--type"=>'directory',
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         "--help"=>false,
         "--version"=>false}
      end

      it 'deploys the local directory and calls the executor to execute all commands' do
        allow(File).to receive(:exist?).with(SAMPLE_DIRECTORY_BUNDLE).and_return(true)
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => EXPECTED_HOOK_MAPPING).
          and_return(executor)

        AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_DIRECTORY_BUNDLE, 'Local Directory', 'directory',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end
    end

    context 'when file-exists-behavior is specified' do
      let(:args) do
        {'--bundle-location'=>SAMPLE_DIRECTORY_BUNDLE,
         '--type'=>'directory',
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         '--file-exists-behavior'=>NON_DEFAULT_FILE_EXISTS_BEHAVIOR}
      end

      it 'deploys the local directory and calls the executor to execute all commands' do
        allow(File).to receive(:exist?).with(SAMPLE_DIRECTORY_BUNDLE).and_return(true)
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => EXPECTED_HOOK_MAPPING).
          and_return(executor)

        AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_DIRECTORY_BUNDLE, 'Local Directory', 'directory',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS,
                            false, false, DEPLOYMENT_GROUP_ID, NON_DEFAULT_FILE_EXISTS_BEHAVIOR)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end
    end

    context 'when anonymous github tarball endpoint is specified' do
      let(:args) do
        {"deploy"=>true,
         "--location"=>true,
         '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--bundle-location"=>SAMPLE_GIT_LOCATION_TARBALL,
         "--type"=>'tar',
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         "--help"=>false,
         "--version"=>false}
      end

      it 'extracts the endpoint parameters, deploys the downloaded file, and calls the executor to execute all commands' do
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => EXPECTED_HOOK_MAPPING).
          and_return(executor)

        AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_GIT_LOCATION_TARBALL, 'GitHub', 'tar',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end
    end

    context 'when anonymous github zipball endpoint is specified' do
      let(:args) do
        {"deploy"=>true,
         "--location"=>true,
         '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--bundle-location"=>SAMPLE_GIT_LOCATION_ZIPBALL,
         "--type"=>'zip',
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         "--help"=>false,
         "--version"=>false}
      end

      it 'extracts the endpoint parameters, deploys the downloaded file, and calls the executor to execute all commands' do
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => EXPECTED_HOOK_MAPPING).
          and_return(executor)

        AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_GIT_LOCATION_ZIPBALL, 'GitHub', 'zip',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end
    end

    context 'when anonymous github endpoint is specified with default github url format' do
      let(:args) do
        {"deploy"=>true,
         "--location"=>true,
         '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--bundle-location"=>SAMPLE_GIT_LOCATION_DEFAULT_GIT_FORMAT,
         "--type"=>'zip',
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         "--help"=>false,
         "--version"=>false}
      end

      it 'extracts the endpoint parameters, deploys the downloaded file, and calls the executor to execute all commands' do
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => EXPECTED_HOOK_MAPPING).
          and_return(executor)

        AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_GIT_LOCATION_DEFAULT_GIT_FORMAT, 'GitHub', 'zip',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end
    end

    context 'when s3 endpoint is specified' do
      let(:args) do
        {"deploy"=>true,
         '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--location"=>true,
         "--bundle-location"=>SAMPLE_S3_LOCATION,
         "--type"=>'zip',
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         "--help"=>false,
         "--version"=>false}
      end

      it 'extracts the endpoint parameters, deploys the downloaded file, and calls the executor to execute all commands' do
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => EXPECTED_HOOK_MAPPING).
          and_return(executor)

        AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_S3_LOCATION, 'S3', 'zip',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end
    end

    context 'when s3 endpoint with version and etag is specified' do
      let(:args) do
        {"deploy"=>true,
         '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
         "--location"=>true,
         "--bundle-location"=>SAMPLE_S3_LOCATION_WITH_VERSION_AND_ETAG,
         "--type"=>'zip',
         '--deployment-group'=>DEPLOYMENT_GROUP_ID,
         "--help"=>false,
         "--version"=>false}
      end

      it 'extracts the endpoint parameters, deploys the downloaded file, and calls the executor to execute all commands' do
        executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

        expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
          with(:hook_mapping => EXPECTED_HOOK_MAPPING).
          and_return(executor)

        AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
          expect(executor).to receive(:execute_command).with(
            OpenStruct.new(:command_name => name),
            deployment_spec(SAMPLE_S3_LOCATION_WITH_VERSION_AND_ETAG, 'S3', 'zip',
                            AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS,
                            true, true)).once.ordered
        end
        AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
      end
    end

    context 'build_spec' do
      context 'when application-name is not in the args' do
        let(:args) do
          {"deploy"=>true,
           '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
           "--location"=>true,
           "--bundle-location"=>SAMPLE_FILE_BUNDLE,
           "--type"=>'tgz',
           '--deployment-group'=>DEPLOYMENT_GROUP_ID,
           "--help"=>false,
           "--version"=>false}
        end
        it 'defaults to LocalFleet' do
          allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
          executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

          expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
              with(:hook_mapping => EXPECTED_HOOK_MAPPING).
              and_return(executor)

          AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
            expect(executor).to receive(:execute_command).with(
                OpenStruct.new(:command_name => name),
                deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tgz',
                                AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).once.ordered
          end
          AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
        end
      end

      context 'when deployment-group-name is not in the args' do
        let(:args) do
          {"deploy"=>true,
           '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
           "--location"=>true,
           "--bundle-location"=>SAMPLE_FILE_BUNDLE,
           "--type"=>'tgz',
           '--deployment-group'=>DEPLOYMENT_GROUP_ID,
           "--help"=>false,
           "--version"=>false}
        end
        it 'defaults to the bundle-location' do
          allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
          executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

          expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
              with(:hook_mapping => EXPECTED_HOOK_MAPPING).
              and_return(executor)

          AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
            expect(executor).to receive(:execute_command).with(
                OpenStruct.new(:command_name => name),
                deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tgz',
                                AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS)).once.ordered
          end
          AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
        end
      end

      context 'when application-name is in the args' do
        let(:args) do
          {"deploy"=>true,
           '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
           "--location"=>true,
           "--bundle-location"=>SAMPLE_FILE_BUNDLE,
           "--type"=>'tgz',
           '--deployment-group'=>DEPLOYMENT_GROUP_ID,
           '--application-name'=> SAMPLE_APPLICATION_NAME,
           "--help"=>false,
           "--version"=>false}
        end
        it 'generates a spec with the provided value' do
          allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
          executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

          expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
              with(:hook_mapping => EXPECTED_HOOK_MAPPING).
              and_return(executor)

          AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
            expect(executor).to receive(:execute_command).with(
                OpenStruct.new(:command_name => name),
                deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tgz',
                                AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS, deployment_group_name:nil, application_name:SAMPLE_APPLICATION_NAME)).once.ordered
          end
          AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
        end
      end

      context 'when deployment-group-name is in the args' do
        let(:args) do
          {"deploy"=>true,
           '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
           "--location"=>true,
           "--bundle-location"=>SAMPLE_FILE_BUNDLE,
           "--type"=>'tgz',
           '--deployment-group'=>DEPLOYMENT_GROUP_ID,
           '--deployment-group-name'=>SAMPLE_DEPLOYMENT_GROUP_NAME,
           "--help"=>false,
           "--version"=>false}
        end
        it 'generates a spec with the provided value' do
          allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
          executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

          expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
              with(:hook_mapping => EXPECTED_HOOK_MAPPING).
              and_return(executor)

          AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
            expect(executor).to receive(:execute_command).with(
                OpenStruct.new(:command_name => name),
                deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tgz',
                                AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS, deployment_group_name:SAMPLE_DEPLOYMENT_GROUP_NAME, application_name: nil)).once.ordered
          end
          AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
        end
      end

      context 'when appspec-filename is not in the args' do
        let(:args) do
          {"deploy"=>true,
           '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
           "--location"=>true,
           "--bundle-location"=>SAMPLE_FILE_BUNDLE,
           "--type"=>'tgz',
           '--deployment-group'=>DEPLOYMENT_GROUP_ID,
           '--deployment-group-name'=>SAMPLE_DEPLOYMENT_GROUP_NAME,
           "--help"=>false,
           "--version"=>false}
        end
        it 'generates a spec with the provided value' do
          allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
          executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

          expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
              with(:hook_mapping => EXPECTED_HOOK_MAPPING).
              and_return(executor)

          AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
            expect(executor).to receive(:execute_command).with(
                OpenStruct.new(:command_name => name),
                deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tgz',
                                AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS, deployment_group_name:SAMPLE_DEPLOYMENT_GROUP_NAME, application_name: nil)).once.ordered
          end
          AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
        end
      end

      context 'when appspec-filename is in the args' do
        let(:args) do
          {"deploy"=>true,
           '--file-exists-behavior'=>InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR,
           "--location"=>true,
           "--appspec-filename"=>"appspec-override.yaml",
           "--bundle-location"=>SAMPLE_FILE_BUNDLE,
           "--type"=>'tgz',
           '--deployment-group'=>DEPLOYMENT_GROUP_ID,
           '--deployment-group-name'=>SAMPLE_DEPLOYMENT_GROUP_NAME,
           "--help"=>false,
           "--version"=>false}
        end
        it 'generates a spec with the provided value' do
          allow(File).to receive(:exist?).with(SAMPLE_FILE_BUNDLE).and_return(true)
          executor = double(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor)

          expect(InstanceAgent::Plugins::CodeDeployPlugin::CommandExecutor).to receive(:new).
              with(:hook_mapping => EXPECTED_HOOK_MAPPING).
              and_return(executor)

          AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS.each do |name|
            expect(executor).to receive(:execute_command).with(
                OpenStruct.new(:command_name => name),
                deployment_spec(SAMPLE_FILE_BUNDLE, 'Local File', 'tgz',
                                AWS::CodeDeploy::Local::Deployer::DEFAULT_ORDERED_LIFECYCLE_EVENTS, deployment_group_name:SAMPLE_DEPLOYMENT_GROUP_NAME, application_name: nil, appspec_filename: "appspec-override.yaml")).once.ordered
          end
          AWS::CodeDeploy::Local::Deployer.new(@config_file_location).execute_events(args)
        end
      end

    end

    def deployment_spec(location, revision_type, bundle_type, all_possible_lifecycle_events, s3revision_includes_version=false, s3revision_includes_etag=false, deployment_group_id=DEPLOYMENT_GROUP_ID, file_exists_behavior=InstanceAgent::Plugins::CodeDeployPlugin::DeploymentSpecification::DEFAULT_FILE_EXISTS_BEHAVIOR, deployment_group_name:nil, application_name:nil, appspec_filename: nil)
      revision_data_key = revision_data(revision_type, location, bundle_type, s3revision_includes_version, s3revision_includes_etag).keys.first
      revision_data_value = revision_data(revision_type, location, bundle_type, s3revision_includes_version, s3revision_includes_etag).values.first
      OpenStruct.new({
        :format => "TEXT/JSON",
        :payload => {
          "ApplicationId" =>  location,
          "ApplicationName" => application_name || location,
          "AppSpecFilename" => appspec_filename || "appspec.yml",
          "DeploymentGroupId" => deployment_group_id,
          "DeploymentGroupName" => deployment_group_name || "LocalFleet",
          "DeploymentId" => TEST_DEPLOYMENT_ID,
          "AgentActionOverrides" => {"AgentOverrides" => {"FileExistsBehavior" => file_exists_behavior}},
          "Revision" => {"RevisionType" => revision_type,
                         revision_data_key => revision_data_value},
          "AllPossibleLifecycleEvents" => all_possible_lifecycle_events
        }.to_json.to_s
      })
    end

    def revision_data(revision_type, location, bundle_type, s3revision_includes_version, s3revision_includes_etag)
      case revision_type
      when 'S3'
        s3_revision = {'S3Revision' => {
          'Bucket' => S3_BUCKET,
          'Key' => S3_KEY,
          'BundleType' => bundle_type}}

        if s3revision_includes_version
          s3_revision['S3Revision']['Version'] = S3_VERSION
        end

        if s3revision_includes_etag
          s3_revision['S3Revision']['ETag'] = S3_ETAG
        end

        s3_revision
      when 'GitHub'
        {'GitHubRevision' => {
          'Account' => GIT_OWNER,
          'Repository' => GIT_REPO,
          'CommitId' => location.include?(GIT_BRANCH_OR_TAG) ? GIT_BRANCH_OR_TAG : 'HEAD',
          'BundleType' => bundle_type}}
      when 'Local File', 'Local Directory'
        {'LocalRevision' => {
          'Location' => location,
          'BundleType' => bundle_type}}
      end
    end
  end
end
