# frozen_string_literal: true

require 'fileutils'
require 'cucumber/configuration'
require 'cucumber/load_path'
require 'cucumber/formatter/duration'
require 'cucumber/file_specs'
require 'cucumber/filters'
require 'cucumber/gherkin/i18n'
require 'cucumber/glue/registry_wrapper'
require 'cucumber/step_match_search'
require 'cucumber/messages'
require 'cucumber/runtime/meta_message_builder'
require 'sys/uname'

module Cucumber
  module FixRuby21Bug9285
    def message
      String(super).gsub('@ rb_sysopen ', '')
    end
  end

  class FileException < RuntimeError
    attr_reader :path

    def initialize(original_exception, path)
      @path = path
      super(original_exception)
    end
  end

  class FileNotFoundException < FileException
  end

  class FeatureFolderNotFoundException < RuntimeError
    def initialize(path)
      @path = path
      super
    end

    def message
      "No such file or directory - #{@path}"
    end
  end

  require 'cucumber/core'
  require 'cucumber/runtime/user_interface'
  require 'cucumber/runtime/support_code'
  class Runtime
    attr_reader :results, :support_code, :configuration

    include Cucumber::Core
    include Formatter::Duration
    include Runtime::UserInterface

    def initialize(configuration = Configuration.default)
      @configuration = Configuration.new(configuration)
      @support_code = SupportCode.new(self, @configuration)
    end

    # Allows you to take an existing runtime and change its configuration
    def configure(new_configuration)
      @configuration = Configuration.new(new_configuration)
      @support_code.configure(@configuration)
    end

    def run!
      @configuration.notify :envelope, Cucumber::Messages::Envelope.new(
        meta: MetaMessageBuilder.build_meta_message
      )

      load_step_definitions
      fire_install_plugin_hook
      create_formatters

      receiver = Test::Runner.new(@configuration.event_bus)
      compile features, receiver, filters, @configuration.event_bus
      fire_after_all_hook unless dry_run?
      @configuration.notify :test_run_finished, !failure?
    end

    def features_paths
      @configuration.paths
    end

    def dry_run?
      @configuration.dry_run?
    end

    def unmatched_step_definitions
      @support_code.unmatched_step_definitions
    end

    def begin_scenario(test_case)
      @support_code.fire_hook(:begin_scenario, test_case)
    end

    def end_scenario(_scenario)
      @support_code.fire_hook(:end_scenario)
    end

    # Returns Ast::DocString for +string_without_triple_quotes+.
    #
    def doc_string(string_without_triple_quotes, content_type = '', _line_offset = 0)
      Core::Test::DocString.new(string_without_triple_quotes, content_type)
    end

    def failure?
      if @configuration.wip?
        summary_report.test_cases.total_passed.positive?
      else
        !summary_report.ok?(strict: @configuration.strict) || !global_hooks_summary_report.ok?
      end
    end

    private

    def fire_install_plugin_hook # :nodoc:
      @support_code.fire_hook(:install_plugin, @configuration, registry_wrapper)
    end

    def fire_after_all_hook # :nodoc:
      @support_code.fire_hook(:after_all)
    end

    require 'cucumber/core/gherkin/document'
    def features
      @features ||= feature_files.map do |path|
        source = NormalisedEncodingFile.read(path)
        @configuration.notify :gherkin_source_read, path, source

        # TODO: When core is v17+ switch the below code out to the following
        # Cucumber::Core::Gherkin::Document.new(path, source).tap do |document|
        #   @configuration.notify(:envelope, document.to_envelope)
        # end
        to_envelope =
          Cucumber::Messages::Envelope.new(
            source: Cucumber::Messages::Source.new(
              uri: path,
              data: source,
              media_type: 'text/x.cucumber.gherkin+plain'
            )
          )

        @configuration.notify(:envelope, to_envelope)

        Cucumber::Core::Gherkin::Document.new(path, source)
      end
    end

    def feature_files
      filespecs.files
    end

    def filespecs
      @filespecs ||= FileSpecs.new(@configuration.feature_files)
    end

    class NormalisedEncodingFile
      COMMENT_OR_EMPTY_LINE_PATTERN = /^\s*#|^\s*$/.freeze # :nodoc:
      ENCODING_PATTERN = /^\s*#\s*encoding\s*:\s*([^\s]+)/.freeze # :nodoc:

      def self.read(path)
        new(path).read
      end

      def initialize(path)
        @file = File.new(path)
        set_encoding
      rescue Errno::EACCES => e
        raise FileNotFoundException.new(e, File.expand_path(path))
      rescue Errno::ENOENT
        raise FeatureFolderNotFoundException, path
      end

      def read
        @file.read.encode('UTF-8')
      end

      private

      def set_encoding
        @file.each do |line|
          if ENCODING_PATTERN =~ line
            @file.set_encoding Regexp.last_match(1)
            break
          end
          break unless COMMENT_OR_EMPTY_LINE_PATTERN =~ line
        end
        @file.rewind
      end
    end

    require 'cucumber/formatter/ignore_missing_messages'
    require 'cucumber/formatter/fail_fast'
    require 'cucumber/formatter/publish_banner_printer'
    require 'cucumber/core/report/summary'

    def create_formatters
      # Define all formatters which are specified via cli options
      formatters
      # Define the MessageBuilder formatter - Required until all messages are generated at the source
      #   Skip when a user formatter already inherits from MessageBuilder (e.g. HTML) to avoid sending every event twice.
      message_builder unless formatters.any? { |f| f.is_a?(Formatter::MessageBuilder) }
      # `summary_report` and `global_hooks_summary_report` are used to determine the exit code
      summary_report
      global_hooks_summary_report
      fail_fast_report if @configuration.fail_fast?
      publish_banner_printer unless @configuration.publish_quiet?
    end

    def message_builder
      @message_builder ||= Formatter::MessageBuilder.new(@configuration)
    end

    def summary_report
      @summary_report ||= Core::Report::Summary.new(@configuration.event_bus)
    end

    def global_hooks_summary_report
      @global_hooks_summary_report ||= Formatter::GlobalHooksSummary.new(@configuration)
    end

    def fail_fast_report
      @fail_fast_report ||= Formatter::FailFast.new(@configuration)
    end

    def publish_banner_printer
      @publish_banner_printer ||= Formatter::PublishBannerPrinter.new(@configuration)
    end

    def formatters
      @formatters ||=
        @configuration.formatter_factories do |factory, formatter_options, path_or_io|
          create_formatter(factory, formatter_options, path_or_io)
        end
    end

    def create_formatter(factory, formatter_options, path_or_io)
      if accept_options?(factory)
        return factory.new(@configuration, formatter_options) if path_or_io.nil?

        factory.new(@configuration.with_options(out_stream: path_or_io),
                    formatter_options)
      else
        return factory.new(@configuration) if path_or_io.nil?

        factory.new(@configuration.with_options(out_stream: path_or_io))
      end
    end

    def accept_options?(factory)
      factory.instance_method(:initialize).arity > 1
    end

    require 'cucumber/core/test/filters'
    def filters
      tag_expressions = @configuration.tag_expressions
      name_regexps = @configuration.name_regexps
      tag_limits = @configuration.tag_limits
      [].tap do |filters|
        filters << Filters::TagLimits.new(tag_limits) if tag_limits.any?
        filters << Cucumber::Core::Test::TagFilter.new(tag_expressions)
        filters << Cucumber::Core::Test::NameFilter.new(name_regexps)
        filters << Cucumber::Core::Test::LocationsFilter.new(filespecs.locations)
        filters << Filters::Randomizer.new(@configuration.seed) if @configuration.randomize?
        filters << Filters::Reverser.new if @configuration.reverse_order?
        # TODO: can we just use Glue::RegistryAndMore's step definitions directly?
        step_match_search = StepMatchSearch.new(@support_code.registry.method(:step_matches), @configuration)
        filters << Filters::ActivateSteps.new(step_match_search, @configuration)
        @configuration.filters.each { |filter| filters << filter }

        unless configuration.dry_run?
          filters << Filters::ApplyAfterStepHooks.new(@support_code)
          filters << Filters::ApplyBeforeHooks.new(@support_code)
          filters << Filters::ApplyAfterHooks.new(@support_code)
          filters << Filters::ApplyAroundHooks.new(@support_code)
          filters << Filters::BroadcastTestRunStartedEvent.new(@configuration)
          filters << Filters::FireBeforeAllHooks.new(@support_code)
          filters << Filters::Quit.new
        end

        filters << Filters::BroadcastTestCaseReadyEvent.new(@configuration)

        unless configuration.dry_run?
          filters << Filters::Retry.new(@configuration)
          # need to do this last so it becomes the first test step
          filters << Filters::PrepareWorld.new(self)
        end
      end
    end

    def load_step_definitions
      files = @configuration.support_to_load + @configuration.step_defs_to_load
      @support_code.load_files!(files)
    end

    def registry_wrapper
      Cucumber::Glue::RegistryWrapper.new(@support_code.registry)
    end

    def log
      Cucumber.logger
    end
  end
end
