# frozen_string_literal: true

require 'cucumber/constantize'
require 'cucumber/cli/rerun_file'
require 'cucumber/events'
require 'cucumber/messages'
require 'cucumber/core/event_bus'
require 'cucumber/core/test/result'
require 'forwardable'
require 'cucumber'

module Cucumber
  # The base class for configuring settings for a Cucumber run.
  class Configuration
    include Constantize
    extend Forwardable

    def self.default
      new
    end

    # Subscribe to an event.
    #
    # See {Cucumber::Events} for the list of possible events.
    #
    # @param event_id [Symbol, Class, String] Identifier for the type of event to subscribe to
    # @param handler_object [Object optional] an object to be called when the event occurs
    # @yield [Object] Block to be called when the event occurs
    # @method on_event
    def_instance_delegator :event_bus, :on, :on_event

    # @private
    def notify(message, *args)
      event_bus.send(message, *args)
    end

    def initialize(user_options = {})
      @options = default_options.merge(Hash(user_options))
    end

    def with_options(new_options)
      self.class.new(@options.merge(new_options))
    end

    def out_stream
      @options[:out_stream]
    end

    def error_stream
      @options[:error_stream]
    end

    def randomize?
      @options[:order] == 'random'
    end

    def seed
      @options[:seed]
    end

    def dry_run?
      @options[:dry_run]
    end

    def publish_enabled?
      @options[:publish_enabled]
    end

    def publish_quiet?
      @options[:publish_quiet]
    end

    def fail_fast?
      @options[:fail_fast]
    end

    def retry_attempts
      @options[:retry]
    end

    def retry_total_tests
      @options[:retry_total]
    end

    def guess?
      @options[:guess]
    end

    def strict
      @options[:strict]
    end

    def wip?
      @options[:wip]
    end

    def expand?
      @options[:expand]
    end

    def source?
      @options[:source]
    end

    def duration?
      @options[:duration]
    end

    def snippets?
      @options[:snippets]
    end

    def skip_profile_information?
      @options[:skip_profile_information]
    end

    def profiles
      @options[:profiles] || []
    end

    def custom_profiles
      profiles - [@options[:default_profile]]
    end

    def paths
      @options[:paths]
    end

    def formats
      @options[:formats]
    end

    def autoload_code_paths
      @options[:autoload_code_paths]
    end

    def snippet_type
      @options[:snippet_type]
    end

    def feature_dirs
      dirs = paths.map { |f| File.directory?(f) ? f : File.dirname(f) }.uniq
      dirs.delete('.') unless paths.include?('.')
      with_default_features_path(dirs)
    end

    def tag_limits
      @options[:tag_limits]
    end

    def tag_expressions
      @options[:tag_expressions]
    end

    def name_regexps
      @options[:name_regexps]
    end

    def filters
      @options[:filters]
    end

    def feature_files
      potential_feature_files = with_default_features_path(paths).map do |path|
        path = path.tr('\\', '/') # In case we're on windows. Globs don't work with backslashes.
        path = path.chomp('/')

        # TODO: Move to using feature loading strategies stored in
        # options[:feature_loaders]
        if File.directory?(path)
          Dir["#{path}/**/*.feature"].sort
        elsif Cli::RerunFile.can_read?(path)
          Cli::RerunFile.new(path).features
        else
          path
        end
      end.flatten.uniq
      remove_excluded_files_from(potential_feature_files)
      potential_feature_files
    end

    def support_to_load
      support_files = all_files_to_load.select { |f| f =~ /\/support\// }

      # env_files are separated from other_files so we can ensure env files
      # load first.
      #
      env_files = support_files.select { |f| f =~ /\/support\/env\..*/ }
      other_files = support_files - env_files
      env_files.reverse + other_files.reverse
    end

    def all_files_to_load
      files = require_dirs.map do |path|
        path = path.tr('\\', '/') # In case we're on windows. Globs don't work with backslashes.
        path = path.gsub(/\/$/, '') # Strip trailing slash.
        File.directory?(path) ? Dir["#{path}/**/*"] : path
      end.flatten.uniq
      remove_excluded_files_from(files)
      files.select! { |f| File.file?(f) }
      files.reject! { |f| File.extname(f) == '.feature' }
      files.reject! { |f| f =~ /^http/ }
      files.sort
    end

    def step_defs_to_load
      all_files_to_load.reject { |f| f =~ /\/support\// }
    end

    def formatter_factories
      formats.map do |format, formatter_options, path_or_io|
        factory = formatter_class(format)
        yield factory,
              formatter_options,
              path_or_io
      rescue Exception => e
        raise e, "#{e.message}\nError creating formatter: #{format}", e.backtrace
      end
    end

    def formatter_class(format)
      if (builtin = Cli::Options::BUILTIN_FORMATS[format])
        constantize(builtin[0])
      else
        constantize(format)
      end
    end

    def to_hash
      @options
    end

    # An array of procs that can generate snippets for undefined steps. These procs may be called if a
    # formatter wants to display snippets to the user.
    #
    # Each proc should take the following arguments:
    #
    #  - keyword
    #  - step text
    #  - multiline argument
    #  - snippet type
    #
    def snippet_generators
      @options[:snippet_generators] ||= []
    end

    def register_snippet_generator(generator)
      snippet_generators << generator
      self
    end

    def event_bus
      @options[:event_bus]
    end

    def id_generator
      @id_generator ||= Cucumber::Messages::Helpers::IdGenerator::UUID.new
    end

    private

    def default_options
      {
        autoload_code_paths: ['features/support', 'features/step_definitions'],
        filters: [],
        strict: Cucumber::Core::Test::Result::StrictConfiguration.new,
        require: [],
        dry_run: false,
        publish_quiet: false,
        fail_fast: false,
        formats: [],
        excludes: [],
        tag_expressions: [],
        name_regexps: [],
        env_vars: {},
        diff_enabled: true,
        snippets: true,
        source: true,
        duration: true,
        event_bus: Cucumber::Events.make_event_bus,
        retry_total: Float::INFINITY
      }
    end

    def default_features_paths
      ['features']
    end

    def with_default_features_path(paths)
      return default_features_paths if paths.empty?

      paths
    end

    def remove_excluded_files_from(files)
      files.reject! { |path| @options[:excludes].detect { |pattern| path =~ pattern } }
    end

    def require_dirs
      if @options[:require].empty?
        default_features_paths + Dir['vendor/{gems,plugins}/*/cucumber']
      else
        @options[:require]
      end
    end
  end
end
