require 'spec_helper'

class ClassMixedWithDSLStructure
  include Beaker::DSL::Structure
  include Beaker::DSL::Helpers::TestHelpers
end

describe ClassMixedWithDSLStructure do
  include Beaker::DSL::Assertions

  let (:logger) { double }
  let (:metadata) { @metadata ||= {} }

  before :each do
    allow( subject ).to receive(:metadata).and_return(metadata)
  end

  describe '#step' do
    it 'requires a name' do
      expect { subject.step do; end }.to raise_error ArgumentError
    end

    it 'notifies the logger' do
      allow( subject ).to receive( :set_current_step_name )
      expect( subject ).to receive( :logger ).and_return( logger )
      expect( logger ).to receive( :notify )
      subject.step 'blah'
    end

    it 'yields if a block is given' do
      expect( subject ).to receive( :logger ).and_return( logger ).exactly(3).times
      allow(  subject ).to receive( :set_current_step_name )
      expect( logger ).to receive( :step_in )
      expect( logger ).to receive( :step_out )
      expect( logger ).to receive( :notify )
      expect( subject ).to receive( :foo )
      subject.step 'blah' do
        subject.foo
      end
    end

    it 'sets the metadata' do
      allow( subject ).to receive( :logger ).and_return( logger )
      allow( logger ).to receive( :notify )
      step_name = 'pierceBrosnanTests'
      subject.step step_name
      expect( metadata[:step][:name] ).to be === step_name
    end
  end

  describe '#test_name' do

    it 'requires a name' do
      expect { subject.test_name do; end }.to raise_error ArgumentError
    end

    it 'notifies the logger' do
      expect( subject ).to receive( :logger ).and_return( logger )
      expect( logger ).to receive( :notify )
      subject.test_name 'blah'
    end

    it 'yields if a block is given' do
      expect( subject ).to receive( :logger ).and_return( logger ).exactly(3).times
      expect( logger ).to receive( :notify )
      expect( logger ).to receive( :step_in )
      expect( logger ).to receive( :step_out )
      expect( subject ).to receive( :foo )
      subject.test_name 'blah' do
        subject.foo
      end
    end

    it 'sets the metadata' do
      allow( subject ).to receive( :logger ).and_return( logger )
      allow( logger ).to receive( :notify )
      test_name = '15-05-08\'s weather is beautiful'
      subject.test_name test_name
      expect( metadata[:case][:name] ).to be === test_name
    end
  end

  describe '#teardown' do
    it 'append a block to the @teardown var' do
      teardown_array = double
      subject.instance_variable_set :@teardown_procs, teardown_array
      block = lambda { 'blah' }
      expect( teardown_array ).to receive( :<< ).with( block )
      subject.teardown &block
    end
  end

  describe '#expect_failure' do
    it 'passes when a MiniTest assertion is raised' do
      expect( subject ).to receive( :logger ).and_return( logger )
      expect( logger ).to receive( :notify )
      # We changed this lambda to use the simplest assert possible; using assert_equal
      # caused an error in minitest 5.9.0 trying to write to the file system.
      block = lambda { assert(false, 'this assertion should be caught') }
      expect{ subject.expect_failure 'this is an expected failure', &block }.to_not raise_error
    end

    it 'passes when a Beaker assertion is raised' do
      expect( subject ).to receive( :logger ).and_return( logger )
      expect( logger ).to receive( :notify )
      block = lambda { assert_no_match('1', '1', '1 and 1 should not match') }
      expect{ subject.expect_failure 'this is an expected failure', &block }.to_not raise_error
    end

    it 'fails when a non-Beaker, non-MiniTest assertion is raised' do
      block = lambda { raise 'not a Beaker or MiniTest error' }
      expect{ subject.expect_failure 'this has a non-Beaker, non-MiniTest exception', &block }.to raise_error(RuntimeError, /not a Beaker or MiniTest error/)
    end

    it 'fails when no assertion is raised' do
      block = lambda { assert_equal('1', '1', '1 should equal 1') }
      expect{ subject.expect_failure 'this has no failure', &block }.to raise_error(RuntimeError, /An assertion was expected to fail, but passed/)
    end
  end

  describe 'confine' do
    let(:logger) { double.as_null_object }
    before do
      allow( subject ).to receive( :logger ).and_return( logger )
    end

    it ':to - skips the test if there are no applicable hosts' do
      allow( subject ).to receive( :hosts ).and_return( [] )
      allow( subject ).to receive( :hosts= )
      expect( logger ).to receive( :warn )
      expect( subject ).to receive( :skip_test ).with( 'No suitable hosts found with {}' )
      subject.confine( :to, {} )
    end

    it ':except - skips the test if there are no applicable hosts' do
      allow( subject ).to receive( :hosts ).and_return( [] )
      allow( subject ).to receive( :hosts= )
      expect( logger ).to receive( :warn )
      expect( subject ).to receive( :skip_test ).with( 'No suitable hosts found without {}' )
      subject.confine( :except, {} )
    end

    it ':to - uses a provided host subset when no criteria is provided' do
      subset = ['host1', 'host2']
      hosts = subset.dup << 'host3'
      allow( subject ).to receive( :hosts ).and_return(hosts).twice
      expect( subject ).to receive( :hosts= ).with( subset )
      subject.confine :to, {}, subset
    end

    it ':except - excludes provided host subset when no criteria is provided' do
      subset = ['host1', 'host2']
      hosts = subset.dup << 'host3'
      allow( subject ).to receive( :hosts ).and_return(hosts).twice
      expect( subject ).to receive( :hosts= ).with( hosts - subset )
      subject.confine :except, {}, subset
    end

    it 'raises when given mode is not :to or :except' do
      hosts = ['host1', 'host2']
      allow( subject ).to receive( :hosts ).and_return(hosts)
      allow( subject ).to receive( :hosts= )

      expect {
        subject.confine( :regardless, {:thing => 'value'} )
      }.to raise_error( 'Unknown option regardless' )
    end

    it 'rejects hosts that do not meet simple hash criteria' do
      hosts = [ {'thing' => 'foo'}, {'thing' => 'bar'} ]

      expect( subject ).to receive( :hosts ).and_return( hosts ).twice
      expect( subject ).to receive( :hosts= ).
        with( [ {'thing' => 'foo'} ] )

      subject.confine :to, :thing => 'foo'
    end

    it 'rejects hosts that match a list of criteria' do
      hosts = [ {'thing' => 'foo'}, {'thing' => 'bar'}, {'thing' => 'baz'} ]

      expect( subject ).to receive( :hosts ).and_return( hosts ).twice
      expect( subject ).to receive( :hosts= ).
        with( [ {'thing' => 'bar'} ] )

      subject.confine :except, :thing => ['foo', 'baz']
    end

    it 'rejects hosts when a passed block returns true' do
      host1 = {'platform' => 'solaris'}
      host2 = {'platform' => 'solaris'}
      host3 = {'platform' => 'windows'}
      ret1 = (Struct.new('Result1', :stdout)).new(':global')
      ret2 = (Struct.new('Result2', :stdout)).new('a_zone')
      hosts = [ host1, host2, host3 ]

      expect( subject ).to receive( :hosts ).and_return( hosts ).twice
      expect( subject ).to receive( :on ).
        with( host1, '/sbin/zonename' ).
        and_return( ret1 )
      expect( subject ).to receive( :on ).
        with( host1, '/sbin/zonename' ).
        and_return( ret2 )

      expect( subject ).to receive( :hosts= ).with( [ host1 ] )

      subject.confine :to, :platform => 'solaris' do |host|
        subject.on( host, '/sbin/zonename' ).stdout =~ /:global/
      end
    end

    it 'doesn\'t corrupt the global hosts hash when confining from a subset of hosts' do
      host1 = {'platform' => 'solaris', :roles => ['master']}
      host2 = {'platform' => 'solaris', :roles => ['agent']}
      host3 = {'platform' => 'windows', :roles => ['agent']}
      hosts = [ host1, host2, host3 ]
      agents = [ host2, host3 ]

      expect( subject ).to receive( :hosts ).and_return( hosts )
      expect( subject ).to receive( :hosts= ).with( [  host2, host1 ] )
      confined_hosts = subject.confine :except, {:platform => 'windows'}, agents
      expect( confined_hosts ).to be === [ host2, host1 ]
    end

    it 'can apply multiple confines correctly' do
      host1 = {'platform' => 'solaris', :roles => ['master']}
      host2 = {'platform' => 'solaris', :roles => ['agent']}
      host3 = {'platform' => 'windows', :roles => ['agent']}
      host4 = {'platform' => 'fedora', :roles => ['agent']}
      host5 = {'platform' => 'fedora', :roles => ['agent']}
      hosts = [ host1, host2, host3, host4, host5 ]
      agents = [ host2, host3, host4, host5 ]

      expect( subject ).to receive( :hosts ).and_return( hosts ).exactly(3).times
      expect( subject ).to receive( :hosts= ).with( [  host1, host2, host4, host5 ] )
      hosts = subject.confine :except, {:platform => 'windows'}
      expect( hosts ).to be === [ host1, host2, host4, host5  ]
      expect( subject ).to receive( :hosts= ).with( [  host4, host5, host1 ] )
      hosts = subject.confine :to, {:platform => 'fedora'}, agents
      expect( hosts ).to be === [ host4, host5, host1 ]
    end
  end

  describe '#select_hosts' do
    let(:logger) { double.as_null_object }
    before do
      allow( subject ).to receive( :logger ).and_return( logger )
    end

    it 'it returns an empty array if there are no applicable hosts' do
      hosts = [ {'thing' => 'foo'}, {'thing' => 'bar'} ]

      expect(subject.select_hosts( {'thing' => 'nope'}, hosts )).to be == []
    end

    it 'selects hosts that match a list of criteria' do
      hosts = [ {'thing' => 'foo'}, {'thing' => 'bar'}, {'thing' => 'baz'} ]

      expect(subject.select_hosts( {:thing => ['foo', 'baz']}, hosts )).to be == [ {'thing' => 'foo'}, {'thing' => 'baz'} ]
    end

    it 'selects hosts when a passed block returns true' do
      host1 = {'platform' => 'solaris1'}
      host2 = {'platform' => 'solaris2'}
      host3 = {'platform' => 'windows'}
      ret1 = double('result1')
      allow( ret1 ).to receive( :stdout ).and_return(':global')
      ret2 = double('result2')
      allow( ret2 ).to receive( :stdout ).and_return('a_zone')
      hosts = [ host1, host2, host3 ]
      expect( subject ).to receive( :hosts ).and_return( hosts )

      expect( subject ).to receive( :on ).with( host1, '/sbin/zonename' ).once.and_return( ret1 )
      expect( subject ).to receive( :on ).with( host2, '/sbin/zonename' ).once.and_return( ret2 )

      selected_hosts = subject.select_hosts 'platform' => 'solaris' do |host|
                             subject.on(host, '/sbin/zonename').stdout =~ /:global/
      end
      expect( selected_hosts ).to be == [ host1 ]
    end
  end

  describe '#tag' do
    let ( :tag_includes ) { @tag_includes || [] }
    let ( :tag_excludes ) { @tag_excludes || [] }
    let ( :options )      {
      opts = Beaker::Options::OptionsHash.new
      opts[:tag_includes] = tag_includes
      opts[:tag_excludes] = tag_excludes
      opts
    }

    before :each do
      allow( subject ).to receive( :platform_specific_tag_confines )
    end

    it 'sets tags on the TestCase\'s metadata object' do
      subject.instance_variable_set(:@options, options)
      tags = ['pants', 'jayjay', 'moguely']
      subject.tag(*tags)
      expect( metadata[:case][:tags] ).to be === tags
    end

    it 'lowercases the tags' do
      subject.instance_variable_set(:@options, options)
      tags_upper = ['pANTs', 'jAYJAy', 'moGUYly']
      tags_lower = tags_upper.map(&:downcase)
      subject.tag(*tags_upper)
      expect( metadata[:case][:tags] ).to be === tags_lower
    end

    it 'skips the test if any of the requested tags isn\'t included in this test' do
      test_tags = ['pants', 'jayjay', 'moguely']
      @tag_includes = test_tags.compact.push('needed_tag_not_in_test')
      subject.instance_variable_set(:@options, options)

      allow( subject ).to receive( :path )
      expect( subject ).to receive( :skip_test )
      subject.tag(*test_tags)
    end

    it 'runs the test if all requested tags are included in this test' do
      @tag_includes = ['pants_on_head', 'jayjay_jayjay', 'mo']
      test_tags = @tag_includes.compact.push('extra_asdf')
      subject.instance_variable_set(:@options, options)

      allow( subject ).to receive( :path )
      expect( subject ).to receive( :skip_test ).never
      subject.tag(*test_tags)
    end

    it 'skips the test if any of the excluded tags are included in this test' do
      test_tags = ['ports', 'jay_john_mary', 'mog_the_dog']
      @tag_excludes = [test_tags[0]]
      subject.instance_variable_set(:@options, options)

      allow( subject ).to receive( :path )
      expect( subject ).to receive( :skip_test )
      subject.tag(*test_tags)
    end

    it 'runs the test if none of the excluded tags are included in this test' do
      @tag_excludes = ['pants_on_head', 'jayjay_jayjay', 'mo']
      test_tags     = ['pants_at_head', 'jayj00_jayjay', 'motly_crew']
      subject.instance_variable_set(:@options, options)

      allow( subject ).to receive( :path )
      expect( subject ).to receive( :skip_test ).never
      subject.tag(*test_tags)
    end

  end
