# A fake Excon::Socket that allows passing in an arbitrary backend @socket
class MockExconSocket < Excon::Socket
  attr_reader :read_count

  def initialize(backend_socket, *args)
    super(*args)
    @read_count = 0
    @socket = backend_socket
  end

  def read_nonblock(*args)
    @read_count += 1
    super
  end

  def connect
    # pass
  end

  def select_with_timeout(*args)
    # don't actually wait, assume we're ready
  end
end

# A socket whose read_nonblock returns from an input list,
# and which counts the number of reads
class MockNonblockRubySocket
  attr_reader :out_buffers, :sequence

  def initialize(nonblock_reads, max_read_size = nil)
    @max_read_size = max_read_size || Float::INFINITY
    @nonblock_reads = nonblock_reads
    @out_buffers = []
    @sequence = []
  end

  def read_nonblock(maxlen, buffer = nil)
    @out_buffers << buffer

    if @nonblock_reads.empty?
      @sequence << 'EOF'
      raise EOFError
    elsif @nonblock_reads.first.empty?
      @nonblock_reads.shift
      if @nonblock_reads.empty?
        @sequence << 'EOF'
        raise EOFError
      end
      @sequence << 'EAGAIN'
      raise Errno::EAGAIN
    else
      len = [maxlen || @nonblock_reads.first.length, @max_read_size].min
      ret = @nonblock_reads.first.slice!(0, len)
      @sequence << ret.length

      if buffer
        buffer.clear
        buffer << ret
        buffer
      else
        ret
      end
    end
  end

  # Returns the results of `block`, as well as how many times we called read on the Excon
  # socket, and the sequence of reads on the backend socket
  def self.check_reads(nonblock_reads, socket_args, &block)
    backend_socket = MockNonblockRubySocket.new(nonblock_reads)
    socket = MockExconSocket.new(backend_socket, { nonblock: true }.merge(socket_args))
    ret = block[socket]
    [ret, socket.read_count, backend_socket.sequence]
  end
end

class MockBlockingRubySocket
  def initialize(error)
    @error = error
  end

  def read(_max_length)
    raise @error
  end
end

Shindo.tests('socket') do
  tests('read_block re-raises SSL errors').raises(OpenSSL::SSL::SSLError) do
    error = OpenSSL::SSL::SSLError.new('SSL error')
    MockExconSocket.new(MockBlockingRubySocket.new(error)).read
  end

  CHUNK_SIZES = [nil, 512]
  CHUNK_SIZES.each do |chunk_size|
    tests("chunk_size: #{chunk_size}") do
      socket_args = {chunk_size: chunk_size}
      tests('read_nonblock') do
        tests('readline nonblock is efficient') do
          returns(["one\n", 1, [8, 'EOF']]) do
            MockNonblockRubySocket.check_reads(["one\ntwo\n"], socket_args) do |sock|
              sock.readline
            end
          end
        end

        tests('readline nonblock works sequentially') do
          returns([["one\n", "two\n"], 1, [8, 'EOF']]) do
            MockNonblockRubySocket.check_reads(["one\ntwo\n"], socket_args) do |sock|
              Array.new(2) { sock.readline }
            end
          end
        end

        tests('readline nonblock can handle partial reads') do
          returns([["one\n", "two\n"], 2, [5, 'EAGAIN', 3, 'EOF']]) do
            MockNonblockRubySocket.check_reads(["one\nt", "wo\n"], socket_args) do |sock|
              Array.new(2) { sock.readline }
            end
          end
        end

        tests('readline nonblock before read') do
          returns([["one\n", "two\n"], 2, [8, 'EOF']]) do
          MockNonblockRubySocket.check_reads(["one\ntwo\n"], socket_args) do |sock|
              [sock.readline, sock.read(6)]
            end
          end
        end

        tests('read_nonblock does not EOF early') do
          returns([["one", "two"], 2, [3, 'EAGAIN', 3, 'EOF']]) do
            # Data, EAGAIN, data, EOF
            MockNonblockRubySocket.check_reads(["one", "two"], socket_args) do |sock|
              [sock.read, sock.read]
            end
          end
        end
      end
    end
  end

  tests('read(max_length) reuses a binary append buffer').returns(true) do
    backend_socket = MockNonblockRubySocket.new(["abcd\xFFf".b], 2)
    socket = MockExconSocket.new(backend_socket, nonblock: true)
    result = socket.read(6)
    append_buffers = backend_socket.out_buffers[1..]
    scratch = append_buffers.first

    result == "abcd\xFFf".b && result.encoding == Encoding::BINARY &&
      append_buffers.length == 2 &&
      append_buffers.all? { |buffer| buffer && buffer.equal?(scratch) && buffer.encoding == Encoding::BINARY }
  end

  tests('read(nil) reuses a binary append buffer').returns(true) do
    backend_socket = MockNonblockRubySocket.new(["ab\xFF".b], 2)
    socket = MockExconSocket.new(backend_socket, nonblock: true, chunk_size: 512)
    result = socket.read(nil)
    append_buffers = backend_socket.out_buffers[1..]
    scratch = append_buffers.first

    result == "ab\xFF".b &&
      append_buffers.length == 2 &&
      append_buffers.all? { |buffer| buffer && buffer.equal?(scratch) && buffer.encoding == Encoding::BINARY }
  end
end
