require 'socket'
require 'timeout'
require 'fileutils'

class RedisInstance
  class << self
    @running = false
    @port = nil
    @pid = nil

    def run_if_needed!
      run! unless running?
    end

    def run!
      ensure_redis_server_present!
      ensure_pid_directory
      reassign_redis_clients
      start_redis_server

      if $?.success?
        wait_for_pid
        puts "Booted isolated Redis on port #{port} with PID #{pid}."

        wait_for_redis_boot

        # Ensure we tear down Redis on Ctrl+C / test failure.
        at_exit { stop! }
      else
        fail "Failed to start Redis on port #{port}."
      end

      @running = true
    end

    def stop!
      $stdout.puts "Sending TERM to Redis (#{pid})..."
      Process.kill('TERM', pid)

      @port = nil
      @running = false
      @pid = nil
    end

    def running?
      @running
    end

    private

    def ensure_redis_server_present!
      if !system('redis-server -v')
        fail "** can't find `redis-server` in your path"
      end
    end

    def wait_for_redis_boot
      Timeout::timeout(10) do
        begin
          while Resque.redis.ping != 'PONG'
          end
        rescue
          # silence all errors
        end
      end
    end

    def ensure_pid_directory
      FileUtils.mkdir_p(File.dirname(pid_file))
    end

    def reassign_redis_clients
      Resque.redis = Redis.new(:hostname => '127.0.0.1', :port => port, :thread_safe => true)
    end

    def start_redis_server
      IO.popen("redis-server -", "w+") do |server|
        server.write(config)
        server.close_write
      end
    end

    def pid
      @pid ||= File.read(pid_file).to_i
    end

    def wait_for_pid
      Timeout::timeout(10) do
        while !File.exist?(pid_file)
        end
      end
    end

    def port
      @port ||= random_port
    end

    def pid_file
      "/tmp/redis-scheduler-test.pid"
    end

    def config
      <<-EOF
        daemonize yes
        pidfile #{pid_file}
        port #{port}
      EOF
    end

    # Returns a random port in the upper (10000-65535) range.
    def random_port
      ports = (10000..65535).to_a

      loop do
        port = ports[rand(ports.size)]
        return port if port_available?('127.0.0.1', port)
      end
    end

    def port_available?(ip, port, seconds=1)
      Timeout::timeout(seconds) do
        begin
          TCPSocket.new(ip, port).close
          false
        rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
          true
        end
      end
    rescue Timeout::Error
      true
    end
  end
end