# frozen_string_literal: true

require 'spec_helper'
require 'sidekiq/testing'

RSpec.describe Gitlab::SidekiqMiddleware do
  before do
    stub_const('TestWorker', Class.new)

    TestWorker.class_eval do
      include Sidekiq::Worker
      include ApplicationWorker

      def perform(_arg)
        Gitlab::SafeRequestStore['gitaly_call_actual'] = 1
        Gitlab::SafeRequestStore[:gitaly_query_time] = 5
      end
    end
  end

  around do |example|
    Sidekiq::Testing.inline! { example.run }
  end

  let(:worker_class) { TestWorker }
  let(:job_args) { [0.01] }

  # The test sets up a new server middleware stack, ensuring that the
  # appropriate middlewares, as passed into server_configurator,
  # are invoked.
  # Additionally the test ensure that each middleware is
  # 1) not failing
  # 2) yielding exactly once
  describe '.server_configurator' do
    around do |example|
      with_sidekiq_server_middleware do |chain|
        described_class.server_configurator(
          metrics: metrics,
          arguments_logger: arguments_logger,
          memory_killer: memory_killer
        ).call(chain)

        example.run
      end
    end

    let(:middleware_expected_args) { [a_kind_of(worker_class), hash_including({ 'args' => job_args }), anything] }
    let(:all_sidekiq_middlewares) do
      [
       Gitlab::SidekiqMiddleware::Monitor,
       Gitlab::SidekiqMiddleware::BatchLoader,
       Labkit::Middleware::Sidekiq::Server,
       Gitlab::SidekiqMiddleware::InstrumentationLogger,
       Gitlab::SidekiqVersioning::Middleware,
       Gitlab::SidekiqStatus::ServerMiddleware,
       Gitlab::SidekiqMiddleware::ServerMetrics,
       Gitlab::SidekiqMiddleware::ArgumentsLogger,
       Gitlab::SidekiqMiddleware::MemoryKiller,
       Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
       Gitlab::SidekiqMiddleware::ExtraDoneLogMetadata,
       Gitlab::SidekiqMiddleware::WorkerContext::Server,
       Gitlab::SidekiqMiddleware::AdminMode::Server,
       Gitlab::SidekiqMiddleware::DuplicateJobs::Server
      ]
    end

    let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }

    shared_examples "a server middleware chain" do
      it "passes through the right server middlewares" do
        enabled_sidekiq_middlewares.each do |middleware|
          expect_next_instance_of(middleware) do |middleware_instance|
            expect(middleware_instance).to receive(:call).with(*middleware_expected_args).once.and_call_original
          end
        end

        disabled_sidekiq_middlewares.each do |middleware|
          expect(middleware).not_to receive(:new)
        end

        worker_class.perform_async(*job_args)
      end
    end

    shared_examples "a server middleware chain for mailer" do
      let(:worker_class) { ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper }
      let(:job_args) do
        [
          {
            "job_class" => "ActionMailer::MailDeliveryJob",
            "job_id" => "a180b47c-3fd6-41b8-81e9-34da61c3400e",
            "provider_job_id" => nil,
            "queue_name" => "mailers",
            "priority" => nil,
            "arguments" => [
              "Notify",
              "test_email",
              "deliver_now",
              {
                "args" => [
                  "test@example.com",
                  "subject",
                  "body"
                ],
                ActiveJob::Arguments.const_get('RUBY2_KEYWORDS_KEY', false) => ["args"]
              }
            ],
            "executions" => 0,
            "exception_executions" => {},
            "locale" => "en",
            "timezone" => "UTC",
            "enqueued_at" => "2020-07-27T07:43:31Z"
          }
        ]
      end

      it_behaves_like "a server middleware chain"
    end

    context "all optional middlewares off" do
      let(:metrics) { false }
      let(:arguments_logger) { false }
      let(:memory_killer) { false }
      let(:disabled_sidekiq_middlewares) do
        [
          Gitlab::SidekiqMiddleware::ServerMetrics,
          Gitlab::SidekiqMiddleware::ArgumentsLogger,
          Gitlab::SidekiqMiddleware::MemoryKiller
        ]
      end

      it_behaves_like "a server middleware chain"
      it_behaves_like "a server middleware chain for mailer"
    end

    context "all optional middlewares on" do
      let(:metrics) { true }
      let(:arguments_logger) { true }
      let(:memory_killer) { true }
      let(:disabled_sidekiq_middlewares) { [] }

      it_behaves_like "a server middleware chain"
      it_behaves_like "a server middleware chain for mailer"

      context "server metrics" do
        let(:gitaly_histogram) { double(:gitaly_histogram) }

        before do
          allow(Gitlab::Metrics).to receive(:histogram).and_call_original

          allow(Gitlab::Metrics).to receive(:histogram)
                                      .with(:sidekiq_jobs_gitaly_seconds, anything, anything, anything)
                                      .and_return(gitaly_histogram)
        end

        it "records correct Gitaly duration" do
          expect(gitaly_histogram).to receive(:observe).with(anything, 5.0)

          worker_class.perform_async(*job_args)
        end
      end
    end
  end

  # The test sets up a new client middleware stack. The test ensures
  # that each middleware is:
  # 1) not failing
  # 2) yielding exactly once
  describe '.client_configurator' do
    let(:chain) { Sidekiq::Middleware::Chain.new }
    let(:job) { { 'args' => job_args } }
    let(:queue) { 'default' }
    let(:redis_pool) { Sidekiq.redis_pool }
    let(:middleware_expected_args) { [worker_class_arg, job, queue, redis_pool] }
    let(:expected_middlewares) do
      [
         ::Gitlab::SidekiqMiddleware::WorkerContext::Client,
         ::Labkit::Middleware::Sidekiq::Client,
         ::Gitlab::SidekiqMiddleware::DuplicateJobs::Client,
         ::Gitlab::SidekiqStatus::ClientMiddleware,
         ::Gitlab::SidekiqMiddleware::AdminMode::Client,
         ::Gitlab::SidekiqMiddleware::SizeLimiter::Client,
         ::Gitlab::SidekiqMiddleware::ClientMetrics
      ]
    end

    before do
      described_class.client_configurator.call(chain)
    end

    shared_examples "a client middleware chain" do
      # Its possible that a middleware could accidentally omit a yield call
      # this will prevent the full middleware chain from being executed.
      # This test ensures that this does not happen
      it "invokes the chain" do
        expected_middlewares do |middleware|
          expect_any_instance_of(middleware).to receive(:call).with(*middleware_expected_args).once.ordered.and_call_original
        end

        expect { |b| chain.invoke(worker_class_arg, job, queue, redis_pool, &b) }.to yield_control.once
      end
    end

    # Sidekiq documentation states that the worker class could be a string
    # or a class reference. We should test for both
    context "handles string worker_class values" do
      let(:worker_class_arg) { worker_class.to_s }

      it_behaves_like "a client middleware chain"
    end

    context "handles string worker_class values" do
      let(:worker_class_arg) { worker_class }

      it_behaves_like "a client middleware chain"
    end
  end
end