
require 'thread'
require 'yaml'
require 'timecop'

def reset_test_state
  FileUtils.mkdir_p 'tmp'
  FileUtils.rm_f 'tmp/state.yml'
  FileUtils.touch 'tmp/state.yml'
  $state_file_mutex = Mutex.new
  if ServerEngine.windows?
    open("tmp/daemon.rb", "w") do |f|
      f.puts <<-'end_of_script'
require "serverengine"
require "rspec"
$state_file_mutex = Mutex.new # TODO
require "server_worker_context"
include ServerEngine
command_pipe = STDIN.dup
STDIN.reopen(File::NULL)
Daemon.run_server(TestServer, TestWorker, command_pipe: command_pipe)
      end_of_script
    end

    open("tmp/supervisor.rb", "w") do |f|
      f.puts <<-'end_of_script'
require "serverengine"
require "rspec"
$state_file_mutex = Mutex.new # TODO
require "server_worker_context"
include ServerEngine
server = TestServer
worker = TestWorker
config = {command_pipe: STDIN.dup}
STDIN.reopen(File::NULL)
ARGV.each do |arg|
  case arg
  when /^server=(.*)$/
    server = Object.const_get($1)
  when /^worker=(.*)$/
    worker = Object.const_get($1)
  when /^(.*)=(\d+)$/
    config[$1.to_sym] = $2.to_i
  when /^(.*)=(.*)$/
    config[$1.to_sym] = $2
  else
    raise "Unknown parameter: [#{arg}]"
  end
end
sv = Supervisor.new(server, worker, config)
s = sv.create_server(nil)
s.install_signal_handlers
t = Thread.new{ s.main }
s.after_start
t.join
      end_of_script
    end
  end
end

def windows_daemon_cmdline
  if ServerEngine.windows?
    [ServerEngine.ruby_bin_path, '-I', File.dirname(__FILE__), 'tmp/daemon.rb']
  else
    nil
  end
end

def windows_supervisor_cmdline(server = nil, worker = nil, config = {})
  if ServerEngine.windows?
    cmd = [ServerEngine.ruby_bin_path, '-I', File.dirname(__FILE__), 'tmp/supervisor.rb']
    cmd << "server=#{server}" if server
    cmd << "worker=#{worker}" if worker
    config.each_pair do |k, v|
      cmd << "#{k}=#{v}"
    end
    cmd
  else
    nil
  end
end

def incr_test_state(key)
  File.open('tmp/state.yml', 'r+') do |f|
    f.flock(File::LOCK_EX)

    $state_file_mutex.synchronize do
      data = YAML.load(f.read) || {} rescue {}
      data[key] ||= 0
      data[key] += 1

      f.pos = 0
      f.write YAML.dump(data)
      data[key]
    end
  end
end

def test_state(key)
  data = YAML.load_file('tmp/state.yml') || {} rescue {}
  return data[key] || 0
end

module TestServer
  def initialize
    incr_test_state :server_initialize
  end

  def before_run
    incr_test_state :server_before_run
  end

  def after_run
    incr_test_state :server_after_run
  end

  def after_start
    incr_test_state :server_after_start
  end

  def stop(stop_graceful)
    incr_test_state :server_stop
    if stop_graceful
      incr_test_state :server_stop_graceful
    else
      incr_test_state :server_stop_immediate
    end
    super
  end

  def restart(stop_graceful)
    incr_test_state :server_restart
    if stop_graceful
      incr_test_state :server_restart_graceful
    else
      incr_test_state :server_restart_immediate
    end
    super
  end

  def reload
    incr_test_state :server_reload
    super
  end

  def detach
    incr_test_state :server_detach
    super
  end

  def dump
    incr_test_state :server_dump
    super
  end
end

module TestWorker
  def initialize
    incr_test_state :worker_initialize
    @stop_flag = ServerEngine::BlockingFlag.new
  end

  def before_fork
    incr_test_state :worker_before_fork
  end

  def run
    incr_test_state :worker_run
    # This means this worker will automatically finish after 50 seconds.
    10.times do
      # repeats multiple times because signal handlers
      # interrupts wait
      @stop_flag.wait(5.0)
    end
    @stop_flag.reset!
  end

  def stop
    incr_test_state :worker_stop
    @stop_flag.set!
  end

  def reload
    incr_test_state :worker_reload
  end

  def after_start
    incr_test_state :worker_after_start
  end

  def spawn(pm)
    script = <<-EOF
    class WorkerClass
      include TestWorker
      def run
        Thread.new do
          command_pipe = STDIN.dup
          STDIN.reopen(File::NULL)
          Thread.new do
            until @stop_flag.set?
              cmd = command_pipe.gets.chomp
              case cmd
              when "GRACEFUL_STOP", "IMMEDIATE_STOP"
                stop
              when "RELOAD"
                reload
              end
            end
          end
        end
        super
      end
    end
    $state_file_mutex = Mutex.new
    WorkerClass.new.run
    EOF
    cmdline = [ServerEngine.ruby_bin_path] + %w[-rbundler/setup -rrspec -I. -Ispec -rserverengine -r] + [__FILE__] + %w[-e] + [script]
    pm.spawn(*cmdline)
  end
end

module RunErrorWorker
  def run
    incr_test_state :worker_run
    raise StandardError, "error test"
  end
end

module TestExitWorker
  def initialize
    @stop_flag = BlockingFlag.new
    @worker_num = incr_test_state :worker_initialize
    @exit_code = case @worker_num
                 when 1 then 5
                 when 4 then 3
                 else 4
                 end
  end

  def run
    incr_test_state :worker_run
    exit_at = Time.now + @worker_num * 2
    until @stop_flag.wait(0.1)
      exit!(@exit_code) if Time.now >= exit_at
    end
    incr_test_state :worker_finished
  end

  def stop
    incr_test_state :worker_stop
    @stop_flag.set!
  end
end

shared_context 'test server and worker' do
  before { reset_test_state }
  after { Timecop.return }

  unless self.const_defined?(:WAIT_RATIO)
    if ServerEngine.windows?
      WAIT_RATIO = 2
    else
      WAIT_RATIO = 1
    end
  end

  def wait_for_fork
    sleep 1.5 * WAIT_RATIO
  end

  def wait_for_stop
    sleep 0.8 * WAIT_RATIO
  end

  def wait_for_restart
    sleep 1.5 * WAIT_RATIO
  end
end
