require "spec_helper"

describe Massive::Step do
  include_context "frozen time"
  include_context "stubbed memory_consumption"

  let(:process) { Massive::Process.new }
  subject(:step) { process.steps.build }

  before { step.stub(:process).and_return(process) }

  describe ".perform" do
    before do
      Massive::Process.stub(:find_step).with(process.id, step.id).and_return(step)
    end

    it "finds the step and calls work on it" do
      step.should_receive(:work)
      Massive::Step.perform(process.id, step.id)
    end
  end

  describe ".queue" do
    it "should be massive_step" do
      Massive::Step.queue.should eq(:massive_step)
    end
  end

  describe ".calculate_total_count_with" do
    after { Massive::Step.calculates_total_count_with { 0 } }

    it "defaults to return 0" do
      step.calculate_total_count.should be_zero
    end

    it "defines the calculate_total_count method, which returns the returned value of the given block" do
      Massive::Step.calculates_total_count_with { 1234 }
      Massive::Step.new.calculate_total_count.should eq(1234)
    end
  end

  describe "#enqueue" do
    before { step.stub(:reload).and_return(step) }

    it "enqueues itself, passing ids as strings" do
      Resque.should_receive(:enqueue).with(step.class, step.process.id.to_s, step.id.to_s)
      step.enqueue
    end

    it "sends a :enqueued notification" do
      step.should_receive(:notify).with(:enqueued)
      step.enqueue
    end

    context "when a subclass redefines calculate_total_count" do
      subject(:step) { CustomStep.new }
      before { process.steps << step }

      it "enqueues itself, passing ids as strings" do
        Resque.should_receive(:enqueue).with(step.class, step.process.id.to_s, step.id.to_s)
        step.enqueue
      end
    end
  end

  describe "#start!" do
    it "persists the total_count" do
      step.start!
      step.reload.total_count.should be_present
    end

    it "sends a :start notification" do
      step.should_receive(:notify).with(:start)
      step.start!
    end

    context "when total_count is not defined" do
      it "updates it to zero" do
        step.start!
        step.total_count.should be_zero
      end
    end

    context "when total_count is defined" do
      before { step.total_count = 10 }

      it "does not change it" do
        expect { step.start! }.to_not change(step, :total_count)
      end
    end

    context "when a subclass redefines calculate_total_count" do
      subject(:step) { CustomStep.new }
      before { process.steps << step }

      context "and the total_count is not defined" do
        it "updates it to the return value of calculate_total_count" do
          step.start!
          step.total_count.should eq(step.send(:calculate_total_count))
        end
      end

      context "when total_count is defined" do
        context "and it is 0" do
          before { step.total_count = 0 }

          it "does not change it" do
            expect { step.work }.to_not change(step, :total_count)
          end
        end

        context "and it is 10" do
          before { step.total_count = 10 }

          it "does not change it" do
            expect { step.work }.to_not change(step, :total_count)
          end
        end
      end
    end
  end

  describe "#work" do
    it "starts the step, then process it" do
      step.should_receive(:start!) do
        step.should_receive(:process_step)
      end

      step.work
    end

    it "calls complete after processing step" do
      step.should_receive(:process_step) do
        step.should_receive(:complete)
      end

      step.work
    end
  end

  describe "jobs completion" do
    context "when it is not persisted" do
      it "does not reloads itself" do
        step.should_not_receive(:reload)
        step.completed_all_jobs?
      end
    end

    context "when it is persisted" do
      before { step.save }

      it "reloads itself, so that it can get the latest information" do
        step.should_receive(:reload).and_return(step)
        step.completed_all_jobs?
      end
    end

    context "when there are no jobs" do
      it { should be_completed_all_jobs }
    end

    context "when there are jobs" do
      let!(:jobs) { step.jobs = 3.times.map { |i| Massive::Job.new } }

      before do
        jobs.each { |job| job.stub(:completed?).and_return(true) }
      end

      context "but there is at least one that is not completed" do
        before do
          jobs.each { |job| job.stub(:completed?).and_return(true) }

          jobs.last.stub(:completed?).and_return(false)
        end

        it { should_not be_completed_all_jobs }
      end

      context "and all jobs are completed" do
        before do
          jobs.each { |job| job.stub(:completed?).and_return(true) }
        end

        it { should be_completed_all_jobs }
      end
    end
  end

  describe "#complete" do
    context "when there is at least one job that is not completed" do
      before { step.stub(:completed_all_jobs?).and_return(false) }

      it "does not updates the finished_at" do
        step.complete
        step.finished_at.should be_nil
      end

      it "does not updates the memory_consumption" do
        step.complete
        step.memory_consumption.should be_zero
      end

      it "does not persists the step" do
        step.should_not be_persisted
      end

      it "does not send a :complete notification" do
        step.should_not_receive(:notify).with(:complete)
        step.complete
      end

      context "when it should not execute next after completion" do
        it "does not enqueues next step of process" do
          process.should_not_receive(:enqueue_next)
          step.complete
        end
      end

      context "when it should execute next after completion" do
        before { step.execute_next = true }

        it "does not enqueues next step of process" do
          process.should_not_receive(:enqueue_next)
          step.complete
        end
      end
    end

    context "when all jobs are completed" do
      let(:lock_key) { step.send(:lock_key_for, :complete) }

      let(:redis) { Resque.redis }

      before { step.stub(:completed_all_jobs?).and_return(true) }

      context "but there is a complete lock for this step" do
        before do
          redis.set(lock_key, 1.minute.from_now)
        end

        it "does not updates the finished_at" do
          step.complete
          step.finished_at.should be_nil
        end

        it "does not updates the memory_consumption" do
          step.complete
          step.memory_consumption.should be_zero
        end

        it "does not persists the step" do
          step.should_not be_persisted
        end

        it "does not send a :complete notification" do
          step.should_not_receive(:notify).with(:complete)
          step.complete
        end

        context "when it should not execute next after completion" do
          it "does not enqueues next step of process" do
            process.should_not_receive(:enqueue_next)
            step.complete
          end
        end

        context "when it should execute next after completion" do
          before { step.execute_next = true }

          it "does not enqueues next step of process" do
            process.should_not_receive(:enqueue_next)
            step.complete
          end
        end
      end

      context "but there is no complete lock for this step" do
        it "updates the finished_at with the current time, persisting it" do
          step.complete
          step.reload.finished_at.to_i.should eq(now.to_i)
        end

        it "updates the memory_consumption, persisting it" do
          step.complete
          step.reload.memory_consumption.should eq(current_memory_consumption)
        end

        it "sends a :complete notification" do
          step.should_receive(:notify).with(:complete)
          step.complete
        end

        context "when it should not execute next after completion" do
          it "does not enqueues next step of process" do
            process.should_not_receive(:enqueue_next)
            step.complete
          end
        end

        context "when it should execute next after completion" do
          before { step.execute_next = true }

          it "enqueues next step of process" do
            process.should_receive(:enqueue_next)
            step.complete
          end
        end
      end
    end
  end

  context "#process_step" do
    context "when total_count is zero" do
      before { step.total_count = 0 }

      it "creates no jobs" do
        step.process_step
        step.jobs.should be_empty
      end
    end

    context "when total_count is 2000" do
      before { step.total_count = 2000 }

      let(:limit) { 100 }

      it "creates 20 jobs, each processing 100 items" do
        step.process_step
        step.jobs.each_with_index do |job, index|
          job.limit.should eq(limit)
          job.offset.should eq(index * limit)
        end
      end

      it "creates jobs of the Massive::Job class" do
        step.process_step
        step.jobs.each do |job|
          job.should be_an_instance_of(Massive::Job)
        end
      end

      context "on custom step class" do
        subject(:step) { CustomStep.new }
        before { process.steps << step }
        let(:limit) { 1000 }

        it "follows redefined limit_ratio, creating 2 jobs, each processing 1000 items" do
          step.process_step
          step.jobs.each_with_index do |job, index|
            job.limit.should eq(limit)
            job.offset.should eq(index * limit)
          end
        end

        it "creates jobs of the redefined job_class" do
          step.process_step
          step.jobs.each do |job|
            job.should be_an_instance_of(CustomJob)
          end
        end
      end

      context "on a inherited step, that didn't redefine any configuration" do
        subject(:step) { InheritedStep.new }
        before { process.steps << step }

        it "follows redefined limit_ratio, creating 2 jobs, each processing 1000 items" do
          step.process_step
          step.jobs.each_with_index do |job, index|
            job.limit.should eq(limit)
            job.offset.should eq(index * limit)
          end
        end

        it "creates jobs of the Massive::Job" do
          step.process_step
          step.jobs.each do |job|
            job.should be_an_instance_of(Massive::Job)
          end
        end
      end
    end

    context "when total_count is 3000" do
      before { step.total_count = 3000 }

      let(:limit) { 1000 }

      it "creates 3 jobs, each processing 1000 items" do
        step.process_step
        step.jobs.each_with_index do |job, index|
          job.limit.should eq(limit)
          job.offset.should eq(index * limit)
        end
      end

      context "on custom step class" do
        subject(:step) { CustomStep.new }
        before { process.steps << step }
        let(:limit) { 1500 }

        it "follows redefined limit_ratio, creating 2 jobs, each processing 1000 items" do
          step.process_step
          step.jobs.each_with_index do |job, index|
            job.limit.should eq(limit)
            job.offset.should eq(index * limit)
          end
        end

        it "creates jobs of the redefined job_class" do
          step.process_step
          step.jobs.each do |job|
            job.should be_an_instance_of(CustomJob)
          end
        end
      end

      context "on a inherited step, that didn't redefine any configuration" do
        subject(:step) { InheritedStep.new }
        before { process.steps << step }

        it "follows redefined limit_ratio, creating 2 jobs, each processing 1000 items" do
          step.process_step
          step.jobs.each_with_index do |job, index|
            job.limit.should eq(limit)
            job.offset.should eq(index * limit)
          end
        end

        it "creates jobs of the Massive::Job" do
          step.process_step
          step.jobs.each do |job|
            job.should be_an_instance_of(Massive::Job)
          end
        end
      end
    end
  end

  describe "processed items and time" do
    context "when the step has no jobs" do
      its(:processed)            { should be_zero }
      its(:processed_percentage) { should be_zero }
      its(:processing_time)      { should be_zero }
    end

    context "when the step has jobs with processed itens" do
      let!(:jobs) { step.jobs = 3.times.map { |i| Massive::Job.new(processed: 100 * i) } }
      let(:total_processed) { jobs.map(&:processed).sum }

      its(:processed) { should eq(total_processed) }

      context "and the total count is zero" do
        its(:processed_percentage) { should be_zero }
      end

      context "and the total count is greater than zero" do
        before { step.total_count = 1000 }

        its(:processed_percentage) { should eq(total_processed.to_f / step.total_count) }
      end
    end

    context "when the step has jobs that have some elapsed time" do
      let!(:jobs) do
        step.jobs = 3.times.map do |i|
          Massive::Job.new.tap { |j| j.stub(:elapsed_time).and_return(100 * i) }
        end
      end

      let(:total_elapsed_time) { jobs.map(&:elapsed_time).sum }

      its(:processing_time) { should eq(total_elapsed_time) }
    end
  end

  context "on a inherited step" do
    subject(:step) { InheritedStep.new }
    before { process.steps << step }

    it "properly sets the _type" do
      step._type.should be_present
    end
  end

  describe "#active_model_serializer" do
    its(:active_model_serializer) { should eq Massive::StepSerializer }

    context "when class inherits from Massive::Step and does not have a serializer" do
      class TestStep < Massive::Step
      end

      it "returns Massive::StepSerializer" do
        process = TestStep.new
        process.active_model_serializer.should eq Massive::StepSerializer
      end
    end
  end
end