# frozen_string_literal: true
# rubocop:todo all

require "spec_helper"
require 'concurrent-ruby'

describe Mongoid::Contextual::Mongo::DocumentsLoader do
  let(:view) do
    double('view').tap do |view|
      allow(view).to receive(:map)
    end
  end

  let(:klass) do
    double
  end

  let(:criteria) do
    double('criteria').tap do |criteria|
      allow(criteria).to receive(:inclusions).and_return([])
    end
  end

  let(:executor) do
    described_class.executor
  end

  let(:subject) do
    described_class.new(view, klass, criteria, executor: executor)
  end

  context 'state management' do
    # Java does not allow creating a ThreadPoolExecutor with 0 max and
    # min threads.
    require_mri

    let(:executor) do
      # Such executor will never execute a task, so it guarantees that
      # our task will stay in its initial state.
      Concurrent::ThreadPoolExecutor.new(
        min_threads: 0,
        max_threads: 0,
      )
    end

    describe '#initialize' do
      it 'initializes in pending state' do
        expect(subject.pending?).to be_truthy
        expect(subject.started?).to be_falsey
      end
    end

    describe '#unschedule' do
      it 'changes state' do
        subject.unschedule
        expect(subject.pending?).to be_falsey
        expect(subject.started?).to be_falsey
      end
    end

    describe '#execute' do
      it 'does not change state' do
        prev_started = subject.started?
        prev_pending = subject.pending?
        subject.execute
        expect(subject.started?).to eq(prev_started)
        expect(subject.pending?).to eq(prev_pending)
      end
    end

    context 'when the task is completed' do
      let(:executor) do
        Concurrent::ImmediateExecutor.new
      end

      it 'changes the state to started' do
        subject.wait!
        expect(subject.started?).to be_truthy
        expect(subject.pending?).to be_falsey
      end
    end
  end

  context 'loading documents' do
    let(:view) do
      klass.collection.find(criteria.selector, session: criteria.send(:_session))
    end

    let(:criteria) do
      Band.where(name: "Depeche Mode")
    end

    let(:klass) do
      Band
    end

    let!(:band) do
      Band.create!(name: 'Depeche Mode')
    end

    context 'asynchronously' do
      it 'loads documents' do
        subject.wait!
        expect(subject.value).to eq([band])
      end
    end

    context 'synchronously' do
      # Java does not allow creating a ThreadPoolExecutor with 0 max and
      # min threads.
      require_mri

      let(:executor) do
        # Such executor will never execute a task, so it guarantees that
        # our task will stay in its initial state.
        Concurrent::ThreadPoolExecutor.new(
          min_threads: 0,
          max_threads: 0,
        )
      end

      it 'loads documents' do
        expect(subject.execute).to eq([band])
      end
    end
  end

  describe '.executor' do
    context 'when immediate executor requested' do
      it 'returns immediate executor' do
        expect(
          described_class.executor(:immediate)
        ).to eq(described_class.immediate_executor)
      end
    end

    context 'when global thread pool executor requested' do
      it 'returns global thread pool executor' do
        expect(
          described_class.executor(:global_thread_pool)
        ).to eq(described_class.global_thread_pool_async_query_executor)
      end
    end

    context 'when an unknown executor requested' do
      it 'raises an error' do
        expect do
          described_class.executor(:i_am_an_invalid_option)
        end.to raise_error(Mongoid::Errors::InvalidQueryExecutor)
      end
    end
  end

  describe ".global_thread_pool_async_query_executor" do
    before(:each) do
      described_class.class_variable_set(:@@global_thread_pool_async_query_executor, nil)
    end

    after(:each) do
      described_class.class_variable_set(:@@global_thread_pool_async_query_executor, nil)
    end

    context 'when global_executor_concurrency option is set' do
      config_override :global_executor_concurrency, 50

      it 'returns an executor' do
        executor = described_class.global_thread_pool_async_query_executor
        expect(executor).not_to be_nil
        expect(executor.max_length).to eq( 50 )
      end
    end

    context 'when global_executor_concurrency option is not set' do
      it 'returns an executor' do
        executor = described_class.global_thread_pool_async_query_executor
        expect(executor).not_to be_nil
        expect(executor.max_length).to eq( 4 )
      end
    end

    context 'when global_executor_concurrency option changes' do
      config_override :global_executor_concurrency, 50

      it 'creates new executor' do
        first_executor = described_class.global_thread_pool_async_query_executor
        Mongoid.global_executor_concurrency = 100
        second_executor = described_class.global_thread_pool_async_query_executor

        expect(first_executor).not_to eq(second_executor)
        expect(second_executor.max_length).to eq( 100 )
      end
    end
  end
end
