require File.expand_path('../../../spec_helper', __FILE__)
require File.expand_path('../fixtures/classes', __FILE__)

describe :io_copy_stream_to_file, :shared => true do
  it "copies the entire IO contents to the file" do
    IO.copy_stream(@object.from, @to_name)
    @to_name.should have_data(@content)
    IO.copy_stream(@from_bigfile, @to_name)
    @to_name.should have_data(@content_bigfile)
  end

  it "returns the number of bytes copied" do
    IO.copy_stream(@object.from, @to_name).should == @size
    IO.copy_stream(@from_bigfile, @to_name).should == @size_bigfile
  end

  it "copies only length bytes when specified" do
    IO.copy_stream(@object.from, @to_name, 8).should == 8
    @to_name.should have_data("Line one")
  end

  it "calls #to_path to convert on object to a file name" do
    obj = mock("io_copy_stream_to")
    obj.should_receive(:to_path).and_return(@to_name)

    IO.copy_stream(@object.from, obj)
    @to_name.should have_data(@content)
  end

  it "raises a TypeError if #to_path does not return a String" do
    obj = mock("io_copy_stream_to")
    obj.should_receive(:to_path).and_return(1)

    lambda { IO.copy_stream(@object.from, obj) }.should raise_error(TypeError)
  end
end

describe :io_copy_stream_to_file_with_offset, :shared => true do
  it "copies only length bytes from the offset" do
    IO.copy_stream(@object.from, @to_name, 8, 4).should == 8
    @to_name.should have_data(" one\n\nLi")
  end
end

describe :io_copy_stream_to_io, :shared => true do
  it "copies the entire IO contents to the IO" do
    IO.copy_stream(@object.from, @to_io)
    @to_name.should have_data(@content)
    IO.copy_stream(@from_bigfile, @to_io)
    @to_name.should have_data(@content + @content_bigfile)
  end

  it "returns the number of bytes copied" do
    IO.copy_stream(@object.from, @to_io).should == @size
    IO.copy_stream(@from_bigfile, @to_io).should == @size_bigfile
  end

  it "starts writing at the destination IO's current position" do
    @to_io.write("prelude ")
    IO.copy_stream(@object.from, @to_io)
    @to_name.should have_data("prelude " + @content)
  end

  it "leaves the destination IO position at the last write" do
    IO.copy_stream(@object.from, @to_io)
    @to_io.pos.should == @size
  end

  it "raises an IOError if the destination IO is not open for writing" do
    @to_io.close
    @to_io = new_io @to_name, "r"
    lambda { IO.copy_stream @object.from, @to_io }.should raise_error(IOError)
  end

  it "does not close the destination IO" do
    IO.copy_stream(@object.from, @to_io)
    @to_io.closed?.should be_false
  end

  it "copies only length bytes when specified" do
    IO.copy_stream(@object.from, @to_io, 8).should == 8
    @to_name.should have_data("Line one")
  end
end

describe :io_copy_stream_to_io_with_offset, :shared => true do
  it "copies only length bytes from the offset" do
    IO.copy_stream(@object.from, @to_io, 8, 4).should == 8
    @to_name.should have_data(" one\n\nLi")
  end
end

describe "IO.copy_stream" do
  before :each do
    @from_name = fixture __FILE__, "copy_stream.txt"
    @to_name = tmp("io_copy_stream_io_name")

    @content = IO.read(@from_name)
    @size = @content.size

    @from_bigfile = tmp("io_copy_stream_bigfile")
    @content_bigfile = "A" * 17_000
    touch(@from_bigfile){|f| f.print @content_bigfile }
    @size_bigfile =  @content_bigfile.size
  end

  after :each do
    rm_r @to_name, @from_bigfile
  end

  describe "from an IO" do
    before :each do
      @from_io = new_io @from_name, "rb"
      IOSpecs::CopyStream.from = @from_io
    end

    after :each do
      @from_io.close
    end

    it "raises an IOError if the source IO is not open for reading" do
      @from_io.close
      @from_io = new_io @from_name, "a"
      lambda { IO.copy_stream @from_io, @to_name }.should raise_error(IOError)
    end

    it "does not close the source IO" do
      IO.copy_stream(@from_io, @to_name)
      @from_io.closed?.should be_false
    end

    it "does not change the IO offset when an offset is specified" do
      @from_io.pos = 10
      IO.copy_stream(@from_io, @to_name, 8, 4)
      @from_io.pos.should == 10
    end

    it "does change the IO offset when an offset is not specified" do
      @from_io.pos = 10
      IO.copy_stream(@from_io, @to_name)
      @from_io.pos.should == 42
    end

    describe "to a file name" do
      it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream
      it_behaves_like :io_copy_stream_to_file_with_offset, nil, IOSpecs::CopyStream
    end

    describe "to an IO" do
      before :each do
        @to_io = new_io @to_name, "wb"
      end

      after :each do
        @to_io.close
      end

      it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
      it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream
    end
  end

  describe "from a file name" do
    before :each do
      IOSpecs::CopyStream.from = @from_name
    end

    it "calls #to_path to convert on object to a file name" do
      obj = mock("io_copy_stream_from")
      obj.should_receive(:to_path).and_return(@from_name)

      IO.copy_stream(obj, @to_name)
      @to_name.should have_data(@content)
    end

    it "raises a TypeError if #to_path does not return a String" do
      obj = mock("io_copy_stream_from")
      obj.should_receive(:to_path).and_return(1)

      lambda { IO.copy_stream(obj, @to_name) }.should raise_error(TypeError)
    end

    describe "to a file name" do
      it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream
      it_behaves_like :io_copy_stream_to_file_with_offset, nil, IOSpecs::CopyStream
    end

    describe "to an IO" do
      before :each do
        @to_io = new_io @to_name, "wb"
      end

      after :each do
        @to_io.close
      end

      it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
      it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream
    end
  end

  describe "from a pipe IO" do
    before :each do
      @from_io = IOSpecs.pipe_fixture(@content)
      IOSpecs::CopyStream.from = @from_io
    end

    after :each do
      @from_io.close
    end

    it "does not close the source IO" do
      IO.copy_stream(@from_io, @to_name)
      @from_io.closed?.should be_false
    end

    it "raises an error when an offset is specified" do
      lambda { IO.copy_stream(@from_io, @to_name, 8, 4) }.should raise_error(Errno::ESPIPE)
    end

    describe "to a file name" do
      it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream
    end

    describe "to an IO" do
      before :each do
        @to_io = new_io @to_name, "wb"
      end

      after :each do
        @to_io.close
      end

      it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
    end
  end

  describe "with non-IO Objects" do
    before do
      @io = new_io @from_name, "rb"
    end

    after do
      @io.close unless @io.closed?
    end

    it "calls #readpartial on the source Object if defined" do
      from = IOSpecs::CopyStreamReadPartial.new @io

      IO.copy_stream(from, @to_name)
      @to_name.should have_data(@content)
    end

    it "calls #read on the source Object" do
      from = IOSpecs::CopyStreamRead.new @io

      IO.copy_stream(from, @to_name)
      @to_name.should have_data(@content)
    end

    it "calls #write on the destination Object" do
      to = mock("io_copy_stream_to_object")
      to.should_receive(:write).with(@content).and_return(@content.size)

      IO.copy_stream(@from_name, to)
    end

    it "does not call #pos on the source if no offset is given" do
      @io.should_not_receive(:pos)
      IO.copy_stream(@io, @to_name)
    end

  end
end
