#!/usr/bin/env ruby

require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))

describe BinData::String, "with mutually exclusive parameters" do
  it ":value and :initial_value" do
    params = {value: "", initial_value: ""}
    _ { BinData::String.new(params) }.must_raise ArgumentError
  end

  it ":length and :read_length" do
    params = {length: 5, read_length: 5}
    _ { BinData::String.new(params) }.must_raise ArgumentError
  end

  it ":value and :length" do
    params = {value: "", length: 5}
    _ { BinData::String.new(params) }.must_raise ArgumentError
  end
end

describe BinData::String, "when assigning" do
  let(:small) { BinData::String.new(length: 3, pad_byte: "A") }
  let(:large) { BinData::String.new(length: 5, pad_byte: "B") }

  it "copies data from small to large" do
    large.assign(small)
    _(large).must_equal "AAABB"
  end

  it "copies data from large to small" do
    small.assign(large)
    _(small).must_equal "BBB"
  end
end

describe BinData::String do
  let(:obj) { BinData::String.new("testing") }

  it "compares with regexp" do
    _((/es/ =~ obj)).must_equal 1
  end

  it "compares with regexp" do
    _((obj =~ /es/)).must_equal 1
  end
end

describe BinData::String, "with :read_length" do
  let(:obj) { BinData::String.new(read_length: 5) }

  specify { _(obj.num_bytes).must_equal 0 }
  specify { _(obj.value).must_equal "" }

  it "reads :read_length bytes" do
    obj.read("abcdefghij")
    _(obj).must_equal "abcde"
  end

  it "remembers :read_length after value is cleared" do
    obj.assign("abc")
    _(obj.num_bytes).must_equal 3
    obj.clear

    obj.read("abcdefghij")
    _(obj).must_equal "abcde"
  end
end

describe BinData::String, "with :length" do
  let(:obj) { BinData::String.new(length: 5) }

  specify { _(obj.num_bytes).must_equal 5 }
  specify { _(obj.value).must_equal "\0\0\0\0\0" }

  it "retains :length after value is set" do
    obj.assign("abcdefghij")
    _(obj.num_bytes).must_equal 5
  end

  it "reads :length bytes" do
    obj.read("abcdefghij")
    _(obj).must_equal "abcde"
  end

  it "pads values less than :length" do
    obj.assign("abc")
    _(obj).must_equal "abc\0\0"
  end

  it "accepts values exactly :length" do
    obj.assign("abcde")
    _(obj).must_equal "abcde"
  end

  it "truncates values greater than :length" do
    obj.assign("abcdefghij")
    _(obj).must_equal "abcde"
  end
end

describe BinData::String, "with :read_length and :initial_value" do
  let(:obj) { BinData::String.new(read_length: 5, initial_value: "abcdefghij") }

  specify { _(obj.num_bytes).must_equal 10 }
  specify { _(obj.value).must_equal "abcdefghij" }

  it "uses :read_length for reading" do
    io = StringIO.new("ABCDEFGHIJKLMNOPQRST")
    obj.read(io)
    _(io.pos).must_equal 5
   end

  it "forgets :initial_value after reading" do
    obj.read("ABCDEFGHIJKLMNOPQRST")
    _(obj.num_bytes).must_equal 5
    _(obj).must_equal "ABCDE"
  end
end

describe BinData::String, "with :read_length and :value" do
  let(:obj) { BinData::String.new(read_length: 5, value: "abcdefghij") }

  specify { _(obj.num_bytes).must_equal 10 }
  specify { _(obj.value).must_equal "abcdefghij" }

  it "uses :read_length for reading" do
    io = StringIO.new("ABCDEFGHIJKLMNOPQRST")
    obj.read(io)
    _(io.pos).must_equal 5
  end

  describe "after reading" do
    before(:each) do
      obj.read("ABCDEFGHIJKLMNOPQRST")
    end

    it "is not affected by :read_length after reading" do
      _(obj.num_bytes).must_equal 10
      _(obj).must_equal "abcdefghij"
    end

    it "returns read value while reading" do
      obj.stub :reading?, true do
        _(obj).must_equal "ABCDE"
      end
    end
  end
end

describe BinData::String, "with :length and :initial_value" do
  let(:obj) { BinData::String.new(length: 5, initial_value: "abcdefghij") }

  specify { _(obj.num_bytes).must_equal 5 }
  specify { _(obj.value).must_equal "abcde" }

  it "forgets :initial_value after reading" do
    io = StringIO.new("ABCDEFGHIJKLMNOPQRST")
    obj.read(io)
    _(io.pos).must_equal 5
    _(obj.num_bytes).must_equal 5
    _(obj).must_equal "ABCDE"
  end
