require File.expand_path(File.dirname(__FILE__) + '/test_helper')

class MultipleFailureTest < MiniTest::Unit::TestCase

  def setup
    Resque.redis.flushall
    @worker = Resque::Worker.new(:testing)
    @worker.register_worker

    @old_failure_backend = Resque::Failure.backend
    MockFailureBackend.errors = []
    Resque::Failure::MultipleWithRetrySuppression.classes = [ MockFailureBackend ]
    Resque::Failure.backend = Resque::Failure::MultipleWithRetrySuppression
  end

  def failure_key_for(klass)
    args = []
    key = 'failure-' + klass.redis_retry_key(args)
  end

  def test_failure_is_passed_on_when_job_class_not_found
    skip 'commit 7113b0df to `resque` gem means the failure backend is never called'
    new_job_class = Class.new(LimitThreeJob).tap { |klass| klass.send(:instance_variable_set, :@queue, LimitThreeJob.instance_variable_get(:@queue)) }
    Object.send(:const_set, 'LimitThreeJobTemp', new_job_class)
    Resque.enqueue(LimitThreeJobTemp)

    Object.send(:remove_const, 'LimitThreeJobTemp')
    perform_next_job(@worker)

    assert_equal 1, MockFailureBackend.errors.count, 'should have one error'
    assert_match /uninitialized constant LimitThreeJobTemp/, MockFailureBackend.errors.first
  end

  def test_last_failure_is_saved_in_redis_if_delay
    Resque.enqueue(LimitThreeJobDelay1Hour)
    perform_next_job(@worker)

    # I don't like this, but...
    key = failure_key_for(LimitThreeJobDelay1Hour)
    assert Resque.redis.exists(key)
  end

  def test_retry_key_splatting_args
    # were expecting this to be called twice:
    # - once before the job is executed.
    # - once by the failure backend.
    RetryDefaultsJob.expects(:redis_retry_key).with({'a' => 1, 'b' => 2}).times(2)

    Resque.enqueue(RetryDefaultsJob, {'a' => 1, 'b' => 2})
    perform_next_job(@worker)
  end

  def test_last_failure_removed_from_redis_after_error_limit
    3.times do
      Resque.enqueue(LimitThreeJobDelay1Hour)
      perform_next_job(@worker)
    end

    key = failure_key_for(LimitThreeJobDelay1Hour)
    assert Resque.redis.exists(key), 'key should still exist'

    Resque.enqueue(LimitThreeJobDelay1Hour)
    perform_next_job(@worker)
    assert !Resque.redis.exists(key), 'key should have been removed.'
  end

  def test_last_failure_has_double_delay_redis_expiry_if_delay
    Resque.enqueue(LimitThreeJobDelay1Hour)
    perform_next_job(@worker)

    # I don't like this, but...
    key = failure_key_for(LimitThreeJobDelay1Hour)
    assert_equal 7200, Resque.redis.ttl(key)
  end

  def test_last_failure_is_not_saved_in_redis_if_no_delay
    Resque.enqueue(LimitThreeJob)
    perform_next_job(@worker)

    # I don't like this, but...
    key = failure_key_for(LimitThreeJob)
    assert !Resque.redis.exists(key)
  end

  def test_errors_are_suppressed_up_to_retry_limit
    Resque.enqueue(LimitThreeJob)
    3.times do
      perform_next_job(@worker)
    end

    assert_equal 0, MockFailureBackend.errors.size
  end

  def test_errors_are_logged_after_retry_limit
    Resque.enqueue(LimitThreeJob)
    4.times do
      perform_next_job(@worker)
    end

    assert_equal 1, MockFailureBackend.errors.size
  end

  def test_jobs_without_retry_log_errors
    5.times do
      Resque.enqueue(NoRetryJob)
      perform_next_job(@worker)
    end

    assert_equal 5, MockFailureBackend.errors.size
  end

  def test_custom_retry_identifier_job
    Resque.enqueue(CustomRetryIdentifierFailingJob, 'qq', 2)
    4.times do
      perform_next_job(@worker)
    end
    assert_equal 1, MockFailureBackend.errors.size
  end

  def teardown
    Resque::Failure.backend = @old_failure_backend
  end
end
