require_relative "spec_helper"

class TestError < Exception; end

describe Retriable do
  subject do
    Retriable
  end

  before do
    srand 0
  end

  describe "with sleep disabled" do
    before do
      Retriable.configure do |c|
        c.sleep_disabled = true
      end
    end

    it "stops at first try if the block does not raise an exception" do
      tries = 0
      subject.retriable do
        tries += 1
      end

      expect(tries).must_equal 1
    end

    it "raises a LocalJumpError if #retriable is not given a block" do
      expect do
        subject.retriable on: StandardError
      end.must_raise LocalJumpError

      expect do
        subject.retriable on: StandardError, timeout: 2
      end.must_raise LocalJumpError
    end

    it "makes 3 tries when retrying block of code raising StandardError with no arguments" do
      tries = 0

      expect do
        subject.retriable do
          tries += 1
          raise StandardError.new, "StandardError occurred"
        end
      end.must_raise StandardError

      expect(tries).must_equal 3
    end

    it "makes only 1 try when exception raised is not ancestor of StandardError" do
      tries = 0

      expect do
        subject.retriable do
          tries += 1
          raise TestError.new, "TestError occurred"
        end
      end.must_raise TestError

      expect(tries).must_equal 1
    end

    it "#retriable with custom exception tries 3 times and re-raises the exception" do
      tries = 0

      expect do
        subject.retriable on: TestError do
          tries += 1
          raise TestError.new, "TestError occurred"
        end
      end.must_raise TestError

      expect(tries).must_equal 3
    end

    it "#retriable tries 10 times" do
      tries = 0

      expect do
        subject.retriable(tries: 10) do
          tries += 1
          raise StandardError.new, "StandardError occurred"
        end
      end.must_raise StandardError

      expect(tries).must_equal 10
    end

    it "#retriable will timeout after 1 second" do
      expect do
        subject.retriable timeout: 1 do
          sleep 1.1
        end
      end.must_raise Timeout::Error
    end

    it "applies a randomized exponential backoff to each try" do
      tries = 0
      time_table = []

      handler = lambda do |exception, _try, _elapsed_time, next_interval|
        expect(exception.class).must_equal ArgumentError
        time_table << next_interval
      end

      expect do
        Retriable.retriable(
          on: [EOFError, ArgumentError],
          on_retry: handler,
          tries: 10,
        ) do
          tries += 1
          raise ArgumentError.new, "ArgumentError occurred"
        end
      end.must_raise ArgumentError

      expect(time_table).must_equal([
        0.5244067512211441,
        0.9113920238761231,
        1.2406087918999114,
        1.7632403621664823,
        2.338001204738311,
        4.350816718580626,
        5.339852157217869,
        11.889873261212443,
        18.756037881636484,
        nil,
      ])

      expect(tries).must_equal(10)
    end

    describe "retries with an on_#retriable handler, 6 max retries, and a 0.0 rand_factor" do
      before do
        tries = 6
        @try_count = 0
        @time_table = {}

        handler = lambda do |exception, try, _elapsed_time, next_interval|
          expect(exception.class).must_equal ArgumentError
          @time_table[try] = next_interval
        end

        Retriable.retriable(
          on: [EOFError, ArgumentError],
          on_retry: handler,
          rand_factor: 0.0,
          tries: tries,
        ) do
          @try_count += 1
          raise ArgumentError.new, "ArgumentError occurred" if @try_count < tries
        end
      end

      it "makes 6 tries" do
        expect(@try_count).must_equal 6
      end

      it "applies a non-randomized exponential backoff to each try" do
        expect(@time_table).must_equal(
          1 => 0.5,
          2 => 0.75,
          3 => 1.125,
          4 => 1.6875,
          5 => 2.53125,
        )
      end
    end

    it "#retriable has a max interval of 1.5 seconds" do
      tries = 0
      time_table = {}

      handler = lambda do |_exception, try, _elapsed_time, next_interval|
        time_table[try] = next_interval
      end

      expect do
        subject.retriable(
          on: StandardError,
          on_retry: handler,
          rand_factor: 0.0,
          tries: 5,
          max_interval: 1.5,
        ) do
          tries += 1
          raise StandardError.new, "StandardError occurred"
        end
      end.must_raise StandardError

      expect(time_table).must_equal(
        1 => 0.5,
        2 => 0.75,
        3 => 1.125,
        4 => 1.5,
        5 => nil,
      )
    end

    it "#retriable with custom defined intervals" do
      intervals = [
        0.5,
        0.75,
        1.125,
        1.5,
        1.5,
      ]
      time_table = {}

      handler = lambda do |_exception, try, _elapsed_time, next_interval|
        time_table[try] = next_interval
      end

      try_count = 0

      expect do
        subject.retriable(
          on_retry: handler,
          intervals: intervals,
        ) do
          try_count += 1
          raise StandardError.new, "StandardError occurred"
        end
      end.must_raise StandardError

      expect(time_table).must_equal(
        1 => 0.5,
        2 => 0.75,
        3 => 1.125,
        4 => 1.5,
        5 => 1.5,
        6 => nil,
      )

      expect(try_count).must_equal(6)
    end

    it "#retriable with a hash exception where the value is an exception message pattern" do
      e = expect do
        subject.retriable on: { TestError => /something went wrong/ } do
          raise TestError, "something went wrong"
        end
      end.must_raise TestError

      expect(e.message).must_equal "something went wrong"
    end

    it "#retriable with a hash exception list matches exception subclasses" do
      class SecondTestError < TestError; end
      class DifferentTestError < Exception; end

      tries = 0
      e = expect do
        subject.retriable on: {
            DifferentTestError => /should never happen/,
            TestError => /something went wrong/,
            DifferentTestError => /also should never happen/,
          }, tries: 4 do
          tries += 1
          raise SecondTestError, "something went wrong"
        end
      end.must_raise SecondTestError

      expect(e.message).must_equal "something went wrong"
      expect(tries).must_equal 4
    end

    it "#retriable with a hash exception list does not retry matching exception subclass but not message" do
      class SecondTestError < TestError; end

      tries = 0
      expect do
        subject.retriable on: { TestError => /something went wrong/ }, tries: 4 do
          tries += 1
          raise SecondTestError, "not a match"
        end
      end.must_raise SecondTestError

      expect(tries).must_equal 1
    end

    it "#retriable with a hash exception list where the values are exception message patterns" do
      tries = 0
      exceptions = []
      handler = lambda do |exception, try, _elapsed_time, _next_interval|
        exceptions[try] = exception
      end

      e = expect do
        subject.retriable tries: 4, on: { StandardError => nil, TestError => [/foo/, /bar/] }, on_retry: handler do
          tries += 1
          case tries
          when 1
            raise TestError, "foo"
          when 2
            raise TestError, "bar"
          when 3
            raise StandardError
          else
            raise TestError, "crash"
          end
        end
      end.must_raise TestError

      expect(e.message).must_equal "crash"
      expect(exceptions[1].class).must_equal TestError
      expect(exceptions[1].message).must_equal "foo"
      expect(exceptions[2].class).must_equal TestError
      expect(exceptions[2].message).must_equal "bar"
      expect(exceptions[3].class).must_equal StandardError
    end

    it "#retriable can be called in the global scope" do
      expect do
        retriable do
          puts "should raise NoMethodError"
        end
      end.must_raise NoMethodError

      require_relative "../lib/retriable/core_ext/kernel"

      tries = 0

      expect do
        retriable do
          tries += 1
          raise StandardError
        end
      end.must_raise StandardError

      expect(tries).must_equal 3
    end
  end

  it "#retriable runs for a max elapsed time of 2 seconds" do
    subject.configure do |c|
      c.sleep_disabled = false
    end

    expect(subject.config.sleep_disabled).must_equal false

    tries = 0
    time_table = {}

    handler = lambda do |_exception, try, elapsed_time, _next_interval|
      time_table[try] = elapsed_time
    end

    expect do
      subject.retriable(
        base_interval: 1.0,
        multiplier: 1.0,
        rand_factor: 0.0,
        max_elapsed_time: 2.0,
        on_retry: handler,
      ) do
        tries += 1
        raise EOFError
      end
    end.must_raise EOFError

    expect(tries).must_equal 2
  end
end
