# frozen_string_literal: true

require 'cucumber/step_match'
require 'cucumber/glue/invoke_in_world'

module Cucumber
  module Glue
    # A Step Definition holds a Regexp pattern and a Proc, and is
    # typically created by calling {Dsl#register_rb_step_definition Given, When or Then}
    # in the step_definitions Ruby files.
    #
    # Example:
    #
    #   Given /I have (\d+) cucumbers in my belly/ do
    #     # some code here
    #   end
    #
    class StepDefinition
      class MissingProc < StandardError
        def message
          'Step definitions must always have a proc or symbol'
        end
      end

      class << self
        def new(id, registry, string_or_regexp, proc_or_sym, options)
          raise MissingProc if proc_or_sym.nil?

          super id, registry, registry.create_expression(string_or_regexp), create_proc(proc_or_sym, options)
        end

        private

        def create_proc(proc_or_sym, options)
          return proc_or_sym if proc_or_sym.is_a?(Proc)
          raise ArgumentError unless proc_or_sym.is_a?(Symbol)

          message = proc_or_sym
          target_proc = parse_target_proc_from(options)
          patch_location_onto lambda { |*args|
            target = instance_exec(&target_proc)
            target.send(message, *args)
          }
        end

        def patch_location_onto(block)
          location = Core::Test::Location.of_caller(5)
          block.define_singleton_method(:source_location) { [location.file, location.line] }
          block
        end

        def parse_target_proc_from(options)
          return -> { self } unless options.key?(:on)

          target = options[:on]
          case target
          when Proc
            target
          when Symbol
            -> { send(target) }
          else
            -> { raise ArgumentError, 'Target must be a symbol or a proc' }
          end
        end
      end

      attr_reader :id, :expression, :registry

      def initialize(id, registry, expression, proc)
        raise 'No regexp' if expression.is_a?(Regexp)

        @id = id
        @registry = registry
        @expression = expression
        @proc = proc
      end

      def to_envelope
        Cucumber::Messages::Envelope.new(
          step_definition: Cucumber::Messages::StepDefinition.new(
            id: id,
            pattern: Cucumber::Messages::StepDefinitionPattern.new(
              source: expression.source.to_s,
              type: expression_type
            ),
            source_reference: Cucumber::Messages::SourceReference.new(
              uri: location.file,
              location: Cucumber::Messages::Location.new(
                line: location.lines.first
              )
            )
          )
        )
      end

      def expression_type
        return Cucumber::Messages::StepDefinitionPatternType::CUCUMBER_EXPRESSION if expression.is_a?(CucumberExpressions::CucumberExpression)

        Cucumber::Messages::StepDefinitionPatternType::REGULAR_EXPRESSION
      end

      # @api private
      def to_hash
        type = expression.is_a?(CucumberExpressions::RegularExpression) ? 'regular expression' : 'cucumber expression'
        regexp = expression.regexp
        flags = ''
        flags += 'm' if (regexp.options & Regexp::MULTILINE) != 0
        flags += 'i' if (regexp.options & Regexp::IGNORECASE) != 0
        flags += 'x' if (regexp.options & Regexp::EXTENDED) != 0
        {
          source: {
            type: type,
            expression: expression.source
          },
          regexp: {
            source: regexp.source,
            flags: flags
          }
        }
      end

      # @api private
      def ==(other)
        expression.source == other.expression.source
      end

      # @api private
      def arguments_from(step_name)
        @expression.match(step_name)
      end

      # @api private
      # TODO: inline this and step definition just be a value object
      def invoke(args)
        InvokeInWorld.cucumber_instance_exec_in(@registry.current_world, true, @expression.to_s, *args, &@proc)
      rescue ArityMismatchError => e
        e.backtrace.unshift(backtrace_line)
        raise e
      end

      # @api private
      def backtrace_line
        "#{location}:in `#{@expression}'"
      end

      # @api private
      def file_colon_line
        case @proc
        when Proc
          location.to_s
        when Symbol
          ":#{@proc}"
        end
      end

      # The source location where the step definition can be found
      def location
        @location ||= Cucumber::Core::Test::Location.from_source_location(*@proc.source_location)
      end

      # @api private
      def file
        @file ||= location.file
      end
    end
  end
end