end

describe BinData::String, "with :pad_byte" do
  it "accepts a numeric value for :pad_byte" do
    str = BinData::String.new(length: 5, pad_byte: 6)
    str.assign("abc")
    _(str).must_equal "abc\x06\x06"
  end

  it "accepts a character for :pad_byte" do
    str = BinData::String.new(length: 5, pad_byte: "R")
    str.assign("abc")
    _(str).must_equal "abcRR"
  end

  it "does not accept a string for :pad_byte" do
    params = {length: 5, pad_byte: "RR"}
    _ { BinData::String.new(params) }.must_raise ArgumentError
  end
end

describe BinData::String, "with :trim_padding" do
  it "set false is the default" do
    str1 = BinData::String.new(length: 5)
    str2 = BinData::String.new(length: 5, trim_padding: false)
    str1.assign("abc")
    str2.assign("abc")
    _(str1).must_equal "abc\0\0"
    _(str2).must_equal "abc\0\0"
  end

  describe "trim padding set" do
    let(:obj) { BinData::String.new(pad_byte: 'R', trim_padding: true) }

    it "trims the value" do
      obj.assign("abcRR")
      _(obj).must_equal "abc"
    end

    it "does not affect num_bytes" do
      obj.assign("abcRR")
      _(obj.num_bytes).must_equal 5
    end

    it "trims if last char is :pad_byte" do
      obj.assign("abcRR")
      _(obj).must_equal "abc"
    end

    it "does not trim if value contains :pad_byte not at the end" do
      obj.assign("abcRRde")
      _(obj).must_equal "abcRRde"
    end
  end
end

describe BinData::String, "with :pad_front" do
  it "set false is the default" do
    str1 = BinData::String.new(length: 5)
    str2 = BinData::String.new(length: 5, pad_front: false)
    str1.assign("abc")
    str2.assign("abc")
    _(str1).must_equal "abc\0\0"
    _(str2).must_equal "abc\0\0"
  end

  it "pads to the front" do
    str = BinData::String.new(length: 5, pad_byte: 'R', pad_front: true)
    str.assign("abc")
    _(str).must_equal "RRabc"
  end

  it "can alternatively be accesses by :pad_left" do
    str = BinData::String.new(length: 5, pad_byte: 'R', pad_left: true)
    str.assign("abc")
    _(str).must_equal "RRabc"
  end

  describe "and :trim_padding" do
    let(:obj) { BinData::String.new(length: 5, pad_byte: 'R', pad_front: true, trim_padding: true) }

    it "assigns" do
      obj.assign("abc")
      _(obj).must_equal "abc"
    end

    it "has to_binary_s" do
      obj.assign("abc")
      _(obj.to_binary_s).must_equal_binary "RRabc"
    end

    it "reads" do
      obj.read "RRabc"
      _(obj).must_equal "abc"
    end
  end
end

describe BinData::String, "with Ruby 1.9 encodings" do
  class UTF8String < BinData::String
    def snapshot
      super.force_encoding('UTF-8')
    end
  end

  let(:obj) { UTF8String.new }
  let(:binary_str) { "\xC3\x85\xC3\x84\xC3\x96" }
  let(:utf8_str) { binary_str.dup.force_encoding('UTF-8') }

  it "stores assigned values as binary" do
    obj.assign(utf8_str)
    _(obj.to_binary_s).must_equal_binary binary_str
  end

  it "stores read values as binary" do
    obj = UTF8String.new(read_length: binary_str.bytesize)
    obj.read(binary_str)

    _(obj.to_binary_s).must_equal_binary binary_str
  end

  it "returns values in correct encoding" do
    obj.assign(utf8_str)

    _(obj.snapshot).must_equal utf8_str
  end

  it "has correct num_bytes" do
    obj.assign(utf8_str)

    _(obj.num_bytes).must_equal binary_str.bytesize
  end
end

describe BinData::String, "warnings" do
  it "warns if has :asserted_value but no :length" do
    obj = BinData::String.new(asserted_value: "ABC")
    _ {
      _ { obj.read("abcde") }.must_raise BinData::ValidityError
    }.must_warn "obj does not have a :read_length parameter - returning empty string"
  end

  it "warns if has :value but no :read_length" do
    obj = BinData::String.new(value: "ABC")
    _ {
      obj.read("abcde")
    }.must_warn "obj does not have a :read_length parameter - returning empty string"
  end
end
