# frozen_string_literal: true

require 'spec_helper'

using Bullet::Ext::Object

module Bullet
  module Detector
    describe UnusedEagerLoading do
      before(:all) do
        @post = Post.first
        @post2 = Post.all[1]
        @post3 = Post.last
      end

      context '.call_associations' do
        it 'should get empty array if eager_loadings' do
          expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to be_empty
        end

        it 'should get call associations if object and association are both in eager_loadings and call_object_associations' do
          UnusedEagerLoading.add_eager_loadings([@post], :association)
          UnusedEagerLoading.add_call_object_associations(@post, :association)
          expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to eq(
            [:association]
          )
        end

        it 'should not get call associations if not exist in call_object_associations' do
          UnusedEagerLoading.add_eager_loadings([@post], :association)
          expect(UnusedEagerLoading.send(:call_associations, @post.bullet_key, Set.new([:association]))).to be_empty
        end
      end

      context '.diff_object_associations' do
        it 'should return associations not exist in call_association' do
          expect(UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))).to eq(
            [:association]
          )
        end

        it 'should return empty if associations exist in call_association' do
          UnusedEagerLoading.add_eager_loadings([@post], :association)
          UnusedEagerLoading.add_call_object_associations(@post, :association)
          expect(
            UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))
          ).to be_empty
        end
      end

      context '.check_unused_preload_associations' do
        let(:paths) { %w[/dir1 /dir1/subdir] }
        it 'should create notification if object_association_diff is not empty' do
          UnusedEagerLoading.add_object_associations(@post, :association)
          allow(UnusedEagerLoading).to receive(:caller_in_project).and_return(paths)
          expect(UnusedEagerLoading).to receive(:create_notification).with(paths, 'Post', [:association])
          UnusedEagerLoading.check_unused_preload_associations
        end

        it 'should not create notification if object_association_diff is empty' do
          UnusedEagerLoading.add_object_associations(@post, :association)
          UnusedEagerLoading.add_eager_loadings([@post], :association)
          UnusedEagerLoading.add_call_object_associations(@post, :association)
          expect(
            UnusedEagerLoading.send(:diff_object_associations, @post.bullet_key, Set.new([:association]))
          ).to be_empty
          expect(UnusedEagerLoading).not_to receive(:create_notification).with('Post', [:association])
          UnusedEagerLoading.check_unused_preload_associations
        end

        it 'should create call stack for notification' do
          UnusedEagerLoading.add_object_associations(@post, :association)
          expect(UnusedEagerLoading.send(:call_stacks).registry).not_to be_empty
        end
      end

      context '.add_eager_loadings' do
        it 'should add objects, associations pair when eager_loadings are empty' do
          UnusedEagerLoading.add_eager_loadings([@post, @post2], :associations)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include(
            [@post.bullet_key, @post2.bullet_key],
            :associations
          )
        end

        it 'should add objects, associations pair for existing eager_loadings' do
          UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1)
          UnusedEagerLoading.add_eager_loadings([@post, @post2], :association2)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include(
            [@post.bullet_key, @post2.bullet_key],
            :association1
          )
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include(
            [@post.bullet_key, @post2.bullet_key],
            :association2
          )
        end

        it 'should merge objects, associations pair for existing eager_loadings' do
          UnusedEagerLoading.add_eager_loadings([@post], :association1)
          UnusedEagerLoading.add_eager_loadings([@post, @post2], :association2)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association2)
        end

        it 'should vmerge objects recursively, associations pair for existing eager_loadings' do
          UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1)
          UnusedEagerLoading.add_eager_loadings([@post, @post3], :association1)
          UnusedEagerLoading.add_eager_loadings([@post, @post3], :association2)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association1)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post3.bullet_key], :association1)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post3.bullet_key], :association2)
        end

        it 'should delete objects, associations pair for existing eager_loadings' do
          UnusedEagerLoading.add_eager_loadings([@post, @post2], :association1)
          UnusedEagerLoading.add_eager_loadings([@post], :association2)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association1)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post.bullet_key], :association2)
          expect(UnusedEagerLoading.send(:eager_loadings)).to be_include([@post2.bullet_key], :association1)
        end

        it 'should merge each association element when associations is an Array (not the Array itself)' do
          # Regression test: ActiveRecord::Associations::JoinDependency#instantiate
          # passes the per-record association set as `to_a`, i.e. an Array. When
          # the eager_loadings group splits on partial overlap, the merge branch
          # must add each Array element to the existing Set rather than the whole
          # Array as a single element. Otherwise similarly_associated's strict
          # `value == associations` comparison stops matching, every read goes
          # uncredited, and the preload is reported as unused.
          UnusedEagerLoading.add_eager_loadings([@post, @post2], %i[association1 association2])
          UnusedEagerLoading.add_eager_loadings([@post2], %i[association1 association2])

          eager_loadings = UnusedEagerLoading.send(:eager_loadings)
          expect(eager_loadings).to be_include([@post.bullet_key], :association1)
          expect(eager_loadings).to be_include([@post.bullet_key], :association2)
          expect(eager_loadings).to be_include([@post2.bullet_key], :association1)
          expect(eager_loadings).to be_include([@post2.bullet_key], :association2)

          eager_loadings.registry.each_value do |value|
            expect(value).to all(be_a(Symbol))
          end
        end
      end
    end
  end
end
