# frozen_string_literal: true
require 'rack'
require 'rack/handler/webrick'
require 'net/http'

# The code for this is inspired by Capybara's server:
#   http://github.com/jnicklas/capybara/blob/0.3.9/lib/capybara/server.rb
class LocalhostServer
  READY_MESSAGE = "Server ready"

  class Identify
    def initialize(app)
      @app = app
    end

    def call(env)
      if env["PATH_INFO"] == "/__identify__"
        [200, {}, [LocalhostServer::READY_MESSAGE]]
      else
        @app.call(env)
      end
    end
  end

  attr_reader :port

  def initialize(rack_app, port = nil)
    @port = port || find_available_port
    @rack_app = rack_app
    concurrently { boot }
    wait_until(10, "Boot failed.") { booted? }
  end

  private

  def find_available_port
    server = TCPServer.new('127.0.0.1', 0)
    server.addr[1]
  ensure
    server.close if server
  end

  def boot
    # Use WEBrick since it's part of the ruby standard library and is available on all ruby interpreters.
    options = { :Port => port }
    options.merge!(:AccessLog => [], :Logger => WEBrick::BasicLog.new(StringIO.new)) unless ENV['VERBOSE_SERVER']
    Rack::Handler::WEBrick.run(Identify.new(@rack_app), **options)
  end

  def booted?
    res = ::Net::HTTP.get_response("localhost", '/__identify__', port)
    if res.is_a?(::Net::HTTPSuccess) or res.is_a?(::Net::HTTPRedirection)
      return res.body == READY_MESSAGE
    end
  rescue Errno::ECONNREFUSED, Errno::EBADF
    return false
  end

  def concurrently
    if should_use_subprocess?
      pid = Process.fork do
        trap(:INT) { ::Rack::Handler::WEBrick.shutdown }
        yield
        exit # manually exit; otherwise this sub-process will re-run the specs that haven't run yet.
      end

      at_exit do
        Process.kill('INT', pid)
        begin
          Process.wait(pid)
        rescue Errno::ECHILD
          # ignore this error...I think it means the child process has already exited.
        end
      end
    else
      Thread.new { yield }
    end
  end

  def should_use_subprocess?
    # !ENV['THREADED']
    false
  end

  def wait_until(timeout, error_message, &block)
    start_time = Time.now

    while true
      return if yield
      raise TimeoutError.new(error_message) if (Time.now - start_time) > timeout
      sleep(0.05)
    end
  end
end

