require 'mspec/guards/platform'
require 'mspec/helpers/tmp'

# The ruby_exe helper provides a wrapper for invoking the
# same Ruby interpreter with the same flags as the one running
# the specs and getting the output from running the code.
#
# If +code+ is a file that exists, it will be run.
# Otherwise, +code+ will be written to a temporary file and be run.
# For example:
#
#   ruby_exe('path/to/some/file.rb')
#
# will be executed as
#
#   `#{RUBY_EXE} 'path/to/some/file.rb'`
#
# The ruby_exe helper also accepts an options hash with four
# keys: :options, :args :env and :exception.
#
# For example:
#
#   ruby_exe('file.rb', :options => "-w",
#                       :args => "arg1 arg2",
#                       :env => { :FOO => "bar" })
#
# will be executed as
#
#   `#{RUBY_EXE} -w file.rb arg1 arg2`
#
# with access to ENV["FOO"] with value "bar".
#
# When `exception: false` and Ruby command fails then exception will not be
# raised.
#
# If +nil+ is passed for the first argument, the command line
# will be built only from the options hash.
#
# If no arguments are passed to ruby_exe, it returns an Array
# containing the interpreter executable and the flags:
#
#    spawn(*ruby_exe, "-e", "puts :hello")
#
# This avoids spawning an extra shell, and ensure the pid returned by spawn
# corresponds to the ruby process and not the shell.
#
# The RUBY_EXE constant is setup by mspec automatically
# and is used by ruby_exe and ruby_cmd. The mspec runner script
# will set ENV['RUBY_EXE'] to the name of the executable used
# to invoke the mspec-run script.
#
# The value will only be used if the file exists and is executable.
# The flags will then be appended to the resulting value, such that
# the RUBY_EXE constant contains both the executable and the flags.
#
# Additionally, the flags passed to mspec
# (with -T on the command line or in the config with set :flags)
# will be appended to RUBY_EXE so that the interpreter
# is always called with those flags.
#
# Failure of a Ruby command leads to raising exception by default.

def ruby_exe_options(option)
  case option
  when :env
    ENV['RUBY_EXE']
  when :engine
    case RUBY_ENGINE
    when 'rbx'
      "bin/rbx"
    when 'jruby'
      "bin/jruby"
    when 'maglev'
      "maglev-ruby"
    when 'topaz'
      "topaz"
    when 'ironruby'
      "ir"
    end
  when :name
    require 'rbconfig'
    bin = RUBY_ENGINE + (RbConfig::CONFIG['EXEEXT'] || '')
    File.join(".", bin)
  when :install_name
    require 'rbconfig'
    bin = RbConfig::CONFIG["RUBY_INSTALL_NAME"] || RbConfig::CONFIG["ruby_install_name"]
    bin << (RbConfig::CONFIG['EXEEXT'] || '')
    File.join(RbConfig::CONFIG['bindir'], bin)
  end
end

def resolve_ruby_exe
  [:env, :engine, :name, :install_name].each do |option|
    next unless exe = ruby_exe_options(option)

    if File.file?(exe) and File.executable?(exe)
      exe = File.expand_path(exe)
      exe = exe.tr('/', '\\') if PlatformGuard.windows?
      flags = ENV['RUBY_FLAGS']
      if flags and !flags.empty?
        return exe + ' ' + flags
      else
        return exe
      end
    end
  end
  raise Exception, "Unable to find a suitable ruby executable."
end

unless Object.const_defined?(:RUBY_EXE) and RUBY_EXE
  RUBY_EXE = resolve_ruby_exe
end

def ruby_exe(code = :not_given, opts = {})
  skip "WASI doesn't provide subprocess" if PlatformGuard.wasi?

  if opts[:dir]
    raise "ruby_exe(..., dir: dir) is no longer supported, use Dir.chdir"
  end

  if code == :not_given
    return RUBY_EXE.split(' ')
  end

  env = opts[:env] || {}
  saved_env = {}
  env.each do |key, value|
    key = key.to_s
    saved_env[key] = ENV[key] if ENV.key? key
    ENV[key] = value
  end

  escape = opts.delete(:escape)
  if code and !File.exist?(code) and escape != false
    tmpfile = tmp("rubyexe.rb")
    File.open(tmpfile, "w") { |f| f.write(code) }
    code = tmpfile
  end

  expected_status = opts.fetch(:exit_status, 0)

  begin
    command = ruby_cmd(code, opts)

    # Try to avoid the extra shell for 2>&1
    # This is notably useful for TimeoutAction which can then signal the ruby subprocess and not the shell
    popen_options = []
    if command.end_with?(' 2>&1')
      command = command[0...-5]
      popen_options = [{ err: [:child, :out] }]
    end

    output = IO.popen(command, *popen_options) do |io|
      pid = io.pid
      MSpec.subprocesses << pid
      begin
        io.read
      ensure
        MSpec.subprocesses.delete(pid)
      end
    end

    status = Process.last_status

    exit_status = if status.exited?
                    status.exitstatus
                  elsif status.signaled?
                    signame = Signal.signame status.termsig
                    raise "No signal name?" unless signame
                    :"SIG#{signame}"
                  else
                    raise SpecExpectationNotMetError, "#{exit_status.inspect} is neither exited? nor signaled?"
                  end
    if exit_status != expected_status
      formatted_output = output.lines.map { |line| "  #{line}" }.join
      raise SpecExpectationNotMetError,
        "Expected exit status is #{expected_status.inspect} but actual is #{exit_status.inspect} for command ruby_exe(#{command.inspect})\nOutput:\n#{formatted_output}"
    end

    output
  ensure
    saved_env.each { |key, value| ENV[key] = value }
    env.keys.each do |key|
      key = key.to_s
      ENV.delete key unless saved_env.key? key
    end
    File.delete tmpfile if tmpfile
  end
end

def ruby_cmd(code, opts = {})
  body = code

  if opts[:escape]
    raise "escape: true is no longer supported in ruby_cmd, use ruby_exe or a fixture"
  end

  if code and !File.exist?(code)
    body = "-e #{code.inspect}"
  end

  command = [RUBY_EXE, opts[:options], body, opts[:args]].compact.join(' ')
  STDERR.puts "\nruby_cmd: #{command}" if ENV["DEBUG_MSPEC_RUBY_CMD"] == "true"
  command
end
