# frozen_string_literal: true

require 'spec_helper'

# We need to capture task state from a closure, which requires instance variables.
# rubocop: disable RSpec/InstanceVariable
RSpec.describe Gitlab::BackgroundTask, feature_category: :build do
  let(:options) { {} }
  let(:task) do
    proc do
      @task_run = true
      @task_thread = Thread.current
    end
  end

  subject(:background_task) { described_class.new(task, **options) }

  def expect_condition
    Timeout.timeout(3) do
      sleep 0.1 until yield
    end
  end

  context 'when stopped' do
    it 'is not running' do
      expect(background_task).not_to be_running
    end

    describe '#start' do
      it 'runs the given task on a background thread' do
        test_thread = Thread.current

        background_task.start

        expect_condition { @task_run == true }
        expect_condition { @task_thread != test_thread }
        expect(background_task).to be_running
      end

      it 'returns self' do
        expect(background_task.start).to be(background_task)
      end

      context 'when installing exit handler' do
        it 'stops a running background task' do
          expect(background_task).to receive(:at_exit).and_yield

          background_task.start

          expect(background_task).not_to be_running
        end
      end

      context 'when task responds to start' do
        let(:task_class) do
          Struct.new(:started, :start_retval, :run) do
            def start
              self.started = true
              self.start_retval
            end

            def call
              self.run = true
            end
          end
        end

        let(:task) { task_class.new }

        it 'calls start' do
          background_task.start

          expect_condition { task.started == true }
        end

        context 'when start returns true' do
          it 'runs the task' do
            task.start_retval = true

            background_task.start

            expect_condition { task.run == true }
          end
        end

        context 'when start returns false' do
          it 'does not run the task' do
            task.start_retval = false

            background_task.start

            expect_condition { task.run.nil? }
          end
        end
      end

      context 'when synchronous is set to true' do
        let(:options) { { synchronous: true } }

        it 'calls join on the thread' do
          # Thread has to be run in a block, expect_next_instance_of does not support this.
          allow_any_instance_of(Thread).to receive(:join) # rubocop:disable RSpec/AnyInstanceOf

          background_task.start

          expect_condition { @task_run == true }
          expect(@task_thread).to have_received(:join)
        end
      end
    end

    describe '#stop' do
      it 'is a no-op' do
        expect { background_task.stop }.not_to change { subject.running? }
        expect_condition { @task_run.nil? }
      end
    end
  end

  context 'when running' do
    before do
      background_task.start
    end

    describe '#start' do
      it 'raises an error' do
        expect { background_task.start }.to raise_error(described_class::AlreadyStartedError)
      end
    end

    describe '#stop' do
      it 'stops running' do
        expect { background_task.stop }.to change { subject.running? }.from(true).to(false)
      end

      context 'when task responds to stop' do
        let(:task_class) do
          Struct.new(:stopped, :call) do
            def stop
              self.stopped = true
            end
          end
        end

        let(:task) { task_class.new }

        it 'calls stop' do
          background_task.stop

          expect_condition { task.stopped == true }
        end
      end

      context 'when task stop raises an error' do
        let(:error) { RuntimeError.new('task error') }
        let(:options) { { name: 'test_background_task' } }

        let(:task_class) do
          Struct.new(:call, :error, keyword_init: true) do
            def stop
              raise error
            end
          end
        end

        let(:task) { task_class.new(error: error) }

        it 'stops gracefully' do
          expect { background_task.stop }.not_to raise_error
          expect(background_task).not_to be_running
        end

        it 'reports the error' do
          expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
            error, { extra: { reported_by: 'test_background_task' } }
          )

          background_task.stop
        end
      end
    end

    context 'when task run raises exception' do
      let(:error) { RuntimeError.new('task error') }
      let(:options) { { name: 'test_background_task' } }
      let(:task) do
        proc do
          @task_run = true
          raise error
        end
      end

      it 'stops gracefully' do
        expect_condition { @task_run == true }
        expect { background_task.stop }.not_to raise_error
        expect(background_task).not_to be_running
      end

      it 'reports the error' do
        expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
          error, { extra: { reported_by: 'test_background_task' } }
        )

        background_task.stop
      end
    end
  end
end
# rubocop: enable RSpec/InstanceVariable