# frozen_string_literal: true

require 'rails_helper'

describe ProjectStatistics do
  let(:project) { create :project }
  let(:statistics) { project.statistics }

  describe 'associations' do
    it { is_expected.to belong_to(:project) }
    it { is_expected.to belong_to(:namespace) }
  end

  describe 'scopes' do
    describe '.for_project_ids' do
      it 'returns only requested projects' do
        stats = create_list(:project_statistics, 3)
        project_ids = stats[0..1].map { |s| s.project_id }
        expected_ids = stats[0..1].map { |s| s.id }

        requested_stats = described_class.for_project_ids(project_ids).pluck(:id)

        expect(requested_stats).to eq(expected_ids)
      end
    end
  end

  describe 'statistics columns' do
    it "support values up to 8 exabytes" do
      statistics.update!(
        commit_count: 8.exabytes - 1,
        repository_size: 2.exabytes,
        wiki_size: 1.exabytes,
        lfs_objects_size: 2.exabytes,
        build_artifacts_size: 3.exabytes - 1
      )

      statistics.reload

      expect(statistics.commit_count).to eq(8.exabytes - 1)
      expect(statistics.repository_size).to eq(2.exabytes)
      expect(statistics.wiki_size).to eq(1.exabytes)
      expect(statistics.lfs_objects_size).to eq(2.exabytes)
      expect(statistics.build_artifacts_size).to eq(3.exabytes - 1)
      expect(statistics.storage_size).to eq(8.exabytes - 1)
    end
  end

  describe '#total_repository_size' do
    it "sums repository and LFS object size" do
      statistics.repository_size = 2
      statistics.wiki_size = 6
      statistics.lfs_objects_size = 3
      statistics.build_artifacts_size = 4

      expect(statistics.total_repository_size).to eq 5
    end
  end

  describe '#wiki_size' do
    it "is initialized with not null value" do
      expect(statistics.wiki_size).to eq 0
    end
  end

  describe '#refresh!' do
    before do
      allow(statistics).to receive(:update_commit_count)
      allow(statistics).to receive(:update_repository_size)
      allow(statistics).to receive(:update_wiki_size)
      allow(statistics).to receive(:update_lfs_objects_size)
      allow(statistics).to receive(:update_storage_size)
    end

    context "without arguments" do
      before do
        statistics.refresh!
      end

      it "sums all counters" do
        expect(statistics).to have_received(:update_commit_count)
        expect(statistics).to have_received(:update_repository_size)
        expect(statistics).to have_received(:update_wiki_size)
        expect(statistics).to have_received(:update_lfs_objects_size)
      end
    end

    context "when passing an only: argument" do
      before do
        statistics.refresh! only: [:lfs_objects_size]
      end

      it "only updates the given columns" do
        expect(statistics).to have_received(:update_lfs_objects_size)
        expect(statistics).not_to have_received(:update_commit_count)
        expect(statistics).not_to have_received(:update_repository_size)
        expect(statistics).not_to have_received(:update_wiki_size)
      end
    end

    context 'without repositories' do
      it 'does not crash' do
        expect(project.repository.exists?).to be_falsey
        expect(project.wiki.repository.exists?).to be_falsey

        statistics.refresh!

        expect(statistics).to have_received(:update_commit_count)
        expect(statistics).to have_received(:update_repository_size)
        expect(statistics).to have_received(:update_wiki_size)
        expect(statistics.repository_size).to eq(0)
        expect(statistics.commit_count).to eq(0)
        expect(statistics.wiki_size).to eq(0)
      end
    end

    context 'with deleted repositories' do
      let(:project) { create(:project, :repository, :wiki_repo) }

      before do
        Gitlab::GitalyClient::StorageSettings.allow_disk_access do
          FileUtils.rm_rf(project.repository.path)
          FileUtils.rm_rf(project.wiki.repository.path)
        end
      end

      it 'does not crash' do
        statistics.refresh!

        expect(statistics).to have_received(:update_commit_count)
        expect(statistics).to have_received(:update_repository_size)
        expect(statistics).to have_received(:update_wiki_size)
        expect(statistics.repository_size).to eq(0)
        expect(statistics.commit_count).to eq(0)
        expect(statistics.wiki_size).to eq(0)
      end
    end

    context 'when the column is namespace relatable' do
      let(:namespace) { create(:group) }
      let(:project) { create(:project, namespace: namespace) }

      context 'when arguments are passed' do
        it 'schedules the aggregation worker' do
          expect(Namespaces::ScheduleAggregationWorker)
            .to receive(:perform_async)

          statistics.refresh!(only: [:lfs_objects_size])
        end
      end

      context 'when no argument is passed' do
        it 'schedules the aggregation worker' do
          expect(Namespaces::ScheduleAggregationWorker)
            .to receive(:perform_async)

          statistics.refresh!
        end
      end
    end

    context 'when the column is not namespace relatable' do
      it 'does not schedules an aggregation worker' do
        expect(Namespaces::ScheduleAggregationWorker)
          .not_to receive(:perform_async)

        statistics.refresh!(only: [:commit_count])
      end
    end
  end

  describe '#update_commit_count' do
    before do
      allow(project.repository).to receive(:commit_count).and_return(23)
      statistics.update_commit_count
    end

    it "stores the number of commits in the repository" do
      expect(statistics.commit_count).to eq 23
    end
  end

  describe '#update_repository_size' do
    before do
      allow(project.repository).to receive(:size).and_return(12)
      statistics.update_repository_size
    end

    it "stores the size of the repository" do
      expect(statistics.repository_size).to eq 12.megabytes
    end
  end

  describe '#update_wiki_size' do
    before do
      allow(project.wiki.repository).to receive(:size).and_return(34)
      statistics.update_wiki_size
    end

    it "stores the size of the wiki" do
      expect(statistics.wiki_size).to eq 34.megabytes
    end
  end

  describe '#update_lfs_objects_size' do
    let!(:lfs_object1) { create(:lfs_object, size: 23.megabytes) }
    let!(:lfs_object2) { create(:lfs_object, size: 34.megabytes) }
    let!(:lfs_objects_project1) { create(:lfs_objects_project, project: project, lfs_object: lfs_object1) }
    let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) }

    before do
      statistics.update_lfs_objects_size
    end

    it "stores the size of related LFS objects" do
      expect(statistics.lfs_objects_size).to eq 57.megabytes
    end
  end

  describe '#update_storage_size' do
    it "sums all storage counters" do
      statistics.update!(
        repository_size: 2,
        wiki_size: 4,
        lfs_objects_size: 3
      )

      statistics.reload

      expect(statistics.storage_size).to eq 9
    end

    it 'works during wiki_size backfill' do
      statistics.update!(
        repository_size: 2,
        wiki_size: nil,
        lfs_objects_size: 3
      )

      statistics.reload

      expect(statistics.storage_size).to eq 5
    end
  end

  describe '.increment_statistic' do
    shared_examples 'a statistic that increases storage_size' do
      it 'increases the statistic by that amount' do
        expect { described_class.increment_statistic(project.id, stat, 13) }
          .to change { statistics.reload.send(stat) || 0 }
          .by(13)
      end

      it 'increases also storage size by that amount' do
        expect { described_class.increment_statistic(project.id, stat, 20) }
         .to change { statistics.reload.storage_size }
         .by(20)
      end
    end

    context 'when adjusting :build_artifacts_size' do
      let(:stat) { :build_artifacts_size }

      it_behaves_like 'a statistic that increases storage_size'
    end

    context 'when adjusting :packages_size' do
      let(:stat) { :packages_size }

      it_behaves_like 'a statistic that increases storage_size'
    end

    context 'when the amount is 0' do
      it 'does not execute a query' do
        project
        expect { described_class.increment_statistic(project.id, :build_artifacts_size, 0) }
          .not_to exceed_query_limit(0)
      end
    end

    context 'when using an invalid column' do
      it 'raises an error' do
        expect { described_class.increment_statistic(project.id, :id, 13) }
          .to raise_error(ArgumentError, "Cannot increment attribute: id")
      end
    end
  end
end
