require_relative '../../../spec_helper'
require_relative '../fixtures/classes'

# Describes Numeric#step shared specs between different argument styles.
# To be able to do it, the @step ivar must contain a Proc that transforms
# the step call arguments passed as positional arguments to the style of
# arguments pretended to test.
describe :numeric_step, shared: true do
  before :each do
    ScratchPad.record []
    @prc = -> x { ScratchPad << x }
  end

  it "defaults to step = 1" do
    @step.call(1, 5, &@prc)
    ScratchPad.recorded.should.eql? [1, 2, 3, 4, 5]
  end

  it "defaults to an infinite limit with a step size of 1 for Integers" do
    1.step.first(5).should == [1, 2, 3, 4, 5]
  end

  it "defaults to an infinite limit with a step size of 1.0 for Floats" do
    1.0.step.first(5).should == [1.0, 2.0, 3.0, 4.0, 5.0]
  end

  describe "when self, stop and step are Integers" do
    it "yields only Integers" do
      @step.call(1, 5, 1) { |x| x.should.instance_of?(Integer) }
    end

    describe "with a positive step" do
      it "yields while increasing self by step until stop is reached" do
        @step.call(1, 5, 1, &@prc)
        ScratchPad.recorded.should.eql? [1, 2, 3, 4, 5]
      end

      it "yields once when self equals stop" do
        @step.call(1, 1, 1, &@prc)
        ScratchPad.recorded.should.eql? [1]
      end

      it "does not yield when self is greater than stop" do
        @step.call(2, 1, 1, &@prc)
        ScratchPad.recorded.should.eql? []
      end
    end

    describe "with a negative step" do
      it "yields while decreasing self by step until stop is reached" do
        @step.call(5, 1, -1, &@prc)
        ScratchPad.recorded.should.eql? [5, 4, 3, 2, 1]
      end

      it "yields once when self equals stop" do
        @step.call(5, 5, -1, &@prc)
        ScratchPad.recorded.should.eql? [5]
      end

      it "does not yield when self is less than stop" do
        @step.call(1, 5, -1, &@prc)
        ScratchPad.recorded.should == []
      end
    end
  end

  describe "when at least one of self, stop or step is a Float" do
    it "yields Floats even if only self is a Float" do
      @step.call(1.5, 5, 1) { |x| x.should.instance_of?(Float) }
    end

    it "yields Floats even if only stop is a Float" do
      @step.call(1, 5.0, 1) { |x| x.should.instance_of?(Float) }
    end

    it "yields Floats even if only step is a Float" do
      @step.call(1, 5, 1.0) { |x| x.should.instance_of?(Float) }
    end

    describe "with a positive step" do
      it "yields while increasing self by step while < stop" do
        @step.call(1.5, 5, 1, &@prc)
        ScratchPad.recorded.should.eql? [1.5, 2.5, 3.5, 4.5]
      end

      it "yields once when self equals stop" do
        @step.call(1.5, 1.5, 1, &@prc)
        ScratchPad.recorded.should.eql? [1.5]
      end

      it "does not yield when self is greater than stop" do
        @step.call(2.5, 1.5, 1, &@prc)
        ScratchPad.recorded.should == []
      end

      it "is careful about not yielding a value greater than limit" do
        # As 9*1.3+1.0 == 12.700000000000001 > 12.7, we test:
        @step.call(1.0, 12.7, 1.3, &@prc)
        ScratchPad.recorded.should.eql? [1.0, 2.3, 3.6, 4.9, 6.2, 7.5, 8.8, 10.1, 11.4, 12.7]
      end
    end

    describe "with a negative step" do
      it "yields while decreasing self by step while self > stop" do
        @step.call(5, 1.5, -1, &@prc)
        ScratchPad.recorded.should.eql? [5.0, 4.0, 3.0, 2.0]
      end

      it "yields once when self equals stop" do
        @step.call(1.5, 1.5, -1, &@prc)
        ScratchPad.recorded.should.eql? [1.5]
      end

      it "does not yield when self is less than stop" do
        @step.call(1, 5, -1.5, &@prc)
        ScratchPad.recorded.should == []
      end

      it "is careful about not yielding a value smaller than limit" do
        # As -9*1.3-1.0 == -12.700000000000001 < -12.7, we test:
        @step.call(-1.0, -12.7, -1.3, &@prc)
        ScratchPad.recorded.should.eql? [-1.0, -2.3, -3.6, -4.9, -6.2, -7.5, -8.8, -10.1, -11.4, -12.7]
      end
    end

    describe "with a positive Infinity step" do
      it "yields once if self < stop" do
        @step.call(42, 100, infinity_value, &@prc)
        ScratchPad.recorded.should.eql? [42.0]
      end

      it "yields once when stop is Infinity" do
        @step.call(42, infinity_value, infinity_value, &@prc)
        ScratchPad.recorded.should.eql? [42.0]
      end

      it "yields once when self equals stop" do
        @step.call(42, 42, infinity_value, &@prc)
        ScratchPad.recorded.should.eql? [42.0]
      end

      it "yields once when self and stop are Infinity" do
        # @step.call(infinity_value, infinity_value, infinity_value, &@prc)
        @step.call(infinity_value, infinity_value, infinity_value, &@prc)
        ScratchPad.recorded.should == [infinity_value]
      end

      it "does not yield when self > stop" do
        @step.call(100, 42, infinity_value, &@prc)
        ScratchPad.recorded.should == []
      end

      it "does not yield when stop is -Infinity" do
        @step.call(42, -infinity_value, infinity_value, &@prc)
        ScratchPad.recorded.should == []
      end
    end

    describe "with a negative Infinity step" do
      it "yields once if self > stop" do
        @step.call(42, 6, -infinity_value, &@prc)
        ScratchPad.recorded.should.eql? [42.0]
      end

      it "yields once if stop is -Infinity" do
        @step.call(42, -infinity_value, -infinity_value, &@prc)
        ScratchPad.recorded.should.eql? [42.0]
      end

      it "yields once when self equals stop" do
        @step.call(42, 42, -infinity_value, &@prc)
        ScratchPad.recorded.should.eql? [42.0]
      end

      it "yields once when self and stop are Infinity" do
        @step.call(infinity_value, infinity_value, -infinity_value, &@prc)
        ScratchPad.recorded.should == [infinity_value]
      end

      it "does not yield when self > stop" do
        @step.call(42, 100, -infinity_value, &@prc)
        ScratchPad.recorded.should == []
      end

      it "does not yield when stop is Infinity" do
        @step.call(42, infinity_value, -infinity_value, &@prc)
        ScratchPad.recorded.should == []
      end
    end

    describe "with a Infinity stop and a positive step" do
      it "does not yield when self is infinity" do
        @step.call(infinity_value, infinity_value, 1, &@prc)
        ScratchPad.recorded.should == []
      end
    end

    describe "with a Infinity stop and a negative step" do
      it "does not yield when self is negative infinity" do
        @step.call(-infinity_value, infinity_value, -1, &@prc)
        ScratchPad.recorded.should == []
      end

      it "does not yield when self is positive infinity" do
        @step.call(infinity_value, infinity_value, -1, &@prc)
        ScratchPad.recorded.should == []
      end
    end

    describe "with a negative Infinity stop and a positive step" do
      it "does not yield when self is negative infinity" do
        @step.call(-infinity_value, -infinity_value, 1, &@prc)
        ScratchPad.recorded.should == []
      end
    end

    describe "with a negative Infinity stop and a negative step" do
      it "does not yield when self is negative infinity" do
        @step.call(-infinity_value, -infinity_value, -1, &@prc)
        ScratchPad.recorded.should == []
      end
    end

  end

  describe "when step is a String" do
    describe "with self and stop as Integers" do
      it "raises an ArgumentError when step is a numeric representation" do
        -> { @step.call(1, 5, "1") {} }.should.raise(ArgumentError)
        -> { @step.call(1, 5, "0.1") {} }.should.raise(ArgumentError)
        -> { @step.call(1, 5, "1/3") {} }.should.raise(ArgumentError)
      end
      it "raises an ArgumentError with step as an alphanumeric string" do
        -> { @step.call(1, 5, "foo") {} }.should.raise(ArgumentError)
      end
    end

    describe "with self and stop as Floats" do
      it "raises an ArgumentError when step is a numeric representation" do
        -> { @step.call(1.1, 5.1, "1") {} }.should.raise(ArgumentError)
        -> { @step.call(1.1, 5.1, "0.1") {} }.should.raise(ArgumentError)
        -> { @step.call(1.1, 5.1, "1/3") {} }.should.raise(ArgumentError)
      end
      it "raises an ArgumentError with step as an alphanumeric string" do
        -> { @step.call(1.1, 5.1, "foo") {} }.should.raise(ArgumentError)
      end
    end
  end

  it "does not rescue ArgumentError exceptions" do
    -> { @step.call(1, 2) { raise ArgumentError, "" }}.should.raise(ArgumentError)
  end

  it "does not rescue TypeError exceptions" do
    -> { @step.call(1, 2) { raise TypeError, "" } }.should.raise(TypeError)
  end

  describe "when no block is given" do
    step_enum_class = Enumerator::ArithmeticSequence

    it "returns an #{step_enum_class} when not passed a block and self > stop" do
      @step.call(1, 0, 2).should.instance_of?(step_enum_class)
    end

    it "returns an #{step_enum_class} when not passed a block and self < stop" do
      @step.call(1, 2, 3).should.instance_of?(step_enum_class)
    end

    it "returns an #{step_enum_class} that uses the given step" do
      @step.call(0, 5, 2).to_a.should.eql? [0, 2, 4]
    end

    describe "when step is a String" do
      describe "with self and stop as Integers" do
        it "returns an Enumerator" do
          @step.call(1, 5, "foo").should.instance_of?(Enumerator)
        end
      end

      describe "with self and stop as Floats" do
        it "returns an Enumerator" do
          @step.call(1.1, 5.1, "foo").should.instance_of?(Enumerator)
        end
      end
    end

    describe "returned Enumerator" do
      describe "size" do
        describe "when step is a String" do
          describe "with self and stop as Integers" do
            it "raises an ArgumentError when step is a numeric representation" do
              -> { @step.call(1, 5, "1").size }.should.raise(ArgumentError)
              -> { @step.call(1, 5, "0.1").size }.should.raise(ArgumentError)
              -> { @step.call(1, 5, "1/3").size }.should.raise(ArgumentError)
            end
            it "raises an ArgumentError with step as an alphanumeric string" do
              -> { @step.call(1, 5, "foo").size }.should.raise(ArgumentError)
            end
          end

          describe "with self and stop as Floats" do
            it "raises an ArgumentError when step is a numeric representation" do
              -> { @step.call(1.1, 5.1, "1").size }.should.raise(ArgumentError)
              -> { @step.call(1.1, 5.1, "0.1").size }.should.raise(ArgumentError)
              -> { @step.call(1.1, 5.1, "1/3").size }.should.raise(ArgumentError)
            end
            it "raises an ArgumentError with step as an alphanumeric string" do
              -> { @step.call(1.1, 5.1, "foo").size }.should.raise(ArgumentError)
            end
          end
        end

        describe "when self, stop and step are Integers and step is positive" do
          it "returns the difference between self and stop divided by the number of steps" do
            @step.call(5, 10, 11).size.should == 1
            @step.call(5, 10, 6).size.should == 1
            @step.call(5, 10, 5).size.should == 2
            @step.call(5, 10, 4).size.should == 2
            @step.call(5, 10, 2).size.should == 3
            @step.call(5, 10, 1).size.should == 6
            @step.call(5, 10).size.should == 6
            @step.call(10, 10, 1).size.should == 1
          end

          it "returns 0 if value > limit" do
            @step.call(11, 10, 1).size.should == 0
          end
        end

        describe "when self, stop and step are Integers and step is negative" do
          it "returns the difference between self and stop divided by the number of steps" do
            @step.call(10, 5, -11).size.should == 1
            @step.call(10, 5, -6).size.should == 1
            @step.call(10, 5, -5).size.should == 2
            @step.call(10, 5, -4).size.should == 2
            @step.call(10, 5, -2).size.should == 3
            @step.call(10, 5, -1).size.should == 6
            @step.call(10, 10, -1).size.should == 1
          end

          it "returns 0 if value < limit" do
            @step.call(10, 11, -1).size.should == 0
          end
        end

        describe "when self, stop or step is a Float" do
          describe "and step is positive" do
            it "returns the difference between self and stop divided by the number of steps" do
              @step.call(5, 10, 11.0).size.should == 1
              @step.call(5, 10, 6.0).size.should == 1
              @step.call(5, 10, 5.0).size.should == 2
              @step.call(5, 10, 4.0).size.should == 2
              @step.call(5, 10, 2.0).size.should == 3
              @step.call(5, 10, 0.5).size.should == 11
              @step.call(5, 10, 1.0).size.should == 6
              @step.call(5, 10.5).size.should == 6
              @step.call(10, 10, 1.0).size.should == 1
            end

            it "returns 0 if value > limit" do
              @step.call(10, 5.5).size.should == 0
              @step.call(11, 10, 1.0).size.should == 0
              @step.call(11, 10, 1.5).size.should == 0
              @step.call(10, 5, infinity_value).size.should == 0
            end

            it "returns 1 if step is infinity_value" do
              @step.call(5, 10, infinity_value).size.should == 1
            end
          end

          describe "and step is negative" do
            it "returns the difference between self and stop divided by the number of steps" do
              @step.call(10, 5, -11.0).size.should == 1
              @step.call(10, 5, -6.0).size.should == 1
              @step.call(10, 5, -5.0).size.should == 2
              @step.call(10, 5, -4.0).size.should == 2
              @step.call(10, 5, -2.0).size.should == 3
              @step.call(10, 5, -0.5).size.should == 11
              @step.call(10, 5, -1.0).size.should == 6
              @step.call(10, 10, -1.0).size.should == 1
            end

            it "returns 0 if value < limit" do
              @step.call(10, 11, -1.0).size.should == 0
              @step.call(10, 11, -1.5).size.should == 0
              @step.call(5, 10, -infinity_value).size.should == 0
            end

            it "returns 1 if step is infinity_value" do
              @step.call(10, 5, -infinity_value).size.should == 1
            end
          end
        end

        describe "when stop is not passed" do
          it "returns infinity_value" do
            @step.call(1).size.should == infinity_value
          end
        end

        describe "when stop is nil" do
          it "returns infinity_value" do
            @step.call(1, nil, 5).size.should == infinity_value
          end
        end
      end
    end
  end
end
