require "fileutils"
require "docile"
require "simplecov/formatter/multi_formatter"
#
# Bundles the configuration options used for SimpleCov. All methods
# defined here are usable from SimpleCov directly. Please check out
# SimpleCov documentation for further info.
#
module SimpleCov
  module Configuration # rubocop:disable ModuleLength
    attr_writer :filters, :groups, :formatter

    #
    # The root for the project. This defaults to the
    # current working directory.
    #
    # Configure with SimpleCov.root('/my/project/path')
    #
    def root(root = nil)
      return @root if defined?(@root) && root.nil?
      @root = File.expand_path(root || Dir.getwd)
    end

    #
    # The name of the output and cache directory. Defaults to 'coverage'
    #
    # Configure with SimpleCov.coverage_dir('cov')
    #
    def coverage_dir(dir = nil)
      return @coverage_dir if defined?(@coverage_dir) && dir.nil?
      @coverage_path = nil # invalidate cache
      @coverage_dir = (dir || "coverage")
    end

    #
    # Returns the full path to the output directory using SimpleCov.root
    # and SimpleCov.coverage_dir, so you can adjust this by configuring those
    # values. Will create the directory if it's missing
    #
    def coverage_path
      @coverage_path ||= begin
        coverage_path = File.expand_path(coverage_dir, root)
        FileUtils.mkdir_p coverage_path
        coverage_path
      end
    end

    #
    # Coverage results will always include files matched by this glob, whether
    # or not they were explicitly required. Without this, un-required files
    # will not be present in the final report.
    #
    def track_files(glob)
      @tracked_files = glob
    end

    #
    # Returns the glob that will be used to include files that were not
    # explicitly required.
    #
    def tracked_files
      @tracked_files if defined?(@tracked_files)
    end

    #
    # Returns the list of configured filters. Add filters using SimpleCov.add_filter.
    #
    def filters
      @filters ||= []
    end

    # The name of the command (a.k.a. Test Suite) currently running. Used for result
    # merging and caching. It first tries to make a guess based upon the command line
    # arguments the current test suite is running on and should automatically detect
    # unit tests, functional tests, integration tests, rpsec and cucumber and label
    # them properly. If it fails to recognize the current command, the command name
    # is set to the shell command that the current suite is running on.
    #
    # You can specify it manually with SimpleCov.command_name("test:units") - please
    # also check out the corresponding section in README.rdoc
    def command_name(name = nil)
      @name = name unless name.nil?
      @name ||= SimpleCov::CommandGuesser.guess
      @name
    end

    #
    # Gets or sets the configured formatter.
    #
    # Configure with: SimpleCov.formatter(SimpleCov::Formatter::SimpleFormatter)
    #
    def formatter(formatter = nil)
      return @formatter if defined?(@formatter) && formatter.nil?
      @formatter = formatter
      raise "No formatter configured. Please specify a formatter using SimpleCov.formatter = SimpleCov::Formatter::SimpleFormatter" unless @formatter
      @formatter
    end

    #
    # Sets the configured formatters.
    #
    def formatters=(formatters)
      @formatter = SimpleCov::Formatter::MultiFormatter.new(formatters)
    end

    #
    # Gets the configured formatters.
    #
    def formatters
      if @formatter.is_a?(SimpleCov::Formatter::MultiFormatter)
        @formatter.formatters
      else
        Array(formatter)
      end
    end

    #
    # Certain code blocks (i.e. Ruby-implementation specific code) can be excluded from
    # the coverage metrics by wrapping it inside # :nocov: comment blocks. The nocov token
    # can be configured to be any other string using this.
    #
    # Configure with SimpleCov.nocov_token('skip') or it's alias SimpleCov.skip_token('skip')
    #
    def nocov_token(nocov_token = nil)
      return @nocov_token if defined?(@nocov_token) && nocov_token.nil?
      @nocov_token = (nocov_token || "nocov")
    end
    alias skip_token nocov_token

    #
    # Returns the configured groups. Add groups using SimpleCov.add_group
    #
    def groups
      @groups ||= {}
    end

    #
    # Returns the hash of available profiles
    #
    def profiles
      @profiles ||= SimpleCov::Profiles.new
    end

    def adapters
      warn "#{Kernel.caller.first}: [DEPRECATION] #adapters is deprecated. Use #profiles instead."
      profiles
    end

    #
    # Allows you to configure simplecov in a block instead of prepending SimpleCov to all config methods
    # you're calling.
    #
    #     SimpleCov.configure do
    #       add_filter 'foobar'
    #     end
    #
    # This is equivalent to SimpleCov.add_filter 'foobar' and thus makes it easier to set a bunch of configure
    # options at once.
    #
    def configure(&block)
      return false unless SimpleCov.usable?
      Docile.dsl_eval(self, &block)
    end

    #
    # Gets or sets the behavior to process coverage results.
    #
    # By default, it will call SimpleCov.result.format!
    #
    # Configure with:
    #
    #     SimpleCov.at_exit do
    #       puts "Coverage done"
    #       SimpleCov.result.format!
    #     end
    #
    def at_exit(&block)
      return proc {} unless running || block_given?
      @at_exit = block if block_given?
      @at_exit ||= proc { SimpleCov.result.format! }
    end

    #
    # Returns the project name - currently assuming the last dirname in
    # the SimpleCov.root is this.
    #
    def project_name(new_name = nil)
      return @project_name if defined?(@project_name) && @project_name && new_name.nil?
      @project_name = new_name if new_name.is_a?(String)
      @project_name ||= File.basename(root.split("/").last).capitalize.tr("_", " ")
    end

    #
    # Defines whether to use result merging so all your test suites (test:units, test:functionals, cucumber, ...)
    # are joined and combined into a single coverage report
    #
    def use_merging(use = nil)
      @use_merging = use unless use.nil?
      @use_merging = true unless defined?(@use_merging) && @use_merging == false
    end

    #
    # Defines the maximum age (in seconds) of a resultset to still be included in merged results.
    # i.e. If you run cucumber features, then later rake test, if the stored cucumber resultset is
    # more seconds ago than specified here, it won't be taken into account when merging (and is also
    # purged from the resultset cache)
    #
    # Of course, this only applies when merging is active (e.g. SimpleCov.use_merging is not false!)
    #
    # Default is 600 seconds (10 minutes)
    #
    # Configure with SimpleCov.merge_timeout(3600) # 1hr
    #
    def merge_timeout(seconds = nil)
      @merge_timeout = seconds if seconds.is_a?(Integer)
      @merge_timeout ||= 600
    end

    #
    # Defines the minimum overall coverage required for the testsuite to pass.
    # SimpleCov will return non-zero if the current coverage is below this threshold.
    #
    # Default is 0% (disabled)
    #
    def minimum_coverage(coverage = nil)
      @minimum_coverage ||= (coverage || 0).to_f.round(2)
    end

    #
    # Defines the maximum coverage drop at once allowed for the testsuite to pass.
    # SimpleCov will return non-zero if the coverage decreases by more than this threshold.
    #
    # Default is 100% (disabled)
    #
    def maximum_coverage_drop(coverage_drop = nil)
      @maximum_coverage_drop ||= (coverage_drop || 100).to_f.round(2)
    end

    #
    # Defines the minimum coverage per file required for the testsuite to pass.
    # SimpleCov will return non-zero if the current coverage of the least covered file
    # is below this threshold.
    #
    # Default is 0% (disabled)
    #
    def minimum_coverage_by_file(coverage = nil)
      @minimum_coverage_by_file ||= (coverage || 0).to_f.round(2)
    end

    #
    # Refuses any coverage drop. That is, coverage is only allowed to increase.
    # SimpleCov will return non-zero if the coverage decreases.
    #
    def refuse_coverage_drop
      maximum_coverage_drop 0
    end

    #
    # Add a filter to the processing chain.
    # There are four ways to define a filter:
    #
    # * as a String that will then be matched against all source files' file paths,
    #     SimpleCov.add_filter 'app/models' # will reject all your models
    # * as a block which will be passed the source file in question and should either
    #   return a true or false value, depending on whether the file should be removed
    #     SimpleCov.add_filter do |src_file|
    #       File.basename(src_file.filename) == 'environment.rb'
    #     end # Will exclude environment.rb files from the results
    # * as an array of strings that are matched against all sorce files' file
    #   paths and then ignored (basically string filter multiple times)
    #     SimpleCov.add_filter ['app/models', 'app/helpers'] # ignores both dirs
    # * as an instance of a subclass of SimpleCov::Filter. See the documentation there
    #   on how to define your own filter classes
    #
    def add_filter(filter_argument = nil, &filter_proc)
      filters << parse_filter(filter_argument, &filter_proc)
    end

    #
    # Define a group for files. Works similar to add_filter, only that the first
    # argument is the desired group name and files PASSING the filter end up in the group
    # (while filters exclude when the filter is applicable).
    #
    def add_group(group_name, filter_argument = nil, &filter_proc)
      groups[group_name] = parse_filter(filter_argument, &filter_proc)
    end

  private

    #
    # The actual filter processor. Not meant for direct use
    #
    def parse_filter(filter_argument = nil, &filter_proc)
      if filter_argument.is_a?(SimpleCov::Filter)
        filter_argument
      elsif filter_argument.is_a?(String)
        SimpleCov::StringFilter.new(filter_argument)
      elsif filter_proc
        SimpleCov::BlockFilter.new(filter_proc)
      elsif filter_argument.is_a?(Array)
        SimpleCov::ArrayFilter.new(filter_argument)
      else
        raise ArgumentError, "Please specify either a string or a block to filter with"
      end
    end
  end
end
