#!/usr/bin/env rspec

require 'spec_helper'

MCollective::PluginManager.clear

require 'mcollective/connector/activemq'

# create the stomp error class here as it does not always exist
# all versions of the stomp gem and we do not want to tie tests
# to the stomp gem
module Stomp
  module Error
    class DuplicateSubscription < RuntimeError; end
  end
end

module MCollective
  module Connector
    describe Activemq do

      let(:config) do
        conf = mock
        conf.stubs(:configured).returns(true)
        conf.stubs(:identity).returns("rspec")
        conf.stubs(:collectives).returns(["mcollective"])
        conf
      end

      let(:logger) do
        log = mock
        log.stubs(:log)
        log.stubs(:start)
        Log.configure(log)
        log
      end

      let(:msg) do
        m = mock
        m.stubs(:base64_encode!)
        m.stubs(:payload).returns("msg")
        m.stubs(:agent).returns("agent")
        m.stubs(:type).returns(:reply)
        m.stubs(:collective).returns("mcollective")
        m
      end

      let(:subscription) do
        sub = mock
        sub.stubs(:<<).returns(true)
        sub.stubs(:include?).returns(false)
        sub.stubs(:delete).returns(false)
        sub
      end

      let(:connection) do
        con = mock
        con.stubs(:subscribe).returns(true)
        con.stubs(:unsubscribe).returns(true)
        con
      end

      let(:connector) do
        Activemq.any_instance.stubs(:get_bool_option).with("activemq.use_exponential_back_off", "true").returns(true)
        Activemq.any_instance.stubs(:get_option).with("activemq.initial_reconnect_delay", 0.01).returns(0.01)
        Activemq.any_instance.stubs(:get_option).with("activemq.back_off_multiplier", 2).returns(2)
        Activemq.any_instance.stubs(:get_option).with("activemq.max_reconnect_delay", 30.0).returns(30.0)
        Activemq.any_instance.stubs(:get_bool_option).with("activemq.agents_multiplex", "false").returns(true)
        c = Activemq.new
        c.instance_variable_set("@subscriptions", subscription)
        c.instance_variable_set("@connection", connection)
        c
      end

      before do
        unless ::Stomp::Error.constants.map{|c| c.to_s}.include?("NoCurrentConnection")
          class ::Stomp::Error::NoCurrentConnection < RuntimeError ; end
        end

        logger
        Config.stubs(:instance).returns(config)
      end

      describe "#initialize" do
        before :each do
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.use_exponential_back_off", "true").returns(true)
          Activemq.any_instance.stubs(:get_option).with("activemq.initial_reconnect_delay", 0.01).returns(0.01)
          Activemq.any_instance.stubs(:get_option).with("activemq.back_off_multiplier", 2).returns(2)
          Activemq.any_instance.stubs(:get_option).with("activemq.max_reconnect_delay", 30.0).returns(30.0)
        end

        it "should set the @config variable" do
          connector_obj = Activemq.new
          connector_obj.instance_variable_get("@config").should == config
        end

        it "should set @subscriptions to an empty list" do
          connector_obj = Activemq.new
          connector_obj.instance_variable_get("@subscriptions").should == []
        end
      end

      describe "#connect" do
        before :each do
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(30)
          Activemq.any_instance.stubs(:get_bool_option).with('activemq.stomp_1_0_fallback', true).returns(true)
          Activemq.any_instance.stubs(:get_bool_option).with('activemq.base64', 'false').returns(false)
          Activemq.any_instance.stubs(:get_option).with('activemq.vhost', 'mcollective').returns('rspec')
          Activemq.any_instance.stubs(:get_option).with("activemq.max_reconnect_attempts", 0).returns(5)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.randomize", "false").returns(true)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.backup", "false").returns(true)
          Activemq.any_instance.stubs(:get_option).with("activemq.timeout", -1).returns(1)
          Activemq.any_instance.stubs(:get_option).with("activemq.connect_timeout", 30).returns(5)
          Activemq.any_instance.stubs(:get_option).with("activemq.max_hbrlck_fails", 0).returns(0)
          Activemq.any_instance.stubs(:get_option).with("activemq.max_hbread_fails", 2).returns(2)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.base64", 'false').returns(false)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.prompt_user", 'false').returns(false)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.prompt_password", 'false').returns(false)
          Activemq.any_instance.stubs(:get_option).with("activemq.priority", 0).returns(0)
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.size").returns(2)
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.host").returns("host1")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.port", 61613).returns(6163)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.pool.1.ssl", "false").returns(false)
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.2.host").returns("host2")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.2.port", 61613).returns(6164)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.pool.2.ssl", "false").returns(true)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.pool.2.ssl.fallback", "false").returns(true)
          Activemq.any_instance.stubs(:get_env_or_option).with("STOMP_USER", "activemq.pool.1.user", '').returns("user1")
          Activemq.any_instance.stubs(:get_env_or_option).with("STOMP_USER", "activemq.pool.2.user", '').returns("user2")
          Activemq.any_instance.stubs(:get_env_or_option).with("STOMP_PASSWORD", "activemq.pool.1.password", '').returns("password1")
          Activemq.any_instance.stubs(:get_env_or_option).with("STOMP_PASSWORD", "activemq.pool.2.password", '').returns("password2")
          Activemq.any_instance.instance_variable_set("@subscriptions", subscription)
          Activemq.any_instance.instance_variable_set("@connection", connection)
        end

        it "should not try to reconnect if already connected" do
          Log.expects(:debug).with("Already connection, not re-initializing connection").once
          connector.connect
        end

        it "should support new style config" do
          ENV.delete("STOMP_USER")
          ENV.delete("STOMP_PASSWORD")

          Activemq::EventLogger.expects(:new).returns("logger")

          connector_obj = mock
          connector_obj.expects(:new).with(:backup => true,
                                       :back_off_multiplier => 2,
                                       :max_reconnect_delay => 30.0,
                                       :timeout => 1,
                                       :connect_timeout => 5,
                                       :use_exponential_back_off => true,
                                       :max_reconnect_attempts => 5,
                                       :initial_reconnect_delay => 0.01,
                                       :max_hbrlck_fails => 0,
                                       :max_hbread_fails => 2,
                                       :randomize => true,
                                       :reliable => true,
                                       :logger => "logger",
                                       :connect_headers => {},
                                       :hosts => [{:passcode => 'password1',
                                                   :host => 'host1',
                                                   :port => 6163,
                                                   :ssl => false,
                                                   :login => 'user1'},
                                                  {:passcode => 'password2',
                                                   :host => 'host2',
                                                   :port => 6164,
                                                   :ssl => true,
                                                   :login => 'user2'}
          ])

          connector.expects(:ssl_parameters).with(2, true).returns(true)
          connector.expects(:connection_headers).returns({})

          connector.instance_variable_set("@connection", nil)
          connector.connect(connector_obj)
        end
      end

      describe "#stomp_version_supports_heartbeat?" do
        it "should not be supported with stomp 1.2.9" do
          connector.stubs(:stomp_version).returns("1.2.9")
          connector.stomp_version_supports_heartbeat? == false
        end

        it "should be supported with stomp 1.2.10" do
          connector.stubs(:stomp_version).returns("1.2.10")
          connector.stomp_version_supports_heartbeat? == true
        end
      end

      describe "#connection_headers" do
        before do
          connector.stubs(:stomp_version).returns("1.2.10")
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(1)
          Activemq.any_instance.stubs(:get_bool_option).with('activemq.stomp_1_0_fallback', true).returns(true)
          Activemq.any_instance.stubs(:get_option).with('activemq.vhost', 'mcollective').returns('rspec')
        end

        it "should default to stomp 1.0 only" do
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(0)
          connector.connection_headers[:"accept-version"] == "1.0"
        end

        it "should support setting the vhost" do
          connector.connection_headers[:host].should == "rspec"
        end

        it "should log an informational message about not using Stomp 1.1" do
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(0)
          Log.expects(:info).with(regexp_matches(/without STOMP 1.1 heartbeats/))
          connector.connection_headers
        end

        it "should not log an informational message about not using Stomp 1.1 if the gem won't support it" do
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(0)
          connector.stubs(:stomp_version).returns("1.0.0")
          Log.expects(:info).with(regexp_matches(/without STOMP 1.1 heartbeats/)).never
          connector.connection_headers
        end

        it "should not support stomp 1.1 with older versions of the stomp gem" do
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(1)
          connector.expects(:stomp_version).returns("1.0.0").once
          expect { connector.connection_headers }.to raise_error("Setting STOMP 1.1 properties like heartbeat intervals require at least version 1.2.10 of the STOMP gem")
        end

        it "should force the heartbeat to min 30 seconds" do
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(1)
          connector.connection_headers[:"heart-beat"].should == "30500,29500"
        end

        it "should default to 1.0 and 1.1 support" do
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(1)
          connector.connection_headers[:"accept-version"].should == "1.1,1.0"
        end

        it "should support stomp 1.1 only operation" do
          Activemq.any_instance.stubs(:get_option).with("activemq.heartbeat_interval", 0).returns(1)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.stomp_1_0_fallback", true).returns(false)
          connector.connection_headers[:"accept-version"].should == "1.1"
        end
      end

      describe "#ssl_paramaters" do

        before :each do
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.host").returns("host1")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.port").returns("6164")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.user").returns("user1")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.password").returns("password1")
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.pool.1.ssl", false).returns(true)
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.ciphers", false).returns(false)
        end

        it "should ensure all settings are provided" do
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.cert", false).returns("rspec")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.key", false).returns(nil)
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.ca", false).returns(nil)

          expect { connector.ssl_parameters(1, false) }.to raise_error("cert, key and ca has to be supplied for verified SSL mode")
        end

        it "should verify the ssl files exist" do
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.cert", false).returns("rspec")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.key", false).returns('rspec.key')
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.ca", false).returns('rspec1.ca,rspec2.ca')

          connector.expects(:get_key_file).returns("rspec.key").at_least_once
          connector.expects(:get_cert_file).returns("rspec.cert").at_least_once

          File.expects(:exist?).with("rspec.cert").twice.returns(true)
          File.expects(:exist?).with("rspec.key").twice.returns(true)
          File.expects(:exist?).with("rspec1.ca").twice.returns(true)
          File.expects(:exist?).with("rspec2.ca").twice.returns(false)

          expect { connector.ssl_parameters(1, false) }.to raise_error("Cannot find CA file rspec2.ca")

          connector.ssl_parameters(1, true).should == true
        end

        it "should support fallback mode when there are errors" do
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.cert", false).returns("rspec")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.key", false).returns('rspec.key')
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.ca", false).returns('rspec1.ca,rspec2.ca')

          connector.ssl_parameters(1, true).should == true
        end

        it "should fail if fallback isnt enabled" do
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.cert", false).returns("rspec")
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.key", false).returns('rspec.key')
          Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.ca", false).returns('rspec1.ca,rspec2.ca')

          expect { connector.ssl_parameters(1, false) }.to raise_error
        end

        context 'ciphers' do
          before :each do
            Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.cert", false).returns("rspec")
            Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.key", false).returns('rspec.key')
            Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.ca", false).returns('rspec1.ca,rspec2.ca')
            File.stubs(:exist?).returns(true)
          end

          it 'should not set ciphers by default' do
            parameters = connector.ssl_parameters(1, false)
            parameters.ciphers.should == false
          end

          it 'should support setting of ciphers' do
            Activemq.any_instance.stubs(:get_option).with("activemq.pool.1.ssl.ciphers", false).returns('TLSv127:!NUTS')
            parameters = connector.ssl_parameters(1, false)
            parameters.ciphers.should == 'TLSv127:!NUTS'
          end
        end

      end

      describe "#get_key_file" do
        it "should return the filename from the environment variable" do
          ENV["MCOLLECTIVE_ACTIVEMQ_POOL2_SSL_KEY"] = "/path/to/rspec/env"
          connector.get_key_file(2).should == "/path/to/rspec/env"
        end

        it "should return the filename defined in the config file if the environment varialbe doesn't exist" do
          ENV.delete("MCOLLECTIVE_ACTIVEMQ_POOL2_SSL_KEY")
          connector.expects(:get_option).with("activemq.pool.2.ssl.key", false).returns("/path/to/rspec/conf")
          connector.get_key_file(2).should == "/path/to/rspec/conf"
        end

      end

      describe "#get_cert_file" do
        it "should return the filename from the environment variable" do
          ENV["MCOLLECTIVE_ACTIVEMQ_POOL2_SSL_CERT"] = "/path/to/rspec/env"
          connector.get_cert_file(2).should == "/path/to/rspec/env"
        end

        it "should return the filename defined in the config file if the environment varialbe doesn't exist" do
          ENV.delete("MCOLLECTIVE_ACTIVEMQ_POOL2_SSL_CERT")
          connector.expects(:get_option).with("activemq.pool.2.ssl.cert", false).returns("/path/to/rspec/conf")
          connector.get_cert_file(2).should == "/path/to/rspec/conf"
        end
      end

      describe '#exponential_back_off' do
        it "should not do anything when use_exponential_back_off is off" do
          connector.instance_variable_set(:@use_exponential_back_off, false)
          connector.exponential_back_off.should == nil
        end

        it 'should return values of the expected sequence on subsequent calls' do
          connector.instance_variable_set(:@use_exponential_back_off, true)
          connector.instance_variable_set(:@initial_reconnect_delay, 5.0)
          connector.instance_variable_set(:@back_off_multiplier, 2)
          connector.instance_variable_set(:@max_reconnect_delay, 30.0)
          connector.instance_variable_set(:@reconnect_delay, 5.0)

          connector.exponential_back_off.should == 5
          connector.exponential_back_off.should == 10
          connector.exponential_back_off.should == 20
          connector.exponential_back_off.should == 30
          connector.exponential_back_off.should == 30
        end
      end

      describe "#receive" do
        it "should receive from the middleware" do
          payload = mock
          payload.stubs(:command).returns("MESSAGE")
          payload.stubs(:body).returns("msg")
          payload.stubs(:headers).returns("headers")

          connection.expects(:receive).returns(payload)

          Message.expects(:new).with("msg", payload, :base64 => true, :headers => "headers").returns("message")
          connector.instance_variable_set("@base64", true)

          received = connector.receive
          received.should == "message"
        end

        it "should sleep and retry if recieving while disconnected" do
          payload = mock
          payload.stubs(:command).returns("MESSAGE")
          payload.stubs(:body).returns("msg")
          payload.stubs(:headers).returns("headers")

          Message.stubs(:new).returns("rspec")
          connection.expects(:receive).raises(::Stomp::Error::NoCurrentConnection).returns(payload).twice
          connector.expects(:sleep).with(1)

          connector.receive.should == "rspec"
        end

        it "should raise an error on failure to receive a frame" do
          connection.expects(:receive).returns(nil)

          expect { connector.receive }.to raise_error(MessageNotReceived, /No message received from ActiveMQ./)
        end

        it "should log and raise UnexpectedMessageType on non-MESSAGE frames" do
          payload = mock
          payload.stubs(:command).returns("ERROR")
          payload.stubs(:body).returns("Out of cheese exception")
          payload.stubs(:headers).returns("headers")

          connection.expects(:receive).returns(payload)

          Message.stubs(:new)

          Log.stubs(:warn)
          Log.expects(:warn).with('Unexpected \'ERROR\' frame.  Headers: "headers" Body: "Out of cheese exception"')

          expect { connector.receive }.to raise_error(UnexpectedMessageType, /Received frame of type 'ERROR' expected 'MESSAGE'/)
        end
      end

      describe "#publish" do
        before do
          connection.stubs(:publish).with("test", "msg", {}).returns(true)
        end

        it "should base64 encode a message if configured to do so" do
          connector.instance_variable_set("@base64", true)
          connector.expects(:headers_for).returns({})
          connector.expects(:target_for).returns({:name => "test", :headers => {}})
          connection.expects(:publish).with("test", "msg", {})
          msg.expects(:base64_encode!)

          connector.publish(msg)
        end

        it "should not base64 encode if not configured to do so" do
          connector.instance_variable_set("@base64", false)
          connector.expects(:headers_for).returns({})
          connector.expects(:target_for).returns({:name => "test", :headers => {}})
          connection.expects(:publish).with("test", "msg", {})
          msg.expects(:base64_encode!).never

          connector.publish(msg)
        end

        it "should publish the correct message to the correct target with msgheaders" do
          connection.expects(:publish).with("test", "msg", {"test" => "test"}).once
          connector.expects(:headers_for).returns({"test" => "test"})
          connector.expects(:target_for).returns({:name => "test", :headers => {}})

          connector.publish(msg)
        end

        it "should publish direct messages based on discovered_hosts" do
          msg = mock
          msg.stubs(:base64_encode!)
          msg.stubs(:payload).returns("msg")
          msg.stubs(:agent).returns("agent")
          msg.stubs(:collective).returns("mcollective")
          msg.stubs(:type).returns(:direct_request)
          msg.expects(:discovered_hosts).returns(["one", "two"])

          connector.expects(:headers_for).with(msg, "one")
          connector.expects(:headers_for).with(msg, "two")
          connection.expects(:publish).with('/queue/mcollective.nodes', 'msg', nil).twice

          connector.publish(msg)
        end
      end

      describe "#subscribe" do
        it "should handle duplicate subscription errors" do
          connection.expects(:subscribe).raises(::Stomp::Error::DuplicateSubscription)
          Log.expects(:error).with(regexp_matches(/already had a matching subscription, ignoring/))
          connector.subscribe("test", :broadcast, "mcollective")
        end

        it "should use the make_target correctly" do
          connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}})
          connector.subscribe("test", :broadcast, "mcollective")
        end

        it "should check for existing subscriptions" do
          connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
          subscription.expects(:include?).with("rspec").returns(false)
          connection.expects(:subscribe).never

          connector.subscribe("test", :broadcast, "mcollective")
        end

        it "should subscribe to the middleware" do
          connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
          connection.expects(:subscribe).with("test", {}, "rspec")
          connector.subscribe("test", :broadcast, "mcollective")
        end

        it "should add to the list of subscriptions" do
          connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
          subscription.expects(:<<).with("rspec")
          connector.subscribe("test", :broadcast, "mcollective")
        end
      end

      describe "#unsubscribe" do
        it "should use make_target correctly" do
          connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}})
          connector.unsubscribe("test", :broadcast, "mcollective")
        end

        it "should unsubscribe from the target" do
          connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
          connection.expects(:unsubscribe).with("test", {}, "rspec").once

          connector.unsubscribe("test", :broadcast, "mcollective")
        end

        it "should delete the source from subscriptions" do
          connector.expects(:make_target).with("test", :broadcast, "mcollective").returns({:name => "test", :headers => {}, :id => "rspec"})
          subscription.expects(:delete).with("rspec").once

          connector.unsubscribe("test", :broadcast, "mcollective")
        end
      end

      describe "#target_for" do
        it "should create reply targets based on reply-to headers in requests" do
          message = mock
          message.expects(:type).returns(:reply)

          request = mock
          request.expects(:headers).returns({"reply-to" => "foo"})

          message.expects(:request).returns(request)

          connector.target_for(message).should == {:name => "foo", :headers => {}}
        end

        it "should create new request targets" do
          message = mock
          message.expects(:type).returns(:request).times(3)
          message.expects(:agent).returns("rspecagent")
          message.expects(:collective).returns("mcollective")

          connector.expects(:make_target).with("rspecagent", :request, "mcollective")
          connector.target_for(message)
        end

        it "should support direct requests" do
          message = mock
          message.expects(:type).returns(:direct_request).times(3)
          message.expects(:agent).returns("rspecagent")
          message.expects(:collective).returns("mcollective")

          connector.expects(:make_target).with("rspecagent", :direct_request, "mcollective")
          connector.target_for(message)
        end

        it "should fail for unknown message types" do
          message = mock
          message.stubs(:type).returns(:fail)

          expect {
            connector.target_for(message)
          }.to raise_error("Don't now how to create a target for message type fail")
        end
      end

      describe "#disconnect" do
        it "should disconnect from the stomp connection" do
          connection.expects(:disconnect)
          connector.disconnect
          connector.connection.should == nil
        end
      end

      describe "#headers_for" do
        it "should not set priority header if priority is 0" do
          message = mock
          message.expects(:type).returns(:foo)
          message.stubs(:ttl).returns(30)

          Time.expects(:now).twice.returns(Time.at(1368557431))

          connector.instance_variable_set("@msgpriority", 0)
          connector.headers_for(message).should_not includes("priority")
        end

        it "should return a priority if priority is non 0" do
          message = mock
          message.expects(:type).returns(:foo)
          message.stubs(:ttl).returns(30)

          Time.expects(:now).twice.returns(Time.at(1368557431))

          connector.instance_variable_set("@msgpriority", 1)
          connector.headers_for(message)["priority"].should == 1
        end

        it "should set mc_identity for direct requests" do
          message = mock
          message.expects(:type).returns(:direct_request).twice
          message.expects(:agent).returns("rspecagent")
          message.expects(:collective).returns("mcollective")
          message.expects(:reply_to).returns(nil)
          message.stubs(:ttl).returns(30)

          Time.expects(:now).twice.returns(Time.at(1368557431))

          connector.instance_variable_set("@msgpriority", 0)
          connector.expects(:make_target).with("rspecagent", :reply, "mcollective").returns({:name => "test"})

          headers = connector.headers_for(message, "some.node")
          headers["mc_identity"].should == "some.node"
          headers["reply-to"].should == "test"
        end

        it "should set a reply-to header for :request type messages" do
          message = mock
          message.expects(:type).returns(:request).twice
          message.expects(:agent).returns("rspecagent")
          message.expects(:collective).returns("mcollective")
          message.expects(:reply_to).returns(nil)
          message.stubs(:ttl).returns(30)

          Time.expects(:now).twice.returns(Time.at(1368557431))

          connector.instance_variable_set("@msgpriority", 0)
          connector.expects(:make_target).with("rspecagent", :reply, "mcollective").returns({:name => "test"})
          connector.headers_for(message)["reply-to"].should == "test"
        end

        it "should set reply-to correctly if the message defines it" do
          message = mock
          message.expects(:type).returns(:request).twice
          message.expects(:agent).returns("rspecagent")
          message.expects(:collective).returns("mcollective")
          message.expects(:reply_to).returns("rspec").twice
          message.stubs(:ttl).returns(30)

          Time.expects(:now).twice.returns(Time.at(1368557431))

          connector.headers_for(message)["reply-to"].should == "rspec"
        end

        it "should set the timestamp and ttl based on the message object" do
          message = mock
          message.expects(:type).returns(:foo)
          message.stubs(:ttl).returns(100)

          Time.expects(:now).twice.returns(Time.at(1368557431))

          headers = connector.headers_for(message)
          headers["timestamp"].should == "1368557431000"
          headers["expires"].should == "1368557541000"
        end

        it "should set mc_sender based on the identity" do
          message = mock
          message.expects(:type).returns(:foo)
          message.stubs(:ttl).returns(100)

          headers = connector.headers_for(message)
          headers['mc_sender'].should == 'rspec'
        end
      end

      describe "#make_target" do
        it "should create correct targets if agents_multiplex is false" do
          # realize the connector let so that we can unstub it
          connector
          Activemq.any_instance.unstub(:get_bool_option)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.agents_multiplex", "false").returns(false)
          Client.stubs(:request_sequence).returns(42)

          connector.make_target("test", :reply, "mcollective").should == {
            :name => "/queue/mcollective.reply.rspec_#{$$}.42",
            :headers => {},
            :id => "/queue/mcollective.reply.rspec_#{$$}.42",
          }

          connector.make_target("test", :broadcast, "mcollective").should == {
            :name => "/topic/mcollective.test.agent",
            :headers => {},
            :id => "/topic/mcollective.test.agent",
          }

          connector.make_target("test", :request, "mcollective").should == {
            :name => "/topic/mcollective.test.agent",
            :headers => {},
            :id => "/topic/mcollective.test.agent",
          }

          connector.make_target("test", :direct_request, "mcollective").should == {
            :name => "/queue/mcollective.nodes",
            :headers => {},
            :id => "/queue/mcollective.nodes",
          }

          connector.make_target("test", :directed, "mcollective").should == {
            :name => "/queue/mcollective.nodes",
            :headers => {
              "selector" => "mc_identity = 'rspec'",
            },
            :id => "mcollective_directed_to_identity",
          }
        end

        it "should create correct targets if agents_multiplex is true" do
          # realize the connector let so that we can unstub it
          connector
          Activemq.any_instance.unstub(:get_bool_option)
          Activemq.any_instance.stubs(:get_bool_option).with("activemq.agents_multiplex", "false").returns(true)
          Client.stubs(:request_sequence).returns(42)

          connector.make_target("test", :reply, "mcollective").should == {
            :name => "/queue/mcollective.reply.rspec_#{$$}.42",
            :headers => {},
            :id => "/queue/mcollective.reply.rspec_#{$$}.42",
          }

          connector.make_target("test", :broadcast, "mcollective").should == {
            :name => "/topic/mcollective.agents",
            :headers => {},
            :id => "/topic/mcollective.agents",
          }

          connector.make_target("test", :request, "mcollective").should == {
            :name => "/topic/mcollective.agents",
            :headers => {},
            :id => "/topic/mcollective.agents",
          }

          connector.make_target("test", :direct_request, "mcollective").should == {
            :name => "/queue/mcollective.nodes",
            :headers => {},
            :id => "/queue/mcollective.nodes",
          }

          connector.make_target("test", :directed, "mcollective").should == {
            :name => "/queue/mcollective.nodes",
            :headers => {
              "selector" => "mc_identity = 'rspec'",
            },
            :id => "mcollective_directed_to_identity",
          }
        end

        it "should raise an error for unknown collectives" do
          expect {
            connector.make_target("test", :broadcast, "foo")
          }.to raise_error("Unknown collective 'foo' known collectives are 'mcollective'")
        end

        it "should raise an error for unknown types" do
          expect {
            connector.make_target("test", :test, "mcollective")
          }.to raise_error("Unknown target type test")
        end
      end


      describe "#get_env_or_option" do
        it "should return the environment variable if set" do
          ENV["test"] = "rspec_env_test"

          connector.get_env_or_option("test", nil, nil).should == "rspec_env_test"

          ENV.delete("test")
        end

        it "should return the config option if set" do
          config.expects(:pluginconf).returns({"test" => "rspec_test"}).twice
          connector.get_env_or_option("test", "test", "test").should == "rspec_test"
        end

        it "should return default if nothing else matched" do
          config.expects(:pluginconf).returns({}).once
          connector.get_env_or_option("test", "test", "test").should == "test"
        end

        it "should raise an error if no default is supplied" do
          config.expects(:pluginconf).returns({}).once

          expect {
            connector.get_env_or_option("test", "test")
          }.to raise_error("No test environment or plugin.test configuration option given")
        end
      end

      describe "#get_option" do
        before :each do
          # realize the connector let so that we can unstub it
          connector
          Activemq.any_instance.unstub(:get_option)
        end

        it "should return the config option if set" do
          config.expects(:pluginconf).returns({"test" => "rspec_test"}).twice
          connector.get_option("test").should == "rspec_test"
        end

        it "should return default option was not found" do
          config.expects(:pluginconf).returns({}).once
          connector.get_option("test", "test").should == "test"
        end

        it "should raise an error if no default is supplied" do
          config.expects(:pluginconf).returns({}).once

          expect {
            connector.get_option("test")
          }.to raise_error("No plugin.test configuration option given")
        end
      end

      describe "#get_bool_option" do
        before :each do
          # realize the connector let so that we can unstub it
          connector
          Activemq.any_instance.unstub(:get_bool_option)
        end

        it "should use Util::str_to_bool to translate a boolean value found in the config" do
          config.expects(:pluginconf).returns({"rspec" => "true"})
          Util.expects(:str_to_bool).with("true").returns(true)

          connector.get_bool_option("rspec", "true").should be_true
        end
      end
    end
  end
end
