# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Database::ConsistencyChecker, feature_category: :cell do
  let(:batch_size) { 10 }
  let(:max_batches) { 4 }
  let(:max_runtime) { described_class::MAX_RUNTIME }
  let(:metrics_counter) { Gitlab::Metrics.client.get(:consistency_checks) }

  subject(:consistency_checker) do
    described_class.new(
      source_model: Namespace,
      target_model: Ci::NamespaceMirror,
      source_columns: %w[id traversal_ids],
      target_columns: %w[namespace_id traversal_ids]
    )
  end

  before do
    stub_const("#{described_class.name}::BATCH_SIZE", batch_size)
    stub_const("#{described_class.name}::MAX_BATCHES", max_batches)
    redis_shared_state_cleanup! # For Prometheus Counters
  end

  after do
    Gitlab::Metrics.reset_registry!
  end

  describe '#over_time_limit?' do
    before do
      allow(consistency_checker).to receive(:start_time).and_return(0)
    end

    it 'returns true only if the running time has exceeded MAX_RUNTIME' do
      allow(consistency_checker).to receive(:monotonic_time).and_return(0, max_runtime - 1, max_runtime + 1)
      expect(consistency_checker.monotonic_time).to eq(0)
      expect(consistency_checker.send(:over_time_limit?)).to eq(false)
      expect(consistency_checker.send(:over_time_limit?)).to eq(true)
    end
  end

  describe '#execute' do
    context 'when empty tables' do
      it 'returns an empty response' do
        expected_result = { matches: 0, mismatches: 0, batches: 0, mismatches_details: [], next_start_id: nil }
        expect(consistency_checker.execute(start_id: 1)).to eq(expected_result)
      end
    end

    context 'when the tables contain matching items' do
      before do
        create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
      end

      it 'does not process more than MAX_BATCHES' do
        max_batches = 3
        stub_const("#{described_class.name}::MAX_BATCHES", max_batches)
        result = consistency_checker.execute(start_id: Namespace.minimum(:id))
        expect(result[:batches]).to eq(max_batches)
        expect(result[:matches]).to eq(max_batches * batch_size)
      end

      it 'doesn not exceed the MAX_RUNTIME' do
        allow(consistency_checker).to receive(:monotonic_time).and_return(0, max_runtime - 1, max_runtime + 1)
        result = consistency_checker.execute(start_id: Namespace.minimum(:id))
        expect(result[:batches]).to eq(1)
        expect(result[:matches]).to eq(1 * batch_size)
      end

      it 'returns the correct number of matches and batches checked' do
        expected_result = {
          next_start_id: Namespace.minimum(:id) + (described_class::MAX_BATCHES * described_class::BATCH_SIZE),
          batches: max_batches,
          matches: max_batches * batch_size,
          mismatches: 0,
          mismatches_details: []
        }
        expect(consistency_checker.execute(start_id: Namespace.minimum(:id))).to eq(expected_result)
      end

      it 'returns the min_id as the next_start_id if the check reaches the last element' do
        expect(Gitlab::Metrics).to receive(:counter).at_most(:once)
          .with(:consistency_checks, "Consistency Check Results")
          .and_call_original

        # Starting from the 5th last element
        start_id = Namespace.all.order(id: :desc).limit(5).pluck(:id).last
        expected_result = {
          next_start_id: Namespace.first.id,
          batches: 1,
          matches: 5,
          mismatches: 0,
          mismatches_details: []
        }
        expect(consistency_checker.execute(start_id: start_id)).to eq(expected_result)

        expect(metrics_counter.get(source_table: "namespaces", result: "mismatch")).to eq(0)
        expect(metrics_counter.get(source_table: "namespaces", result: "match")).to eq(5)
      end
    end

    context 'when some items are missing from the first table' do
      let(:missing_namespace) { Namespace.all.order(:id).limit(2).last }

      before do
        create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
        missing_namespace.delete
      end

      it 'reports the missing elements' do
        expected_result = {
          next_start_id: Namespace.first.id + (described_class::MAX_BATCHES * described_class::BATCH_SIZE),
          batches: max_batches,
          matches: 39,
          mismatches: 1,
          mismatches_details: [{
            id: missing_namespace.id,
            source_table: nil,
            target_table: [missing_namespace.traversal_ids]
          }]
        }
        expect(consistency_checker.execute(start_id: Namespace.first.id)).to eq(expected_result)

        expect(metrics_counter.get(source_table: "namespaces", result: "mismatch")).to eq(1)
        expect(metrics_counter.get(source_table: "namespaces", result: "match")).to eq(39)
      end
    end

    context 'when some items are missing from the second table' do
      let(:missing_ci_namespace_mirror) { Ci::NamespaceMirror.all.order(:id).limit(2).last }

      before do
        create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects
        missing_ci_namespace_mirror.delete
      end

      it 'reports the missing elements' do
        expected_result = {
          next_start_id: Namespace.first.id + (described_class::MAX_BATCHES * described_class::BATCH_SIZE),
          batches: 4,
          matches: 39,
          mismatches: 1,
          mismatches_details: [{
            id: missing_ci_namespace_mirror.namespace_id,
            source_table: [missing_ci_namespace_mirror.traversal_ids],
            target_table: nil
          }]
        }
        expect(consistency_checker.execute(start_id: Namespace.first.id)).to eq(expected_result)

        expect(metrics_counter.get(source_table: "namespaces", result: "mismatch")).to eq(1)
        expect(metrics_counter.get(source_table: "namespaces", result: "match")).to eq(39)
      end
    end

    context 'when elements are different between the two tables' do
      let(:different_namespaces) { Namespace.order(:id).limit(max_batches * batch_size).sample(3).sort_by(&:id) }

      before do
        create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects

        different_namespaces.each do |namespace|
          namespace.update_attribute(:traversal_ids, [])
        end
      end

      it 'reports the difference between the two tables' do
        expected_result = {
          next_start_id: Namespace.first.id + (described_class::MAX_BATCHES * described_class::BATCH_SIZE),
          batches: 4,
          matches: 37,
          mismatches: 3,
          mismatches_details: different_namespaces.map do |namespace|
            {
              id: namespace.id,
              source_table: [[]],
              target_table: [[namespace.id]] # old traversal_ids of the namespace
            }
          end
        }
        expect(consistency_checker.execute(start_id: Namespace.first.id)).to eq(expected_result)

        expect(metrics_counter.get(source_table: "namespaces", result: "mismatch")).to eq(3)
        expect(metrics_counter.get(source_table: "namespaces", result: "match")).to eq(37)
      end
    end
  end
end
