# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Doctor::ResetTokens, feature_category: :fleet_visibility do
  let(:logger) { instance_double('Logger') }
  let(:model_names) { %w[Project Group] }
  let(:token_names) { %w[runners_token] }
  let(:dry_run) { false }
  let(:doctor) { described_class.new(logger, model_names: model_names, token_names: token_names, dry_run: dry_run) }

  let_it_be(:functional_project) { create(:project).tap(&:runners_token) }
  let_it_be(:functional_group) { create(:group).tap(&:runners_token) }

  let(:broken_project) do
    create(:project, :allow_runner_registration_token).tap do |project|
      project.update_columns(runners_token_encrypted: 'aaa')
    end
  end

  let(:project_with_cipher_error) do
    create(:project, :allow_runner_registration_token).tap do |project|
      project.update_columns(
        runners_token_encrypted: '|rXs75DSHXPE9MGAIgyxcut8pZc72gaa/2ojU0GS1+R+cXNqkbUB13Vb5BaMwf47d98980fc1')
    end
  end

  let(:broken_group) { create(:group, :allow_runner_registration_token, runners_token_encrypted: 'aaa') }

  subject(:run!) do
    expect(logger).to receive(:info).with(
      "Resetting #{token_names.join(', ')} on #{model_names.join(', ')} if they can not be read"
    )
    expect(logger).to receive(:info).with('Done!')
    doctor.run!
  end

  before do
    allow(logger).to receive(:info).with(%r{Checked \d/\d Projects})
    allow(logger).to receive(:info).with(%r{Checked \d Projects})
    allow(logger).to receive(:info).with(%r{Checked \d/\d Groups})
    allow(logger).to receive(:info).with(%r{Checked \d Groups})
  end

  it 'fixes broken project and not the functional project' do
    expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token")

    expect { run! }.to change { broken_project.reload.runners_token_encrypted }.from('aaa')
      .and not_change { functional_project.reload.runners_token_encrypted }
    expect { broken_project.runners_token }.not_to raise_error
  end

  it 'fixes project with cipher error' do
    expect { project_with_cipher_error.runners_token }.to raise_error(OpenSSL::Cipher::CipherError)
    expect(logger).to receive(:debug).with("> Fix Project[#{project_with_cipher_error.id}].runners_token")

    expect { run! }.to change { project_with_cipher_error.reload.runners_token_encrypted }
    expect { project_with_cipher_error.runners_token }.not_to raise_error
  end

  it 'fixes broken group and not the functional group' do
    expect(logger).to receive(:debug).with("> Fix Group[#{broken_group.id}].runners_token")

    expect { run! }.to change { broken_group.reload.runners_token_encrypted }.from('aaa')
      .and not_change { functional_group.reload.runners_token_encrypted }

    expect { broken_group.runners_token }.not_to raise_error
  end

  context 'when one model specified' do
    let(:model_names) { %w[Project] }

    it 'fixes broken project' do
      expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token")

      expect { run! }.to change { broken_project.reload.runners_token_encrypted }.from('aaa')
      expect { broken_project.runners_token }.not_to raise_error
    end

    it 'does not fix other models' do
      expect { run! }.not_to change { broken_group.reload.runners_token_encrypted }.from('aaa')
    end
  end

  context 'when non-existing token field is given' do
    let(:token_names) { %w[nonexisting_token] }

    it 'does not fix anything' do
      expect { run! }.not_to change { broken_project.reload.runners_token_encrypted }.from('aaa')
    end
  end

  context 'when executing in a dry-run mode' do
    let(:dry_run) { true }

    it 'prints info about fixed project, but does not actually do anything' do
      expect(logger).to receive(:info).with('Executing in DRY RUN mode, no records will actually be updated')
      expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token")

      expect { run! }.not_to change { broken_project.reload.runners_token_encrypted }.from('aaa')
      expect { broken_project.runners_token }.to raise_error(TypeError)
    end
  end

  context 'when db is in read-only mode' do
    before do
      allow(Gitlab::Database).to receive(:read_write?).and_return(false)
    end

    it 'prints info about fix, but does not actually do anything' do
      expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token")
      expect(broken_project).not_to receive(:save!)

      expect { run! }.not_to change { broken_project.reload.runners_token_encrypted }.from('aaa')
      expect { broken_project.runners_token }.to raise_error(TypeError)
    end
  end

  it 'prints progress along the way' do
    stub_const('Gitlab::Doctor::ResetTokens::PRINT_PROGRESS_EVERY', 1)

    broken_project
    project_with_cipher_error

    expect(logger).to receive(:info).with(
      "Resetting #{token_names.join(', ')} on #{model_names.join(', ')} if they can not be read"
    )
    expect(logger).to receive(:info).with('Checked 1/3 Projects')
    expect(logger).to receive(:debug).with("> Fix Project[#{broken_project.id}].runners_token")
    expect(logger).to receive(:info).with('Checked 2/3 Projects')
    expect(logger).to receive(:debug).with("> Fix Project[#{project_with_cipher_error.id}].runners_token")
    expect(logger).to receive(:info).with('Checked 3/3 Projects')
    expect(logger).to receive(:info).with('Done!')

    doctor.run!
  end

  it "prints 'Something went wrong' error when encounters unexpected exception, but continues" do
    broken_project
    project_with_cipher_error

    expect(logger).to receive(:debug).with(
      "> Something went wrong for Project[#{broken_project.id}].runners_token: Error message")
    expect(logger).to receive(:debug).with(
      "Something went wrong for Project[#{broken_project.id}]: no implicit conversion of nil into String")
    expect(logger).to receive(:debug).with("> Fix Project[#{project_with_cipher_error.id}].runners_token")

    expect(broken_project).to receive(:runners_token).and_raise("Error message")
    expect(Project).to receive(:find_each).and_return([broken_project, project_with_cipher_error].each)

    expect { run! }.to not_change { broken_project.reload.runners_token_encrypted }.from('aaa')
      .and change { project_with_cipher_error.reload.runners_token_encrypted }
  end

  context 'with multiple cipher error attributes' do
    let(:application_setting_with_cipher_errors) do
      create(:application_setting).tap do |setting|
        setting.update_columns(
          runners_registration_token_encrypted: 'aaa',
          error_tracking_access_token_encrypted: 'bbb'
        )
      end
    end

    let(:model_names) { %w[ApplicationSetting] }
    let(:token_names) { %w[runners_registration_token error_tracking_access_token] }

    before do
      allow(logger).to receive(:info)
      allow(logger).to receive(:debug)
    end

    it 'fixes all attributes' do
      expect { run! }.to change {
        application_setting_with_cipher_errors.reload.runners_registration_token_encrypted
      }.from('aaa')
       .and change {
         application_setting_with_cipher_errors.reload.error_tracking_access_token_encrypted
       }.from('bbb')

      expect { application_setting_with_cipher_errors.reload.runners_registration_token }.not_to raise_error
      expect { application_setting_with_cipher_errors.reload.error_tracking_access_token }.not_to raise_error
    end
  end
end
