require_relative '../../spec_helper'
require_relative 'fixtures/classes'

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)
    File.read(@to_name).should == @content
    IO.copy_stream(@from_bigfile, @to_name)
    File.read(@to_name).should == @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
    File.read(@to_name).should == "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)
    File.read(@to_name).should == @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)

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

describe :io_copy_stream_to_file_with_offset, shared: true do
  platform_is_not :windows do
    it "copies only length bytes from the offset" do
      IO.copy_stream(@object.from, @to_name, 8, 4).should == 8
      File.read(@to_name).should == " one\n\nLi"
    end
  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)
    File.read(@to_name).should == @content
    IO.copy_stream(@from_bigfile, @to_io)
    File.read(@to_name).should == (@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)
    File.read(@to_name).should == ("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 = new_io __FILE__, "r"
    begin
      -> { IO.copy_stream @object.from, to_io }.should raise_error(IOError)
    ensure
      to_io.close
    end
  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
    File.read(@to_name).should == "Line one"
  end
end

describe :io_copy_stream_to_io_with_offset, shared: true do
  platform_is_not :windows do
    it "copies only length bytes from the offset" do
      IO.copy_stream(@object.from, @to_io, 8, 4).should == 8
      File.read(@to_name).should == " one\n\nLi"
    end
  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 if @to_name
    rm_r @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_bigfile, "a"
      -> { 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

    platform_is_not :windows do
      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
    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

    describe "to a Tempfile" do
      before :all do
        require 'tempfile'
      end

      before :each do
        @to_io = Tempfile.new("rubyspec_copy_stream", encoding: Encoding::BINARY, mode: File::RDONLY)
        @to_name = @to_io.path
      end

      after :each do
        @to_io.close!
        @to_name = nil # do not rm_r it, already done by Tempfile#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)
      File.read(@to_name).should == @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)

      -> { 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

    platform_is_not :windows do
      it "raises an error when an offset is specified" do
        -> { IO.copy_stream(@from_io, @to_name, 8, 4) }.should raise_error(Errno::ESPIPE)
      end
    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
      NATFIXME 'Implement IO#readpartial', exception: NoMethodError, message: /undefined method [`']readpartial' for an instance of File/ do
        from = IOSpecs::CopyStreamReadPartial.new @io

        IO.copy_stream(from, @to_name)
        File.read(@to_name).should == @content
      end
    end

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

      IO.copy_stream(from, @to_name)
      File.read(@to_name).should == @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

  describe "with a destination that does partial reads" do
    before do
      @from_out, @from_in = IO.pipe
      @to_out, @to_in = IO.pipe
    end

    after do
      [@from_out, @from_in, @to_out, @to_in].each {|io| io.close rescue nil}
    end

    it "calls #write repeatedly on the destination Object" do
      @from_in.write "1234"
      @from_in.close

      th = Thread.new do
        IO.copy_stream(@from_out, @to_in)
      end

      copied = ""
      4.times do
        copied += @to_out.read(1)
      end

      th.join

      copied.should == "1234"
    end

  end
end

describe "IO.copy_stream" do
  it "does not use buffering when writing to STDOUT" do
    NATFIXME 'Support array argument', exception: TypeError, message: 'no implicit conversion of Array into String' do
      IO.popen([*ruby_exe, fixture(__FILE__ , "copy_in_out.rb")], "r+") do |io|
        io.write("bar")
        io.read(3).should == "bar"
      end
    end
  end
end