end

describe Beaker::DSL::Structure::PlatformTagConfiner do
  let ( :confines_array ) { @confines_array || [] }
  let ( :confiner ) {
    Beaker::DSL::Structure::PlatformTagConfiner.new( confines_array )
  }

  describe '#initialize' do
    it 'transforms one entry' do
      platform_regex = /^ubuntu$/
      tag_reason_hash = {
        'tag1' => 'reason1',
        'tag2' => 'reason2'
      }
      @confines_array = [ {
          :platform => platform_regex,
          :tag_reason_hash => tag_reason_hash
        }
      ]

      internal_hash = confiner.instance_variable_get( :@tag_confine_details_hash )
      expect( internal_hash.keys() ).to include( 'tag1' )
      expect( internal_hash.keys() ).to include( 'tag2' )
      expect( internal_hash.keys().length() ).to be === 2

      tag_reason_hash.each do |tag, reason|
        tag_array = internal_hash[tag]
        expect( tag_array.length() ).to be === 1
        tag_hash = tag_array[0]
        expect( tag_hash[:platform_regex] ).to eql( platform_regex )
        expect( tag_hash[:log_message] ).to match( /#{reason}/ )
        expect( tag_hash[:type] ).to be === :except
      end
    end

    it 'deals with the same tag being used on multiple platforms correctly' do
      @confines_array = [
        {
          :platform => /^el-/,
          :tag_reason_hash => {
            'tag1' => 'reason el 1',
            'tag2' => 'reason2'
          }
        }, {
          :platform => /^cisco-/,
          :tag_reason_hash => {
            'tag1' => 'reason cisco 1',
            'tag3' => 'reason3'
          }
        }
      ]

      internal_hash = confiner.instance_variable_get( :@tag_confine_details_hash )
      expect( internal_hash.keys() ).to include( 'tag1' )
      expect( internal_hash.keys() ).to include( 'tag2' )
      expect( internal_hash.keys() ).to include( 'tag3' )
      expect( internal_hash.keys().length() ).to be === 3

      shared_tag_array = internal_hash['tag1']
      expect( shared_tag_array.length() ).to be === 2

      platform_el_found = false
      platform_cisco_found = false
      shared_tag_array.each do |confine_details|
        case confine_details[:log_message]
        when /\ el\ 1/
          platform_el_found = true
          platform_to_match = /^el-/
          reason_to_match = /reason\ el\ 1/
        when /\ cisco\ 1/
          platform_cisco_found = true
          platform_to_match = /^cisco-/
          reason_to_match = /reason\ cisco\ 1/
        else
          log_msg = "unexpected log message for confine_details: "
          log_msg << confine_details[:log_message]
          fail( log_msg )
        end

        expect( confine_details[:platform_regex] ).to eql( platform_to_match )
        expect( confine_details[:log_message] ).to match( reason_to_match )
      end
      expect( platform_el_found ).to be === true
      expect( platform_cisco_found ).to be === true
    end
  end

  describe '#confine_details' do
    it 'returns an empty array if no tags match' do
      fake_confine_details_hash = { 'tag1' => [ {:type => 1}, {:type => 2} ]}
      confiner.instance_variable_set(
        :@tag_confine_details_hash, fake_confine_details_hash
      )
      expect( confiner.confine_details( [ 'tag2', 'tag3' ] ) ).to be === []
    end

    context 'descriminates on tag name' do
      fake_confine_details_hash = {
        'tag0' => [ 10, 20, 30, 40 ],
        'tag1' => [ 41, 51, 61, 71 ],
        'tag2' => [ 22, 32, 42, 52 ],
        'tag3' => [ 63, 73, 83, 93 ],
        'tag4' => [ 34, 44, 54, 64 ],
      }

      key_combos_to_test = fake_confine_details_hash.keys.map { |key| [key] }
      key_combos_to_test << [ 'tag0', 'tag2' ]
      key_combos_to_test << [ 'tag1', 'tag4' ]
      key_combos_to_test << [ 'tag2', 'tag3', 'tag4' ]
      key_combos_to_test << fake_confine_details_hash.keys()

      before :each do
        confiner.instance_variable_set(
          :@tag_confine_details_hash, fake_confine_details_hash
        )
      end

      key_combos_to_test.each do |key_combo_to_have|
        it "selects key(s) #{key_combo_to_have} from #{fake_confine_details_hash.keys}" do
          haves = []
          key_combo_to_have.each do |key_to_have|
            haves += fake_confine_details_hash[key_to_have]
          end
          keys_not_to_have = fake_confine_details_hash.keys.reject { |key_trial|
            key_combo_to_have.include?( key_trial )
          }
          have_nots = []
          keys_not_to_have.each do |key_not_to_have|
            have_nots += fake_confine_details_hash[key_not_to_have]
          end

          details = confiner.confine_details( key_combo_to_have )
          have_nots.each do |confine_details|
            expect( details ).to_not include( confine_details )
          end
          haves.each do |confine_details|
            expect( details ).to     include( confine_details )
          end
        end
      end
    end
  end
end
