module Spec::Methods
  # Spec helper for generic iteration methods which tests both yielding and
  # iterator overloads.
  #
  # This helper creates two spec examples named *description* with suffixes
  # `" yielding"` and `" iterator"`.
  # The yielding example calls *method* with a block and expects the iteration
  # elements to be yielded to the block. The iterator example calls *method*
  # without a block and expects it to return an `Iterator` which it then consumes.
  #
  # The iterated elements are collected in an array and compared to *expected*,
  # ensuring type-equality of the elements.
  #
  # By default, both examples make sure that the iteration is finished after
  # iterating all elements from *expected*. If the iteration is infinite,
  # passing `infinite: true` skips that check and allows to test a finite sample
  # of an infinite iteration.
  #
  # ```
  # require "spec/helpers/iterate"
  #
  # it_iterates "Array#each", [1, 2, 3], (1..3).each
  # it_iterates "infinite #cycle", [1, 2, 3, 1, 2, 3, 1], (1..3).cycle, infinite: true
  # ```
  #
  # If the iteration elements are tuples (i.e. multiple values), the yielding
  # variant by default only catches the first value because of the block argument
  # mechanics. Passing `tuple: true` ensures all yielded arguments are collected
  # using a splat.
  #
  # ```
  # require "spec/helpers/iterate"
  #
  # it_iterates "Array#each_with_index", [{1, 0}, {2, 1}, {3, 2}], (1..3).each_with_index, tuple: true
  # ```
  macro it_iterates(description, expected, method, *, infinite = false, tuple = false, file = __FILE__, line = __LINE__)
    it {{ "#{description} yielding" }}, file: {{ file }}, line: {{ line }} do
      assert_iterates_yielding {{ expected }}, {{ method }}, infinite: {{ infinite }}, tuple: {{ tuple }}
    end
    it {{ "#{description} iterator" }}, file: {{ file }}, line: {{ line }} do
      assert_iterates_iterator {{ expected }}, {{ method }}, infinite: {{ infinite }}
    end
  end

  # Calls *method* with a block and compares yielded values with *expected*.
  #
  # See `.it_iterates` for details.
  macro assert_iterates_yielding(expected, method, *, infinite = false, tuple = false)
    %remaining = ({{expected}}).size
    %ary = [] of typeof(Enumerable.element_type({{ expected }}))
    {{ method.id }} do |{% if tuple %}*{% end %}x|
      if %remaining == 0
        if {{ infinite }}
          break
        else
          fail "Reached iteration limit #{({{ expected }}).size} receiving value #{x.inspect}"
        end
      end

      %ary << x
      %remaining -= 1
    end

    %ary.should eq({{ expected }})
    %ary.zip({{ expected }}).each_with_index do |(actual, expected), i|
      if actual.class != expected.class
        fail "Mismatching type, expected: #{expected} (#{expected.class}), got: #{actual} (#{actual.class}) at #{i}"
      end
    end
  end

  # Calls *method* expecting an iterator and compares iterated values with *expected*.
  #
  # See `.it_iterates` for details.
  macro assert_iterates_iterator(expected, method, *, infinite = false)
    %ary = [] of typeof(Enumerable.element_type({{ expected }}))
    %iter = {{ method.id }}
    ({{ expected }}).size.times do
      %v = %iter.next
      if %v.is_a?(Iterator::Stop)
        # Compare the actual value directly. Since there are less
        # then expected values, the expectation will fail and raise.
        %ary.should eq({{ expected }})
        raise "unreachable"
      end
      %ary << %v
    end
    unless {{ infinite }}
      %iter.next.should be_a(Iterator::Stop)
    end

    %ary.should eq({{ expected }})
    %ary.zip({{ expected }}).each_with_index do |(actual, expected), i|
      if actual.class != expected.class
        fail "Mismatching type, expected: #{expected} (#{expected.class}), got: #{actual} (#{actual.class}) at #{i}"
      end
    end
  end
end
