# frozen_string_literal: true

require "open3"

module Spec
  module Helpers
    def reset!
      Dir.glob("#{tmp}/{gems/*,*}", File::FNM_DOTMATCH).each do |dir|
        next if %w[base remote1 gems rubygems . ..].include?(File.basename(dir))
        if ENV["BUNDLER_SUDO_TESTS"]
          `sudo rm -rf "#{dir}"`
        else
          FileUtils.rm_rf(dir)
        end
      end
      FileUtils.mkdir_p(home)
      FileUtils.mkdir_p(tmpdir)
      Bundler.reset!
      Bundler.ui = nil
      Bundler.ui # force it to initialize
    end

    def self.bang(method)
      define_method("#{method}!") do |*args, &blk|
        send(method, *args, &blk).tap do
          unless last_command.success?
            raise RuntimeError,
              "Invoking #{method}!(#{args.map(&:inspect).join(", ")}) failed:\n#{last_command.stdboth}",
              caller.drop_while {|bt| bt.start_with?(__FILE__) }
          end
        end
      end
    end

    def the_bundle(*args)
      TheBundle.new(*args)
    end

    def last_command
      @command_executions.last || raise("There is no last command")
    end

    def out
      last_command.stdout
    end

    def err
      last_command.stderr
    end

    MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/.freeze

    def err_without_deprecations
      err.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "")
    end

    def deprecations
      err.split("\n").select {|l| l =~ MAJOR_DEPRECATION }.join("\n").split(MAJOR_DEPRECATION)
    end

    def exitstatus
      last_command.exitstatus
    end

    def in_app_root(&blk)
      Dir.chdir(bundled_app, &blk)
    end

    def in_app_root2(&blk)
      Dir.chdir(bundled_app2, &blk)
    end

    def in_app_root_custom(root, &blk)
      Dir.chdir(root, &blk)
    end

    def run(cmd, *args)
      opts = args.last.is_a?(Hash) ? args.pop : {}
      groups = args.map(&:inspect).join(", ")
      setup = "require 'bundler' ; Bundler.setup(#{groups})\n"
      ruby(setup + cmd, opts)
    end
    bang :run

    def load_error_run(ruby, name, *args)
      cmd = <<-RUBY
        begin
          #{ruby}
        rescue LoadError => e
          $stderr.puts "ZOMG LOAD ERROR" if e.message.include?("-- #{name}")
        end
      RUBY
      opts = args.last.is_a?(Hash) ? args.pop : {}
      args += [opts]
      run(cmd, *args)
    end

    def lib
      root.join("lib")
    end

    def spec
      spec_dir.to_s
    end

    def bundle(cmd, options = {})
      with_sudo = options.delete(:sudo)
      sudo = with_sudo == :preserve_env ? "sudo -E" : "sudo" if with_sudo

      bundle_bin = options.delete("bundle_bin") || bindir.join("bundle")

      if system_bundler = options.delete(:system_bundler)
        bundle_bin = system_bundle_bin_path
      end

      env = options.delete(:env) || {}
      env["PATH"].gsub!("#{Path.root}/exe", "") if env["PATH"] && system_bundler

      requires = options.delete(:requires) || []
      requires << "support/hax"

      artifice = options.delete(:artifice) do
        if RSpec.current_example.metadata[:realworld]
          "vcr"
        else
          "fail"
        end
      end
      if artifice
        requires << File.expand_path("../artifice/#{artifice}", __FILE__)
      end

      requires_str = requires.map {|r| "-r#{r}" }.join(" ")

      load_path = []
      load_path << lib unless system_bundler
      load_path << spec
      load_path_str = "-I#{load_path.join(File::PATH_SEPARATOR)}"

      args = options.map do |k, v|
        case v
        when nil
          next
        when true
          " --#{k}"
        when false
          " --no-#{k}"
        else
          " --#{k} #{v}"
        end
      end.join

      cmd = "#{sudo} #{Gem.ruby} #{load_path_str} #{requires_str} #{bundle_bin} #{cmd}#{args}"
      sys_exec(cmd, env) {|i, o, thr| yield i, o, thr if block_given? }
    end
    bang :bundle

    def forgotten_command_line_options(options)
      remembered = Bundler::VERSION.split(".", 2).first == "2"
      options = options.map do |k, v|
        k = Array(k)[remembered ? 0 : -1]
        v = '""' if v && v.to_s.empty?
        [k, v]
      end
      return Hash[options] if remembered
      options.each do |k, v|
        if v.nil?
          bundle! "config unset #{k}"
        else
          bundle! "config set --local #{k} #{v}"
        end
      end
      {}
    end

    def bundler(cmd, options = {})
      options["bundle_bin"] = bindir.join("bundler")
      bundle(cmd, options)
    end

    def ruby(ruby, options = {})
      env = (options.delete(:env) || {}).map {|k, v| "#{k}='#{v}' " }.join
      ruby = ruby.gsub(/["`\$]/) {|m| "\\#{m}" }
      lib_option = options[:no_lib] ? "" : " -I#{lib}"
      sys_exec(%(#{env}#{Gem.ruby}#{lib_option} -e "#{ruby}"))
    end
    bang :ruby

    def load_error_ruby(ruby, name, opts = {})
      ruby(<<-R)
        begin
          #{ruby}
        rescue LoadError => e
          $stderr.puts "ZOMG LOAD ERROR"# if e.message.include?("-- #{name}")
        end
      R
    end

    def gembin(cmd)
      lib = File.expand_path("../../../lib", __FILE__)
      old = ENV["RUBYOPT"]
      ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -I#{lib}"
      cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/")
      sys_exec(cmd.to_s)
    ensure
      ENV["RUBYOPT"] = old
    end

    def gem_command(command, args = "", options = {})
      if command == :exec && !options[:no_quote]
        args = args.gsub(/(?=")/, "\\")
        args = %("#{args}")
      end
      gem = ENV["BUNDLE_GEM"] || "#{Gem.ruby} -rrubygems -S gem --backtrace"
      sys_exec("#{gem} #{command} #{args}")
    end
    bang :gem_command

    def rake
      "#{Gem.ruby} -S #{ENV["GEM_PATH"]}/bin/rake"
    end

    def sys_exec(cmd, env = {})
      command_execution = CommandExecution.new(cmd.to_s, Dir.pwd)

      env = env.map {|k, v| [k.to_s, v.to_s] }.to_h # convert env keys and values to string

      Open3.popen3(env, cmd.to_s) do |stdin, stdout, stderr, wait_thr|
        yield stdin, stdout, wait_thr if block_given?
        stdin.close

        command_execution.stdout = Thread.new { stdout.read }.value.strip
        command_execution.stderr = Thread.new { stderr.read }.value.strip
        command_execution.exitstatus = wait_thr && wait_thr.value.exitstatus
      end

      (@command_executions ||= []) << command_execution

      command_execution.stdout
    end
    bang :sys_exec

    def config(config = nil, path = bundled_app(".bundle/config"))
      return YAML.load_file(path) unless config
      FileUtils.mkdir_p(File.dirname(path))
      File.open(path, "w") do |f|
        f.puts config.to_yaml
      end
      config
    end

    def global_config(config = nil)
      config(config, home(".bundle/config"))
    end

    def create_file(*args)
      path = bundled_app(args.shift)
      path = args.shift if args.first.is_a?(Pathname)
      str  = args.shift || ""
      path.dirname.mkpath
      File.open(path.to_s, "w") do |f|
        f.puts strip_whitespace(str)
      end
    end

    def gemfile(*args)
      contents = args.shift

      if contents.nil?
        File.open("Gemfile", "r", &:read)
      else
        create_file("Gemfile", contents, *args)
      end
    end

    def lockfile(*args)
      contents = args.shift

      if contents.nil?
        File.open("Gemfile.lock", "r", &:read)
      else
        create_file("Gemfile.lock", normalize_uri_file(contents), *args)
      end
    end

    def strip_whitespace(str)
      # Trim the leading spaces
      spaces = str[/\A\s+/, 0] || ""
      str.gsub(/^#{spaces}/, "")
    end

    def normalize_uri_file(str)
      # URI::File of Ruby 2.6 normalize localhost variable with file protocol.
      if defined?(URI::File)
        str.gsub(%r{file:\/\/localhost}, "file://")
      else
        str
      end
    end

    def install_gemfile(*args)
      gemfile(*args)
      opts = args.last.is_a?(Hash) ? args.last : {}
      opts[:retry] ||= 0
      bundle :install, opts
    end
    bang :install_gemfile

    def lock_gemfile(*args)
      gemfile(*args)
      opts = args.last.is_a?(Hash) ? args.last : {}
      opts[:retry] ||= 0
      bundle :lock, opts
    end

    def install_gems(*gems)
      options = gems.last.is_a?(Hash) ? gems.pop : {}
      gem_repo = options.fetch(:gem_repo) { gem_repo1 }
      gems.each do |g|
        path = if g == :bundler
          if ruby_core?
            spec = Gem::Specification.load(gemspec.to_s)
            spec.bindir = "libexec"
            File.open(root.join("bundler.gemspec").to_s, "w") {|f| f.write spec.to_ruby }
            Dir.chdir(root) { gem_command! :build, root.join("bundler.gemspec").to_s }
            FileUtils.rm(root.join("bundler.gemspec"))
          else
            Dir.chdir(root) { gem_command! :build, gemspec.to_s }
          end
          bundler_path = root + "bundler-#{Bundler::VERSION}.gem"
        elsif g.to_s =~ %r{\A(?:[A-Z]:)?/.*\.gem\z}
          g
        else
          "#{gem_repo}/gems/#{g}.gem"
        end

        raise "OMG `#{path}` does not exist!" unless File.exist?(path)

        gem_command! :install, "--no-document --ignore-dependencies '#{path}'"

        bundler_path && bundler_path.rmtree
      end
    end

    alias_method :install_gem, :install_gems

    def with_gem_path_as(path)
      backup = ENV.to_hash
      ENV["GEM_HOME"] = path.to_s
      ENV["GEM_PATH"] = path.to_s
      ENV["BUNDLER_ORIG_GEM_PATH"] = nil
      yield
    ensure
      ENV.replace(backup)
    end

    def with_path_as(path)
      backup = ENV.to_hash
      ENV["PATH"] = path.to_s
      ENV["BUNDLER_ORIG_PATH"] = nil
      yield
    ensure
      ENV.replace(backup)
    end

    def with_path_added(path)
      with_path_as(path.to_s + ":" + ENV["PATH"]) do
        yield
      end
    end

    def break_git!
      FileUtils.mkdir_p(tmp("broken_path"))
      File.open(tmp("broken_path/git"), "w", 0o755) do |f|
        f.puts "#!/usr/bin/env ruby\nSTDERR.puts 'This is not the git you are looking for'\nexit 1"
      end

      ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}"
    end

    def with_fake_man
      FileUtils.mkdir_p(tmp("fake_man"))
      File.open(tmp("fake_man/man"), "w", 0o755) do |f|
        f.puts "#!/usr/bin/env ruby\nputs ARGV.inspect\n"
      end
      with_path_added(tmp("fake_man")) { yield }
    end

    def system_gems(*gems)
      opts = gems.last.is_a?(Hash) ? gems.last : {}
      path = opts.fetch(:path, system_gem_path)
      if path == :bundle_path
        path = ruby!(<<-RUBY)
          require "bundler"
          begin
            puts Bundler.bundle_path
          rescue Bundler::GemfileNotFound
            ENV["BUNDLE_GEMFILE"] = "Gemfile"
            retry
          end

        RUBY
      end
      gems = gems.flatten

      unless opts[:keep_path]
        FileUtils.rm_rf(path)
        FileUtils.mkdir_p(path)
      end

      Gem.clear_paths

      env_backup = ENV.to_hash
      ENV["GEM_HOME"] = path.to_s
      ENV["GEM_PATH"] = path.to_s
      ENV["BUNDLER_ORIG_GEM_PATH"] = nil

      install_gems(*gems)
      return unless block_given?
      begin
        yield
      ensure
        ENV.replace(env_backup)
      end
    end

    def realworld_system_gems(*gems)
      gems = gems.flatten

      FileUtils.rm_rf(system_gem_path)
      FileUtils.mkdir_p(system_gem_path)

      Gem.clear_paths

      gem_home = ENV["GEM_HOME"]
      gem_path = ENV["GEM_PATH"]
      path = ENV["PATH"]
      ENV["GEM_HOME"] = system_gem_path.to_s
      ENV["GEM_PATH"] = system_gem_path.to_s

      gems.each do |gem|
        gem_command! :install, "--no-document #{gem}"
      end
      return unless block_given?
      begin
        yield
      ensure
        ENV["GEM_HOME"] = gem_home
        ENV["GEM_PATH"] = gem_path
        ENV["PATH"] = path
      end
    end

    def cache_gems(*gems)
      gems = gems.flatten

      FileUtils.rm_rf("#{bundled_app}/vendor/cache")
      FileUtils.mkdir_p("#{bundled_app}/vendor/cache")

      gems.each do |g|
        path = "#{gem_repo1}/gems/#{g}.gem"
        raise "OMG `#{path}` does not exist!" unless File.exist?(path)
        FileUtils.cp(path, "#{bundled_app}/vendor/cache")
      end
    end

    def simulate_new_machine
      system_gems []
      FileUtils.rm_rf system_gem_path
      FileUtils.rm_rf bundled_app(".bundle")
    end

    def simulate_platform(platform)
      old = ENV["BUNDLER_SPEC_PLATFORM"]
      ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s
      yield if block_given?
    ensure
      ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given?
    end

    def simulate_ruby_version(version)
      return if version == RUBY_VERSION
      old = ENV["BUNDLER_SPEC_RUBY_VERSION"]
      ENV["BUNDLER_SPEC_RUBY_VERSION"] = version
      yield if block_given?
    ensure
      ENV["BUNDLER_SPEC_RUBY_VERSION"] = old if block_given?
    end

    def simulate_ruby_engine(engine, version = "1.6.0")
      return if engine == local_ruby_engine

      old = ENV["BUNDLER_SPEC_RUBY_ENGINE"]
      ENV["BUNDLER_SPEC_RUBY_ENGINE"] = engine
      old_version = ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"]
      ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = version
      yield if block_given?
    ensure
      ENV["BUNDLER_SPEC_RUBY_ENGINE"] = old if block_given?
      ENV["BUNDLER_SPEC_RUBY_ENGINE_VERSION"] = old_version if block_given?
    end

    def simulate_bundler_version(version)
      old = ENV["BUNDLER_SPEC_VERSION"]
      ENV["BUNDLER_SPEC_VERSION"] = version.to_s
      yield if block_given?
    ensure
      ENV["BUNDLER_SPEC_VERSION"] = old if block_given?
    end

    def simulate_rubygems_version(version)
      old = ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"]
      ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] = version.to_s
      yield if block_given?
    ensure
      ENV["BUNDLER_SPEC_RUBYGEMS_VERSION"] = old if block_given?
    end

    def simulate_windows(platform = mswin)
      old = ENV["BUNDLER_SPEC_WINDOWS"]
      ENV["BUNDLER_SPEC_WINDOWS"] = "true"
      simulate_platform platform do
        yield
      end
    ensure
      ENV["BUNDLER_SPEC_WINDOWS"] = old
    end

    def revision_for(path)
      Dir.chdir(path) { `git rev-parse HEAD`.strip }
    end

    def capture_output
      capture(:stdout)
    end

    def with_read_only(pattern)
      chmod = lambda do |dirmode, filemode|
        lambda do |f|
          mode = File.directory?(f) ? dirmode : filemode
          File.chmod(mode, f)
        end
      end

      Dir[pattern].each(&chmod[0o555, 0o444])
      yield
    ensure
      Dir[pattern].each(&chmod[0o755, 0o644])
    end

    def process_file(pathname)
      changed_lines = pathname.readlines.map do |line|
        yield line
      end
      File.open(pathname, "w") {|file| file.puts(changed_lines.join) }
    end

    def with_env_vars(env_hash, &block)
      current_values = {}
      env_hash.each do |k, v|
        current_values[k] = ENV[k]
        ENV[k] = v
      end
      block.call if block_given?
      env_hash.each do |k, _|
        ENV[k] = current_values[k]
      end
    end

    def require_rack
      # need to hack, so we can require rack
      old_gem_home = ENV["GEM_HOME"]
      ENV["GEM_HOME"] = Spec::Path.base_system_gems.to_s
      require "rack"
      ENV["GEM_HOME"] = old_gem_home
    end

    def wait_for_server(host, port, seconds = 15)
      tries = 0
      sleep 0.5
      TCPSocket.new(host, port)
    rescue StandardError => e
      raise(e) if tries > (seconds * 2)
      tries += 1
      retry
    end

    def find_unused_port
      port = 21_453
      begin
        port += 1 while TCPSocket.new("127.0.0.1", port)
      rescue StandardError
        false
      end
      port
    end

    def bundler_fileutils
      if RUBY_VERSION >= "2.4"
        ::Bundler::FileUtils
      else
        ::FileUtils
      end
    end
  end
end
