require "../spec_helper"
require "../../support/string"

private class BufferedWrapper < IO
  include IO::Buffered

  getter called_unbuffered_read

  @io : IO
  @called_unbuffered_read : Bool

  def initialize(@io)
    @called_unbuffered_read = false
  end

  def self.new(io, &)
    buffered_io = new(io)
    yield buffered_io
    buffered_io.flush
    io
  end

  private def unbuffered_read(slice : Bytes) : Int32
    @called_unbuffered_read = true
    @io.read(slice)
  end

  private def unbuffered_write(slice : Bytes) : Nil
    @io.write(slice)
  end

  private def unbuffered_flush
    @io.flush
  end

  def fd
    @io.fd
  end

  private def unbuffered_close
    @io.close
  end

  def closed?
    @io.closed?
  end

  private def unbuffered_rewind
    @io.rewind
  end
end

DEFAULT_BUFFER_SIZE = BufferedWrapper.new(IO::Memory.new("")).buffer_size

describe "IO::Buffered" do
  it "can report buffer_size" do
    io = BufferedWrapper.new(IO::Memory.new(""))
    io.buffer_size.should eq(DEFAULT_BUFFER_SIZE)
  end

  it "can set buffer_size" do
    io = BufferedWrapper.new(IO::Memory.new(""))
    io.buffer_size = 16_384
    io.buffer_size.should eq(16_384)
  end

  it "can not set buffer_size after first use" do
    io = BufferedWrapper.new(IO::Memory.new("hello\r\nworld\n"))
    io.gets
    expect_raises ArgumentError, "buffer_size" do
      io.buffer_size = 16_384
    end
  end

  it "can set buffer_size to the same value after first use" do
    io = BufferedWrapper.new(IO::Memory.new("hello\r\nworld\n"))
    io.buffer_size = 16_384
    io.gets

    io.buffer_size = 16_384
    io.buffer_size.should eq(16_384)
  end

  it "does gets" do
    io = BufferedWrapper.new(IO::Memory.new("hello\r\nworld\n"))
    io.gets.should eq("hello")
    io.gets.should eq("world")
    io.gets.should be_nil
  end

  it "does gets with chomp = false" do
    io = BufferedWrapper.new(IO::Memory.new("hello\nworld\n"))
    io.gets(chomp: false).should eq("hello\n")
    io.gets(chomp: false).should eq("world\n")
    io.gets(chomp: false).should be_nil
  end

  it "does gets with big line" do
    big_line = "a" * 20_000
    io = BufferedWrapper.new(IO::Memory.new("#{big_line}\nworld\n"))
    io.gets.should eq(big_line)
  end

  it "does gets with big line and \\r\\n" do
    big_line = "a" * 20_000
    io = BufferedWrapper.new(IO::Memory.new("#{big_line}\r\nworld\n"))
    io.gets.should eq(big_line)
  end

  it "does gets with big line and chomp = false" do
    big_line = "a" * 20_000
    io = BufferedWrapper.new(IO::Memory.new("#{big_line}\nworld\n"))
    io.gets(chomp: false).should eq("#{big_line}\n")
  end

  it "does gets with char delimiter" do
    io = BufferedWrapper.new(IO::Memory.new("hello world"))
    io.gets('w').should eq("hello w")
    io.gets('r').should eq("or")
    io.gets('r').should eq("ld")
    io.gets('r').should be_nil
  end

  it "does gets with unicode char delimiter" do
    io = BufferedWrapper.new(IO::Memory.new("こんにちは"))
    io.gets('ち').should eq("こんにち")
    io.gets('ち').should eq("は")
    io.gets('ち').should be_nil
  end

  it "does gets with limit" do
    io = BufferedWrapper.new(IO::Memory.new("hello\nworld\n"))
    io.gets(3).should eq("hel")
    io.gets(10_000).should eq("lo\n")
    io.gets(10_000).should eq("world\n")
    io.gets(3).should be_nil
  end

  it "does gets with char and limit" do
    io = BufferedWrapper.new(IO::Memory.new("hello\nworld\n"))
    io.gets('o', 2).should eq("he")
    io.gets('w', 10_000).should eq("llo\nw")
    io.gets('z', 10_000).should eq("orld\n")
    io.gets('a', 3).should be_nil
  end

  it "does gets with char and limit without off-by-one" do
    io = BufferedWrapper.new(IO::Memory.new("test\nabc"))
    io.gets('a', 5).should eq("test\n")
    io = BufferedWrapper.new(IO::Memory.new("test\nabc"))
    io.gets('a', 6).should eq("test\na")
  end

  it "does gets with char and limit when not found in buffer" do
    io = BufferedWrapper.new(IO::Memory.new(("a" * (DEFAULT_BUFFER_SIZE + 10)) + "b"))
    io.gets('b', 2).should eq("aa")
  end

  it "does gets with char and limit when not found in buffer (2)" do
    base = "a" * (DEFAULT_BUFFER_SIZE + 10)
    io = BufferedWrapper.new(IO::Memory.new(base + "aabaaa"))
    io.gets('b', DEFAULT_BUFFER_SIZE + 11).should eq(base + "a")
  end

  it "raises if invoking gets with negative limit" do
    io = BufferedWrapper.new(IO::Memory.new("hello\nworld\n"))
    expect_raises ArgumentError, "Negative limit" do
      io.gets(-1)
    end
  end

  it "writes bytes" do
    str = IO::Memory.new
    io = BufferedWrapper.new(str)
    10_000.times { io.write_byte 'a'.ord.to_u8 }
    io.flush
    str.to_s.should eq("a" * 10_000)
  end

  it "reads char" do
    io = BufferedWrapper.new(IO::Memory.new("hi 世界"))
    io.read_char.should eq('h')
    io.read_char.should eq('i')
    io.read_char.should eq(' ')
    io.read_char.should eq('世')
    io.read_char.should eq('界')
    io.read_char.should be_nil

    {% for bytes, char in VALID_UTF8_BYTE_SEQUENCES %}
      BufferedWrapper.new(IO::Memory.new(Bytes{{ bytes }})).read_char.should eq({{ char }})
    {% end %}

    {% for bytes in INVALID_UTF8_BYTE_SEQUENCES %}
      expect_raises(InvalidByteSequenceError) do
        BufferedWrapper.new(IO::Memory.new(Bytes{{ bytes }})).read_char
      end
    {% end %}
  end

  it "reads byte" do
    io = BufferedWrapper.new(IO::Memory.new("hello"))
    io.read_byte.should eq('h'.ord)
    io.read_byte.should eq('e'.ord)
    io.read_byte.should eq('l'.ord)
    io.read_byte.should eq('l'.ord)
    io.read_byte.should eq('o'.ord)
    io.read_char.should be_nil
  end

  it "does new with block" do
    str = IO::Memory.new
    res = BufferedWrapper.new str, &.print "Hello"
    res.should be(str)
    str.to_s.should eq("Hello")
  end

  it "rewinds" do
    str = IO::Memory.new("hello\nworld\n")
    io = BufferedWrapper.new str
    io.gets.should eq("hello")
    io.rewind
    io.gets.should eq("hello")
  end

  it "reads more than the buffer's internal capacity" do
    s = String.build do |str|
      900.times do
        10.times do |i|
          str << ('a' + i)
        end
      end
    end
    io = BufferedWrapper.new(IO::Memory.new(s))

    slice = Bytes.new(9000)
    count = io.read(slice)
    count.should eq(9000)

    900.times do
      10.times do |i|
        slice[i].should eq('a'.ord + i)
      end
    end
  end

  it "writes more than the buffer's internal capacity" do
    s = String.build do |str|
      900.times do
        10.times do |i|
          str << ('a' + i)
        end
      end
    end
    strio = IO::Memory.new
    strio << s
    strio.rewind
    io = BufferedWrapper.new(strio)
    io.write(s.to_slice)
    strio.rewind.gets_to_end.should eq(s)
  end

  it "does puts" do
    str = IO::Memory.new
    io = BufferedWrapper.new(str)
    io.puts "Hello"
    str.to_s.should eq("")
    io.flush
    str.to_s.should eq("Hello\n")
  end

  it "does puts with big string" do
    str = IO::Memory.new
    io = BufferedWrapper.new(str)
    s = "*" * 20_000
    io << "hello"
    io << s
    io.flush
    str.to_s.should eq("hello#{s}")
  end

  it "does puts many times" do
    str = IO::Memory.new
    io = BufferedWrapper.new(str)
    10_000.times { io << "hello" }
    io.flush
    str.to_s.should eq("hello" * 10_000)
  end

  describe "flush_on_newline" do
    it "flushes on \n" do
      str = IO::Memory.new
      io = BufferedWrapper.new(str)
      io.flush_on_newline = true

      io << "hello\nworld"
      str.to_s.should eq("hello\n")
      io.flush
      str.to_s.should eq("hello\nworld")
    end

    it "doesn't write past count" do
      str = IO::Memory.new
      io = BufferedWrapper.new(str)
      io.flush_on_newline = true

      slice = Slice.new(10) { |i| i == 9 ? '\n'.ord.to_u8 : ('a'.ord + i).to_u8 }
      io.write slice[0, 4]
      io.flush
      str.to_s.should eq("abcd")
    end
  end

  describe "sync" do
    it "syncs (write)" do
      str = IO::Memory.new

      io = BufferedWrapper.new(str)
      io.sync?.should be_false

      io.sync = true
      io.sync?.should be_true

      io.write_byte 1_u8

      str.rewind
      str.read_byte.should eq(1_u8)
    end
  end

  describe "read_buffering" do
    it "works with IO#read" do
      str = IO::Memory.new "abc"

      io = BufferedWrapper.new(str)
      io.read_buffering?.should be_true

      io.read_buffering = false
      io.read_buffering?.should be_false

      byte = Bytes.new(1)
      io.read_fully(byte)
      byte[0].should eq('a'.ord.to_u8)

      str.gets_to_end.should eq("bc")
    end

    it "works with IO#read (already buffered)" do
      str = IO::Memory.new
      str << "a" * DEFAULT_BUFFER_SIZE
      str.pos = 0

      io = BufferedWrapper.new(str)
      io.read_buffering?.should be_true

      io.buffer_size.times do
        byte = Bytes.new(1)
        io.read_fully(byte)
        byte[0].should eq('a'.ord.to_u8)
      end

      io.read_buffering = false
      io.read_buffering?.should be_false

      str << "bcde"
      str.pos -= 4

      byte = Bytes.new(1)
      io.read_fully(byte)
      byte[0].should eq('b'.ord.to_u8)

      str.gets_to_end.should eq("cde")
    end

    it "works with IO#read_byte" do
      str = IO::Memory.new "abc"

      io = BufferedWrapper.new(str)
      io.read_buffering?.should be_true

      io.read_buffering = false
      io.read_buffering?.should be_false

      io.read_byte.should eq('a'.ord.to_u8)

      str.gets_to_end.should eq("bc")
    end

    it "works with IO#read_byte (already buffered)" do
      str = IO::Memory.new
      str << "a" * DEFAULT_BUFFER_SIZE
      str.pos = 0

      io = BufferedWrapper.new(str)
      io.read_buffering?.should be_true

      io.buffer_size.times do
        io.read_byte.should eq('a'.ord.to_u8)
      end

      io.read_buffering = false
      io.read_buffering?.should be_false

      str << "bcde"
      str.pos -= 4

      io.read_byte.should eq('b'.ord.to_u8)

      str.gets_to_end.should eq("cde")
    end
  end

  it "shouldn't call unbuffered read if reading to an empty slice" do
    str = IO::Memory.new("foo")
    io = BufferedWrapper.new(str)
    io.read(Bytes[])
    io.called_unbuffered_read.should be_false
  end

  it "peeks" do
    str = IO::Memory.new("foo")
    io = BufferedWrapper.new(str)

    io.peek.should eq("foo".to_slice)

    # Peek doesn't advance
    io.gets_to_end.should eq("foo")

    # Returns EOF if no more data
    io.peek.should eq(Bytes.empty)
  end

  it "skips" do
    str = IO::Memory.new("123456789")
    io = BufferedWrapper.new(str)
    io.skip(3)
    io.read_char.should eq('4')
  end

  it "skips big" do
    str = IO::Memory.new(("a" * 10_000) + "b")
    io = BufferedWrapper.new(str)
    io.skip(10_000)
    io.read_char.should eq('b')
  end

  {% unless flag?(:without_iconv) %}
    describe "encoding" do
      describe "decode" do
        it "gets_to_end" do
          str = "Hello world" * 200
          base_io = IO::Memory.new(str.encode("UCS-2LE"))
          io = BufferedWrapper.new(base_io)
          io.set_encoding("UCS-2LE")
          io.gets_to_end.should eq(str)
        end

        it "gets" do
          str = "Hello world\nFoo\nBar\n" + ("1234567890" * 1000)
          base_io = IO::Memory.new(str.encode("UCS-2LE"))
          io = BufferedWrapper.new(base_io)
          io.set_encoding("UCS-2LE")
          io.gets.should eq("Hello world")
          io.gets.should eq("Foo")
          io.gets.should eq("Bar")
        end

        it "gets with chomp = false" do
          str = "Hello world\nFoo\nBar\n" + ("1234567890" * 1000)
          base_io = IO::Memory.new(str.encode("UCS-2LE"))
          io = BufferedWrapper.new(base_io)
          io.set_encoding("UCS-2LE")
          io.gets(chomp: false).should eq("Hello world\n")
          io.gets(chomp: false).should eq("Foo\n")
          io.gets(chomp: false).should eq("Bar\n")
        end

        it "gets big string" do
          str = "Hello\nWorld\n" * 10_000
          base_io = IO::Memory.new(str.encode("UCS-2LE"))
          io = BufferedWrapper.new(base_io)
          io.set_encoding("UCS-2LE")
          10_000.times do |i|
            io.gets(chomp: false).should eq("Hello\n")
            io.gets(chomp: false).should eq("World\n")
          end
        end

        it "gets big EUC-JP string" do
          str = ("好我是人\n" * 1000).encode("EUC-JP")
          base_io = IO::Memory.new(str)
          io = BufferedWrapper.new(base_io)
          io.set_encoding("EUC-JP")
          1000.times do
            io.gets(chomp: false).should eq("好我是人\n")
          end
        end

        it "reads char" do
          str = "x\nHello world" + ("1234567890" * 1000)
          base_io = IO::Memory.new(str.encode("UCS-2LE"))
          io = BufferedWrapper.new(base_io)
          io.set_encoding("UCS-2LE")
          io.gets(chomp: false).should eq("x\n")
          str = str[2..-1]
          str.each_char do |char|
            io.read_char.should eq(char)
          end
          io.read_char.should be_nil
        end
      end
    end
  {% end %}
end
