require 'socket'

module SocketSpecs
  # helper to get the hostname associated to 127.0.0.1 or the given ip
  def self.hostname(ip = "127.0.0.1")
    # Calculate each time, without caching, since the result might
    # depend on things like do_not_reverse_lookup mode, which is
    # changing from test to test
    Socket.getaddrinfo(ip, nil)[0][2]
  end

  def self.hostname_reverse_lookup(ip = "127.0.0.1")
    Socket.getaddrinfo(ip, nil, 0, 0, 0, 0, true)[0][2]
  end

  def self.addr(which=:ipv4)
    case which
    when :ipv4
      host = "127.0.0.1"
    when :ipv6
      host = "::1"
    end
    Socket.getaddrinfo(host, nil)[0][3]
  end

  def self.reserved_unused_port
    # https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
    0
  end

  def self.sockaddr_in(port, host)
    Socket::SockAddr_In.new(Socket.sockaddr_in(port, host))
  end

  def self.socket_path
    path = tmp("unix.sock", false)
    # Check for too long unix socket path (max 104 bytes on macOS)
    # Note that Linux accepts not null-terminated paths but the man page advises against it.
    if path.bytesize > 104
      path = "/tmp/unix_server_spec.socket"
    end
    rm_socket(path)
    path
  end

  def self.rm_socket(path)
    File.delete(path) if File.exist?(path)
  end

  def self.ipv6_available?
    @ipv6_available ||= begin
      server = TCPServer.new('::1', 0)
    rescue Errno::EAFNOSUPPORT, Errno::EADDRNOTAVAIL, SocketError
      :no
    else
      server.close
      :yes
    end
    @ipv6_available == :yes
  end

  def self.each_ip_protocol
    describe 'using IPv4' do
      yield Socket::AF_INET, '127.0.0.1', 'AF_INET'
    end

    guard -> { SocketSpecs.ipv6_available? } do
      describe 'using IPv6' do
        yield Socket::AF_INET6, '::1', 'AF_INET6'
      end
    end
  end

  def self.loop_with_timeout(timeout = TIME_TOLERANCE)
    start = Process.clock_gettime(Process::CLOCK_MONOTONIC)

    while yield == :retry
      if Process.clock_gettime(Process::CLOCK_MONOTONIC) - start >= timeout
        raise RuntimeError, "Did not succeed within #{timeout} seconds"
      end
    end
  end

  def self.dest_addr_req_error
    error = Errno::EDESTADDRREQ
    platform_is :windows do
      error = Errno::ENOTCONN
    end
    error
  end

  # TCPServer echo server accepting one connection
  class SpecTCPServer
    attr_reader :hostname, :port

    def initialize
      @hostname = SocketSpecs.hostname
      @server = TCPServer.new @hostname, 0
      @port = @server.addr[1]

      log "SpecTCPServer starting on #{@hostname}:#{@port}"

      @thread = Thread.new do
        socket = @server.accept
        log "SpecTCPServer accepted connection: #{socket}"
        service socket
      end
    end

    def service(socket)
      begin
        data = socket.recv(1024)

        return if data.empty?
        log "SpecTCPServer received: #{data.inspect}"

        return if data == "QUIT"

        socket.send data, 0
      ensure
        socket.close
      end
    end

    def shutdown
      log "SpecTCPServer shutting down"
      @thread.join
      @server.close
    end

    def log(message)
      @logger.puts message if @logger
    end
  end

  # We need to find a free port for Socket.tcp_server_loop and Socket.udp_server_loop,
  # and the only reliable way to do that is to pass 0 as the port, but then we need to
  # find out which one was chosen and the API doesn't let us find what it is. So we
  # intercept one of the public API methods called by these methods.
  class ServerLoopPortFinder < Socket
    def self.tcp_server_sockets(*args)
      super(*args) { |sockets|
        @port = sockets.first.local_address.ip_port
        yield(sockets)
      }
    end

    def self.udp_server_sockets(*args, &block)
      super(*args) { |sockets|
        @port = sockets.first.local_address.ip_port
        yield(sockets)
      }
    end

    def self.cleanup
      @port = nil
    end

    def self.port
      sleep 0.001 until @port
      @port
    end
  end
end
