# encoding: BINARY
# Encoding is set to binary, so that the binary packets aren't validated as UTF-8

$:.unshift(File.dirname(__FILE__))

require 'spec_helper'
require 'mqtt'

describe MQTT::Client do

  before(:each) do
    # Reset environment variable
    ENV.delete('MQTT_SERVER')
  end

  let(:client) { MQTT::Client.new(:host => 'localhost') }
  let(:socket) do
    socket = StringIO.new
    if socket.respond_to?(:set_encoding)
      socket.set_encoding("binary")
    else
      socket
    end
  end

  if Process.const_defined? :CLOCK_MONOTONIC
    def now
      Process.clock_gettime(Process::CLOCK_MONOTONIC)
    end
  else
    # Support older Ruby
    def now
      Time.now.to_f
    end
  end

  describe "initializing a client" do
    it "with no arguments, it should use the defaults" do
      client = MQTT::Client.new
      expect(client.host).to eq(nil)
      expect(client.port).to eq(1883)
      expect(client.version).to eq('3.1.1')
      expect(client.keep_alive).to eq(15)
    end

    it "with a single string argument, it should use it has the host" do
      client = MQTT::Client.new('otherhost.mqtt.org')
      expect(client.host).to eq('otherhost.mqtt.org')
      expect(client.port).to eq(1883)
      expect(client.keep_alive).to eq(15)
    end

    it "with two arguments, it should use it as the host and port" do
      client = MQTT::Client.new('otherhost.mqtt.org', 1000)
      expect(client.host).to eq('otherhost.mqtt.org')
      expect(client.port).to eq(1000)
      expect(client.keep_alive).to eq(15)
    end

    it "with names arguments, it should use those as arguments" do
      client = MQTT::Client.new(:host => 'otherhost.mqtt.org', :port => 1000)
      expect(client.host).to eq('otherhost.mqtt.org')
      expect(client.port).to eq(1000)
      expect(client.keep_alive).to eq(15)
    end

    it "with a hash, it should use those as arguments" do
      client = MQTT::Client.new({:host => 'otherhost.mqtt.org', :port => 1000})
      expect(client.host).to eq('otherhost.mqtt.org')
      expect(client.port).to eq(1000)
      expect(client.keep_alive).to eq(15)
    end

    it "with a hash containing just a keep alive setting" do
      client = MQTT::Client.new(:host => 'localhost', :keep_alive => 60)
      expect(client.host).to eq('localhost')
      expect(client.port).to eq(1883)
      expect(client.keep_alive).to eq(60)
    end

    it "with a combination of a host name and a hash of settings" do
      client = MQTT::Client.new('localhost', :keep_alive => 65)
      expect(client.host).to eq('localhost')
      expect(client.port).to eq(1883)
      expect(client.keep_alive).to eq(65)
    end

    it "with a combination of a host name, port and a hash of settings" do
      client = MQTT::Client.new('localhost', 1888, :keep_alive => 65)
      expect(client.host).to eq('localhost')
      expect(client.port).to eq(1888)
      expect(client.keep_alive).to eq(65)
    end

    it "with a mqtt:// URI containing just a hostname" do
      client = MQTT::Client.new(URI.parse('mqtt://mqtt.example.com'))
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1883)
      expect(client.ssl).to be_falsey
    end

    it "with a mqtts:// URI containing just a hostname" do
      client = MQTT::Client.new(URI.parse('mqtts://mqtt.example.com'))
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(8883)
      expect(client.ssl).to be_truthy
    end

    it "with a mqtt:// URI containing a custom port number" do
      client = MQTT::Client.new(URI.parse('mqtt://mqtt.example.com:1234/'))
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1234)
      expect(client.ssl).to be_falsey
    end

    it "with a mqtts:// URI containing a custom port number" do
      client = MQTT::Client.new(URI.parse('mqtts://mqtt.example.com:1234/'))
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1234)
      expect(client.ssl).to be_truthy
    end

    it "with a URI containing a username and password" do
      client = MQTT::Client.new(URI.parse('mqtt://auser:bpass@mqtt.example.com'))
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1883)
      expect(client.username).to eq('auser')
      expect(client.password).to eq('bpass')
    end

    it "with a URI containing an escaped username and password" do
      client = MQTT::Client.new(URI.parse('mqtt://foo%20bar:%40123%2B%25@mqtt.example.com'))
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1883)
      expect(client.username).to eq('foo bar')
      expect(client.password).to eq('@123+%')
    end

    it "with a URI containing a double escaped username and password" do
      client = MQTT::Client.new(URI.parse('mqtt://foo%2520bar:123%2525@mqtt.example.com'))
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1883)
      expect(client.username).to eq('foo%20bar')
      expect(client.password).to eq('123%25')
    end

    it "with a URI as a string" do
      client = MQTT::Client.new('mqtt://mqtt.example.com')
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1883)
    end

    it "with a URI as a string including port" do
      client = MQTT::Client.new('mqtt://user:pass@m10.cloudmqtt.com:13858', nil)
      expect(client.host).to eq('m10.cloudmqtt.com')
      expect(client.port).to eq(13858)
    end

    it "with a URI and a hash of settings" do
      client = MQTT::Client.new('mqtt://mqtt.example.com', :keep_alive => 65)
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1883)
      expect(client.keep_alive).to eq(65)
    end

    it "with no arguments uses the MQTT_SERVER environment variable as connect URI" do
      ENV['MQTT_SERVER'] = 'mqtt://mqtt.example.com:1234'
      client = MQTT::Client.new
      expect(client.host).to eq('mqtt.example.com')
      expect(client.port).to eq(1234)
    end

    it "with an unsupported URI scheme" do
      expect {
        client = MQTT::Client.new(URI.parse('http://mqtt.example.com/'))
      }.to raise_error(
        'Only the mqtt:// and mqtts:// schemes are supported'
      )
    end

    it "with three arguments" do
      expect {
        client = MQTT::Client.new(1, 2, 3)
      }.to raise_error(
        'Unsupported number of arguments'
      )
    end
  end

  describe "setting a client certificate file path" do
    it "should add a certificate to the SSL context" do
      expect(client.ssl_context.cert).to be_nil
      client.cert_file = fixture_path('client.pem')
      expect(client.ssl_context.cert).to be_a(OpenSSL::X509::Certificate)
    end
  end

  describe "setting a client certificate directly" do
    it "should add a certificate to the SSL context" do
      expect(client.ssl_context.cert).to be_nil
      client.cert = File.read(fixture_path('client.pem'))
      expect(client.ssl_context.cert).to be_a(OpenSSL::X509::Certificate)
    end
  end

  describe "setting a client private key file path" do
    it "should add a certificate to the SSL context" do
      expect(client.ssl_context.key).to be_nil
      client.key_file = fixture_path('client.key')
      expect(client.ssl_context.key).to be_a(OpenSSL::PKey::RSA)
    end
  end

  describe "setting a client private key directly" do
    it "should add a certificate to the SSL context" do
      expect(client.ssl_context.key).to be_nil
      client.key = File.read(fixture_path('client.key'))
      expect(client.ssl_context.key).to be_a(OpenSSL::PKey::RSA)
    end
  end

  describe "setting an encrypted client private key, w/the correct passphrase" do
    let(:key_pass) { 'mqtt' }

    it "should add the decrypted certificate to the SSL context" do
      expect(client.ssl_context.key).to be_nil
      client.key_file = [fixture_path('client.pass.key'), key_pass]
      expect(client.ssl_context.key).to be_a(OpenSSL::PKey::RSA)
    end
  end

  describe "setting an encrypted client private key, w/an incorrect passphrase" do
    let(:key_pass) { 'ttqm' }

    it "should raise an OpenSSL::PKey::RSAError exception" do
      expect(client.ssl_context.key).to be_nil
      expect { client.key_file = [fixture_path('client.pass.key'), key_pass] }.to(
        raise_error(OpenSSL::PKey::RSAError, /Neither PUB key nor PRIV key/))
    end
  end

  describe "setting a Certificate Authority file path" do
    it "should add a CA file path to the SSL context" do
      expect(client.ssl_context.ca_file).to be_nil
      client.ca_file = fixture_path('root-ca.pem')
      expect(client.ssl_context.ca_file).to eq(fixture_path('root-ca.pem'))
    end

    it "should enable peer verification" do
      client.ca_file = fixture_path('root-ca.pem')
      expect(client.ssl_context.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
    end
  end

  describe "deprecated attributes" do
    it "should allow getting and setting the host name using the remote_host method" do
      client.remote_host = 'remote-host.example.com'
      expect(client.host).to eq('remote-host.example.com')
      expect(client.remote_host).to eq('remote-host.example.com')
      client.host = 'foo.example.org'
      expect(client.host).to eq('foo.example.org')
      expect(client.remote_host).to eq('foo.example.org')
    end

    it "should allow getting and setting the port using the remote_port method" do
      client.remote_port = 9999
      expect(client.port).to eq(9999)
      expect(client.remote_port).to eq(9999)
      client.port = 1234
      expect(client.port).to eq(1234)
      expect(client.remote_port).to eq(1234)
    end
  end

  describe "when calling the 'connect' method on a client" do
    before(:each) do
      allow(TCPSocket).to receive(:new).and_return(socket)
      allow(Thread).to receive(:new)
      allow(client).to receive(:receive_connack)
    end

    it "should create a TCP Socket if not connected" do
      expect(TCPSocket).to receive(:new).once.and_return(socket)
      client.connect('myclient')
    end

    it "should not create a new TCP Socket if connected" do
      allow(client).to receive(:connected?).and_return(true)
      expect(TCPSocket).to receive(:new).never
      client.connect('myclient')
    end

    it "should start the reader thread if not connected" do
      expect(Thread).to receive(:new).once
      client.connect('myclient')
    end

    context "protocol version 3.1.0" do
      it "should write a valid CONNECT packet to the socket if not connected" do
        client.version = '3.1.0'
        client.connect('myclient')
        expect(socket.string).to eq("\020\026\x00\x06MQIsdp\x03\x02\x00\x0f\x00\x08myclient")
      end
    end

    context "protocol version 3.1.1" do
      it "should write a valid CONNECT packet to the socket if not connected" do
        client.version = '3.1.1'
        client.connect('myclient')
        expect(socket.string).to eq("\020\024\x00\x04MQTT\x04\x02\x00\x0f\x00\x08myclient")
      end
    end

    it "should try and read an acknowledgement packet to the socket if not connected" do
      expect(client).to receive(:receive_connack).once
      client.connect('myclient')
    end

    it "should raise an exception if no host is configured" do
      expect {
        client = MQTT::Client.new
        client.connect
      }.to raise_error(
        'No MQTT server host set when attempting to connect'
      )
    end

    context "if a block is given" do
      it "should disconnect after connecting" do
        expect(client).to receive(:disconnect).once
        client.connect('myclient') { nil }
      end

      it "should disconnect even if the block raises an exception" do
        expect(client).to receive(:disconnect).once
        begin
          client.connect('myclient') { raise StandardError }
        rescue StandardError
        end
      end
    end

    it "should not disconnect after connecting, if no block is given" do
      expect(client).to receive(:disconnect).never
      client.connect('myclient')
    end

    it "should include the username and password for an authenticated connection" do
      client.username = 'username'
      client.password = 'password'
      client.connect('myclient')
      expect(socket.string).to eq(
        "\x10\x28"+
        "\x00\x04MQTT"+
        "\x04\xC2\x00\x0f"+
        "\x00\x08myclient"+
        "\x00\x08username"+
        "\x00\x08password"
      )
    end

    context "no client id is given" do
      it "should raise an exception if the clean session flag is false" do
        expect {
          client.client_id = nil
          client.clean_session = false
          client.connect
        }.to raise_error(
          'Must provide a client_id if clean_session is set to false'
        )
      end

      context "protocol version 3.1.0" do
        it "should generate a client if the clean session flag is true" do
          client.version = '3.1.0'
          client.client_id = nil
          client.clean_session = true
          client.connect
          expect(client.client_id).to match(/^\w+$/)
        end
      end

      context "protocol version 3.1.1" do
        it "should send empty client if the clean session flag is true" do
          client.version = '3.1.1'
          client.client_id = nil
          client.clean_session = true
          client.connect
          expect(client.client_id).to be_nil
          expect(socket.string).to eq("\020\014\x00\x04MQTT\x04\x02\x00\x0f\x00\x00")
        end
      end
    end

    context "and using ssl" do
      let(:ssl_socket) {
        double(
          "SSLSocket",
          :sync_close= => true,
          :write => true,
          :connect => true,
          :closed? => false
        )
      }

      it "should use ssl if it enabled using the :ssl => true parameter" do
        expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
        expect(ssl_socket).to receive(:connect)

        client = MQTT::Client.new('mqtt.example.com', :ssl => true)
        allow(client).to receive(:receive_connack)
        client.connect
      end

      it "should use ssl if it enabled using the mqtts:// scheme" do
        expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
        expect(ssl_socket).to receive(:connect)

        client = MQTT::Client.new('mqtts://mqtt.example.com')
        allow(client).to receive(:receive_connack)
        client.connect
      end

      it "should use set the SSL version, if the :ssl parameter is a symbol" do
        expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
        expect(ssl_socket).to receive(:connect)

        client = MQTT::Client.new('mqtt.example.com', :ssl => :TLSv1)
        expect(client.ssl_context).to receive('ssl_version=').with(:TLSv1)
        allow(client).to receive(:receive_connack)
        client.connect
      end

      it "should use set hostname on the SSL socket for SNI" do
        expect(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(ssl_socket)
        expect(ssl_socket).to receive(:hostname=).with('mqtt.example.com')

        client = MQTT::Client.new('mqtts://mqtt.example.com')
        allow(client).to receive(:receive_connack)
        client.connect
      end
    end

    context "with a last will and testament set" do
      before(:each) do
        client.set_will('topic', 'hello', retain=false, qos=1)
      end

      it "should have set the Will's topic" do
        expect(client.will_topic).to eq('topic')
      end

      it "should have set the Will's payload" do
        expect(client.will_payload).to eq('hello')
      end

      it "should have set the Will's retain flag to true" do
        expect(client.will_retain).to be_falsey
      end

      it "should have set the Will's retain QoS value to 1" do
        expect(client.will_qos).to eq(1)
      end

      it "should include the will in the CONNECT message" do
        client.connect('myclient')
        expect(socket.string).to eq(
          "\x10\x22"+
          "\x00\x04MQTT"+
          "\x04\x0e\x00\x0f"+
          "\x00\x08myclient"+
          "\x00\x05topic\x00\x05hello"
        )
      end
    end

  end

  describe "calling 'connect' on the class" do
    it "should create a new client object" do
      client = double("MQTT::Client")
      allow(client).to receive(:connect)
      expect(MQTT::Client).to receive(:new).once.and_return(client)
      MQTT::Client.connect
    end

    it "should call connect new client object" do
      client = double("MQTT::Client")
      expect(client).to receive(:connect)
      allow(MQTT::Client).to receive(:new).once.and_return(client)
      MQTT::Client.connect
    end

    it "should return the new client object" do
      client = double("MQTT::Client")
      allow(client).to receive(:connect)
      allow(MQTT::Client).to receive(:new).once.and_return(client)
      expect(MQTT::Client.connect).to eq(client)
    end
  end

  describe "when calling the 'receive_connack' method" do
    before(:each) do
      client.instance_variable_set('@socket', socket)
      allow(IO).to receive(:select).and_return([[socket], [], []])
    end

    it "should not raise an exception for a successful CONNACK packet" do
      socket.write("\x20\x02\x00\x00")
      socket.rewind
      expect { client.send(:receive_connack) }.not_to raise_error
      expect(socket).not_to be_closed
    end

    it "should raise an exception if the packet type isn't CONNACK" do
      socket.write("\xD0\x00")
      socket.rewind
      expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException)
    end

    it "should raise an exception if the CONNACK packet return code is 'unacceptable protocol version'" do
      socket.write("\x20\x02\x00\x01")
      socket.rewind
      expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /unacceptable protocol version/i)
    end

    it "should raise an exception if the CONNACK packet return code is 'client identifier rejected'" do
      socket.write("\x20\x02\x00\x02")
      socket.rewind
      expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /client identifier rejected/i)
    end

    it "should raise an exception if the CONNACK packet return code is 'server unavailable'" do
      socket.write("\x20\x02\x00\x03")
      socket.rewind
      expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /server unavailable/i)
    end

    it "should raise an exception if the CONNACK packet return code is an unknown" do
      socket.write("\x20\x02\x00\xAA")
      socket.rewind
      expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /connection refused/i)
    end

    it "should close the socket for an unsuccessful CONNACK packet" do
      socket.write("\x20\x02\x00\x05")
      socket.rewind
      expect { client.send(:receive_connack) }.to raise_error(MQTT::ProtocolException, /not authorised/i)
      expect(socket).to be_closed
    end
  end

  describe "when calling the 'disconnect' method" do
    before(:each) do
      thread = double('Read Thread', :alive? => true, :kill => true)
      client.instance_variable_set('@socket', socket)
      client.instance_variable_set('@read_thread', thread)
    end

    it "should not do anything if the socket is already disconnected" do
      allow(client).to receive(:connected?).and_return(false)
      client.disconnect(true)
      expect(socket.string).to eq("")
    end

    it "should write a valid DISCONNECT packet to the socket if connected and the send_msg=true an" do
      allow(client).to receive(:connected?).and_return(true)
      client.disconnect(true)
      expect(socket.string).to eq("\xE0\x00")
    end

    it "should not write anything to the socket if the send_msg=false" do
      allow(client).to receive(:connected?).and_return(true)
      client.disconnect(false)
      expect(socket.string).to be_empty
    end

    it "should call the close method on the socket" do
      expect(socket).to receive(:close)
      client.disconnect
    end
  end

  describe "when calling the 'publish' method" do
    class ClientWithPubackInjection < MQTT::Client
      def initialize
        super(:host => 'localhost')
        @injected_pubacks = {}
      end

      def inject_puback(packet)
        @injected_pubacks[packet.id] = packet
      end

      def wait_for_puback(id, queue)
        packet = @injected_pubacks.fetch(id) {
          return super
        }
        queue << packet
      end
    end

    let(:client) { ClientWithPubackInjection.new }

    before(:each) do
      client.instance_variable_set('@socket', socket)
    end

    it "should respect timeouts" do
      require "socket"
      rd, wr = UNIXSocket.pair
      client = MQTT::Client.new(:host => 'localhost', :ack_timeout => 1.0)
      client.instance_variable_set('@socket', rd)
      t = Thread.new {
        Thread.current[:parent] = Thread.main
        loop do
          client.send :receive_packet
        end
      }
      start = now
      expect(client.publish('topic','payload', false, 1)).to eq(-1)
      elapsed = now - start
      t.kill
      expect(elapsed).to be_within(0.1).of(1.0)
    end

    it "should write a valid PUBLISH packet to the socket without the retain flag" do
      client.publish('topic','payload', false, 0)
      expect(socket.string).to eq("\x30\x0e\x00\x05topicpayload")
    end

    it "should write a valid PUBLISH packet to the socket with the retain flag set" do
      client.publish('topic','payload', true, 0)
      expect(socket.string).to eq("\x31\x0e\x00\x05topicpayload")
    end

    it "should write a valid PUBLISH packet to the socket with the QoS set to 1" do
      inject_puback(1)
      client.publish('topic','payload', false, 1)
      expect(socket.string).to eq("\x32\x10\x00\x05topic\x00\x01payload")
    end

    it "should wrap the packet id after 65535" do
      0xffff.times do |n|
        inject_puback(n + 1)
        client.publish('topic','payload', false, 1)
      end
      expect(client.instance_variable_get(:@last_packet_id)).to eq(0xffff)

      socket.string = ""
      inject_puback(1)
      client.publish('topic','payload', false, 1)
      expect(socket.string).to eq("\x32\x10\x00\x05topic\x00\x01payload")
    end

    it "should write a valid PUBLISH packet to the socket with the QoS set to 2" do
      inject_puback(1)
      client.publish('topic','payload', false, 2)
      expect(socket.string).to eq("\x34\x10\x00\x05topic\x00\x01payload")
    end

    it "should write a valid PUBLISH packet with no payload" do
      client.publish('test')
      expect(socket.string).to eq("\x30\x06\x00\x04test")
    end

    it "should write a valid PUBLISH packet with frozen payload" do
      client.publish('topic', 'payload'.freeze, false, 0)
      expect(socket.string).to eq("\x30\x0e\x00\x05topicpayload")
    end

    it "should raise an ArgumentError exception, if the topic is nil" do
      expect {
        client.publish(nil)
      }.to raise_error(
        ArgumentError,
        'Topic name cannot be nil'
      )
    end

    it "should raise an ArgumentError exception, if the topic is empty" do
      expect {
        client.publish("")
      }.to raise_error(
        ArgumentError,
        'Topic name cannot be empty'
      )
    end

    it "correctly assigns consecutive ids to packets with QoS 1" do
      inject_puback(1)
      inject_puback(2)

      expect(client).to receive(:send_packet) { |packet| expect(packet.id).to eq(1) }
      client.publish "topic", "message", false, 1
      expect(client).to receive(:send_packet) { |packet| expect(packet.id).to eq(2) }
      client.publish "topic", "message", false, 1
    end
  end

  describe "when calling the 'subscribe' method" do
    before(:each) do
      client.instance_variable_set('@socket', socket)
    end

    it "should write a valid SUBSCRIBE packet to the socket if given a single topic String" do
      client.subscribe('a/b')
      expect(socket.string).to eq("\x82\x08\x00\x01\x00\x03a/b\x00")
    end

    it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings in an Array" do
      client.subscribe('a/b','c/d')
      expect(socket.string).to eq("\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x00")
    end

    it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings with QoS in an Array" do
      client.subscribe(['a/b',0],['c/d',1])
      expect(socket.string).to eq("\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x01")
    end

    it "should write a valid SUBSCRIBE packet to the socket if given a two topic Strings with QoS in a Hash" do
      client.subscribe('a/b' => 0,'c/d' => 1)
      expect(socket.string).to eq("\x82\x0e\x00\x01\x00\x03a/b\x00\x00\x03c/d\x01")
    end
  end

  describe "when calling the 'queue_length' method" do
    it "should return 0 if there are no incoming messages waiting" do
      expect(client.queue_length).to eq(0)
    end

    it "should return 1 if there is one incoming message waiting" do
      inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
      expect(client.queue_length).to eq(1)
    end

    it "should return 2 if there are two incoming message waiting" do
      inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
      inject_packet(:topic => 'topic0', :payload => 'payload1', :qos => 0)
      expect(client.queue_length).to eq(2)
    end
  end

  describe "when calling the 'queue_emtpy?' method" do
    it "should return return true if there no incoming messages waiting" do
      expect(client.queue_empty?).to be_truthy
    end

    it "should return return false if there is an incoming messages waiting" do
      inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
      expect(client.queue_empty?).to be_falsey
    end
  end

  describe "when calling the 'clear_queue' method" do
    it "should clear the waiting incoming messages" do
      inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
      expect(client.queue_length).to eq(1)
      client.clear_queue
      expect(client.queue_length).to eq(0)
    end
  end

  describe "when calling the 'get' method" do
    before(:each) do
      client.instance_variable_set('@socket', socket)
    end

    it "should successfully receive a valid PUBLISH packet with a QoS 0" do
      inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
      topic,payload = client.get
      expect(topic).to eq('topic0')
      expect(payload).to eq('payload0')
    end

    it "should successfully receive a valid PUBLISH packet with a QoS 1" do
      inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
      topic,payload = client.get
      expect(topic).to eq('topic1')
      expect(payload).to eq('payload1')
      expect(client.queue_empty?).to be_truthy
    end

    it "should successfully receive a valid PUBLISH packet, but not return it, if omit_retained is set" do
      inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1, :retain => 1)
      inject_packet(:topic => 'topic1', :payload => 'payload2', :qos => 1)
      topic,payload = client.get(nil, :omit_retained => true)
      expect(topic).to eq('topic1')
      expect(payload).to eq('payload2')
      expect(client.queue_empty?).to be_truthy
    end

    it "acks calling #get_packet and qos=1" do
      inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
      expect(client).to receive(:send_packet).with(an_instance_of(MQTT::Packet::Puback))
      client.get_packet
    end

    it "acks calling #get and qos=1" do
      inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
      expect(client).to receive(:send_packet).with(an_instance_of(MQTT::Packet::Puback))
      client.get
    end

    context "with a block" do
      it "should successfully receive more than 1 message" do
        inject_packet(:topic => 'topic0', :payload => 'payload0')
        inject_packet(:topic => 'topic1', :payload => 'payload1')
        payloads = []
        client.get do |topic,payload|
          payloads << payload
          break if payloads.size > 1
        end
        expect(payloads.size).to eq(2)
        expect(payloads).to eq(['payload0', 'payload1'])
      end

      it "acks when qos > 1 after running the block" do
        inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
        inject_packet(:topic => 'topic2', :payload => 'payload1')
        expect(client).to receive(:send_packet).with(an_instance_of(MQTT::Packet::Puback))
        payloads = []
        client.get do |topic,payload|
          payloads << payload
          break if payloads.size > 1
        end
      end

      it "should ignore a PUBLISH message when it is marked as retained and omit_retained is set" do
        inject_packet(:topic => 'topic0', :payload => 'payload0', :retain => 1)
        inject_packet(:topic => 'topic1', :payload => 'payload1')
        payloads = []
        client.get(nil, :omit_retained => true) do |topic,payload|
          payloads << payload
          break if payloads.size > 0
        end
        expect(payloads.size).to eq(1)
        expect(payloads).to eq(['payload1'])
      end
    end
  end

  describe "when calling the 'get_packet' method" do
    before(:each) do
      client.instance_variable_set('@socket', socket)
    end

    it "should successfully receive a valid PUBLISH packet with a QoS 0" do
      inject_packet(:topic => 'topic0', :payload => 'payload0', :qos => 0)
      packet = client.get_packet
      expect(packet.class).to eq(MQTT::Packet::Publish)
      expect(packet.qos).to eq(0)
      expect(packet.topic).to eq('topic0')
      expect(packet.payload).to eq('payload0')
    end

    it "should successfully receive a valid PUBLISH packet with a QoS 1" do
      inject_packet(:topic => 'topic1', :payload => 'payload1', :qos => 1)
      packet = client.get_packet
      expect(packet.class).to eq(MQTT::Packet::Publish)
      expect(packet.qos).to eq(1)
      expect(packet.topic).to eq('topic1')
      expect(packet.payload).to eq('payload1')
      expect(client.queue_empty?).to be_truthy
    end

    context "with a block" do
      it "should successfully receive more than 1 packet" do
        inject_packet(:topic => 'topic0', :payload => 'payload0')
        inject_packet(:topic => 'topic1', :payload => 'payload1')
        packets = []
        client.get_packet do |packet|
          packets << packet
          break if packets.size > 1
        end
        expect(packets.size).to eq(2)
        expect(packets.map{|p| p.payload}).to eq(['payload0', 'payload1'])
      end
    end
  end

  describe "when calling the 'unsubscribe' method" do
    before(:each) do
      client.instance_variable_set('@socket', socket)
    end

    it "should write a valid UNSUBSCRIBE packet to the socket if given a single topic String" do
      client.unsubscribe('a/b')
      expect(socket.string).to eq("\xa2\x07\x00\x01\x00\x03a/b")
    end

    it "should write a valid UNSUBSCRIBE packet to the socket if given a two topic Strings" do
      client.unsubscribe('a/b','c/d')
      expect(socket.string).to eq("\xa2\x0c\x00\x01\x00\x03a/b\x00\x03c/d")
    end

    it "should write a valid UNSUBSCRIBE packet to the socket if given an array of Strings" do
      client.unsubscribe(['a/b','c/d'])
      expect(socket.string).to eq("\xa2\x0c\x00\x01\x00\x03a/b\x00\x03c/d")
    end
  end

  describe "when calling the 'receive_packet' method" do
    before(:each) do
      client.instance_variable_set('@socket', socket)
      allow(IO).to receive(:select).and_return([[socket], [], []])
      @read_queue = client.instance_variable_get('@read_queue')
      @parent_thread = Thread.current[:parent] = double('Parent Thread')
      allow(@parent_thread).to receive(:raise)
    end

    it "should put PUBLISH messages on to the read queue" do
      socket.write("\x30\x0e\x00\x05topicpayload")
      socket.rewind
      client.send(:receive_packet)
      expect(@read_queue.size).to eq(1)
    end

    it "should not put other messages on to the read queue" do
      socket.write("\x20\x02\x00\x00")
      socket.rewind
      client.send(:receive_packet)
      expect(@read_queue.size).to eq(0)
    end

    it "should close the socket if there is an exception" do
      expect(socket).to receive(:close).once
      allow(MQTT::Packet).to receive(:read).and_raise(MQTT::Exception)
      client.send(:receive_packet)
    end

    it "should pass exceptions up to parent thread" do
      expect(@parent_thread).to receive(:raise).once
      allow(MQTT::Packet).to receive(:read).and_raise(MQTT::Exception)
      client.send(:receive_packet)
    end

    it "should update last_ping_response when receiving a Pingresp" do
      allow(MQTT::Packet).to receive(:read).and_return MQTT::Packet::Pingresp.new
      client.instance_variable_set '@last_ping_response', Time.at(0)
      client.send :receive_packet
      expect(client.last_ping_response).to be_within(1).of now
    end
  end

  describe "when calling the 'keep_alive!' method" do
    before(:each) do
      client.instance_variable_set('@socket', socket)
    end

    it "should send a ping packet if one is due" do
      client.instance_variable_set('@last_ping_request', 0.0)
      client.send('keep_alive!')
      expect(socket.string).to eq("\xC0\x00")
    end

    it "should update the time a ping was last sent" do
      client.instance_variable_set('@last_ping_request', 0.0)
      client.send('keep_alive!')
      expect(client.instance_variable_get('@last_ping_request')).to be_within(0.01).of(now)
    end

    it "should raise an exception if no ping response has been received" do
      client.instance_variable_set('@last_ping_request', now)
      client.instance_variable_set('@last_ping_response', 0.0)
      expect {
        client.send('keep_alive!')
      }.to raise_error(
        MQTT::ProtocolException,
        /No Ping Response received for \d+ seconds/
      )
    end

    it "should not raise an exception if no ping response received and client is disconnected" do
      client.instance_variable_set('@last_ping_request', Time.now)
      client.instance_variable_set('@last_ping_response', Time.at(0))
      client.disconnect(false)
      client.send('keep_alive!')
    end
  end

  describe "generating a client identifier" do
    context "with default parameters" do
      let(:client_id) { MQTT::Client.generate_client_id }

      it "should be less or equal to 23 characters long" do
        expect(client_id.length).to be <= 23
      end

      it "should have a prefix of ruby" do
        expect(client_id).to match(/^ruby/)
      end

      it "should end in 16 characters of lowercase letters and numbers" do
        expect(client_id).to match(/^ruby[a-z0-9]{16}$/)
      end
    end

    context "with an alternative prefix" do
      let(:client_id) { MQTT::Client.generate_client_id('test') }

      it "should be less or equal to 23 characters long" do
        expect(client_id.length).to be <= 23
      end

      it "should have a prefix of test" do
        expect(client_id).to match(/^test/)
      end

      it "should end in 16 characters of lowercase letters and numbers" do
        expect(client_id).to match(/^test[a-z0-9]{16}$/)
      end
    end
  end

  private

  def inject_packet(opts={})
    packet = MQTT::Packet::Publish.new(opts)
    client.instance_variable_get('@read_queue').push(packet)
  end

  def inject_puback(packet_id)
    packet = MQTT::Packet::Puback.new(:id => packet_id)
    client.inject_puback packet
  end

end
