# frozen_string_literal: true

require 'spec_helper'
require 'ostruct'

using Bullet::Ext::Object

module Bullet
  module Detector
    describe NPlusOneQuery do
      before(:all) do
        @post = Post.first
        @post2 = Post.last
      end

      context '.call_association' do
        it 'should add call_object_associations' do
          expect(NPlusOneQuery).to receive(:add_call_object_associations).with(@post, :associations)
          NPlusOneQuery.call_association(@post, :associations)
        end
      end

      context '.possible?' do
        it 'should be true if possible_objects contain' do
          NPlusOneQuery.add_possible_objects(@post)
          expect(NPlusOneQuery.possible?(@post)).to eq true
        end
      end

      context '.impossible?' do
        it 'should be true if impossible_objects contain' do
          NPlusOneQuery.add_impossible_object(@post)
          expect(NPlusOneQuery.impossible?(@post)).to eq true
        end
      end

      context '.association?' do
        it 'should be true if object, associations pair is already existed' do
          NPlusOneQuery.add_object_associations(@post, :association)
          expect(NPlusOneQuery.association?(@post, :association)).to eq true
        end

        it 'should be false if object, association pair is not existed' do
          NPlusOneQuery.add_object_associations(@post, :association1)
          expect(NPlusOneQuery.association?(@post, :association2)).to eq false
        end
      end

      context '.conditions_met?' do
        it 'should be true if object is possible, not impossible and object, associations pair is not already existed' do
          allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
          allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
          allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
          expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq true
        end

        it 'should be false if object is not possible, not impossible and object, associations pair is not already existed' do
          allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(false)
          allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
          allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
          expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
        end

        it 'should be false if object is possible, but impossible and object, associations pair is not already existed' do
          allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
          allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(true)
          allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(false)
          expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
        end

        it 'should be false if object is possible, not impossible and object, associations pair is already existed' do
          allow(NPlusOneQuery).to receive(:possible?).with(@post).and_return(true)
          allow(NPlusOneQuery).to receive(:impossible?).with(@post).and_return(false)
          allow(NPlusOneQuery).to receive(:association?).with(@post, :associations).and_return(true)
          expect(NPlusOneQuery.conditions_met?(@post, :associations)).to eq false
        end
      end

      context '.call_association' do
        it 'should create notification if conditions met' do
          expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
          expect(NPlusOneQuery).to receive(:caller_in_project).and_return(%w[caller])
          expect(NPlusOneQuery).to receive(:create_notification).with(%w[caller], 'Post', :association)
          NPlusOneQuery.call_association(@post, :association)
        end

        it 'should not create notification if conditions not met' do
          expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(false)
          expect(NPlusOneQuery).not_to receive(:caller_in_project!)
          expect(NPlusOneQuery).not_to receive(:create_notification).with('Post', :association)
          NPlusOneQuery.call_association(@post, :association)
        end

        it 'stores provided caller stack in call_stacks for use by caller_in_project' do
          root_path = Dir.pwd
          bundler_path = Bundler.bundle_path.to_s

          in_project = File.join(root_path, 'app/models/post.rb')
          vendored = File.join(root_path, 'vendor/some_gem.rb')
          from_bundle = File.join(bundler_path, 'rack.rb')

          call_stacks = NPlusOneQuery.send(:call_stacks)
          bullet_key = @post.bullet_key
          call_stacks.delete(bullet_key)

          custom_stack = [in_project, vendored, from_bundle]

          NPlusOneQuery.call_association(@post, :association, custom_stack)

          expect(call_stacks[bullet_key]).to eq(custom_stack)
          expect(NPlusOneQuery.caller_in_project(bullet_key)).to eq([in_project])
        end

        context 'stacktrace_excludes' do
          before { Bullet.stacktrace_excludes = [/def/] }
          after { Bullet.stacktrace_excludes = nil }

          it 'should not create notification when stacktrace contains paths that are in the exclude list' do
            in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
            included_path = OpenStruct.new(absolute_path: '/ghi/ghi.rb')
            excluded_path = OpenStruct.new(absolute_path: '/def/def.rb')

            expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, included_path, excluded_path])
            expect(NPlusOneQuery).to_not receive(:create_notification)
            NPlusOneQuery.call_association(@post, :association)
          end

          # just a sanity spec to make sure the following spec works correctly
          it "should create notification when stacktrace contains methods that aren't in the exclude list" do
            method = NPlusOneQuery.method(:excluded_stacktrace_path?).source_location
            in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
            excluded_path = OpenStruct.new(absolute_path: method.first, lineno: method.last)

            expect(NPlusOneQuery).to receive(:caller_locations).at_least(1).and_return([in_project, excluded_path])
            expect(NPlusOneQuery).to receive(:conditions_met?).and_return(true)
            expect(NPlusOneQuery).to receive(:create_notification)
            NPlusOneQuery.call_association(@post, :association)
          end

          it 'should not create notification when stacktrace contains methods that are in the exclude list' do
            method = NPlusOneQuery.method(:excluded_stacktrace_path?).source_location
            Bullet.stacktrace_excludes = [method]
            in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
            excluded_path = OpenStruct.new(absolute_path: method.first, lineno: method.last)

            expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, excluded_path])
            expect(NPlusOneQuery).to_not receive(:create_notification)
            NPlusOneQuery.call_association(@post, :association)
          end
        end
      end

      context '.add_possible_objects' do
        it 'should add possible objects' do
          NPlusOneQuery.add_possible_objects([@post, @post2])
          expect(NPlusOneQuery.possible_objects).to be_include(@post.bullet_key)
          expect(NPlusOneQuery.possible_objects).to be_include(@post2.bullet_key)
        end

        it 'should not raise error if object is nil' do
          expect { NPlusOneQuery.add_possible_objects(nil) }
            .not_to raise_error
        end
      end

      context '.add_impossible_object' do
        it 'should add impossible object' do
          NPlusOneQuery.add_impossible_object(@post)
          expect(NPlusOneQuery.impossible_objects).to be_include(@post.bullet_key)
        end
      end
    end
  end
end
