require 'spec_helper'

describe SimpleDeploy::Stack do
  include_context 'double stubbed config', :access_key => 'access',
                                           :secret_key => 'secret',
                                           :region     => 'us-west-1'
  include_context 'double stubbed logger'

  before do
    @environment_config_mock = mock 'environment config mock'

    @config_stub.stub(:environment).and_return(@environment_config_mock)
    @config_stub.stub(:artifacts).and_return(['chef_repo', 'cookbooks', 'app'])
    @config_stub.stub(:artifact_cloud_formation_url).and_return('CookBooksURL')

    @entry_mock = mock 'entry mock'
    SimpleDeploy::Entry.should_receive(:new).
                        at_least(:once).
                        with(:name   => 'test-stack').
                        and_return @entry_mock

    @stack = SimpleDeploy::Stack.new :name        => 'test-stack',
                                     :environment => 'test-env'
  end

  describe "creating a stack" do
    before do
      @stack_creator_mock = mock 'stack creator'
      @new_attributes = [
        { 'chef_repo' => 'test123' },
        { 'chef_repo_bucket_prefix' => 'test-prefix' },
        { 'chef_repo_domain' => 'test-domain' }
      ]

      @expected_attributes = @new_attributes +
        [{ 'CookBooksURL' => 's3://test-prefix-us-west-1/test-domain/test123.tar.gz' }]
    end

    it "should set the attributes using what is passed to the create command" do
      @entry_mock.should_receive(:attributes).and_return({})
      @entry_mock.should_receive(:set_attributes).with(@expected_attributes)
      @entry_mock.should_receive(:save).and_return(true)

      SimpleDeploy::StackCreator.should_receive(:new).
                                 with(:name          => 'test-stack',
                                      :entry         => @entry_mock,
                                      :template_file => 'some_json').
                                and_return @stack_creator_mock
      @stack_creator_mock.should_receive(:create)
        
      @stack.create :attributes => @new_attributes, :template => 'some_json'
    end
  end

  describe "updating a stack" do
    before do
      @stack_updater_mock = mock 'stack updater'
      @stack_reader_mock = mock 'stack reader'
      @new_attributes = [
        { 'chef_repo' => 'test123' },
        { 'chef_repo_bucket_prefix' => 'test-prefix' },
        { 'chef_repo_domain' => 'test-domain' }
      ]

      @expected_attributes = [
        { 'chef_repo' => 'test123' },
        { 'chef_repo_bucket_prefix' => 'test-prefix' },
        { 'chef_repo_domain' => 'test-domain' },
        { 'CookBooksURL' => 's3://test-prefix-us-west-1/test-domain/test123.tar.gz' }
      ]
    end

    it "should update when the deployment is not locked" do
      deployment_stub = stub 'deployment', :clear_for_deployment? => true
      @stack.stub(:deployment).and_return(deployment_stub)

      @entry_mock.should_receive(:set_attributes).with(@expected_attributes)
      @entry_mock.should_receive(:save).and_return(true)

      SimpleDeploy::StackUpdater.should_receive(:new).
                                 with(:name          => 'test-stack',
                                      :entry         => @entry_mock,
                                      :template_body => 'some_json').
                                 and_return @stack_updater_mock
      @stack_updater_mock.stub(:update_stack).and_return(true)
      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:attributes).and_return({})
      @stack_reader_mock.should_receive(:template).and_return('some_json')

      @stack.update(:attributes => @new_attributes).should be_true
    end

    it "should not update when the deployment is locked and force is not set" do
      deployment_stub = stub 'deployment', :clear_for_deployment? => false
      @stack.stub(:deployment).and_return(deployment_stub)

      SimpleDeploy::StackAttributeFormatter.should_not_receive(:new)

      @stack.update(:attributes => { 'arg1' => 'val' }).should_not be_true
    end

    it "should update when the deployment is locked and force is set true" do
      deployment_mock = mock 'deployment'
      deployment_mock.should_receive(:clear_for_deployment?).and_return(false, true, true)
      deployment_mock.should_receive(:clear_deployment_lock).with(true)
      @stack.stub(:deployment).and_return(deployment_mock)
      @stack.stub(:sleep).and_return(false)

      @entry_mock.should_receive(:set_attributes).with(@expected_attributes)
      @entry_mock.should_receive(:save).and_return(true)

      SimpleDeploy::StackUpdater.should_receive(:new).
                                 with(:name          => 'test-stack',
                                      :entry         => @entry_mock,
                                      :template_body => 'some_json').
                                 and_return @stack_updater_mock
      @stack_updater_mock.stub(:update_stack).and_return(true)
      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:attributes).and_return({})
      @stack_reader_mock.should_receive(:template).and_return('some_json')

      @stack.update(:force => true, :attributes => @new_attributes).should be_true
    end

    it "should not update when the deployment is locked and force is set false" do
      deployment_stub = stub 'deployment', :clear_for_deployment? => false
      @stack.stub(:deployment).and_return(deployment_stub)

      SimpleDeploy::StackAttributeFormatter.should_not_receive(:new)

      @stack.update(:force => false, :attributes => { 'arg1' => 'val' }).should_not be_true
    end
  end

  describe "destroying a stack" do
    before do
      @stack_reader_mock = mock 'stack reader'
      @stack_destroyer_mock = mock 'stack destroyer'
    end

    it "should destroy if the stack is not protected" do
      @stack.stub(:exists?).and_return(true)
      @entry_mock.should_receive(:delete_attributes)

      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:attributes).and_return('protection' => 'off')
      SimpleDeploy::StackDestroyer.should_receive(:new).
                                   with(:name   => 'test-stack').
                                   and_return @stack_destroyer_mock
      @stack_destroyer_mock.should_receive(:destroy).and_return(true)

      @stack.destroy.should be_true
    end

    it "should not destroy if the stack is protected" do
      @stack.stub(:exists?).and_return(true)
      @entry_mock.should_receive(:delete_attributes).never

      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:attributes).and_return('protection' => 'on')
      SimpleDeploy::StackDestroyer.should_receive(:new).never

      @stack.destroy.should_not be_true
    end

    it "should destroy if protection is undefined" do
      @stack.stub(:exists?).and_return(true)
      @entry_mock.should_receive(:delete_attributes)

      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:attributes).and_return({})
      SimpleDeploy::StackDestroyer.should_receive(:new).
                                   with(:name   => 'test-stack').
                                   and_return @stack_destroyer_mock
      @stack_destroyer_mock.should_receive(:destroy).and_return(true)

      @stack.destroy.should be_true
    end

    it "should not destroy if stack does not exist" do
      @stack.stub(:exists?).and_return(false)
      @entry_mock.should_receive(:delete_attributes).never
      @stack_destroyer_mock.should_receive(:destroy).never
      @stack.destroy.should_not be_true
    end
  end

  describe 'instances' do
    before do
      @stack_reader_mock = mock 'stack reader'

      @instances = [
        { 'instancesSet' => [{ 'ipAddress' => '50.40.30.20',
                               'privateIpAddress' => '10.1.2.3' }]}
      ]
      @private_instances = [
        { 'instancesSet' => [{ 'ipAddress' => nil,
                               'privateIpAddress' => '10.3.2.1' }]}
      ]
    end

    it 'should use the private IP when vpc and not internal or external' do
      @instances.first['instancesSet'].first['vpcId'] = 'my-vpc'

      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:instances).and_return(@instances)

      @stack.instances.should == ['10.1.2.3']
    end

    it 'should use the public IP when not vpc and not internal or external' do
      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:instances).and_return(@instances)

      @stack.instances.should == ['50.40.30.20']
    end

    it 'should use the private IP when internal' do
      stack = SimpleDeploy::Stack.new :name        => 'test-stack',
                                      :environment => 'test-env',
                                      :internal    => true

      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:instances).and_return(@instances)

      stack.instances.should == ['10.1.2.3']
    end

    it 'should use the public IP when external' do
      @instances.first['instancesSet'].first['vpcId'] = 'my-vpc'
      stack = SimpleDeploy::Stack.new :name        => 'test-stack',
                                      :environment => 'test-env',
                                      :external    => true

      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:instances).and_return(@instances)

      stack.instances.should == ['50.40.30.20']
    end

    it 'should not include the instance when external and no public IP' do
      @instances.first['instancesSet'].first['vpcId'] = 'my-vpc'
      stack = SimpleDeploy::Stack.new :name        => 'test-stack',
                                      :environment => 'test-env',
                                      :external    => true

      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:instances).and_return(@private_instances)

      stack.instances.should == []
    end

    it 'should handle instanceSets with multiple intances' do
      @instances = [{ 'instancesSet' => [
        { 'ipAddress' => '50.40.30.20', 'privateIpAddress' => '10.1.2.3' },
        { 'ipAddress' => '50.40.30.21', 'privateIpAddress' => '10.1.2.4' }] }]

      SimpleDeploy::StackReader.should_receive(:new).
                                 with(:name   => 'test-stack').
                                 and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:instances).and_return(@instances)

      @stack.instances.should == ['50.40.30.20', '50.40.30.21']
    end
  end

  describe 'deploy' do
    before do
      @stack = SimpleDeploy::Stack.new :environment => 'test-env',
                                       :name        => 'test-stack',
                                       :config      => @config_stub,
                                       :internal    => false
      @deployment_mock = mock "deployment"
      @stack.stub(:deployment).and_return(@deployment_mock)
    end

    it "should call exec on deployment" do
      @deployment_mock.should_receive(:execute).with(true).and_return true
      @stack.deploy(true).should be_true
    end

    it "should not force the deployment by default" do
      @deployment_mock.should_receive(:execute).with(false).and_return true
      @stack.deploy.should be_true
    end

    it "should return false if the deployment fails" do
      @deployment_mock.should_receive(:execute).with(false).and_return false
      @stack.deploy.should be_false
    end
  end

  describe 'execute' do
    before do
      @stack = SimpleDeploy::Stack.new :environment => 'test-env',
                                       :name        => 'test-stack',
                                       :config      => @config_stub,
                                       :internal    => false
      @execute_mock = mock "execute"
      @stack.stub(:executer).and_return(@execute_mock)
    end

    it "should call exec with the given args" do
      @execute_mock.should_receive(:execute).
                    with(:arg => 'val').and_return true
      @stack.execute(:arg => 'val').should be_true
    end

    it "should return false if the exec fails" do
      @execute_mock.should_receive(:execute).
                    with(:arg => 'val').and_return false
      @stack.execute(:arg => 'val').should be_false
    end
  end

  describe 'in_progress_update' do
    before do
      @stack = SimpleDeploy::Stack.new :environment => 'test-env',
                                       :name        => 'test-stack',
                                       :config      => @config_stub,
                                       :internal    => false
    end

    it "should update the stack to not be in progress" do
      status_stub = stub 'status', :kind_of? => SimpleDeploy::Stack::Deployment::Status
      stack_updater_mock = mock 'stack_updater'
      @entry_mock.stub :attributes => { 'deployment_in_progress' => 'true' }
      @stack.stub :template => 'some_json'
      @entry_mock.should_receive(:set_attributes).with([{ 'deployment_in_progress' => 'false' }])
      SimpleDeploy::StackUpdater.should_receive(:new).
                                 with(:name          => 'test-stack',
                                      :entry         => @entry_mock,
                                      :template_body => 'some_json').
                                 and_return stack_updater_mock
      stack_updater_mock.should_receive(:update_stack).with([{ 'deployment_in_progress' => 'false' }])
      @entry_mock.should_receive(:save)
      @stack.in_progress_update(:attributes => [ { 'deployment_in_progress' => 'false' } ], :caller => status_stub).should be_true
    end
  end

  describe "wait_for_stable" do
    before do
      @status_mock = mock 'status'
      @stack = SimpleDeploy::Stack.new :name        => 'test-stack',
                                       :config      => @config_stub,
                                       :internal    => false
    end

    it "should call wait_for_stable on status" do
      SimpleDeploy::Status.should_receive(:new).
                           with(:name   => 'test-stack').
                           and_return @status_mock
      @status_mock.should_receive(:wait_for_stable)

      @stack.wait_for_stable
    end
  end

  describe "exists?" do
    before do
      @stack_reader_mock = mock 'stack reader'
      @stack = SimpleDeploy::Stack.new :name        => 'test-stack',
                                       :config      => @config_stub,
                                       :internal    => false
    end

    it "should return true if stack exists" do
      SimpleDeploy::StackReader.should_receive(:new).
                                with(:name   => 'test-stack').
                                and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:status).and_return('CREATE_COMPLETE')

      @stack.exists?.should be_true
    end

    it "should return false if the stack does not exist" do
      SimpleDeploy::StackReader.should_receive(:new).
                                with(:name   => 'test-stack').
                                and_return @stack_reader_mock
      @stack_reader_mock.should_receive(:status).
                         and_raise(SimpleDeploy::Exceptions::UnknownStack.new(
                                  'Stack:test-stack does not exist'))

      @stack.exists?.should be_false
    end
  end
end
