#!/usr/bin/env ruby

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

describe BinData::DelayedIO, "when instantiating" do
  describe "with no mandatory parameters supplied" do
    it "raises an error" do
      args = {}
      lambda { BinData::DelayedIO.new(args) }.must_raise ArgumentError
    end
  end

  describe "with some but not all mandatory parameters supplied" do
    it "raises an error" do
      args = {:read_abs_offset => 3}
      lambda { BinData::DelayedIO.new(args) }.must_raise ArgumentError
    end
  end

  it "fails if a given type is unknown" do
    args = {:type => :does_not_exist, :length => 3}
    lambda { BinData::DelayedIO.new(args) }.must_raise BinData::UnRegisteredTypeError
  end

  it "accepts BinData::Base as :type" do
    obj = BinData::Int8.new(:initial_value => 5)
    array = BinData::DelayedIO.new(:type => obj, :read_abs_offset => 3)
    array.must_equal 5
  end
end

describe BinData::DelayedIO, "subclassed with a single type" do
  class IntDelayedIO < BinData::DelayedIO
    endian :big
    default_parameter :read_abs_offset => 5

    uint16
  end

  it "behaves as type" do
    obj = IntDelayedIO.new(3)
    obj.must_equal 3
  end

  it "does not read" do
    obj = IntDelayedIO.read "\001\002\003\004\005\006\007"
    assert obj.clear?
  end

  it "does not do_num_bytes" do
    obj = IntDelayedIO.new(3)
    obj.do_num_bytes.must_equal 0
  end

  it "does num_bytes" do
    obj = IntDelayedIO.new(3)
    obj.num_bytes.must_equal 2
  end

  it "does not write" do
    io = StringIO.new
    obj = IntDelayedIO.new(3)
    obj.write(io)
    io.value.must_equal ""
  end

  it "uses read_abs_offset" do
    obj = IntDelayedIO.new(3)
    obj.abs_offset.must_equal 5
    obj.rel_offset.must_equal 5
  end

  it "uses abs_offset if set" do
    obj = IntDelayedIO.new(3)
    obj.abs_offset = 10
    obj.abs_offset.must_equal 10
    obj.rel_offset.must_equal 10
  end

  it "must call #read before #read_now!" do
    obj = IntDelayedIO.new(3)
    lambda {
      obj.read_now!
    }.must_raise IOError
  end

  it "reads explicitly" do
    obj = IntDelayedIO.read "\001\002\003\004\005\006\007"
    obj.read_now!

    obj.must_equal 0x0607
  end

  it "must call #write before #write_now!" do
    obj = IntDelayedIO.new(3)
    lambda {
      obj.write_now!
    }.must_raise IOError
  end

  it "writes explicitly" do
    io = StringIO.new "\001\002\003\004\005\006\007\010\011"
    obj = IntDelayedIO.new(3)
    obj.write(io)
    obj.write_now!
    io.value.must_equal "\001\002\003\004\005\000\003\010\011"
  end

  it "writes explicitly after setting abs_offset" do
    io = StringIO.new "\001\002\003\004\005\006\007\010\011"
    obj = IntDelayedIO.new(7)
    obj.write(io)

    obj.abs_offset = 1
    obj.write_now!
    io.value.must_equal "\001\000\007\004\005\006\007\010\011"
  end
end

describe BinData::DelayedIO, "subclassed with multiple types" do
  class StringDelayedIO < BinData::DelayedIO
    endian :big
    default_parameter :read_abs_offset => 5

    uint16 :len, :value => lambda { str.length }
    string :str, :read_length => :len
  end

  it "behaves as type" do
    obj = StringDelayedIO.new(:str => "hello")
    obj.snapshot.must_equal({:len => 5, :str => "hello"})
  end

  it "reads explicitly" do
    obj = StringDelayedIO.read "\001\002\003\004\005\000\003abc\013"
    obj.read_now!

    obj.snapshot.must_equal({:len => 3, :str => "abc"})
  end

  it "writes explicitly" do
    io = StringIO.new "\001\002\003\004\005\006\007\010\011\012\013\014\015"
    obj = StringDelayedIO.new(:str => "hello")
    obj.write(io)
    obj.write_now!
    io.value.must_equal "\001\002\003\004\005\000\005hello\015"
  end
end

describe BinData::DelayedIO, "inside a Record" do
  class DelayedIORecord < BinData::Record
    endian :little

    uint16 :str_length, :value => lambda { str.length }
    delayed_io :str, :read_abs_offset => 4 do
      string :read_length => :str_length
    end
    delayed_io :my_int, :read_abs_offset => 2 do
      uint16 :initial_value => 7
    end
  end

  it "reads" do
    obj = DelayedIORecord.read "\x05\x00\x03\x0012345"
    obj.num_bytes.must_equal 2
    obj.snapshot.must_equal({:str_length => 0, :str => "", :my_int => 7})
  end

  it "reads explicitly" do
    obj = DelayedIORecord.read "\x05\x00\x03\x0012345"
    obj.str.read_now!
    obj.my_int.read_now!
    obj.num_bytes.must_equal 2
    obj.snapshot.must_equal({:str_length => 5, :str => "12345", :my_int => 3})
  end

  it "writes" do
    obj = DelayedIORecord.new(:str => "abc", :my_int => 2)
    io = StringIO.new
    obj.write(io)
    obj.str.write_now!
    obj.my_int.write_now!
    io.value.must_equal "\x03\x00\x02\x00abc"
  end
end

describe BinData::DelayedIO, "with auto_call" do
  class AutoCallDelayedIORecord < BinData::Record
    auto_call_delayed_io
    uint8 :a
    delayed_io :b, :read_abs_offset => 1 do
      uint8
    end
  end

  it "class reads" do
    obj = AutoCallDelayedIORecord.read "\x01\x02"
    obj.snapshot.must_equal({:a => 1, :b => 2})
  end

  it "reads" do
    obj = AutoCallDelayedIORecord.new
    obj.read "\x01\x02"
    obj.snapshot.must_equal({:a => 1, :b => 2})
  end

  it "writes" do
    obj = AutoCallDelayedIORecord.new(:a => 1, :b => 2)
    io = StringIO.new
    obj.write(io)
    io.value.must_equal "\x01\x02"
  end

  it "to_binary_s" do
    obj = AutoCallDelayedIORecord.new(:a => 1, :b => 2)
    obj.to_binary_s.must_equal_binary "\x01\x02"
  end

  it "num_bytes" do
    obj = AutoCallDelayedIORecord.new(:a => 1, :b => 2)
    obj.num_bytes.must_equal 2
  end
end
