module CLI
  module Kit
    module Support
      module TestHelper
        def setup
          super
          CLI::Kit::System.reset!
        end

        def assert_all_commands_run(should_raise: true)
          errors = CLI::Kit::System.error_message
          CLI::Kit::System.reset!
          assert(false, errors) if should_raise && !errors.nil?
          errors
        end

        def teardown
          super
          assert_all_commands_run
        end

        module FakeConfig
          require 'tmpdir'
          require 'fileutils'

          def setup
            super
            @tmpdir = Dir.mktmpdir
            @prev_xdg = ENV['XDG_CONFIG_HOME']
            ENV['XDG_CONFIG_HOME'] = @tmpdir
          end

          def teardown
            FileUtils.rm_rf(@tmpdir)
            ENV['XDG_CONFIG_HOME'] = @prev_xdg
            super
          end
        end

        class FakeSuccess
          def initialize(success)
            @success = success
          end

          def success?
            @success
          end
        end

        module ::CLI
          module Kit
            module System
              class << self
                alias_method :original_system, :system
                def system(*a, sudo: false, env: {}, **kwargs)
                  expected_command = expected_command(*a, sudo: sudo, env: env)

                  # In the case of an unexpected command, expected_command will be nil
                  return FakeSuccess.new(false) if expected_command.nil?

                  # Otherwise handle the command
                  if expected_command[:allow]
                    original_system(*a, sudo: sudo, env: env, **kwargs)
                  else
                    FakeSuccess.new(expected_command[:success])
                  end
                end

                alias_method :original_capture2, :capture2
                def capture2(*a, sudo: false, env: {}, **kwargs)
                  expected_command = expected_command(*a, sudo: sudo, env: env)

                  # In the case of an unexpected command, expected_command will be nil
                  return [nil, FakeSuccess.new(false)] if expected_command.nil?

                  # Otherwise handle the command
                  if expected_command[:allow]
                    original_capture2(*a, sudo: sudo, env: env, **kwargs)
                  else
                    [
                      expected_command[:stdout],
                      FakeSuccess.new(expected_command[:success]),
                    ]
                  end
                end

                alias_method :original_capture2e, :capture2e
                def capture2e(*a, sudo: false, env: {}, **kwargs)
                  expected_command = expected_command(*a, sudo: sudo, env: env)

                  # In the case of an unexpected command, expected_command will be nil
                  return [nil, FakeSuccess.new(false)] if expected_command.nil?

                  # Otherwise handle the command
                  if expected_command[:allow]
                    original_capture2ecapture2e(*a, sudo: sudo, env: env, **kwargs)
                  else
                    [
                      expected_command[:stdout],
                      FakeSuccess.new(expected_command[:success]),
                    ]
                  end
                end

                alias_method :original_capture3, :capture3
                def capture3(*a, sudo: false, env: {}, **kwargs)
                  expected_command = expected_command(*a, sudo: sudo, env: env)

                  # In the case of an unexpected command, expected_command will be nil
                  return [nil, nil, FakeSuccess.new(false)] if expected_command.nil?

                  # Otherwise handle the command
                  if expected_command[:allow]
                    original_capture3(*a, sudo: sudo, env: env, **kwargs)
                  else
                    [
                      expected_command[:stdout],
                      expected_command[:stderr],
                      FakeSuccess.new(expected_command[:success]),
                    ]
                  end
                end

                # Sets up an expectation for a command and stubs out the call (unless allow is true)
                #
                # #### Parameters
                # `*a` : the command, represented as a splat
                # `stdout` : stdout to stub the command with (defaults to empty string)
                # `stderr` : stderr to stub the command with (defaults to empty string)
                # `allow` : allow determines if the command will be actually run, or stubbed. Defaults to nil (stub)
                # `success` : success status to stub the command with (Defaults to nil)
                # `sudo` : expectation of sudo being set or not (defaults to false)
                # `env` : expectation of env being set or not (defaults to {})
                #
                # Note: Must set allow or success
                #
                def fake(*a, stdout: "", stderr: "", allow: nil, success: nil, sudo: false, env: {})
                  raise ArgumentError, "success or allow must be set" if success.nil? && allow.nil?

                  @delegate_open3 ||= {}
                  @delegate_open3[a.join(' ')] = {
                    expected: {
                      sudo: sudo,
                      env: env,
                    },
                    actual: {
                      sudo: nil,
                      env: nil,
                    },
                    stdout: stdout,
                    stderr: stderr,
                    allow: allow,
                    success: success,
                    run: false,
                  }
                end

                # Resets the faked commands
                #
                def reset!
                  @delegate_open3 = {}
                end

                # Returns the errors associated to a test run
                #
                # #### Returns
                # `errors` (String) a string representing errors found on this run, nil if none
                def error_message
                  errors = {
                    unexpected: [],
                    not_run: [],
                    other: {},
                  }

                  @delegate_open3.each do |cmd, opts|
                    if opts[:unexpected]
                      errors[:unexpected] << cmd
                    elsif opts[:run]
                      error = []

                      if opts[:expected][:sudo] != opts[:actual][:sudo]
                        error << "- sudo was supposed to be #{opts[:expected][:sudo]} but was #{opts[:actual][:sudo]}"
                      end

                      if opts[:expected][:env] != opts[:actual][:env]
                        error << "- env was supposed to be #{opts[:expected][:env]} but was #{opts[:actual][:env]}"
                      end

                      errors[:other][cmd] = error.join("\n") unless error.empty?
                    else
                      errors[:not_run] << cmd
                    end
                  end

                  final_error = []

                  unless errors[:unexpected].empty?
                    final_error << CLI::UI.fmt(<<~EOF)
                      {{bold:Unexpected command invocations:}}
                      {{command:#{errors[:unexpected].join("\n")}}}
                    EOF
                  end

                  unless errors[:not_run].empty?
                    final_error << CLI::UI.fmt(<<~EOF)
                      {{bold:Expected commands were not run:}}
                      {{command:#{errors[:not_run].join("\n")}}}
                    EOF
                  end

                  unless errors[:other].empty?
                    final_error << CLI::UI.fmt(<<~EOF)
                      {{bold:Commands were not run as expected:}}
                      #{errors[:other].map { |cmd, msg| "{{command:#{cmd}}}\n#{msg}" }.join("\n\n")}
                    EOF
                  end

                  return nil if final_error.empty?
                  "\n" + final_error.join("\n") # Initial new line for formatting reasons
                end

                private

                def expected_command(*a, sudo: raise, env: raise)
                  expected_cmd = @delegate_open3[a.join(' ')]

                  if expected_cmd.nil?
                    @delegate_open3[a.join(' ')] = { unexpected: true }
                    return nil
                  end

                  expected_cmd[:run] = true
                  expected_cmd[:actual][:sudo] = sudo
                  expected_cmd[:actual][:env] = env
                  expected_cmd
                end
              end
            end
          end
        end
      end
    end
  end
end