require 'helper'

describe Delayed::Job do
  let(:worker) { Delayed::Worker.new }

  def create_job(opts = {})
    described_class.create(opts.merge(payload_object: SimpleJob.new))
  end

  before do
    Delayed::Worker.max_priority = nil
    Delayed::Worker.min_priority = nil
    Delayed::Worker.max_claims = 1 # disable multithreading because SimpleJob is not threadsafe
    Delayed::Worker.default_priority = 99
    Delayed::Worker.delay_jobs = true
    Delayed::Worker.default_queue_name = 'default_tracking'
    SimpleJob.runs = 0
    described_class.delete_all
  end

  it 'sets run_at automatically if not set' do
    expect(described_class.create(payload_object: ErrorJob.new).run_at).not_to be_nil
  end

  it 'does not set run_at automatically if already set' do
    later = described_class.db_time_now + 5.minutes
    job = described_class.create(payload_object: ErrorJob.new, run_at: later)
    expect(job.run_at).to be_within(1).of(later)
  end

  describe '#reload' do
    it 'reloads the payload' do
      job = described_class.enqueue payload_object: SimpleJob.new
      expect(job.payload_object.object_id).not_to eq(job.reload.payload_object.object_id)
    end
  end

  describe 'enqueue' do
    it "allows enqueue hook to modify job at DB level" do
      later = described_class.db_time_now + 20.minutes
      job = described_class.enqueue payload_object: EnqueueJobMod.new
      expect(described_class.find(job.id).run_at).to be_within(1).of(later)
    end

    context 'with a hash' do
      it "raises ArgumentError when handler doesn't respond_to :perform" do
        expect { described_class.enqueue(payload_object: Object.new) }.to raise_error(ArgumentError)
      end

      it 'is able to set priority' do
        job = described_class.enqueue payload_object: SimpleJob.new, priority: 5
        expect(job.priority).to eq(5)
      end

      it 'is able to set priority by symbol name' do
        job = described_class.enqueue SimpleJob.new, priority: :eventual
        expect(job.priority).to be_eventual
        expect(job.priority).to eq(20)
      end

      it 'uses default priority' do
        job = described_class.enqueue payload_object: SimpleJob.new
        expect(job.priority).to eq(99)
      end

      it 'is able to set run_at' do
        later = described_class.db_time_now + 5.minutes
        job = described_class.enqueue payload_object: SimpleJob.new, run_at: later
        expect(job.run_at).to be_within(1).of(later)
      end

      it 'is able to set queue' do
        job = described_class.enqueue payload_object: NamedQueueJob.new, queue: 'tracking'
        expect(job.queue).to eq('tracking')
      end

      it 'uses default queue' do
        job = described_class.enqueue payload_object: SimpleJob.new
        expect(job.queue).to eq(Delayed::Worker.default_queue_name)
      end

      it "uses the payload object's queue" do
        job = described_class.enqueue payload_object: NamedQueueJob.new
        expect(job.queue).to eq(NamedQueueJob.new.queue_name)
      end
    end

    context 'with multiple arguments' do
      it "raises ArgumentError when handler doesn't respond_to :perform" do
        expect { described_class.enqueue(Object.new) }.to raise_error(ArgumentError)
      end

      it 'increases count after enqueuing items' do
        described_class.enqueue SimpleJob.new
        expect(described_class.count).to eq(1)
      end

      it 'uses default priority when it is not set' do
        @job = described_class.enqueue SimpleJob.new
        expect(@job.priority).to eq(99)
      end

      it 'works with jobs in modules' do
        M::ModuleJob.runs = 0
        job = described_class.enqueue M::ModuleJob.new
        expect { job.invoke_job }.to change { M::ModuleJob.runs }.from(0).to(1)
      end

      it 'does not mutate the options hash' do
        options = { priority: 1 }
        described_class.enqueue SimpleJob.new, options
        expect(options).to eq(priority: 1)
      end
    end

    context 'with delay_jobs = false' do
      before(:each) do
        Delayed::Worker.delay_jobs = false
      end

      it 'does not increase count after enqueuing items' do
        described_class.enqueue SimpleJob.new
        expect(described_class.count).to eq(0)
      end

      it 'invokes the enqueued job' do
        job = SimpleJob.new
        expect(job).to receive(:perform)
        described_class.enqueue job
      end

      it 'returns a job, not the result of invocation' do
        expect(described_class.enqueue(SimpleJob.new)).to be_instance_of(described_class)
      end
    end
  end

  describe 'callbacks' do
    before(:each) do
      CallbackJob.messages = []
    end

    %w(before success after).each do |callback|
      it "calls #{callback} with job" do
        job = described_class.enqueue(CallbackJob.new)
        expect(job.payload_object).to receive(callback).with(job)
        job.invoke_job
      end
    end

    it 'calls before and after callbacks' do
      job = described_class.enqueue(CallbackJob.new)
      expect(CallbackJob.messages).to eq(['enqueue'])
      job.invoke_job
      expect(CallbackJob.messages).to eq(%w(enqueue before perform success after))
    end

    it 'calls the after callback with an error' do
      job = described_class.enqueue(CallbackJob.new)
      expect(job.payload_object).to receive(:perform).and_raise(RuntimeError.new('fail'))

      expect { job.invoke_job }.to raise_error(RuntimeError, 'fail')
      expect(CallbackJob.messages).to eq(['enqueue', 'before', 'error: RuntimeError', 'after'])
    end

    it 'calls error when before raises an error' do
      job = described_class.enqueue(CallbackJob.new)
      expect(job.payload_object).to receive(:before).and_raise(RuntimeError.new('fail'))
      expect { job.invoke_job }.to raise_error(RuntimeError, 'fail')
      expect(CallbackJob.messages).to eq(['enqueue', 'error: RuntimeError', 'after'])
    end
  end

  describe 'payload_object' do
    it 'raises a DeserializationError when the job class is totally unknown' do
      job = described_class.new handler: '--- !ruby/object:JobThatDoesNotExist {}'
      expect { job.payload_object }.to raise_error(Delayed::DeserializationError)
    end

    it 'raises a DeserializationError when the job struct is totally unknown' do
      job = described_class.new handler: '--- !ruby/struct:StructThatDoesNotExist {}'
      expect { job.payload_object }.to raise_error(Delayed::DeserializationError)
    end

    it 'raises a DeserializationError when the YAML.load raises argument error' do
      job = described_class.new handler: '--- !ruby/struct:GoingToRaiseArgError {}'
      expect(YAML).to receive(:load_dj).and_raise(ArgumentError)
      expect { job.payload_object }.to raise_error(Delayed::DeserializationError)
    end

    it 'raises a DeserializationError when the YAML.load raises syntax error' do
      # only test with Psych since the other YAML parsers don't raise a SyntaxError
      unless /syck|yecht/i.match?(YAML.parser.class.name)
        job = described_class.new handler: 'message: "no ending quote'
        expect { job.payload_object }.to raise_error(Delayed::DeserializationError)
      end
    end
  end

  describe 'reserve' do
    before do
      Delayed::Worker.max_run_time = 2.minutes
    end

    after do
      Time.zone = nil
    end

    it 'does not reserve failed jobs' do
      create_job attempts: 50, failed_at: described_class.db_time_now
      expect(described_class.reserve(worker)).to eq []
    end

    it 'does not reserve jobs scheduled for the future' do
      create_job run_at: described_class.db_time_now + 1.minute
      expect(described_class.reserve(worker)).to eq []
    end

    it 'reserves jobs scheduled for the past' do
      job = create_job run_at: described_class.db_time_now - 1.minute
      expect(described_class.reserve(worker)).to eq([job])
    end

    it 'reserves jobs scheduled for the past when time zones are involved' do
      Time.zone = 'US/Eastern'
      job = create_job run_at: described_class.db_time_now - 1.minute
      expect(described_class.reserve(worker)).to eq([job])
    end

    it 'does not reserve jobs locked by other workers' do
      job = create_job
      other_worker = Delayed::Worker.new
      other_worker.name = 'other_worker'
      expect(described_class.reserve(other_worker)).to eq([job])
      expect(described_class.reserve(worker)).to eq []
    end

    it 'reserves open jobs' do
      job = create_job
      expect(described_class.reserve(worker)).to eq([job])
    end

    it 'reserves expired jobs' do
      job = create_job(locked_by: 'some other worker',
                       locked_at: described_class.db_time_now - Delayed::Worker.max_run_time - 1.minute)
      expect(described_class.reserve(worker)).to eq([job])
    end

    it 'reserves own jobs' do
      job = create_job(locked_by: worker.name, locked_at: (described_class.db_time_now - 1.minute))
      expect(described_class.reserve(worker)).to eq([job])
    end
  end

  describe '#name' do
    context 'when name column is populated' do
      it 'is the class name of the job that was enqueued' do
        job = described_class.new(payload_object: ErrorJob.new)
        expect(job.name).to eq('ErrorJob')
        job.save!
        expect(job.reload.name).to eq('ErrorJob')
        expect(described_class.group(:name).count).to eq('ErrorJob' => 1)
      end

      it 'is the class name of the performable job if it is an ActiveJob' do
        job_wrapper = ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(ActiveJobJob.new.serialize)
        job = described_class.new(payload_object: job_wrapper)
        expect(job.name).to eq('ActiveJobJob')
        job.save!
        expect(job.reload.name).to eq('ActiveJobJob')
        expect(described_class.group(:name).count).to eq('ActiveJobJob' => 1)
      end

      it 'is the returned display_name if display_name is defined on the job object' do
        job = described_class.new(payload_object: NamedJob.new)
        expect(job.name).to eq('named_job')
        job.save!
        expect(job.reload.name).to eq('named_job')
        expect(described_class.group(:name).count).to eq('named_job' => 1)
      end

      it 'is the instance method that will be called if its a performable method object' do
        job = Story.create(text: '...').delay.save
        expect(job.name).to eq('Story#save')
        expect(described_class.group(:name).count).to eq('Story#save' => 1)
      end

      it 'is the custom name value when set explicitly' do
        job = described_class.new(payload_object: ErrorJob.new)
        job.name = 'Custom Name'
        job.save!
        expect(job.reload.name).to eq('Custom Name')
        expect(described_class.group(:name).count).to eq('Custom Name' => 1)
      end
    end

    context 'when name column is NULL' do
      it 'is the class name of the job that was enqueued' do
        job = described_class.create(payload_object: ErrorJob.new)
        job.update_column(:name, nil) # rubocop:disable Rails/SkipsModelValidations
        expect(job.reload.name).to eq('ErrorJob')
      end

      it 'is the class name of the performable job if it is an ActiveJob' do
        job_wrapper = ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(ActiveJobJob.new.serialize)
        job = described_class.create(payload_object: job_wrapper)
        job.update_column(:name, nil) # rubocop:disable Rails/SkipsModelValidations
        expect(job.reload.name).to eq('ActiveJobJob')
      end

      it 'parses from handler on deserialization error' do
        job = Story.create(text: '...').delay.text
        job.payload_object.object.destroy
        job.save!
        job.update_column(:name, nil) # rubocop:disable Rails/SkipsModelValidations
        expect(job.reload.name).to eq('Delayed::PerformableMethod')
      end
    end

    context 'when name column does not exist' do
      before do
        ActiveRecord::Schema.define do
          AddIndexToDelayedJobsName.migrate(:down)
          AddNameToDelayedJobs.migrate(:down)
        end
        described_class.reset_column_information
      end

      after do
        ActiveRecord::Schema.define do
          AddNameToDelayedJobs.migrate(:up)
          AddIndexToDelayedJobsName.migrate(:up)
        end
        described_class.reset_column_information
      end

      it 'is the class name of the job that was enqueued' do
        job = described_class.new(payload_object: ErrorJob.new)
        expect(job.name).to eq('ErrorJob')
        job.save!
        expect(job.reload.name).to eq('ErrorJob')
      end

      it 'is the class name of the performable job if it is an ActiveJob' do
        job_wrapper = ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper.new(ActiveJobJob.new.serialize)
        job = described_class.new(payload_object: job_wrapper)
        expect(job.name).to eq('ActiveJobJob')
        job.save!
        expect(job.reload.name).to eq('ActiveJobJob')
      end

      it 'is the returned display_name if display_name is defined on the job object' do
        job = described_class.new(payload_object: NamedJob.new)
        expect(job.name).to eq('named_job')
        job.save!
        expect(job.reload.name).to eq('named_job')
      end

      it 'is the instance method that will be called if its a performable method object' do
        job = Story.create(text: '...').delay.save
        expect(job.name).to eq('Story#save')
      end

      it 'parses from handler on deserialization error' do
        job = Story.create(text: '...').delay.text
        job.payload_object.object.destroy
        expect(job.reload.name).to eq('Delayed::PerformableMethod')
      end
    end
  end

  context 'worker prioritization' do
    after do
      Delayed::Worker.max_claims = nil
      Delayed::Worker.max_priority = nil
      Delayed::Worker.min_priority = nil
      Delayed::Worker.read_ahead = nil
    end

    it 'fetches jobs ordered by priority' do
      10.times { described_class.enqueue SimpleJob.new, priority: rand(10) }
      Delayed::Worker.read_ahead = 10
      Delayed::Worker.max_claims = 10
      jobs = described_class.reserve(worker)
      expect(jobs.size).to eq(10)
      jobs.each_cons(2) do |a, b|
        expect(a.priority).to be <= b.priority
      end
    end

    it 'only finds jobs greater than or equal to min priority' do
      min = 5
      Delayed::Worker.min_priority = min
      Delayed::Worker.max_claims = 2
      [4, 5, 6].sort_by { |_i| rand }.each { |i| create_job priority: i }
      jobs = described_class.reserve(worker)
      expect(jobs.map(&:priority).min).to be >= min
      jobs.map(&:destroy)
      expect(described_class.reserve(worker)).to eq []
    end

    it 'only finds jobs less than or equal to max priority' do
      max = 5
      Delayed::Worker.max_priority = max
      Delayed::Worker.max_claims = 2
      [4, 5, 6].sort_by { |_i| rand }.each { |i| create_job priority: i }
      jobs = described_class.reserve(worker)
      expect(jobs.map(&:priority).max).to be <= max
      jobs.map(&:destroy)
      expect(described_class.reserve(worker)).to eq []
    end
  end

  context 'clear_locks!' do
    before do
      @job = create_job(locked_by: 'worker1', locked_at: described_class.db_time_now)
    end

    it 'clears locks for the given worker' do
      described_class.clear_locks!('worker1')
      expect(described_class.reserve(worker)).to eq([@job])
    end

    it 'does not clear locks for other workers' do
      described_class.clear_locks!('different_worker')
      expect(described_class.reserve(worker)).not_to include(@job)
    end
  end

  context 'unlock' do
    before do
      @job = create_job(locked_by: 'worker', locked_at: described_class.db_time_now)
    end

    it 'clears locks' do
      @job.unlock
      expect(@job.locked_by).to be_nil
      expect(@job.locked_at).to be_nil
    end
  end

  context 'large handler' do
    before do
      text = 'Lorem ipsum dolor sit amet. ' * 1000
      @job = described_class.enqueue Delayed::PerformableMethod.new(text, :length, [], {})
    end

    it 'has an id' do
      expect(@job.id).not_to be_nil
    end
  end

  context 'named queues' do
    context 'when worker has one queue set' do
      before do
        Delayed::Worker.queues = ['large']
      end

      it 'only works off jobs which are from its queue' do
        expect(SimpleJob.runs).to eq(0)

        create_job(queue: 'large')
        create_job(queue: 'small')
        worker.work_off

        expect(SimpleJob.runs).to eq(1)
      end
    end

    context 'when worker has two queue set' do
      before do
        Delayed::Worker.queues = %w(large small)
      end

      it 'only works off jobs which are from its queue' do
        expect(SimpleJob.runs).to eq(0)

        create_job(queue: 'large')
        create_job(queue: 'small')
        create_job(queue: 'medium')
        create_job
        worker.work_off

        expect(SimpleJob.runs).to eq(2)
      end
    end

    context 'when worker does not have queue set' do
      before(:each) do
        Delayed::Worker.queues = []
      end

      it 'works off all jobs' do
        expect(SimpleJob.runs).to eq(0)

        create_job(queue: 'one')
        create_job(queue: 'two')
        create_job
        worker.work_off

        expect(SimpleJob.runs).to eq(3)
      end
    end
  end

  context 'max_attempts' do
    before(:each) do
      @job = described_class.enqueue SimpleJob.new
    end

    it 'is not defined' do
      expect(@job.max_attempts).to be_nil
    end

    it 'uses the max_attempts value on the payload when defined' do
      expect(@job.payload_object).to receive(:max_attempts).and_return(99)
      expect(@job.max_attempts).to eq(99)
    end
  end

  describe '#max_run_time' do
    before(:each) { @job = described_class.enqueue SimpleJob.new }

    it 'is not defined' do
      expect(@job.max_run_time).to be_nil
    end

    it 'results in a default run time when not defined' do
      expect(worker.max_run_time(@job)).to eq(20.minutes)
    end

    it 'uses the max_run_time value on the payload when defined' do
      expect(@job.payload_object).to receive(:max_run_time).and_return(10.minutes)
      expect(@job.max_run_time).to eq(10.minutes)
    end

    it 'results in an overridden run time when defined' do
      expect(@job.payload_object).to receive(:max_run_time).and_return(15.minutes)
      expect(worker.max_run_time(@job)).to eq(15.minutes)
    end

    it 'job set max_run_time can not exceed default max run time' do
      expect(@job.payload_object).to receive(:max_run_time).and_return(20.minutes + 60)
      expect(worker.max_run_time(@job)).to eq(20.minutes)
    end
  end

  describe 'destroy_failed_jobs' do
    context 'with a SimpleJob' do
      before(:each) do
        @job = described_class.enqueue SimpleJob.new
      end

      it 'is not defined' do
        expect(@job.destroy_failed_jobs?).to be false
      end

      it 'uses the destroy failed jobs value on the payload when defined' do
        expect(@job.payload_object).to receive(:destroy_failed_jobs?).and_return(true)
        expect(@job.destroy_failed_jobs?).to be true
      end
    end

    context 'with a job that raises DserializationError' do
      before(:each) do
        @job = described_class.new handler: '--- !ruby/struct:GoingToRaiseArgError {}'
      end

      it 'falls back reasonably' do
        expect(YAML).to receive(:load_dj).and_raise(ArgumentError)
        expect(@job.destroy_failed_jobs?).to be false
      end
    end
  end

  describe 'yaml serialization' do
    context 'when serializing jobs' do
      it 'raises error ArgumentError for new records' do
        story = Story.new(text: 'hello')
        if story.respond_to?(:new_record?)
          expect { story.delay.tell }.to raise_error(
            ArgumentError,
            "job cannot be created for non-persisted record: #{story.inspect}",
          )
        end
      end

      it 'raises error ArgumentError for destroyed records' do
        story = Story.create(text: 'hello')
        story.destroy
        expect { story.delay.tell }.to raise_error(
          ArgumentError,
          "job cannot be created for non-persisted record: #{story.inspect}",
        )
      end
    end

    context 'when reload jobs back' do
      it 'reloads changed attributes' do
        story = Story.create(text: 'hello')
        job = story.delay.tell
        story.text = 'goodbye'
        story.save!
        expect(job.reload.payload_object.object.text).to eq('goodbye')
      end

      it 'raises deserialization error for destroyed records' do
        story = Story.create(text: 'hello')
        job = story.delay.tell
        story.destroy
        expect { job.reload.payload_object }.to raise_error(Delayed::DeserializationError)
      end
    end
  end

  describe 'worker integration' do
    before do
      described_class.delete_all
      SimpleJob.runs = 0
    end

    describe 'running a job' do
      it 'fails after Worker.max_run_time' do
        Delayed::Worker.max_run_time = 1.second
        job = described_class.create payload_object: LongRunningJob.new
        worker.perform(job)
        expect(job.error).not_to be_nil
        expect(job.reload.last_error).to match(/expired/)
        expect(job.reload.last_error).to match(/Delayed::Worker\.max_run_time is only 1 second/)
        expect(job.attempts).to eq(1)
      end

      context 'when the job raises a deserialization error' do
        it 'marks the job as failed' do
          job = described_class.create! payload_object: LongRunningJob.new
          job.update_columns(handler: '--- !ruby/object:JobThatDoesNotExist {}') # rubocop:disable Rails/SkipsModelValidations
          expect_any_instance_of(described_class).to receive(:destroy_failed_jobs?).and_return(false)
          worker.work_off
          job.reload
          expect(job).to be_failed
        end
      end
    end

    describe 'failed jobs' do
      before do
        @job = described_class.enqueue(ErrorJob.new, run_at: described_class.db_time_now - 1)
      end

      it 'records last_error when destroy_failed_jobs = false, max_attempts = 1' do
        Delayed::Worker.max_attempts = 1
        worker.perform(@job)
        @job.reload
        expect(@job.error).not_to be_nil
        expect(@job.last_error).to match(/did not work/)
        expect(@job.attempts).to eq(1)
        expect(@job).to be_failed
      end

      it 're-schedules jobs after failing' do
        worker.work_off
        @job.reload
        expect(@job.last_error).to match(/did not work/)
        expect(@job.last_error).to match(/sample_jobs.rb:\d+:in `perform'/)
        expect(@job.attempts).to eq(1)
        expect(@job.run_at).to be > described_class.db_time_now - 10.minutes
        expect(@job.run_at).to be < described_class.db_time_now + 10.minutes
        expect(@job.locked_by).to be_nil
        expect(@job.locked_at).to be_nil
      end

      it 're-schedules jobs with handler provided time if present' do
        job = described_class.enqueue(CustomRescheduleJob.new(99.minutes))
        worker.perform(job)
        job.reload

        expect((described_class.db_time_now + 99.minutes - job.run_at).abs).to be < 1
      end

      it "does not fail when the triggered error doesn't have a message" do
        error_with_nil_message = StandardError.new
        expect(error_with_nil_message).to receive(:message).twice.and_return(nil)
        expect(@job).to receive(:invoke_job).and_raise error_with_nil_message
        expect { worker.perform(@job) }.not_to raise_error
      end
    end

    context 'reschedule' do
      before do
        @job = described_class.create payload_object: SimpleJob.new
      end

      shared_examples_for 'any failure more than Worker.max_attempts times' do
        context "when the job's payload has a #failure hook" do
          before do
            @job = described_class.create payload_object: OnPermanentFailureJob.new
            expect(@job.payload_object).to respond_to(:failure)
          end

          it 'runs that hook' do
            expect(@job.payload_object).to receive(:failure)
            worker.reschedule(@job)
          end

          it 'handles error in hook' do
            Delayed::Worker.destroy_failed_jobs = false
            @job.payload_object.raise_error = true
            expect { worker.reschedule(@job) }.not_to raise_error
            expect(@job.failed_at).not_to be_nil
          end
        end

        context "when the job's payload has no #failure hook" do
          # It's a little tricky to test this in a straightforward way,
          # because putting a not_to receive expectation on
          # @job.payload_object.failure makes that object incorrectly return
          # true to payload_object.respond_to? :failure, which is what
          # reschedule uses to decide whether to call failure. So instead, we
          # just make sure that the payload_object as it already stands doesn't
          # respond_to? failure, then shove it through the iterated reschedule
          # loop and make sure we don't get a NoMethodError (caused by calling
          # that nonexistent failure method).

          before do
            expect(@job.payload_object).not_to respond_to(:failure)
          end

          it 'does not try to run that hook' do
            expect {
              Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
            }.not_to raise_exception
          end
        end
      end

      context 'and we want to destroy jobs' do
        before do
          Delayed::Worker.destroy_failed_jobs = true
        end

        it_behaves_like 'any failure more than Worker.max_attempts times'

        it 'is destroyed if it failed more than Worker.max_attempts times' do
          expect(@job).to receive(:destroy)
          Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
        end

        it 'is destroyed if the job has destroy failed jobs set' do
          Delayed::Worker.destroy_failed_jobs = false
          expect(@job).to receive(:destroy_failed_jobs?).and_return(true)
          expect(@job).to receive(:destroy)
          Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
        end

        it 'is not destroyed if failed fewer than Worker.max_attempts times' do
          expect(@job).not_to receive(:destroy)
          (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
        end
      end

      context "and we don't want to destroy jobs" do
        it_behaves_like 'any failure more than Worker.max_attempts times'

        context 'and destroy failed jobs is false' do
          it 'is failed if it failed more than Worker.max_attempts times' do
            expect(@job.reload).not_to be_failed
            Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
            expect(@job.reload).to be_failed
          end

          it 'is not failed if it failed fewer than Worker.max_attempts times' do
            (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
            expect(@job.reload).not_to be_failed
          end
        end

        context 'and destroy failed jobs for job is false' do
          before do
            Delayed::Worker.destroy_failed_jobs = true
          end

          it 'is failed if it failed more than Worker.max_attempts times' do
            expect(@job).to receive(:destroy_failed_jobs?).and_return(false)
            expect(@job.reload).not_to be_failed
            Delayed::Worker.max_attempts.times { worker.reschedule(@job) }
            expect(@job.reload).to be_failed
          end

          it 'is not failed if it failed fewer than Worker.max_attempts times' do
            (Delayed::Worker.max_attempts - 1).times { worker.reschedule(@job) }
            expect(@job.reload).not_to be_failed
          end
        end
      end
    end
  end

  describe "reserve_with_scope" do
    let(:relation_class) { described_class.limit(1).class }
    let(:worker) { instance_double(Delayed::Worker, name: "worker01", read_ahead: 1, max_claims: 1) }
    let(:scope) do
      instance_double(relation_class, update_all: nil, limit: [job]).tap do |s|
        allow(s).to receive(:where).and_return(s)
      end
    end
    let(:job) { instance_double(described_class, id: 1, assign_attributes: true, changes_applied: true) }

    before do
      allow(described_class.connection).to receive(:adapter_name).at_least(:once).and_return(dbms)
    end

    context "for mysql adapters" do
      let(:dbms) { "MySQL" }

      it "uses the optimized sql version" do
        allow(described_class).to receive(:reserve_with_scope_using_default_sql)
        described_class.reserve_with_scope(scope, worker, Time.current)
        expect(described_class).not_to have_received(:reserve_with_scope_using_default_sql)
      end
    end

    context "for a dbms without a specific implementation" do
      let(:dbms) { "OtherDB" }

      it "uses the plain sql version" do
        allow(described_class).to receive(:reserve_with_scope_using_default_sql)
        described_class.reserve_with_scope(scope, worker, Time.current)
        expect(described_class).to have_received(:reserve_with_scope_using_default_sql).once
      end
    end
  end

  if ActiveRecord::VERSION::MAJOR >= 7
    delegate :default_timezone=, to: ActiveRecord
  else
    delegate :default_timezone=, to: ActiveRecord::Base
  end

  context "db_time_now" do
    after do
      Time.zone = nil
      self.default_timezone = :local
    end

    it "returns time in current time zone if set" do
      Time.zone = "Arizona"
      expect(described_class.db_time_now.zone).to eq("MST")
    end

    it "returns UTC time if that is the AR default" do
      Time.zone = nil
      self.default_timezone = :utc
      expect(described_class.db_time_now.zone).to eq "UTC"
    end

    it "returns local time if that is the AR default" do
      Time.zone = "Arizona"
      self.default_timezone = :local
      expect(described_class.db_time_now.zone).to eq("MST")
    end
  end

  context "ActiveRecord::Base.table_name_prefix" do
    it "when prefix is not set, use 'delayed_jobs' as table name" do
      ::ActiveRecord::Base.table_name_prefix = nil
      described_class.set_delayed_job_table_name

      expect(described_class.table_name).to eq "delayed_jobs"
    end

    it "when prefix is set, prepend it before default table name" do
      ::ActiveRecord::Base.table_name_prefix = "custom_"
      described_class.set_delayed_job_table_name

      expect(described_class.table_name).to eq "custom_delayed_jobs"

      ::ActiveRecord::Base.table_name_prefix = nil
      described_class.set_delayed_job_table_name
    end
  end

  describe '#age_alert?' do
    let(:now) { described_class.db_time_now }
    let(:run_at) { now - 1.minute }
    let(:locked_at) { nil }

    around do |example|
      Delayed::Priority.names = { high: 0 }
      Delayed::Priority.alerts = { high: { age: 5.minutes } }
      Timecop.freeze(now) { example.run }
    ensure
      Delayed::Priority.names = nil
    end

    subject { described_class.enqueue(SimpleJob.new, run_at: run_at, locked_at: locked_at) }

    it 'returns false' do
      expect(subject.alert_age).to eq(5.minutes)
      expect(subject.age).to be_within(1).of(1.minute)
      expect(subject.age_alert?).to eq(false)
    end

    context 'when the job is older than specified alert age' do
      let(:run_at) { now - 6.minutes }

      it 'returns true' do
        expect(subject.alert_age).to eq(5.minutes)
        expect(subject.age).to be_within(1).of(6.minutes)
        expect(subject.age_alert?).to eq(true)
      end
    end

    context 'when the job has been running for a long time but was picked up quickly' do
      let(:run_at) { now - 1.hour - 1.minute }
      let(:locked_at) { now - 1.hour }

      it 'returns false' do
        expect(subject.alert_age).to eq 5.minutes
        expect(subject.age).to be_within(1).of(1.minute)
        expect(subject.age_alert?).to eq(false)
      end
    end

    context 'when the job class defines an alert_age override' do
      before do
        stub_const('JobWithAlertAge', Struct.new(:perform) do
          def alert_age
            30.seconds
          end
        end)
      end

      subject { described_class.enqueue(JobWithAlertAge.new, run_at: run_at, locked_at: locked_at) }

      it 'obeys the override' do
        expect(subject.alert_age).to eq(30.seconds)
        expect(subject.age).to be_within(1).of(1.minute)
        expect(subject.age_alert?).to eq(true)
      end
    end
  end

  describe '#run_time_alert?' do
    let(:now) { described_class.db_time_now }
    let(:locked_at) { now - 1.minute }

    around do |example|
      Delayed::Priority.names = { high: 0 }
      Delayed::Priority.alerts = { high: { run_time: 5.minutes } }
      Timecop.freeze(now) { example.run }
    ensure
      Delayed::Priority.names = nil
    end

    subject { described_class.enqueue(SimpleJob.new, locked_at: locked_at) }

    it 'returns false' do
      expect(subject.alert_run_time).to eq(5.minutes)
      expect(subject.run_time).to be_within(1).of(1.minute)
      expect(subject.run_time_alert?).to eq(false)
    end

    context 'when the job is not locked (e.g. delay_jobs is false)' do
      let(:locked_at) { nil }

      it 'returns nil' do
        expect(subject.alert_run_time).to eq(5.minutes)
        expect(subject.run_time).to be_nil
        expect(subject.run_time_alert?).to be_nil
      end
    end

    context 'when the job has been running longer than specified alert run_time' do
      let(:locked_at) { now - 6.minutes }

      it 'returns true' do
        expect(subject.alert_run_time).to eq(5.minutes)
        expect(subject.run_time).to be_within(1).of(6.minutes)
        expect(subject.run_time_alert?).to eq(true)
      end
    end

    context 'when the job class defines an alert_run_time override' do
      before do
        stub_const('JobWithAlertRunTime', Struct.new(:perform) do
          def alert_run_time
            30.seconds
          end
        end)
      end

      subject { described_class.enqueue(JobWithAlertRunTime.new, locked_at: locked_at) }

      it 'obeys the override' do
        expect(subject.alert_run_time).to eq(30.seconds)
        expect(subject.run_time).to be_within(1).of(1.minute)
        expect(subject.run_time_alert?).to eq(true)
      end
    end
  end

  describe '#attempts_alert?' do
    let(:now) { described_class.db_time_now }
    let(:attempts) { 1 }

    around do |example|
      Delayed::Priority.names = { high: 0 }
      Delayed::Priority.alerts = { high: { attempts: 5 } }
      Timecop.freeze(now) { example.run }
    ensure
      Delayed::Priority.names = nil
    end

    subject { described_class.enqueue(SimpleJob.new, attempts: attempts) }

    it 'returns false' do
      expect(subject.alert_attempts).to eq 5
      expect(subject.attempts).to eq 1
      expect(subject.attempts_alert?).to eq false
    end

    context 'when the job reaches the specified alert attempts' do
      let(:attempts) { 6 }

      it 'returns true' do
        expect(subject.alert_attempts).to eq 5
        expect(subject.attempts).to eq 6
        expect(subject.attempts_alert?).to eq true
      end
    end

    context 'when the job class defines an alert_attempts override' do
      before do
        stub_const('JobWithAlertAttempts', Struct.new(:perform) do
          def alert_attempts
            1
          end
        end)
      end

      subject { described_class.enqueue(JobWithAlertAttempts.new, attempts: attempts) }

      it 'obeys the override' do
        expect(subject.alert_attempts).to eq 1
        expect(subject.attempts).to eq 1
        expect(subject.attempts_alert?).to eq true
      end
    end
  end
end
