$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../../lib"

require_relative '../common'
require 'mocha/minitest'
require 'pty'
require 'expect'

module IntegrationTestHelpers
  VERBOSE = false
  def sh(command)
    puts "$ #{command}" if VERBOSE
    res = system(command)
    status = $?
    raise "Command: #{command} failed:#{status.exitstatus}" unless res
  end

  def tmpdir(&block)
    Dir.mktmpdir do |dir|
      yield(dir)
    end
  end

  def sshd_8_or_later?
    !!(`sshd  -v 2>&1 |grep 'OpenSSH_'` =~ /OpenSSH_8./)
  end

  def ssh_keygen(file, type = 'rsa', password = '')
    sh "rm -rf #{file} #{file}.pub"
    sh "ssh-keygen #{ssh_keygen_format} -q -f #{file} -t #{type} -N '#{password}'"
  end

  def ssh_keygen_format
    if Net::SSH::Authentication::ED25519Loader::LOADED
      ""
    else
      "-m PEM"
    end
  end

  def set_authorized_key(user, pubkey)
    authorized_key = "/home/#{user}/.ssh/authorized_keys"
    sh "sudo cp #{pubkey} #{authorized_key}"
    sh "sudo chown #{user} #{authorized_key}"
    sh "sudo chmod 0744 #{authorized_key}"
  end

  def sign_user_key(user, pubkey)
    cert = "/etc/ssh/users_ca"
    sh "sudo ssh-keygen -s #{cert} -I user_#{user} -n #{user} -V +52w #{pubkey}"
  end

  def with_agent(&block)
    puts "/usr/bin/ssh-agent -c" if VERBOSE
    agent_out = `/usr/bin/ssh-agent -c`
    agent_out.split("\n").each do |line|
      if line =~ /setenv (\S+) (\S+);/
        ENV[$1] = $2
        puts "ENV[#{$1}]=#{$2}" if VERBOSE
      end
    end
    begin
      yield
    ensure
      sh "/usr/bin/ssh-agent -k > /dev/null"
    end
  end

  def ssh_add(key, password)
    command = "ssh-add #{key}"
    status = nil
    PTY.spawn(command) do |reader, writer, pid|
      begin
        reader.expect(/Enter passphrase for .*:/) { |data| puts data }
        writer.puts(password)
        until reader.eof? do
          line = reader.readline
          puts line if VERBOSE
        end
      rescue Errno::EIO => _e
      end
      pid, status = Process.wait2 pid
    end
    raise "Command: #{command} failed:#{status.exitstatus}" unless status

    status.exitstatus
  end

  def with_sshd_config(sshd_config, &block)
    raise "Failed to copy config" unless system("sudo cp -f /etc/ssh/sshd_config /etc/ssh/sshd_config.original")

    begin
      Tempfile.open('sshd_config') do |f|
        f.write(sshd_config)
        f.close
        system("sudo cp -f #{f.path} /etc/ssh/sshd_config")
      end
      system("sudo chmod 0644 /etc/ssh/sshd_config")
      raise "Failed to restart sshd" unless system("sudo service ssh restart")

      yield
    ensure
      system("sudo cp -f /etc/ssh/sshd_config.original /etc/ssh/sshd_config")
      system("sudo service ssh restart")
    end
  end

  def with_lines_as_tempfile(lines = [], add_pid: true, debug: false, &block)
    Tempfile.open('sshd_config') do |f|
      f.write(lines.join("\n"))
      pidpath = nil
      if add_pid
        pidpath = f.path + '.pid'
        f.write("\nPidFile #{pidpath}\n")
      end
      f.write("\nLogLevel DEBUG3\n") if debug
      f.close
      puts "CONFIG: #{f.path} PID: #{pidpath}" if debug
      yield(f.path, pidpath)
    end
  end

  def port_open?(path)
    Socket.tcp("localhost", 10567, connect_timeout: 1) { true } rescue false # rubocop:disable Style/RescueModifier
  end

  # @yield [pid, port]
  def start_sshd_7_or_later(port = '2200', config: nil, debug: false)
    pid = nil
    sshpidfile = nil
    sshlogpath = nil
    if config
      with_lines_as_tempfile(config, debug: debug) do |path, pidpath|
        puts "DEBUG - SSH LOG: #{path}-log.txt config: #{path}" if debug
        raise "A leftover sshd is already running" if port_open?(port)

        extra_params = []
        sshlogpath = "#{path}-log.txt"
        extra_params = ['-E', sshlogpath]
        pid = spawn('sudo', '/opt/net-ssh-openssh/sbin/sshd', '-D', '-f', path, '-p', port, *extra_params)
        sshpidfile = pidpath

        yield pid, port
      end
    else
      with_lines_as_tempfile(['']) do |path, pidpath|
        pid = spawn('sudo', '/opt/net-ssh-openssh/sbin/sshd', '-D', '-f', path, '-p', port)
        sshpidfile = pidpath
        yield pid, port
      end
    end
  ensure
    # Our pid is sudo and not sshd, -9 (KILL) on sudo will not clean up its children
    # properly, so we just have to hope that -15 (TERM) will manage to bring
    # down sshd.
    if sshpidfile
      if !File.exist?(sshpidfile) && !sshlogpath.nil?
        loglines = `sudo tail -n 10 #{sshlogpath}`.split("\n")
        puts "ERROR: sshpidfile #{sshpidfile} does not exist\n\nSSH server logs:\n#{loglines.join("\n")}"
      end
      sshpid = File.read(sshpidfile).strip
      system('sudo', 'kill', '-15', sshpid.to_s)
      begin
        Timeout.timeout(20) do
          Process.wait(pid)
        end
      rescue Timeout::Error
        warn "Failed to kill openssh process: #{sshpid}"
        system('sudo', 'kill', '-9', sshpid.to_s)
        raise
      end
    elsif pid
      system('sudo', 'kill', '-15', pid.to_s)
      begin
        Timeout.timeout(20) do
          Process.wait(pid)
        end
      rescue Timeout::Error
        warn "Failed to kill openssh process: #{pid}"
        system('sudo', 'kill', '-9', pid.to_s)
        raise
      end
    end
  end

  def localhost
    'localhost'
  end

  def user
    'net_ssh_1'
  end
end
