# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Rails::InverseOf, :config do
  context 'with scope' do
    it 'registers an offense when not specifying `:inverse_of`' do
      expect_offense(<<~RUBY)
        class Person
          has_one :foo, -> () { where(bar: true) }
          ^^^^^^^ Specify an `:inverse_of` option.
        end
      RUBY
    end

    it 'does not register an offense when specifying `inverse_of: false`' do
      expect_no_offenses('has_many :foo, -> () { where(bar: true) }, inverse_of: false')
    end

    it 'registers an offense when specifying `inverse_of: nil`' do
      expect_offense(<<~RUBY)
        class Person
          has_many :foo, -> () { where(bar: true) }, inverse_of: nil
          ^^^^^^^^ You specified `inverse_of: nil`, you probably meant to use `inverse_of: false`.
        end
      RUBY
    end

    it 'registers an offense when specifying `inverse_of: nil` with dynamic options' do
      expect_offense(<<~RUBY)
        class Person
          def define_association(**options)
            has_many :foo, -> () { where(bar: true) }, inverse_of: nil, **options
            ^^^^^^^^ You specified `inverse_of: nil`, you probably meant to use `inverse_of: false`.
          end
        end
      RUBY
    end

    context 'when `IgnoreScopes: true`' do
      let(:cop_config) do
        { 'IgnoreScopes' => true }
      end

      it 'does not register an offense when not specifying `:inverse_of`' do
        expect_no_offenses('has_many :foo, -> () { where(bar: true) }')
      end
    end
  end

  context 'with option preventing automatic inverse' do
    it 'registers an offense when not specifying `:inverse_of`' do
      expect_offense(<<~RUBY)
        class Person
          belongs_to :foo, foreign_key: 'foo_id'
          ^^^^^^^^^^ Specify an `:inverse_of` option.
        end
      RUBY
    end

    it 'does not register an offense when specifying `:inverse_of`' do
      expect_no_offenses("has_one :foo, foreign_key: 'foo_id', inverse_of: :bar")
    end

    it 'does not register an offense with `:inverse_of` as first option' do
      expect_no_offenses("has_one :foo, inverse_of: :bar, foreign_key: 'foo_id'")
    end

    it 'registers an offense with other option and `:inverse_of` unset' do
      expect_offense(<<~RUBY)
        class Person
          has_many :foo, dependent: :destroy, foreign_key: 'foo_id'
          ^^^^^^^^ Specify an `:inverse_of` option.
        end
      RUBY
    end

    it 'registers an offense when including `conditions` option' do
      expect_offense(<<~RUBY)
        class Person
          has_many :foo, conditions: -> { where(bar: true) }
          ^^^^^^^^ Specify an `:inverse_of` option.
        end
      RUBY
    end

    it 'does not register an offense with dynamic options' do
      expect_no_offenses(<<~RUBY)
        class Person
          def define_association(**options)
            has_many :foo, conditions: -> { where(bar: true) }, **options
          end
        end
      RUBY
    end
  end

  context 'with scope and options' do
    it 'registers an offense when not specifying `:inverse_of`' do
      expect_offense(<<~RUBY)
        class Person
          has_many :foo, -> { group 'x' }, dependent: :destroy
          ^^^^^^^^ Specify an `:inverse_of` option.
        end
      RUBY
    end

    it 'does not register an offense with dynamic options' do
      expect_no_offenses(<<~RUBY)
        class Person
          def define_association(**options)
            has_many(:foo, -> { group 'x' }, dependent: :destroy, **options)
          end
        end
      RUBY
    end

    it 'does not register an offense when specifying `:inverse_of`' do
      expect_no_offenses("has_many :foo, -> { group 'x' }, dependent: :destroy, inverse_of: :baz")
    end
  end

  context '`:as` option' do
    context 'Rails <= 5.1', :rails51 do
      it 'registers an offense when not specifying `:inverse_of`' do
        expect_offense(<<~RUBY)
          class Person
            has_many :pictures, as: :imageable
            ^^^^^^^^ Specify an `:inverse_of` option.
          end
        RUBY
      end
    end

    context 'Rails >= 5.2', :rails52 do
      it 'does not register an offense when not specifying `:inverse_of`' do
        expect_no_offenses('has_many :pictures, as: :imageable')
      end
    end
  end

  context 'with no options' do
    it 'does not register an offense' do
      expect_no_offenses('has_one :foo')
    end
  end

  context 'with other options' do
    it 'does not register an offense' do
      expect_no_offenses('has_one :foo, dependent: :nullify')
    end
  end

  context 'with option ignoring `:inverse_of`' do
    it 'does not register an offense when including `through` option' do
      expect_no_offenses(<<~RUBY)
        class Physician < ApplicationRecord
          has_many :appointments
          has_many :patients, -> () { where(bar: true) }, through: :appointments
        end
      RUBY
    end

    it 'does not register an offense when including `polymorphic` option' do
      expect_no_offenses(<<~RUBY)
        class Picture < ApplicationRecord
          belongs_to :imageable, -> () { where(bar: true) }, polymorphic: true
        end
      RUBY
    end
  end

  context 'with valid options in `with_options`' do
    it 'does not register an offense' do
      expect_no_offenses(<<~RUBY)
        class Person
          with_options inverse_of: false do
            has_one :foo, -> () { where(bar: true) }
          end
        end
      RUBY
    end

    it 'does not register an offense when using the explicit receiver' do
      expect_no_offenses(<<~RUBY)
        class Person
          with_options inverse_of: :bar do |assoc|
            assoc.belongs_to :foo, foreign_key: 'foo_id'
          end
        end
      RUBY
    end

    it 'registers an offense when using the invalid explicit receiver' do
      expect_offense(<<~RUBY)
        class Person
          with_options inverse_of: :bar do |_assoc|
            belongs_to :foo, -> () { where(baz: true) }
            ^^^^^^^^^^ Specify an `:inverse_of` option.
          end
        end
      RUBY
    end

    it 'does not register an offense when using multiple blocks' do
      expect_no_offenses(<<~RUBY)
        class Book < ApplicationRecord
          with_options inverse_of: :book do
            with_helper do |helper|
              helper.define_assoc
              with_options foreign_key: 'book_id' do
                belongs_to :author
              end
            end
          end
        end
      RUBY
    end
  end

  context 'with invalid options in `with_options`' do
    it 'registers an offense' do
      expect_offense(<<~RUBY)
        class Person
          with_options foreign_key: 'foo_id' do
            has_one :foo
            ^^^^^^^ Specify an `:inverse_of` option.
          end
        end
      RUBY
    end

    it 'registers an offense when using the explicit receiver' do
      expect_offense(<<~RUBY)
        class Person
          with_options foreign_key: 'foo_id' do |assoc|
            assoc.belongs_to :foo, -> () { where(baz: true) }
                  ^^^^^^^^^^ Specify an `:inverse_of` option.
          end
        end
      RUBY
    end

    it 'registers an offense when using multiple blocks' do
      expect_offense(<<~RUBY)
        class Book < ApplicationRecord
          with_options conditions: -> () { where(famous: true) } do
            with_helper do |helper|
              helper.define_assoc
              with_options foreign_key: "patron_id" do
                belongs_to :author
                ^^^^^^^^^^ Specify an `:inverse_of` option.
              end
            end
          end
        end
      RUBY
    end
  end
end
