require "socket"

module RedisMock
  class Server
    def initialize(options = {}, &block)
      @server = TCPServer.new(options[:host] || "127.0.0.1", 0)
      @server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
    end

    def port
      @server.addr[1]
    end

    def start(&block)
      @thread = Thread.new { run(&block) }
    end

    def shutdown
      @thread.kill
    end

    def run
      begin
        loop do
          session = @server.accept

          begin
            return if yield(session) == :exit
          ensure
            session.close
          end
        end
      rescue => ex
        $stderr.puts "Error running mock server: #{ex.message}"
        $stderr.puts ex.backtrace
        retry
      ensure
        @server.close
      end
    end
  end

  # Starts a mock Redis server in a thread.
  #
  # The server will use the lambda handler passed as argument to handle
  # connections. For example:
  #
  #   handler = lambda { |session| session.close }
  #   RedisMock.start_with_handler(handler) do
  #     # Every connection will be closed immediately
  #   end
  #
  def self.start_with_handler(blk, options = {})
    server = Server.new(options)
    port = server.port

    begin
      server.start(&blk)
      yield(port)
    ensure
      server.shutdown
    end
  end

  # Starts a mock Redis server in a thread.
  #
  # The server will reply with a `+OK` to all commands, but you can
  # customize it by providing a hash. For example:
  #
  #   RedisMock.start(:ping => lambda { "+PONG" }) do |port|
  #     assert_equal "PONG", Redis.new(:port => port).ping
  #   end
  #
  def self.start(commands, options = {}, &blk)
    handler = lambda do |session|
      while line = session.gets
        argv = Array.new(line[1..-3].to_i) do
          bytes = session.gets[1..-3].to_i
          arg = session.read(bytes)
          session.read(2) # Discard \r\n
          arg
        end

        command = argv.shift
        blk = commands[command.to_sym]
        blk ||= lambda { |*_| "+OK" }

        response = blk.call(*argv)

        # Convert a nil response to :close
        response ||= :close

        if response == :exit
          break :exit
        elsif response == :close
          break :close
        elsif response.is_a?(Array)
          session.write("*%d\r\n" % response.size)

          response.each do |resp|
            if resp.is_a?(Array)
              session.write("*%d\r\n" % resp.size)
              resp.each do |r|
                session.write("$%d\r\n%s\r\n" % [r.length, r])
              end
            else
              session.write("$%d\r\n%s\r\n" % [resp.length, resp])
            end
          end
        else
          session.write(response)
          session.write("\r\n") unless response.end_with?("\r\n")
        end
      end
    end

    start_with_handler(handler, options, &blk)
  end
end
