# frozen_string_literal: true

require 'cucumber/constantize'
require 'cucumber/runtime/for_programming_languages'
require 'cucumber/runtime/step_hooks'
require 'cucumber/runtime/before_hooks'
require 'cucumber/runtime/after_hooks'
require 'cucumber/gherkin/steps_parser'
require 'cucumber/step_match_search'

module Cucumber
  class Runtime
    class SupportCode
      require 'forwardable'
      class StepInvoker
        def initialize(support_code)
          @support_code = support_code
        end

        def steps(steps)
          steps.each { |step| step(step) }
        end

        def step(step)
          location = Core::Test::Location.of_caller
          @support_code.invoke_dynamic_step(step[:text], multiline_arg(step, location))
        end

        def multiline_arg(step, location)
          if !step[:doc_string].nil?
            MultilineArgument.from(step[:doc_string][:content], location, step[:doc_string][:content_type])
          elsif !step[:data_table].nil?
            MultilineArgument::DataTable.from(step[:data_table][:rows].map { |row| row[:cells].map { |cell| cell[:value] } })
          else
            MultilineArgument.from(nil)
          end
        end
      end

      include Constantize

      attr_reader :registry

      def initialize(user_interface, configuration = Configuration.default)
        @configuration = configuration
        # TODO: needs a better name, or inlining its methods
        @runtime_facade = Runtime::ForProgrammingLanguages.new(self, user_interface)
        @registry = Cucumber::Glue::RegistryAndMore.new(@runtime_facade, @configuration)
      end

      def configure(new_configuration)
        @configuration = Configuration.new(new_configuration)
      end

      # Invokes a series of steps +steps_text+. Example:
      #
      #   invoke(%Q{
      #     Given I have 8 cukes in my belly
      #     Then I should not be thirsty
      #   })
      def invoke_dynamic_steps(steps_text, iso_code, _location)
        parser = Cucumber::Gherkin::StepsParser.new(StepInvoker.new(self), iso_code)
        parser.parse(steps_text)
      end

      # @api private
      # This allows users to attempt to find, match and execute steps
      # from code as the features are running, as opposed to regular
      # steps which are compiled into test steps before execution.
      #
      # These are commonly called nested steps.
      def invoke_dynamic_step(step_name, multiline_argument, _location = nil)
        matches = step_matches(step_name)
        raise UndefinedDynamicStep, step_name if matches.empty?

        matches.first.invoke(multiline_argument)
      end

      def load_files!(files)
        log.debug("Code:\n")
        files.each do |file|
          load_file(file)
        end
        log.debug("\n")
      end

      def load_files_from_paths(paths)
        files = paths.map { |path| Dir["#{path}/**/*.rb"] }.flatten
        load_files! files
      end

      def unmatched_step_definitions
        registry.unmatched_step_definitions
      end

      def fire_hook(name, *args)
        # TODO: kill with fire
        registry.send(name, *args)
      end

      def step_definitions
        registry.step_definitions
      end

      def find_after_step_hooks(test_case)
        scenario = RunningTestCase.new(test_case)
        hooks = registry.hooks_for(:after_step, scenario)
        StepHooks.new(@configuration.id_generator, hooks, @configuration.event_bus)
      end

      def apply_before_hooks(test_case)
        return test_case if test_case.test_steps.empty?

        scenario = RunningTestCase.new(test_case)
        hooks = registry.hooks_for(:before, scenario)
        BeforeHooks.new(@configuration.id_generator, hooks, scenario, @configuration.event_bus).apply_to(test_case)
      end

      def apply_after_hooks(test_case)
        return test_case if test_case.test_steps.empty?

        scenario = RunningTestCase.new(test_case)
        hooks = registry.hooks_for(:after, scenario)
        AfterHooks.new(@configuration.id_generator, hooks, scenario, @configuration.event_bus).apply_to(test_case)
      end

      def find_around_hooks(test_case)
        scenario = RunningTestCase.new(test_case)

        registry.hooks_for(:around, scenario).map do |hook|
          Hooks.around_hook do |run_scenario|
            hook.invoke('Around', scenario, &run_scenario)
          end
        end
      end

      private

      def step_matches(step_name)
        StepMatchSearch.new(registry.method(:step_matches), @configuration).call(step_name)
      end

      def load_file(file)
        log.debug("  * #{file}\n")
        registry.load_code_file(file)
      end

      def log
        Cucumber.logger
      end
    end
  end
end
