# frozen_string_literal: true

require 'cucumber/multiline_argument'

module Cucumber
  # Represents the match found between a Test Step and its activation
  class StepMatch # :nodoc:
    attr_reader :step_definition, :step_arguments

    def initialize(step_definition, step_name, step_arguments)
      raise "step_arguments can't be nil (but it can be an empty array)" if step_arguments.nil?

      @step_definition = step_definition
      @name_to_match = step_name
      @step_arguments = step_arguments
    end

    def args
      current_world = @step_definition.registry.current_world
      @step_arguments.map do |arg|
        arg.value(current_world)
      end
    end

    def activate(test_step)
      test_step.with_action(@step_definition.location) do
        invoke(MultilineArgument.from_core(test_step.multiline_arg))
      end
    end

    def invoke(multiline_arg)
      all_args = args
      multiline_arg.append_to(all_args)
      @step_definition.invoke(all_args)
    end

    # Formats the matched arguments of the associated Step. This method
    # is usually called from visitors, which render output.
    #
    # The +format+ can either be a String or a Proc.
    #
    # If it is a String it should be a format string according to
    # <tt>Kernel#sprinf</tt>, for example:
    #
    #   '<span class="param">%s</span></tt>'
    #
    # If it is a Proc, it should take one argument and return the formatted
    # argument, for example:
    #
    #   lambda { |param| "[#{param}]" }
    #
    def format_args(format = ->(a) { a }, &proc)
      replace_arguments(@name_to_match, @step_arguments, format, &proc)
    end

    def location
      @step_definition.location
    end

    def file_colon_line
      location.to_s
    end

    def backtrace_line
      "#{file_colon_line}:in `#{@step_definition.expression}'"
    end

    def text_length
      @step_definition.expression.source.to_s.unpack('U*').length
    end

    def replace_arguments(string, step_arguments, format)
      s = string.dup
      offset = past_offset = 0
      step_arguments.each do |step_argument|
        group = step_argument.group
        next if group.value.nil? || group.start < past_offset

        replacement = if block_given?
                        yield(group.value)
                      elsif format.instance_of?(Proc)
                        format.call(group.value)
                      else
                        format % group.value
                      end

        s[group.start + offset, group.value.length] = replacement
        offset += replacement.unpack('U*').length - group.value.unpack('U*').length
        past_offset = group.start + group.value.length
      end
      s
    end

    def inspect # :nodoc:
      "#<#{self.class}: #{location}>"
    end
  end

  class SkippingStepMatch
    def activate(test_step)
      test_step.with_action { raise Core::Test::Result::Skipped }
    end
  end

  class NoStepMatch # :nodoc:
    attr_reader :step_definition, :name

    def initialize(step, name)
      @step = step
      @name = name
    end

    def format_args(*_args)
      @name
    end

    def location
      raise "No location for #{@step}" unless @step.location

      @step.location
    end

    def file_colon_line
      location.to_s
    end

    def backtrace_line
      @step.backtrace_line
    end

    def text_length
      @step.text.length
    end

    def step_arguments
      []
    end

    def activate(test_step)
      # noop
      test_step
    end
  end

  class AmbiguousStepMatch
    def initialize(error)
      @error = error
    end

    def activate(test_step)
      test_step.with_unskippable_action { raise Core::Test::Result::Ambiguous, @error.message }
    end
  end
end
