require File.expand_path('../../test_helper', __FILE__)
require File.expand_path('../../fixtures/test_jobs', __FILE__)
require File.expand_path('../../fixtures/hooked', __FILE__)

describe "Backburner::Workers::Simple module" do
  before do
    Backburner.default_queues.clear
    @worker_class = Backburner::Workers::Simple
  end

  describe "for prepare method" do
    it "should make tube names array always unique to avoid duplication" do
      worker = @worker_class.new(["foo", "demo.test.foo"])
      capture_stdout { worker.prepare }
      assert_equal ["demo.test.foo"], worker.tube_names
    end

    it "should watch specified tubes" do
      worker = @worker_class.new(["foo", "bar"])
      out = capture_stdout { worker.prepare }
      assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
      assert_same_elements ["demo.test.foo", "demo.test.bar"], worker.connection.tubes.watched.map(&:name)
      assert_match(/demo\.test\.foo/, out)
    end # multiple

    it "should watch single tube" do
      worker = @worker_class.new("foo")
      out = capture_stdout { worker.prepare }
      assert_equal ["demo.test.foo"], worker.tube_names
      assert_same_elements ["demo.test.foo"], worker.connection.tubes.watched.map(&:name)
      assert_match(/demo\.test\.foo/, out)
    end # single

    it "should respect default_queues settings" do
      Backburner.default_queues.concat(["foo", "bar"])
      worker = @worker_class.new
      out = capture_stdout { worker.prepare }
      assert_equal ["demo.test.foo", "demo.test.bar"], worker.tube_names
      assert_same_elements ["demo.test.foo", "demo.test.bar"], worker.connection.tubes.watched.map(&:name)
      assert_match(/demo\.test\.foo/, out)
    end

    it "should assign based on all tubes" do
      @worker_class.any_instance.expects(:all_existing_queues).once.returns("bar")
      worker = @worker_class.new
      out = capture_stdout { worker.prepare }
      assert_equal ["demo.test.bar"], worker.tube_names
      assert_same_elements ["demo.test.bar"], worker.connection.tubes.watched.map(&:name)
      assert_match(/demo\.test\.bar/, out)
    end # all assign

    it "should properly retrieve all tubes" do
      worker = @worker_class.new
      out = capture_stdout { worker.prepare }
      assert_contains worker.tube_names, "demo.test.backburner-jobs"
      assert_contains worker.connection.tubes.watched.map(&:name), "demo.test.backburner-jobs"
      assert_match(/demo\.test\.backburner-jobs/, out)
    end # all read
  end # prepare

  describe "for work_one_job method" do
    before do
      $worker_test_count = 0
      $worker_success = false
    end

    it "should work a plain enqueued job" do
      clear_jobs!("foo.bar")
      @worker_class.enqueue TestPlainJob, [1, 2], :queue => "foo.bar"
      silenced(2) do
        worker = @worker_class.new('foo.bar')
        worker.prepare
        worker.work_one_job
      end
      assert_equal 4, $worker_test_count
    end # plain enqueue

    it "should work an enqueued job" do
      clear_jobs!("foo.bar")
      @worker_class.enqueue TestJob, [1, 2], :queue => "foo.bar"
      silenced(2) do
        worker = @worker_class.new('foo.bar')
        worker.prepare
        worker.work_one_job
      end
      assert_equal 3, $worker_test_count
    end # enqueue

    it "should fail quietly if there's an argument error" do
      clear_jobs!("foo.bar")
      @worker_class.enqueue TestJob, ["bam", "foo", "bar"], :queue => "foo.bar"
      out = silenced(2) do
        worker = @worker_class.new('foo.bar')
        worker.prepare
        worker.work_one_job
      end
      assert_match(/Exception ArgumentError/, out)
      assert_equal 0, $worker_test_count
    end # fail, argument

    it "should work an enqueued failing job" do
      # NB: The #bury expectation below leaves the job in the queue (as reserved!)
      # since bury is never actually called on the task. Therefore, clear_jobs!()
      # can't remove it which can break a lot of things depending on the order the
      # tests are run. So we ensure that it's using a unique queue name. Mocha
      # lacks expectations with proxies (where we could actually call bury)
      clear_jobs!('foo.bar.failed')
      @worker_class.enqueue TestFailJob, [1, 2], :queue => 'foo.bar.failed'
      Backburner::Job.any_instance.expects(:bury).once
      out = silenced(2) do
        worker = @worker_class.new('foo.bar.failed')
        worker.prepare
        worker.work_one_job
      end
      assert_match(/Exception RuntimeError/, out)
      assert_equal 0, $worker_test_count
    end # fail, runtime error

    it "should work an invalid job parsed" do
      Beaneater::Tubes.any_instance.expects(:reserve).returns(stub(:body => "{%$^}", :bury => true))
      out = silenced(2) do
        worker = @worker_class.new('foo.bar')
        worker.prepare
        worker.work_one_job
      end
      assert_match(/Exception Backburner::Job::JobFormatInvalid/, out)
      assert_equal 0, $worker_test_count
    end # fail, runtime error

    it "should work for an async job" do
      clear_jobs!('foo.bar')
      TestAsyncJob.async(:queue => 'foo.bar').foo(3, 5)
      silenced(2) do
        worker = @worker_class.new('foo.bar')
        worker.prepare
        worker.work_one_job
      end
      assert_equal 15, $worker_test_count
    end # async

    it "should support retrying jobs and burying" do
      clear_jobs!('foo.bar')
      Backburner.configure { |config| config.max_job_retries = 1; config.retry_delay = 0 }
      @worker_class.enqueue TestRetryJob, ["bam", "foo"], :queue => 'foo.bar'
      out = []
      2.times do
        out << silenced(2) do
          worker = @worker_class.new('foo.bar')
          worker.prepare
          worker.work_one_job
        end
      end
      assert_match(/attempt 1 of 2, retrying/, out.first)
      assert_match(/Finished TestRetryJob/m, out.last)
      assert_match(/attempt 2 of 2, burying/m, out.last)
      assert_equal 2, $worker_test_count
      assert_equal false, $worker_success
    end # retry, bury

    it "should support retrying jobs and succeeds" do
      clear_jobs!('foo.bar')
      Backburner.configure { |config| config.max_job_retries = 2; config.retry_delay = 0 }
      @worker_class.enqueue TestRetryJob, ["bam", "foo"], :queue => 'foo.bar'
      out = []
      3.times do
        out << silenced(2) do
          worker = @worker_class.new('foo.bar')
          worker.prepare
          worker.work_one_job
        end
      end
      assert_match(/attempt 1 of 3, retrying/, out.first)
      assert_match(/attempt 2 of 3, retrying/, out[1])
      assert_match(/Completed TestRetryJob/m, out.last)
      refute_match(/failed/, out.last)
      assert_equal 3, $worker_test_count
      assert_equal true, $worker_success
    end # retrying, succeeds

    it "should back off retries exponentially" do
      max_job_retries = 3
      clear_jobs!('foo.bar')
      Backburner.configure do |config|
        config.max_job_retries = max_job_retries
        config.retry_delay = 0
        #config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 3) } # default retry_delay_proc
      end
      @worker_class.enqueue TestConfigurableRetryJob, [max_job_retries], :queue => 'foo.bar'
      out = []
      (max_job_retries + 1).times do
        out << silenced(10) do
          worker = @worker_class.new('foo.bar')
          worker.prepare
          worker.work_one_job
        end
      end
      assert_match(/attempt 1 of 4, retrying in 0/, out.first)
      assert_match(/attempt 2 of 4, retrying in 1/, out[1])
      assert_match(/attempt 3 of 4, retrying in 8/, out[2])
      assert_match(/Completed TestConfigurableRetryJob/m, out.last)
      refute_match(/failed/, out.last)
      assert_equal 4, $worker_test_count
      assert_equal true, $worker_success
    end

    it "should allow configurable back off retry delays" do
      max_job_retries = 3
      clear_jobs!('foo.bar')
      Backburner.configure do |config|
        config.max_job_retries = max_job_retries
        config.retry_delay = 0
        config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 2) }
      end
      @worker_class.enqueue TestConfigurableRetryJob, [max_job_retries], :queue => 'foo.bar'
      out = []
      (max_job_retries + 1).times do
        out << silenced(5) do
          worker = @worker_class.new('foo.bar')
          worker.prepare
          worker.work_one_job
        end
      end
      assert_match(/attempt 1 of 4, retrying in 0/, out.first)
      assert_match(/attempt 2 of 4, retrying in 1/, out[1])
      assert_match(/attempt 3 of 4, retrying in 4/, out[2])
      assert_match(/Completed TestConfigurableRetryJob/m, out.last)
      refute_match(/failed/, out.last)
      assert_equal 4, $worker_test_count
      assert_equal true, $worker_success
    end

    it "should allow queue override of retries" do
      max_job_retries = TestRetryWithQueueOverridesJob.queue_max_job_retries
      clear_jobs!('foo.bar')
      Backburner.configure do |config|
         # Config should be overridden by queue overrides
        config.max_job_retries = 20
        config.retry_delay = 60
        #config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 3) } # default retry_delay_proc
      end
      @worker_class.enqueue TestRetryWithQueueOverridesJob, [max_job_retries], :queue => 'foo.bar'
      out = []
      (max_job_retries + 1).times do
        out << silenced(5) do
          worker = @worker_class.new('foo.bar')
          worker.prepare
          worker.work_one_job
        end
      end
      assert_match(/attempt 1 of 4, retrying in 0/, out.first)
      assert_match(/attempt 2 of 4, retrying in 1/, out[1])
      assert_match(/attempt 3 of 4, retrying in 4/, out[2])
      assert_match(/Completed TestRetryWithQueueOverridesJob/m, out.last)
      refute_match(/failed/, out.last)
      assert_equal 4, $worker_test_count
      assert_equal true, $worker_success
    end

    it "should support event hooks without retry" do
      $hooked_fail_count = 0
      clear_jobs!('foo.bar.events')
      out = silenced(2) do
        HookedObjectSuccess.async(:queue => 'foo.bar.events').foo(5)
        worker = @worker_class.new('foo.bar.events')
        worker.prepare
        worker.work_one_job
      end
      assert_match(/before_enqueue.*after_enqueue.*Working 1 queues/m, out)
      assert_match(/!!before_enqueue_bar!! \[nil, :foo, 5\]/, out)
      assert_match(/!!after_enqueue_bar!! \[nil, :foo, 5\]/, out)
      assert_match(/!!before_perform_foo!! \[nil, "foo", 5\]/, out)
      assert_match(/!!BEGIN around_perform_bar!! \[nil, "foo", 5\]/, out)
      assert_match(/!!BEGIN around_perform_cat!! \[nil, "foo", 5\]/, out)
      assert_match(/!!on_failure_foo!!.*HookFailError/, out)
      assert_match(/!!on_bury_foo!! \[nil, "foo", 5\]/, out)
      assert_match(/attempt 1 of 1, burying/, out)
    end # event hooks, no retry

    it "should support event hooks with retry" do
      $hooked_fail_count = 0
      clear_jobs!('foo.bar.events.retry')
      Backburner.configure { |config| config.max_job_retries = 1; config.retry_delay = 0 }
      out = silenced(2) do
        HookedObjectSuccess.async(:queue => 'foo.bar.events.retry').foo(5)
        worker = @worker_class.new('foo.bar.events.retry')
        worker.prepare
        2.times do
          worker.work_one_job
        end
      end
      assert_match(/before_enqueue.*after_enqueue.*Working 1 queues/m, out)
      assert_match(/!!before_enqueue_bar!! \[nil, :foo, 5\]/, out)
      assert_match(/!!after_enqueue_bar!! \[nil, :foo, 5\]/, out)
      assert_match(/!!before_perform_foo!! \[nil, "foo", 5\]/, out)
      assert_match(/!!BEGIN around_perform_bar!! \[nil, "foo", 5\]/, out)
      assert_match(/!!BEGIN around_perform_cat!! \[nil, "foo", 5\]/, out)
      assert_match(/!!on_failure_foo!!.*HookFailError/, out)
      assert_match(/!!on_failure_foo!!.*retrying.*around_perform_bar.*around_perform_cat/m, out)
      assert_match(/!!on_retry_foo!! 1 0 \[nil, "foo", 5\]/, out)
      assert_match(/attempt 1 of 2, retrying/, out)
      assert_match(/!!before_perform_foo!! \[nil, "foo", 5\]/, out)
      assert_match(/!!END around_perform_bar!! \[nil, "foo", 5\]/, out)
      assert_match(/!!END around_perform_cat!! \[nil, "foo", 5\]/, out)
      assert_match(/!!after_perform_foo!! \[nil, "foo", 5\]/, out)
      assert_match(/Finished HookedObjectSuccess/, out)
    end # event hooks, with retry

    it "should support event hooks with stopping enqueue" do
      $hooked_fail_count = 0
      worker = @worker_class.new('foo.bar.events.retry2')
      clear_jobs!('foo.bar.events.retry2')
      silenced(2) do
        HookedObjectBeforeEnqueueFail.async(:queue => 'foo.bar.events.retry2').foo(5)
      end
      expanded_tube = [Backburner.configuration.tube_namespace, 'foo.bar.events.retry2'].join(".")
      assert_nil worker.connection.tubes[expanded_tube].peek(:ready)
    end # stopping enqueue

    it "should support event hooks with stopping perform" do
      $hooked_fail_count = 0
      clear_jobs!('foo.bar.events.retry3')
      [Backburner.configuration.tube_namespace, 'foo.bar.events.retry3'].join(".")
      out = silenced(2) do
        HookedObjectBeforePerformFail.async(:queue => 'foo.bar.events.retry3').foo(10)
        worker = @worker_class.new('foo.bar.events.retry3')
        worker.prepare
        worker.work_one_job
      end
      assert_match(/!!before_perform_foo!! \[nil, "foo", 10\]/, out)
      assert_match(/before_perform_foo.*Completed/m, out)
      refute_match(/Fail ran!!/, out)
      refute_match(/HookFailError/, out)
    end # stopping perform

    it "should use the connection given as an argument" do
      worker = @worker_class.new('foo.bar')
      connection = mock('connection')
      worker.expects(:reserve_job).with(connection).returns(stub_everything('job'))
      capture_stdout { worker.work_one_job(connection) }
    end

    after do
      Backburner.configure do |config|
        config.max_job_retries = 0
        config.retry_delay = 5
        config.retry_delay_proc = lambda { |min_retry_delay, num_retries| min_retry_delay + (num_retries ** 3) }
      end
    end
  end # work_one_job
end # Worker
