# frozen_string_literal: true

require 'spec_helper'
require 'rspec-parameterized'

RSpec.describe Gitlab::ErrorTracking::Processor::SidekiqProcessor do
  after do
    if described_class.instance_variable_defined?(:@permitted_arguments_for_worker)
      described_class.remove_instance_variable(:@permitted_arguments_for_worker)
    end
  end

  describe '.filter_arguments' do
    it 'returns a lazy enumerator' do
      filtered = described_class.filter_arguments([1, 'string'], 'TestWorker')

      expect(filtered).to be_a(Enumerator::Lazy)
      expect(filtered.to_a).to eq([1, described_class::FILTERED_STRING])
    end

    context 'arguments filtering' do
      using RSpec::Parameterized::TableSyntax

      where(:klass, :expected) do
        'UnknownWorker' | [1, described_class::FILTERED_STRING, described_class::FILTERED_STRING, described_class::FILTERED_STRING]
        'NoPermittedArguments' | [1, described_class::FILTERED_STRING, described_class::FILTERED_STRING, described_class::FILTERED_STRING]
        'OnePermittedArgument' | [1, 'string', described_class::FILTERED_STRING, described_class::FILTERED_STRING]
        'AllPermittedArguments' | [1, 'string', [1, 2], { a: 1 }]
      end

      with_them do
        before do
          stub_const('NoPermittedArguments', double(loggable_arguments: []))
          stub_const('OnePermittedArgument', double(loggable_arguments: [1]))
          stub_const('AllPermittedArguments', double(loggable_arguments: [0, 1, 2, 3]))
        end

        it do
          expect(described_class.filter_arguments([1, 'string', [1, 2], { a: 1 }], klass).to_a)
            .to eq(expected)
        end
      end
    end
  end

  describe '.permitted_arguments_for_worker' do
    it 'returns the loggable_arguments for a worker class as a set' do
      stub_const('TestWorker', double(loggable_arguments: [1, 1]))

      expect(described_class.permitted_arguments_for_worker('TestWorker'))
        .to eq([1].to_set)
    end

    it 'returns an empty set when the worker class does not exist' do
      expect(described_class.permitted_arguments_for_worker('TestWorker'))
        .to eq(Set.new)
    end

    it 'returns an empty set when the worker class does not respond to loggable_arguments' do
      stub_const('TestWorker', 1)

      expect(described_class.permitted_arguments_for_worker('TestWorker'))
        .to eq(Set.new)
    end

    it 'returns an empty set when loggable_arguments cannot be converted to a set' do
      stub_const('TestWorker', double(loggable_arguments: 1))

      expect(described_class.permitted_arguments_for_worker('TestWorker'))
        .to eq(Set.new)
    end

    it 'memoizes the results' do
      worker_class = double

      stub_const('TestWorker', worker_class)

      expect(worker_class).to receive(:loggable_arguments).once.and_return([])

      described_class.permitted_arguments_for_worker('TestWorker')
      described_class.permitted_arguments_for_worker('TestWorker')
    end
  end

  describe '.loggable_arguments' do
    it 'filters and limits the arguments, then converts to strings' do
      half_limit = Gitlab::Utils::LogLimitedArray::MAXIMUM_ARRAY_LENGTH / 2
      args = [[1, 2], 'a' * half_limit, 'b' * half_limit, 'c' * half_limit, 'd']

      stub_const('LoggableArguments', double(loggable_arguments: [0, 1, 3, 4]))

      expect(described_class.loggable_arguments(args, 'LoggableArguments'))
        .to eq(['[1, 2]', 'a' * half_limit, '[FILTERED]', '...'])
    end
  end

  describe '.call' do
    let(:required_options) do
      {
        configuration: Raven.configuration,
        context: Raven.context,
        breadcrumbs: Raven.breadcrumbs
      }
    end

    let(:event) { Raven::Event.new(required_options.merge(wrapped_value)) }
    let(:result_hash) { described_class.call(event).to_hash }

    context 'when there is Sidekiq data' do
      let(:wrapped_value) { { extra: { sidekiq: value } } }

      shared_examples 'Sidekiq arguments' do |args_in_job_hash: true|
        let(:path) { [:extra, :sidekiq, args_in_job_hash ? :job : nil, 'args'].compact }
        let(:args) { [1, 'string', { a: 1 }, [1, 2]] }

        context 'for an unknown worker' do
          let(:value) do
            hash = { 'args' => args, 'class' => 'UnknownWorker' }

            args_in_job_hash ? { job: hash } : hash
          end

          it 'only allows numeric arguments for an unknown worker' do
            expect(result_hash.dig(*path))
              .to eq([1, described_class::FILTERED_STRING, described_class::FILTERED_STRING, described_class::FILTERED_STRING])
          end
        end

        context 'for a permitted worker' do
          let(:value) do
            hash = { 'args' => args, 'class' => 'PostReceive' }

            args_in_job_hash ? { job: hash } : hash
          end

          it 'allows all argument types for a permitted worker' do
            expect(result_hash.dig(*path)).to eq(args)
          end
        end
      end

      context 'when processing via the default error handler' do
        include_examples 'Sidekiq arguments', args_in_job_hash: true
      end

      context 'when processing via Gitlab::ErrorTracking' do
        include_examples 'Sidekiq arguments', args_in_job_hash: false
      end

      context 'when a jobstr field is present' do
        let(:value) do
          {
            job: { 'args' => [1] },
            jobstr: { 'args' => [1] }.to_json
          }
        end

        it 'removes the jobstr' do
          expect(result_hash.dig(:extra, :sidekiq)).to eq(value.except(:jobstr))
        end
      end

      context 'when no jobstr value is present' do
        let(:value) { { job: { 'args' => [1] } } }

        it 'does nothing' do
          expect(result_hash.dig(:extra, :sidekiq)).to eq(value)
        end
      end
    end

    context 'when there is no Sidekiq data' do
      let(:value) { { tags: { foo: 'bar', baz: 'quux' } } }
      let(:wrapped_value) { value }

      it 'does nothing' do
        expect(result_hash).to include(value)
        expect(result_hash.dig(:extra, :sidekiq)).to be_nil
      end
    end
  end
end
