require 'spec_helper'

describe BSON::ByteBuffer do

  let(:buffer) do
    described_class.new
  end

  shared_examples_for 'does not write' do
    it 'raises ArgumentError' do
      expect do
        modified
      end.to raise_error(ArgumentError)
    end

    it 'does not change write position' do
      expect do
        modified
      end.to raise_error(ArgumentError)

      expect(buffer.write_position).to eq(0)
    end
  end

  describe '#put_byte' do

    let(:modified) do
      buffer.put_byte(BSON::Int32::BSON_TYPE)
    end

    it 'appends the byte to the byte buffer' do
      expect(modified.to_s).to eq(BSON::Int32::BSON_TYPE.chr)
    end

    it 'increments the write position by 1' do
      expect(modified.write_position).to eq(1)
    end

    context 'when it receives a numeric value' do
      it 'raises the ArgumentError exception' do
        expect{buffer.put_byte(1)}.to raise_error(ArgumentError)
      end
    end

    context 'when it receives a nil value' do
      it 'raises the ArgumentError exception' do
        expect{buffer.put_byte(nil)}.to raise_error(ArgumentError)
      end
    end

    context 'when given a string of length > 1' do

      let(:modified) do
        buffer.put_byte('xx')
      end

      it_behaves_like 'does not write'
    end

    context 'when given a string of length 0' do

      let(:modified) do
        buffer.put_byte('')
      end

      it_behaves_like 'does not write'
    end

    context 'when byte is not valid utf-8' do
      let(:string) do
        Utils.make_byte_string([254]).freeze
      end

      let(:modified) do
        buffer.put_byte(string)
      end

      it 'writes the byte' do
        expect(modified.to_s).to eq(string)
      end
    end
  end

  describe '#put_bytes' do

    let(:modified) do
      buffer.put_bytes(BSON::Int32::BSON_TYPE)
      buffer
    end

    it 'increments the write position by 1' do
      expect(modified.write_position).to eq(1)
    end

    context 'when it receives a numeric value' do
      it 'raises the ArgumentError exception' do
        expect{buffer.put_bytes(1)}.to raise_error(ArgumentError)
      end
    end

    context 'when it receives a nil value' do
      it 'raises the ArgumentError exception' do
        expect{buffer.put_bytes(nil)}.to raise_error(ArgumentError)
      end
    end

    context 'when given a string with null bytes' do
      let(:byte_str) { [0, 239, 254, 0].map(&:chr).join }

      let(:modified) do
        buffer.put_bytes(byte_str)
      end

      before do
        expect(buffer.write_position).to eq(0)
        expect(byte_str.length).to eq(4)
      end

      it 'writes the string' do
        expect(modified.write_position).to eq(4)
      end
    end

    context 'when bytes are not valid utf-8' do
      let(:string) do
        Utils.make_byte_string([254, 0, 255]).freeze
      end

      let(:modified) do
        buffer.put_bytes(string)
      end

      it 'writes the bytes' do
        expect(modified.to_s).to eq(string)
      end
    end
  end

  shared_examples_for 'bson string writer' do

    context 'given empty string' do
      let(:modified) do
        buffer.put_string('')
      end

      it 'writes length and null terminator' do
        expect(modified.write_position).to eq(5)
      end
    end

    context 'when string is not valid utf-8 in utf-8 encoding' do
      let(:string) do
        Utils.make_byte_string([254, 253, 255], 'utf-8')
      end

      before do
        expect(string.encoding.name).to eq('UTF-8')
      end

      it 'raises EncodingError' do
        # Precise exception classes and messages differ between MRI and JRuby
        expect do
          modified
        end.to raise_error(EncodingError)
      end
    end

    context 'when string is in binary encoding and cannot be encoded in utf-8' do
      let(:string) do
        Utils.make_byte_string([254, 253, 255], 'binary')
      end

      before do
        expect(string.encoding.name).to eq('ASCII-8BIT')
      end

      it 'raises Encoding::UndefinedConversionError' do
        expect do
          modified
        end.to raise_error(Encoding::UndefinedConversionError, /from ASCII-8BIT to UTF-8/)
      end
    end
  end

  describe '#put_string' do

    let(:modified) do
      buffer.put_string(string)
    end

    it_behaves_like 'bson string writer'

    context 'when the buffer does not need to be expanded' do

      context 'when the string is UTF-8' do

        let!(:modified) do
          buffer.put_string('testing')
        end

        it 'appends the string to the byte buffer' do
          expect(modified.to_s).to eq("#{8.to_bson.to_s}testing#{BSON::NULL_BYTE}")
        end

        it 'increments the write position by length + 5' do
          expect(modified.write_position).to eq(12)
        end
      end
    end

    context 'when the buffer needs to be expanded' do

      let(:string) do
        300.times.inject(""){ |s, i| s << "#{i}" }
      end

      context 'when no bytes exist in the buffer' do

        let!(:modified) do
          buffer.put_string(string)
        end

        it 'appends the string to the byte buffer' do
          expect(modified.to_s).to eq("#{(string.bytesize + 1).to_bson.to_s}#{string}#{BSON::NULL_BYTE}")
        end

        it 'increments the write position by length + 5' do
          expect(modified.write_position).to eq(string.bytesize + 5)
        end
      end

      context 'when bytes exist in the buffer' do

        let!(:modified) do
          buffer.put_int32(4).put_string(string)
        end

        it 'appends the string to the byte buffer' do
          expect(modified.to_s).to eq(
            "#{[ 4 ].pack(BSON::Int32::PACK)}#{(string.bytesize + 1).to_bson.to_s}#{string}#{BSON::NULL_BYTE}"
          )
        end

        it 'increments the write position by length + 5' do
          expect(modified.write_position).to eq(string.bytesize + 9)
        end
      end
    end

    context 'when string is in an encoding other than utf-8' do
      let(:string) do
        # "\xfe"
        Utils.make_byte_string([254], 'iso-8859-1')
      end

      let(:expected) do
        # \xc3\xbe == \u00fe
        Utils.make_byte_string([3, 0, 0, 0, 0xc3, 0xbe, 0])
      end

      it 'is written as utf-8' do
        expect(modified.to_s).to eq(expected)
      end
    end
  end

  describe '#put_cstring' do

    let(:modified) do
      buffer.put_cstring(string)
    end

    it_behaves_like 'bson string writer'

    context 'when argument is a string' do
      context 'when the string is valid' do

        let!(:modified) do
          buffer.put_cstring('testing')
        end

        it 'appends the string plus null byte to the byte buffer' do
          expect(modified.to_s).to eq("testing#{BSON::NULL_BYTE}")
        end

        it 'increments the write position by the length + 1' do
          expect(modified.write_position).to eq(8)
        end

        it 'mutates receiver' do
          modified
          expect(buffer.write_position).to eq(8)
        end
      end

      context "when the string contains a null byte" do

        let(:string) do
          "test#{BSON::NULL_BYTE}ing"
        end

        it 'raises ArgumentError' do
          expect {
            modified
          }.to raise_error(ArgumentError, /String .* contains null bytes/)
        end
      end

      context 'when string is in an encoding other than utf-8' do
        let(:string) do
          # "\xfe"
          Utils.make_byte_string([254], 'iso-8859-1')
        end

        let(:expected) do
          # \xc3\xbe == \u00fe
          # No length prefix
          Utils.make_byte_string([0xc3, 0xbe, 0])
        end

        it 'is written as utf-8' do
          expect(modified.to_s).to eq(expected)
        end
      end
    end

    context 'when argument is a symbol' do
      let(:modified) do
        buffer.put_cstring(:testing)
      end

      it 'writes' do
        expect(modified.to_s).to eq("testing#{BSON::NULL_BYTE}")
      end

      it 'increments the write position by the length + 1' do
        expect(modified.write_position).to eq(8)
      end

      it 'mutates receiver' do
        modified
        expect(buffer.write_position).to eq(8)
      end

      context 'when symbol includes a null byte' do
        let(:modified) do
          buffer.put_cstring(:"tes\x00ing")
        end

        it 'raises ArgumentError' do
          expect {
            modified
          }.to raise_error(ArgumentError, /String .* contains null bytes/)
        end

        it 'does not change write position' do
          begin
            buffer.put_cstring(:"tes\x00ing")
          rescue ArgumentError
          end

          expect(buffer.write_position).to eq(0)
        end
      end
    end

    context 'when argument is a Fixnum' do
      let(:modified) do
        buffer.put_cstring(1234)
      end

      it 'writes' do
        expect(modified.to_s).to eq("1234#{BSON::NULL_BYTE}")
      end

      it 'increments the write position by the length + 1' do
        expect(modified.write_position).to eq(5)
      end
    end

    context 'when argument is of an unsupported type' do
      let(:modified) do
        buffer.put_cstring(1234.0)
      end

      it 'raises TypeError' do
        expect do
          modified
        end.to raise_error(TypeError, /Invalid type for put_cstring/)
      end

      it 'does not change write position' do
        begin
          buffer.put_cstring(1234.0)
        rescue TypeError
        end

        expect(buffer.write_position).to eq(0)
      end
    end
  end

  describe '#put_symbol' do
    context 'normal symbol' do
      let(:modified) do
        buffer.put_symbol(:hello)
      end

      it 'writes the symbol as string' do
        expect(modified.to_s).to eq("\x06\x00\x00\x00hello\x00")
      end

      it 'advances write position' do
        # 4 byte length + 5 byte string + null byte
        expect(modified.write_position).to eq(10)
      end
    end

    context 'symbol with null byte' do
      let(:modified) do
        buffer.put_symbol(:"he\x00lo")
      end

      it 'writes the symbol as string' do
        expect(modified.to_s).to eq("\x06\x00\x00\x00he\x00lo\x00")
      end

      it 'advances write position' do
        # 4 byte length + 5 byte string + null byte
        expect(modified.write_position).to eq(10)
      end
    end

    context 'when symbol is not valid utf-8' do
      let(:symbol) do
        Utils.make_byte_string([254, 0, 255]).to_sym
      end

      let(:modified) do
        buffer.put_symbol(symbol)
      end

      it 'raises EncodingError' do
        # Precise exception classes and messages differ between MRI and JRuby
        expect do
          modified
        end.to raise_error(EncodingError)
      end
    end
  end

  describe '#put_double' do

    let(:modified) do
      buffer.put_double(1.2332)
    end

    it 'appends the double to the buffer' do
      expect(modified.to_s).to eq([ 1.2332 ].pack(Float::PACK))
    end

    it 'increments the write position by 8' do
      expect(modified.write_position).to eq(8)
    end

    context 'when argument is an integer' do

      let(:modified) do
        buffer.put_double(3)
      end

      it 'writes a double' do
        expect(modified.to_s).to eq([ 3 ].pack(Float::PACK))
      end

      it 'increments the write position by 8' do
        expect(modified.write_position).to eq(8)
      end
    end

    context 'when argument is a BigNum' do
      let(:value) { 123456789012345678901234567890 }

      let(:modified) do
        buffer.put_double(value)
      end

      let(:actual) do
        described_class.new(modified.to_s).get_double
      end

      it 'writes a double' do
        expect(actual).to be_within(1).of(value)
      end

      it 'increments the write position by 8' do
        expect(modified.write_position).to eq(8)
      end
    end

    context 'when argument is a string' do

      let(:modified) do
        buffer.put_double("hello")
      end

      it 'raises TypeError' do
        expect do
          modified
        end.to raise_error(TypeError, /no implicit conversion to float from string|ClassCastException:.*RubyString cannot be cast to.*RubyFloat/)
        expect(buffer.write_position).to eq(0)
      end
    end
  end

  describe '#put_int32' do

    context 'when the integer is 32 bit' do

      context 'when the integer is positive' do

        let!(:modified) do
          buffer.put_int32(Integer::MAX_32BIT - 1)
        end

        let(:expected) do
          [ Integer::MAX_32BIT - 1 ].pack(BSON::Int32::PACK)
        end

        it 'appends the int32 to the byte buffer' do
          expect(modified.to_s).to eq(expected)
        end

        it 'increments the write position by 4' do
          expect(modified.write_position).to eq(4)
        end
      end

      context 'when the integer is negative' do

        let!(:modified) do
          buffer.put_int32(Integer::MIN_32BIT + 1)
        end

        let(:expected) do
          [ Integer::MIN_32BIT + 1 ].pack(BSON::Int32::PACK)
        end

        it 'appends the int32 to the byte buffer' do
          expect(modified.to_s).to eq(expected)
        end

        it 'increments the write position by 4' do
          expect(modified.write_position).to eq(4)
        end
      end

      context 'when the integer is not 32 bit' do

        it 'raises an exception' do
          expect {
            buffer.put_int32(Integer::MAX_64BIT - 1)
          }.to raise_error(RangeError)
        end
      end
    end

    context 'when argument is a float' do

      let(:modified) do
        buffer.put_int32(4.934)
      end

        let(:expected) do
          [ 4 ].pack(BSON::Int32::PACK)
        end

      it 'appends the int32 to the byte buffer' do
        expect(modified.to_s).to eq(expected)
      end

      it 'increments the write position by 4' do
        expect(modified.write_position).to eq(4)
      end
    end
  end

  describe '#put_int64' do

    context 'when the integer is 64 bit' do

      context 'when the integer is positive' do

        let!(:modified) do
          buffer.put_int64(Integer::MAX_64BIT - 1)
        end

        let(:expected) do
          [ Integer::MAX_64BIT - 1 ].pack(BSON::Int64::PACK)
        end

        it 'appends the int64 to the byte buffer' do
          expect(modified.to_s).to eq(expected)
        end

        it 'increments the write position by 8' do
          expect(modified.write_position).to eq(8)
        end
      end

      context 'when the integer is negative' do

        let!(:modified) do
          buffer.put_int64(Integer::MIN_64BIT + 1)
        end

        let(:expected) do
          [ Integer::MIN_64BIT + 1 ].pack(BSON::Int64::PACK)
        end

        it 'appends the int64 to the byte buffer' do
          expect(modified.to_s).to eq(expected)
        end

        it 'increments the write position by 8' do
          expect(modified.write_position).to eq(8)
        end
      end

      context 'when the integer is larger than 64 bit' do

        it 'raises an exception' do
          expect {
            buffer.put_int64(Integer::MAX_64BIT + 1)
          }.to raise_error(RangeError)
        end
      end
    end

    context 'when integer fits in 32 bits' do
      let(:modified) do
        buffer.put_int64(1)
      end

      it 'increments the write position by 8' do
        expect(modified.write_position).to eq(8)
      end
    end

    context 'when argument is a float' do

      let(:modified) do
        buffer.put_int64(4.934)
      end

        let(:expected) do
          [ 4 ].pack(BSON::Int64::PACK)
        end

      it 'appends the int64 to the byte buffer' do
        expect(modified.to_s).to eq(expected)
      end

      it 'increments the write position by 8' do
        expect(modified.write_position).to eq(8)
      end
    end
  end

  describe '#replace_int32' do

    let(:exp_0) do
      [ 0 ].pack(BSON::Int32::PACK)
    end

    let(:exp_first) do
      [ 5 ].pack(BSON::Int32::PACK)
    end

    let(:exp_second) do
      [ 4 ].pack(BSON::Int32::PACK)
    end

    let(:exp_42) do
      [ 42 ].pack(BSON::Int32::PACK)
    end

    let(:modified) do
      buffer.replace_int32(0, 5)
    end

    context 'when there is sufficient data in buffer' do

      before do
        buffer.put_int32(0).put_int32(4)
      end

      it 'replaces the int32 at the location' do
        expect(modified.to_s).to eq("#{exp_first}#{exp_second}")
      end

      context 'when the position is negative' do

        let(:modified) do
          buffer.replace_int32(-1, 5)
        end

        it 'raises ArgumentError' do
          expect do
            modified
          end.to raise_error(ArgumentError, /Position.*cannot be negative/)
        end
      end

      context 'when the position is 4 bytes prior to write position' do

        let(:modified) do
          buffer.replace_int32(4, 42)
        end

        it 'replaces the int32 at the location' do
          expect(modified.to_s).to eq("#{exp_0}#{exp_42}")
        end
      end

      context 'when the position exceeds allowed range' do

        let(:modified) do
          # Buffer has 8 bytes but we can only write up to position 4
          buffer.replace_int32(5, 42)
        end

        it 'raises ArgumentError' do
          expect do
            modified
          end.to raise_error(ArgumentError, /Position.*is out of bounds/)
        end
      end
    end

    context 'when there is insufficient data in buffer' do

      before do
        buffer.put_bytes("aa")
      end

      let(:modified) do
        buffer.replace_int32(1, 42)
      end

      it 'raises ArgumentError' do
        expect do
          modified
        end.to raise_error(ArgumentError, /Buffer does not have enough data/)
      end
    end
  end
end
