require "spec_helper"

describe Moped::Protocol::Message do

  let(:message_class) do
    Class.new do
      include Moped::Protocol::Message
    end
  end

  describe ".fields" do
    it "returns an array of defined fields" do
      message_class.cstring :full_collection_name
      message_class.int64 :cursor_id

      message_class.fields.should include(:full_collection_name, :cursor_id)
    end

    it "does not leak fields between messages" do
      message_class.int64 :cursor_id
      Class.new do
        include Moped::Protocol::Message
      end.fields.should_not include :cursor_id
    end
  end

  describe ".cstring" do
    let(:instance_methods) { message_class.instance_methods(false) }

    before do
      message_class.cstring :full_collection_name
    end

    it "defines an accessor method" do
      instance_methods.should include :full_collection_name=
    end

    it "defines a reader method" do
      instance_methods.should include :full_collection_name
    end

    it "defines a serialize method" do
      instance_methods.should include :serialize_full_collection_name
    end

    describe "#serialize" do
      let(:message) do
        message_class.new.tap do |message|
          message.full_collection_name = "moped.$cmd"
        end
      end

      it "appends the field value and a null byte to the buffer" do
        buffer = ""
        message.serialize_full_collection_name(buffer)
        buffer.should eq "moped.$cmd\u0000"
      end
    end
  end

  describe ".document" do
    let(:instance_methods) { message_class.instance_methods(false) }

    let(:message_class) do
      Class.new do
        include Moped::Protocol::Message
        document :selector
      end
    end

    it "defines an accessor method" do
      instance_methods.should include :selector=
    end

    it "defines a reader method" do
      instance_methods.should include :selector
    end

    it "defines a serialize method" do
      instance_methods.should include :serialize_selector
    end

    describe "#serialize" do
      let(:message) { message_class.new }
      let(:buffer) { "" }

      it "appends the serialized document to the buffer" do
        message.selector = { a: 1 }
        expect(message.serialize_selector(buffer)).to_not be_nil
      end

      context "when optional and not present" do
        let(:message_class) do
          Class.new do
            include Moped::Protocol::Message
            document :selector, optional: true
          end
        end

        it "appends nothing" do
          message.serialize_selector(buffer)
          buffer.should eq ""
        end
      end

      context "when type is array" do
        let(:message_class) do
          Class.new do
            include Moped::Protocol::Message
            document :documents, type: :array
          end
        end

        it "appends each document" do
          message.documents = [{ a: 1 }, { b: 2 }]
          expect(message.serialize_documents(buffer)).to_not be_nil
        end
      end
    end
  end

  describe ".flags" do
    let(:instance_methods) { message_class.instance_methods(false) }

    before do
      message_class.flags :flags, upsert: 2 ** 0,
                                  multi:  2 ** 1
    end

    it "defines an accessor method" do
      instance_methods.should include :flags=
    end

    it "defines a reader method" do
      instance_methods.should include :flags
    end

    it "defines a serialize method" do
      instance_methods.should include :serialize_flags
    end

    it "defaults to an empty array" do
      message_class.new.flags.should eq []
    end

    describe "accessor" do
      let(:message) { message_class.new }

      it "accepts an array of flags" do
        message.flags = [:upsert]
        message.flags.should eq [:upsert]
      end

      it "accepts an integer of bits" do
        message.flags = 1
        message.flags.should eq [:upsert]
      end
    end

    describe "#serialize" do
      let(:message) do
        message_class.new.tap do |message|
          message.flags = [:upsert]
        end
      end

      it "appends the value to the buffer" do
        buffer = ""
        message.serialize_flags(buffer)
        buffer.should eq [1].pack('l<')
      end
    end

    describe "#deserialize" do
      let(:message) { message_class.new }

      it "extracts the flags" do
        buffer = StringIO.new [1].pack('l<')
        message.deserialize_flags buffer
        message.flags.should eq [:upsert]
      end
    end
  end

  describe ".int32" do
    let(:instance_methods) { message_class.instance_methods(false) }

    before do
      message_class.int32 :request_id
    end

    it "defines an accessor method" do
      instance_methods.should include :request_id=
    end

    it "defines a reader method" do
      instance_methods.should include :request_id
    end

    it "defines a serialize method" do
      instance_methods.should include :serialize_request_id
    end

    it "defines a deserialize method" do
      instance_methods.should include :deserialize_request_id
    end

    describe "#accessor" do
      it "defaults to 0" do
        message_class.new.request_id.should eq 0
      end
    end

    describe "#serialize" do
      let(:message) do
        message_class.new.tap do |message|
          message.request_id = 32
        end
      end

      it "appends the value to the buffer" do
        buffer = ""
        message.serialize_request_id(buffer)
        buffer.should eq [32].pack('l<')
      end
    end

    describe "#deserialize" do
      let(:message) { message_class.new }

      it "sets the value from the buffer" do
        buffer = StringIO.new [32].pack('l<')
        message.deserialize_request_id(buffer)
        message.request_id.should eq 32
      end
    end
  end

  describe ".int64" do
    let(:instance_methods) { message_class.instance_methods(false) }

    before do
      message_class.int64 :cursor_id
    end

    it "defines an accessor method" do
      instance_methods.should include :cursor_id=
    end

    it "defines a reader method" do
      instance_methods.should include :cursor_id
    end

    it "defines a serialize method" do
      instance_methods.should include :serialize_cursor_id
    end

    it "defines a deserialize method" do
      instance_methods.should include :deserialize_cursor_id
    end

    describe "#accessor" do
      it "defaults to 0" do
        message_class.new.cursor_id.should eq 0
      end
    end

    describe "#serialize" do
      context "when no type is provided" do
        let(:message) do
          message_class.new.tap do |message|
            message.cursor_id = 10
          end
        end

        it "appends the field value and a null byte to the buffer" do
          buffer = ""
          message.serialize_cursor_id(buffer)
          buffer.should eq [message.cursor_id].pack('q<')
        end
      end

      context "when type is array" do
        let(:message_class) do
          Class.new do
            include Moped::Protocol::Message
            int64 :cursor_ids, type: :array
          end
        end

        let(:message) do
          message_class.new
        end

        context "and value is blank" do
          it "does not modify the buffer" do
            buffer = ""
            message.serialize_cursor_ids(buffer)
            buffer.should eq ""
          end
        end

        context "and value is set" do
          it "appends each value to the buffer" do
            buffer = ""
            message.cursor_ids = [1, 2, 3]
            message.serialize_cursor_ids(buffer)
            buffer.should eq [1, 2, 3].pack("q3<")
          end
        end

      end
    end

    describe "#deserialize" do
      let(:message) { message_class.new }

      context "when no type is provided" do
        it "sets the value from the buffer" do
          buffer = StringIO.new [32].pack('q<')
          message.deserialize_cursor_id(buffer)
          message.cursor_id.should eq 32
        end
      end

      context "when type is array" do
        let(:message_class) do
          Class.new do
            include Moped::Protocol::Message

            int64 :cursor_ids, type: :array
          end
        end

        it "raises NotImplementedError" do
          buffer = StringIO.new [1, 2, 3].pack('q*<')
          lambda { message.deserialize_cursor_ids(buffer) }.should raise_error NotImplementedError
        end
      end
    end
  end

  describe "#inspect" do
    let(:message_class) do
      Class.new do
        include Moped::Protocol::Message
        int32    :length
        flags    :flags, remove_single: 2 ** 0
        finalize
      end
    end

    let(:message) { message_class.new }

    it "does not attempt to serialize the message" do
      message.inspect.should_not eq message.to_s
    end
  end

  describe "#serialize" do
    let(:message_class) do
      Class.new do
        include Moped::Protocol::Message
        int32    :length
        int64    :cursor_id
        cstring  :collection
        flags    :flags, remove_single: 2 ** 0
        document :selector
        finalize
      end
    end

    let(:message) { message_class.new }
    let(:buffer) { "" }

    it "is aliased to #to_s" do
      message.collection = "db.$cmd"
      message.selector = {}
      message.serialize.should eq message.to_s
    end

    it "serializes all fields and returns the buffer" do
      message_class.fields.each do |field|
        message.should_receive(:"serialize_#{field}")
          .at_least(1).with(buffer).and_return("")
      end

      message.serialize(buffer).should eql buffer
    end

    it "sets the length to the length of the buffer" do
      message_class.fields.each do |field|
        message.stub(:"serialize_#{field}" => "")
      end

      message.should_receive(:length=).with(buffer.length)
      message.serialize(buffer)
    end

    it "prepends the buffer with the new length" do
      message_class.fields.each do |field|
        message.stub(:"serialize_#{field}") unless field == :length
      end

      message.serialize(buffer)

      buffer[0, 4].should eq message.serialize_length("")
    end

    context "when the buffer already contains data" do
      before do
        message.collection = "admin.$cmd"
        message.selector = {}
      end

      it "appends the data" do
        serialized_data = message.serialize

        buffer = "existing data"
        message.serialize(buffer)
        buffer.should eq "existing data" + serialized_data
      end
    end
  end

end
