# frozen_string_literal: true

RSpec.describe RuboCop::Options, :isolated_environment do
  include FileHelper

  subject(:options) { described_class.new }

  before do
    $stdout = StringIO.new
    $stderr = StringIO.new
  end

  after do
    $stdout = STDOUT
    $stderr = STDERR
  end

  def abs(path)
    File.expand_path(path)
  end

  describe 'option' do
    describe '-h/--help' do
      it 'exits cleanly' do
        expect { options.parse ['-h'] }.to exit_with_code(0)
        expect { options.parse ['--help'] }.to exit_with_code(0)
      end

      it 'shows help text' do
        begin
          options.parse(['--help'])
        rescue SystemExit # rubocop:disable Lint/SuppressedException
        end

        expected_help = <<~OUTPUT
          Usage: rubocop [options] [file1, file2, ...]
              -L, --list-target-files          List all files RuboCop will inspect.
                  --except [COP1,COP2,...]     Disable the given cop(s).
                  --only [COP1,COP2,...]       Run only the given cop(s).
                  --only-guide-cops            Run only cops for rules that link to a
                                               style guide.
              -c, --config FILE                Specify configuration file.
                  --force-exclusion            Force excluding files specified in the
                                               configuration `Exclude` even if they are
                                               explicitly passed as arguments.
                  --only-recognized-file-types Inspect files given on the command line only if
                                               they are listed in AllCops/Include parameters
                                               of user configuration or default configuration.
                  --ignore-parent-exclusion    Prevent from inheriting AllCops/Exclude from
                                               parent folders.
                  --force-default-config       Use default configuration even if configuration
                                               files are present in the directory tree.
                  --auto-gen-config            Generate a configuration file acting as a
                                               TODO list.
                  --exclude-limit COUNT        Used together with --auto-gen-config to
                                               set the limit for how many Exclude
                                               properties to generate. Default is 15.
                  --disable-uncorrectable      Used with --auto-correct to annotate any
                                               offenses that do not support autocorrect
                                               with `rubocop:todo` comments.
                  --no-offense-counts          Do not include offense counts in configuration
                                               file generated by --auto-gen-config.
                  --auto-gen-only-exclude      Generate only Exclude parameters and not Max
                                               when running --auto-gen-config, except if the
                                               number of files with offenses is bigger than
                                               exclude-limit.
                  --no-auto-gen-timestamp      Do not include the date and time when
                                               the --auto-gen-config was run in the file it
                                               generates.
                  --init                       Generate a .rubocop.yml file in the current directory.
              -f, --format FORMATTER           Choose an output formatter. This option
                                               can be specified multiple times to enable
                                               multiple formatters at the same time.
                                               [p]rogress is used by default
                                                 [a]utogenconf
                                                 [c]lang
                                                 [e]macs
                                                 [fi]les
                                                 [fu]ubar
                                                 [h]tml
                                                 [j]son
                                                 [ju]nit
                                                 [o]ffenses
                                                 [pa]cman
                                                 [p]rogress
                                                 [q]uiet
                                                 [s]imple
                                                 [t]ap
                                                 [w]orst
                                                 custom formatter class name
              -o, --out FILE                   Write output to a file instead of STDOUT.
                                               This option applies to the previously
                                               specified --format, or the default format
                                               if no format is specified.
                  --display-only-failed        Only output offense messages. Omit passing
                                               cops. Only valid for --format junit.
              -r, --require FILE               Require Ruby file.
                  --fail-level SEVERITY        Minimum severity (A/R/C/W/E/F) for exit
                                               with error code.
                  --display-only-fail-level-offenses
                                               Only output offense messages at
                                               the specified --fail-level or above
                  --show-cops [COP1,COP2,...]  Shows the given cops, or all cops by
                                               default, and their configurations for the
                                               current directory.
              -F, --fail-fast                  Inspect files in order of modification
                                               time and stop after the first file
                                               containing offenses.
              -C, --cache FLAG                 Use result caching (FLAG=true) or don't
                                               (FLAG=false), default determined by
                                               configuration parameter AllCops: UseCache.
              -d, --debug                      Display debug info.
              -D, --[no-]display-cop-names     Display cop names in offense messages.
                                               Default is true.
              -E, --extra-details              Display extra details in offense messages.
              -S, --display-style-guide        Display style guide URLs in offense messages.
              -a, --auto-correct               Auto-correct offenses (only when it's safe).
                  --safe-auto-correct          (same, deprecated)
              -A, --auto-correct-all           Auto-correct offenses (safe and unsafe)
                  --disable-pending-cops       Run without pending cops.
                  --enable-pending-cops        Run with pending cops.
                  --ignore-disable-comments    Run cops even when they are disabled locally
                                               with a comment.
                  --safe                       Run only safe cops.
                  --[no-]color                 Force color output on or off.
              -v, --version                    Display version.
              -V, --verbose-version            Display verbose version.
              -P, --parallel                   Use available CPUs to execute inspection in
                                               parallel.
              -l, --lint                       Run only lint cops.
              -x, --fix-layout                 Run only layout cops, with auto-correct on.
              -s, --stdin FILE                 Pipe source from STDIN, using FILE in offense
                                               reports. This is useful for editor integration.
        OUTPUT

        expect($stdout.string).to eq(expected_help)
      end

      it 'lists all builtin formatters' do
        begin
          options.parse(['--help'])
        rescue SystemExit # rubocop:disable Lint/SuppressedException
        end

        option_sections = $stdout.string.lines.slice_before(/^\s*-/)

        format_section = option_sections.find do |lines|
          /^\s*-f/.match?(lines.first)
        end

        formatter_keys = format_section.reduce([]) do |keys, line|
          match = line.match(/^ {39}(\[[a-z\]]+)/)
          next keys unless match

          keys << match.captures.first
        end.sort

        expected_formatter_keys =
          RuboCop::Formatter::FormatterSet::BUILTIN_FORMATTERS_FOR_KEYS
          .keys.sort

        expect(formatter_keys).to eq(expected_formatter_keys)
      end
    end

    describe 'incompatible cli options' do
      it 'rejects using -v with -V' do
        msg = 'Incompatible cli options: [:version, :verbose_version]'
        expect { options.parse %w[-vV] }
          .to raise_error(RuboCop::OptionArgumentError, msg)
      end

      it 'rejects using -v with --show-cops' do
        msg = 'Incompatible cli options: [:version, :show_cops]'
        expect { options.parse %w[-v --show-cops] }
          .to raise_error(RuboCop::OptionArgumentError, msg)
      end

      it 'rejects using -V with --show-cops' do
        msg = 'Incompatible cli options: [:verbose_version, :show_cops]'
        expect { options.parse %w[-V --show-cops] }
          .to raise_error(RuboCop::OptionArgumentError, msg)
      end

      it 'mentions all incompatible options when more than two are used' do
        msg = 'Incompatible cli options: [:version, :verbose_version,' \
              ' :show_cops]'
        expect { options.parse %w[-vV --show-cops] }
          .to raise_error(RuboCop::OptionArgumentError, msg)
      end
    end

    describe '--parallel' do
      context 'combined with --cache false' do
        it 'fails with an error message' do
          msg = ['-P/--parallel uses caching to speed up execution, so ',
                 'combining with --cache false is not allowed.'].join
          expect { options.parse %w[--parallel --cache false] }
            .to raise_error(RuboCop::OptionArgumentError, msg)
        end
      end

      context 'combined with --auto-correct' do
        it 'fails with an error message' do
          msg = '-P/--parallel cannot be combined with --auto-correct.'
          expect { options.parse %w[--parallel --auto-correct] }
            .to raise_error(RuboCop::OptionArgumentError, msg)
        end
      end

      context 'combined with --auto-gen-config' do
        it 'fails with an error message' do
          msg = '-P/--parallel uses caching to speed up execution, while ' \
                '--auto-gen-config needs a non-cached run, so they cannot be ' \
                'combined.'
          expect { options.parse %w[--parallel --auto-gen-config] }
            .to raise_error(RuboCop::OptionArgumentError, msg)
        end
      end

      context 'combined with --fail-fast' do
        it 'fails with an error message' do
          msg = '-P/--parallel cannot be combined with -F/--fail-fast.'
          expect { options.parse %w[--parallel --fail-fast] }
            .to raise_error(RuboCop::OptionArgumentError, msg)
        end
      end
    end

    describe '--display-only-failed' do
      it 'fails if given without --format junit' do
        expect { options.parse %w[--display-only-failed] }
          .to raise_error(RuboCop::OptionArgumentError)
      end

      it 'works if given with --format junit' do
        expect { options.parse %w[--format junit --display-only-failed] }
          .not_to raise_error(RuboCop::OptionArgumentError)
      end
    end

    describe '--fail-level' do
      it 'accepts full severity names' do
        %w[refactor convention warning error fatal].each do |severity|
          expect { options.parse(['--fail-level', severity]) }
            .not_to raise_error
        end
      end

      it 'accepts severity initial letters' do
        %w[R C W E F].each do |severity|
          expect { options.parse(['--fail-level', severity]) }
            .not_to raise_error
        end
      end

      it 'accepts the "fake" severities A/autocorrect' do
        %w[autocorrect A].each do |severity|
          expect { options.parse(['--fail-level', severity]) }
            .not_to raise_error
        end
      end
    end

    describe '--require' do
      let(:required_file_path) { './path/to/required_file.rb' }

      before do
        create_empty_file('example.rb')

        create_file(required_file_path, "puts 'Hello from required file!'")
      end

      it 'requires the passed path' do
        options.parse(['--require', required_file_path, 'example.rb'])
        expect($stdout.string).to start_with('Hello from required file!')
      end
    end

    describe '--cache' do
      it 'fails if no argument is given' do
        expect { options.parse %w[--cache] }
          .to raise_error(OptionParser::MissingArgument)
      end

      it 'fails if unrecognized argument is given' do
        expect { options.parse %w[--cache maybe] }
          .to raise_error(RuboCop::OptionArgumentError)
      end

      it 'accepts true as argument' do
        expect { options.parse %w[--cache true] }.not_to raise_error
      end

      it 'accepts false as argument' do
        expect { options.parse %w[--cache false] }.not_to raise_error
      end
    end

    describe '--disable-uncorrectable' do
      it 'accepts together with --auto-correct' do
        expect { options.parse %w[--auto-correct --disable-uncorrectable] }
          .not_to raise_error
      end

      it 'accepts together with --auto-correct-all' do
        expect { options.parse %w[--auto-correct-all --disable-uncorrectable] }
          .not_to raise_error
      end

      it 'fails if given alone without --auto-correct/-a' do
        expect { options.parse %w[--disable-uncorrectable] }
          .to raise_error(RuboCop::OptionArgumentError)
      end
    end

    describe '--exclude-limit' do
      it 'fails if given last without argument' do
        expect { options.parse %w[--auto-gen-config --exclude-limit] }
          .to raise_error(OptionParser::MissingArgument)
      end

      it 'fails if given alone without argument' do
        expect { options.parse %w[--exclude-limit] }
          .to raise_error(OptionParser::MissingArgument)
      end

      it 'fails if given first without argument' do
        expect { options.parse %w[--exclude-limit --auto-gen-config] }
          .to raise_error(OptionParser::MissingArgument)
      end

      it 'fails if given without --auto-gen-config' do
        expect { options.parse %w[--exclude-limit 10] }
          .to raise_error(RuboCop::OptionArgumentError)
      end
    end

    describe '--auto-gen-only-exclude' do
      it 'fails if given without --auto-gen-config' do
        expect { options.parse %w[--auto-gen-only-exclude] }
          .to raise_error(RuboCop::OptionArgumentError)
      end
    end

    describe '--auto-gen-config' do
      it 'accepts other options' do
        expect { options.parse %w[--auto-gen-config --lint] }
          .not_to raise_error
      end
    end

    describe '-s/--stdin' do
      before do
        $stdin = StringIO.new
        $stdin.puts("{ foo: 'bar' }")
        $stdin.rewind
      end

      it 'fails if no paths are given' do
        expect { options.parse %w[-s] }
          .to raise_error(OptionParser::MissingArgument)
      end

      it 'succeeds with exactly one path' do
        expect { options.parse %w[--stdin foo] }.not_to raise_error
      end

      it 'fails if more than one path is given' do
        expect { options.parse %w[--stdin foo bar] }
          .to raise_error(RuboCop::OptionArgumentError)
      end
    end

    describe '--safe-auto-correct' do
      it 'is a deprecated alias' do
        expect { options.parse %w[--safe-auto-correct] }
          .to output(/deprecated/).to_stderr
      end
    end
  end

  describe 'options precedence' do
    def with_env_options(options)
      ENV['RUBOCOP_OPTS'] = options
      yield
    ensure
      ENV.delete('RUBOCOP_OPTS')
    end

    subject(:parsed_options) { options.parse(command_line_options).first }

    let(:command_line_options) { %w[--no-color] }

    describe '.rubocop file' do
      before do
        create_file('.rubocop', '--color --fail-level C')
      end

      it 'has lower precedence then command line options' do
        expect(parsed_options).to eq(color: false, fail_level: :convention)
      end

      it 'has lower precedence then options from RUBOCOP_OPTS env variable' do
        with_env_options '--fail-level W' do
          expect(parsed_options).to eq(color: false, fail_level: :warning)
        end
      end
    end

    describe '.rubocop directory' do
      before do
        FileUtils.mkdir '.rubocop'
      end

      it 'is ignored and command line options are used' do
        expect(parsed_options).to eq(color: false)
      end
    end

    context 'RUBOCOP_OPTS environment variable' do
      it 'has lower precedence then command line options' do
        with_env_options '--color' do
          expect(parsed_options).to eq(color: false)
        end
      end

      it 'has higher precedence then options from .rubocop file' do
        create_file('.rubocop', '--color --fail-level C')

        with_env_options '--fail-level W' do
          expect(parsed_options).to eq(color: false, fail_level: :warning)
        end
      end
    end
  end
end
