# frozen_string_literal: true

require 'cucumber/cucumber_expressions/parameter_type_registry'
require 'cucumber/cucumber_expressions/cucumber_expression'
require 'cucumber/cucumber_expressions/regular_expression'
require 'cucumber/cucumber_expressions/cucumber_expression_generator'
require 'cucumber/messages/helpers/time_conversion'

require 'cucumber/glue/dsl'
require 'cucumber/glue/snippet'
require 'cucumber/glue/hook'
require 'cucumber/glue/proto_world'
require 'cucumber/glue/step_definition'
require 'cucumber/glue/world_factory'
require 'cucumber/gherkin/i18n'
require 'multi_test'
require 'cucumber/step_match'
require 'cucumber/events/base'
require 'cucumber/events/step_definition_registered'

module Cucumber
  module Glue
    def self.backtrace_line(proc, name)
      location = Cucumber::Core::Test::Location.from_source_location(*proc.source_location)
      "#{location}:in `#{name}'"
    end

    # Raised if a World block returns Nil.
    class NilWorld < StandardError
      def initialize
        super('World procs should never return nil')
      end
    end

    # Raised if there are 2 or more World blocks.
    class MultipleWorld < StandardError
      def initialize(first_proc, second_proc)
        # TODO: [LH] - Just use a heredoc here to fix this up
        message = String.new
        message << "You can only pass a proc to #World once, but it's happening\n"
        message << "in 2 places:\n\n"
        message << Glue.backtrace_line(first_proc, 'World') << "\n"
        message << Glue.backtrace_line(second_proc, 'World') << "\n\n"
        message << "Use Ruby modules instead to extend your worlds. See the Cucumber::Glue::Dsl#World RDoc\n"
        message << "or http://wiki.github.com/cucumber/cucumber/a-whole-new-world.\n\n"
        super(message)
      end
    end

    # TODO: This class has too many responsibilities, split off
    class RegistryAndMore
      attr_reader :current_world, :step_definitions

      include Cucumber::Messages::Helpers::TimeConversion

      all_keywords = ::Gherkin::DIALECTS.keys.map do |dialect_name|
        dialect = ::Gherkin::Dialect.for(dialect_name)
        dialect.given_keywords + dialect.when_keywords + dialect.then_keywords + dialect.and_keywords + dialect.but_keywords
      end
      Cucumber::Gherkin::I18n.code_keywords_for(all_keywords.flatten.uniq.sort).each do |adverb|
        Glue::Dsl.alias_adverb(adverb.strip)
      end

      def self.cli_snippet_type_options
        registry = CucumberExpressions::ParameterTypeRegistry.new
        cucumber_expression_generator = CucumberExpressions::CucumberExpressionGenerator.new(registry)
        Snippet::SNIPPET_TYPES.keys.sort_by(&:to_s).map do |type|
          Snippet::SNIPPET_TYPES[type].cli_option_string(type, cucumber_expression_generator)
        end
      end

      def initialize(runtime, configuration)
        @runtime = runtime
        @configuration = configuration
        @step_definitions = []
        Glue::Dsl.rb_language = self
        @world_proc = @world_modules = nil
        @parameter_type_registry = CucumberExpressions::ParameterTypeRegistry.new
        cucumber_expression_generator = CucumberExpressions::CucumberExpressionGenerator.new(@parameter_type_registry)
        @configuration.register_snippet_generator(Snippet::Generator.new(cucumber_expression_generator))
      end

      def step_matches(name_to_match)
        @step_definitions.each_with_object([]) do |step_definition, result|
          if (arguments = step_definition.arguments_from(name_to_match))
            result << StepMatch.new(step_definition, name_to_match, arguments)
          end
        end
      end

      def register_rb_hook(type, tag_expressions, proc, name: nil)
        hook = add_hook(type, Hook.new(@configuration.id_generator.new_id, self, tag_expressions, proc, name: name))
        @configuration.notify(:envelope, hook.to_envelope(type))
        hook
      end

      def define_parameter_type(parameter_type)
        @configuration.notify :envelope, parameter_type_envelope(parameter_type)

        @parameter_type_registry.define_parameter_type(parameter_type)
      end

      def register_rb_step_definition(string_or_regexp, proc_or_sym, options)
        step_definition = StepDefinition.new(@configuration.id_generator.new_id, self, string_or_regexp, proc_or_sym, options)
        @step_definitions << step_definition
        @configuration.notify(:step_definition_registered, step_definition)
        @configuration.notify(:envelope, step_definition.to_envelope)
        step_definition
      rescue Cucumber::CucumberExpressions::UndefinedParameterTypeError => e
        @configuration.notify(:undefined_parameter_type, e.undefined_parameter_type_name, string_or_regexp)
        # Move the below code into cucumber-expressions. Once done. Switch the line for
        # @configuration.notify(:envelope, e.to_envelope(string_or_regexp))
        to_envelope = Cucumber::Messages::Envelope.new(
          undefined_parameter_type: Cucumber::Messages::UndefinedParameterType.new(
            name: e.undefined_parameter_type_name,
            expression: string_or_regexp
          )
        )
        @configuration.notify(:envelope, to_envelope)
      end

      def build_rb_world_factory(world_modules, namespaced_world_modules, proc)
        if proc
          raise MultipleWorld.new(@world_proc, proc) if @world_proc

          @world_proc = proc
        end
        @world_modules ||= []
        @world_modules += world_modules

        @namespaced_world_modules ||= Hash.new { |h, k| h[k] = [] }
        namespaced_world_modules.each do |namespace, world_module|
          @namespaced_world_modules[namespace] << world_module unless @namespaced_world_modules[namespace].include?(world_module)
        end
      end

      def load_code_file(code_file)
        return unless File.extname(code_file) == '.rb'

        # This will cause self.add_step_definition, self.add_hook, and self.define_parameter_type to be called from Glue::Dsl
        require File.expand_path(code_file)
      end

      def begin_scenario(test_case)
        @current_world = WorldFactory.new(@world_proc).create_world
        @current_world.extend(ProtoWorld.for(@runtime, test_case&.language))
        MultiTest.extend_with_best_assertion_library(@current_world)
        @current_world.add_modules!(@world_modules || [], @namespaced_world_modules || {})
      end

      def end_scenario
        @current_world = nil
      end

      def install_plugin(configuration, registry)
        hooks[:install_plugin].each do |hook|
          hook.invoke('InstallPlugin', [configuration, registry])
        end
      end

      def before_all
        set_up_world_for_global_hooks
        all_succeeded = true
        # Run each `BeforeAll` hook. Ensuring that we store the overall result as the worst status
        hooks[:before_all].each do |hook|
          result = invoke_run_hook(hook, 'BeforeAll')
          all_succeeded = false unless result
        end
        @current_world = nil
        all_succeeded
      end

      def after_all
        set_up_world_for_global_hooks
        all_succeeded = true
        # Run each `AfterAll` hook. Ensuring that we store the overall result as the worst status
        hooks[:after_all].each do |hook|
          result = invoke_run_hook(hook, 'AfterAll')
          all_succeeded = false unless result
        end
        @current_world = nil
        all_succeeded
      end

      def add_hook(type, hook)
        hooks[type.to_sym] << hook
        hook
      end

      def clear_hooks
        @hooks = nil
      end

      def hooks_for(type, scenario) # :nodoc:
        hooks[type.to_sym].select { |hook| scenario.accept_hook?(hook) }
      end

      def create_expression(string_or_regexp)
        return CucumberExpressions::CucumberExpression.new(string_or_regexp, @parameter_type_registry) if string_or_regexp.is_a?(String)
        return CucumberExpressions::RegularExpression.new(string_or_regexp, @parameter_type_registry) if string_or_regexp.is_a?(Regexp)

        raise ArgumentError, 'Expression must be a String or Regexp'
      end

      private

      def invoke_run_hook(hook, pseudo_method)
        @configuration.notify(:test_run_hook_started, hook)

        current_test_run_hook_started_id = @configuration.id_generator.new_id
        started_envelope = test_run_hook_started_envelope(hook, current_test_run_hook_started_id)
        @configuration.notify(:envelope, started_envelope)

        timer = Core::Test::Timer.new.start
        begin
          hook.invoke(pseudo_method, [])
          @configuration.notify(:test_run_hook_finished, hook, Core::Test::Result::Passed.new(timer.duration))
          finished_envelope = test_run_hook_finished_envelope(Core::Test::Result::Passed.new(timer.duration), current_test_run_hook_started_id)
          @configuration.notify(:envelope, finished_envelope)
          true
        rescue StandardError => e
          @configuration.notify(:test_run_hook_finished, hook, Core::Test::Result::Failed.new(timer.duration, e))
          finished_envelope = test_run_hook_finished_envelope(Core::Test::Result::Failed.new(timer.duration, e), current_test_run_hook_started_id)
          @configuration.notify(:envelope, finished_envelope)
          false
        end
      end

      def test_run_hook_started_envelope(hook, id)
        Cucumber::Messages::Envelope.new(
          test_run_hook_started: Cucumber::Messages::TestRunHookStarted.new(
            id: id,
            hook_id: hook.id,
            test_run_started_id: @configuration.test_run_started_id,
            timestamp: time_to_timestamp(Time.now)
          )
        )
      end

      def test_run_hook_finished_envelope(test_result, test_run_hook_started_id)
        result = test_result
        result_message = result.to_message

        if result.failed?
          result_message = Cucumber::Messages::TestStepResult.new(
            status: result_message.status,
            duration: result_message.duration,
            message: create_error_message(result.exception),
            exception: create_exception_object(result, result.exception)
          )
        end

        Cucumber::Messages::Envelope.new(
          test_run_hook_finished: Cucumber::Messages::TestRunHookFinished.new(
            test_run_hook_started_id: test_run_hook_started_id,
            timestamp: time_to_timestamp(Time.now),
            result: result_message
          )
        )
      end

      def create_error_message(message_element)
        <<~ERROR_MESSAGE
          #{message_element.message} (#{message_element.class})
          #{message_element.backtrace}
        ERROR_MESSAGE
      end

      def create_exception_object(result, message_element)
        return unless result.failed?

        Cucumber::Messages::Exception.new(
          type: message_element.class,
          message: message_element.message,
          stack_trace: message_element.backtrace.join("\n")
        )
      end

      def parameter_type_envelope(parameter_type)
        # TODO: should this be moved to Cucumber::Expression::ParameterType#to_envelope ??
        # Note: that would mean that cucumber-expression would depend on cucumber-messages
        Cucumber::Messages::Envelope.new(
          parameter_type: Cucumber::Messages::ParameterType.new(
            id: @configuration.id_generator.new_id,
            name: parameter_type.name,
            regular_expressions: parameter_type.regexps.map(&:to_s),
            prefer_for_regular_expression_match: parameter_type.prefer_for_regexp_match,
            use_for_snippets: parameter_type.use_for_snippets,
            source_reference: source_reference_for(parameter_type.transformer)
          )
        )
      end

      def source_reference_for(transformer)
        # #source_location may return nil if no definition was found
        # This is the case for transformers created using method(sym) or similar
        return nil if transformer.source_location.nil?

        Cucumber::Messages::SourceReference.new(
          uri: transformer.source_location[0],
          location: Cucumber::Messages::Location.new(line: transformer.source_location[1])
        )
      end

      def hooks
        @hooks ||= Hash.new { |h, k| h[k] = [] }
      end

      def set_up_world_for_global_hooks
        # We don't need a language as we're just creating a world object to attach the BeforeAll / AfterAll hooks to
        begin_scenario(nil)
      end
    end
  end
end
