# frozen_string_literal: true

require "spec_helper"

# standard:disable RSpec/NestedGroups
describe "a pg_search_scope" do
  context "when joining to another table" do
    context "without an :against" do
      with_model :AssociatedModel do
        table do |t|
          t.string "title"
        end
      end

      with_model :ModelWithoutAgainst do
        table do |t|
          t.string "title"
          t.belongs_to :another_model, index: false
        end

        model do
          include PgSearch::Model

          belongs_to :another_model, class_name: "AssociatedModel"

          pg_search_scope :with_another, associated_against: {another_model: :title}
        end
      end

      it "returns rows that match the query in the columns of the associated model only" do
        associated = AssociatedModel.create!(title: "abcdef")
        included = [
          ModelWithoutAgainst.create!(title: "abcdef", another_model: associated),
          ModelWithoutAgainst.create!(title: "ghijkl", another_model: associated)
        ]
        excluded = [
          ModelWithoutAgainst.create!(title: "abcdef")
        ]

        results = ModelWithoutAgainst.with_another("abcdef")
        expect(results.map(&:title)).to match_array(included.map(&:title))
        expect(results).not_to include(excluded)
      end
    end

    context "via a belongs_to association" do
      with_model :AssociatedModel do
        table do |t|
          t.string "title"
        end
      end

      with_model :ModelWithBelongsTo do
        table do |t|
          t.string "title"
          t.belongs_to "another_model", index: false
        end

        model do
          include PgSearch::Model

          belongs_to :another_model, class_name: "AssociatedModel"

          pg_search_scope :with_associated, against: :title, associated_against: {another_model: :title}
        end
      end

      it "returns rows that match the query in either its own columns or the columns of the associated model" do
        associated = AssociatedModel.create!(title: "abcdef")
        included = [
          ModelWithBelongsTo.create!(title: "ghijkl", another_model: associated),
          ModelWithBelongsTo.create!(title: "abcdef")
        ]
        excluded = ModelWithBelongsTo.create!(title: "mnopqr",
          another_model: AssociatedModel.create!(title: "stuvwx"))

        results = ModelWithBelongsTo.with_associated("abcdef")
        expect(results.map(&:title)).to match_array(included.map(&:title))
        expect(results).not_to include(excluded)
      end
    end

    context "via a has_many association" do
      with_model :AssociatedModelWithHasMany do
        table do |t|
          t.string "title"
          t.belongs_to "ModelWithHasMany", index: false
        end
      end

      with_model :ModelWithHasMany do
        table do |t|
          t.string "title"
        end

        model do
          include PgSearch::Model

          has_many :other_models, class_name: "AssociatedModelWithHasMany", foreign_key: "ModelWithHasMany_id"

          pg_search_scope :with_associated, against: [:title], associated_against: {other_models: :title}
        end
      end

      it "returns rows that match the query in either its own columns or the columns of the associated model" do
        included = [
          ModelWithHasMany.create!(title: "abcdef", other_models: [
            AssociatedModelWithHasMany.create!(title: "foo"),
            AssociatedModelWithHasMany.create!(title: "bar")
          ]),
          ModelWithHasMany.create!(title: "ghijkl", other_models: [
            AssociatedModelWithHasMany.create!(title: "foo bar"),
            AssociatedModelWithHasMany.create!(title: "mnopqr")
          ]),
          ModelWithHasMany.create!(title: "foo bar")
        ]
        excluded = ModelWithHasMany.create!(title: "stuvwx", other_models: [
          AssociatedModelWithHasMany.create!(title: "abcdef")
        ])

        results = ModelWithHasMany.with_associated("foo bar")
        expect(results.map(&:title)).to match_array(included.map(&:title))
        expect(results).not_to include(excluded)
      end

      it "uses an unscoped relation of the associated model" do
        excluded = ModelWithHasMany.create!(title: "abcdef", other_models: [
          AssociatedModelWithHasMany.create!(title: "abcdef")
        ])

        included = [
          ModelWithHasMany.create!(title: "abcdef", other_models: [
            AssociatedModelWithHasMany.create!(title: "foo"),
            AssociatedModelWithHasMany.create!(title: "bar")
          ])
        ]

        results = ModelWithHasMany
          .limit(1)
          .order(Arel.sql("#{ModelWithHasMany.quoted_table_name}.id ASC"))
          .with_associated("foo bar")

        expect(results.map(&:title)).to match_array(included.map(&:title))
        expect(results).not_to include(excluded)
      end
    end

    context "when across multiple associations" do
      context "when on different tables" do
        with_model :FirstAssociatedModel do
          table do |t|
            t.string "title"
            t.belongs_to "ModelWithManyAssociations", index: false
          end
        end

        with_model :SecondAssociatedModel do
          table do |t|
            t.string "title"
          end
        end

        with_model :ModelWithManyAssociations do
          table do |t|
            t.string "title"
            t.belongs_to "model_of_second_type", index: false
          end

          model do
            include PgSearch::Model

            has_many :models_of_first_type,
              class_name: "FirstAssociatedModel",
              foreign_key: "ModelWithManyAssociations_id"

            belongs_to :model_of_second_type,
              class_name: "SecondAssociatedModel"

            pg_search_scope :with_associated,
              against: :title,
              associated_against: {models_of_first_type: :title, model_of_second_type: :title}
          end
        end

        it "returns rows that match the query in either its own columns or the columns of the associated model" do
          matching_second = SecondAssociatedModel.create!(title: "foo bar")
          unmatching_second = SecondAssociatedModel.create!(title: "uiop")

          included = [
            ModelWithManyAssociations.create!(title: "abcdef", models_of_first_type: [
              FirstAssociatedModel.create!(title: "foo"),
              FirstAssociatedModel.create!(title: "bar")
            ]),
            ModelWithManyAssociations.create!(title: "ghijkl", models_of_first_type: [
              FirstAssociatedModel.create!(title: "foo bar"),
              FirstAssociatedModel.create!(title: "mnopqr")
            ]),
            ModelWithManyAssociations.create!(title: "foo bar"),
            ModelWithManyAssociations.create!(title: "qwerty", model_of_second_type: matching_second)
          ]
          excluded = [
            ModelWithManyAssociations.create!(title: "stuvwx", models_of_first_type: [
              FirstAssociatedModel.create!(title: "abcdef")
            ]),
            ModelWithManyAssociations.create!(title: "qwerty", model_of_second_type: unmatching_second)
          ]

          results = ModelWithManyAssociations.with_associated("foo bar")
          expect(results.map(&:title)).to match_array(included.map(&:title))
          excluded.each { |object| expect(results).not_to include(object) }
        end
      end

      context "when on the same table" do
        with_model :DoublyAssociatedModel do
          table do |t|
            t.string "title"
            t.belongs_to "ModelWithDoubleAssociation", index: false
            t.belongs_to "ModelWithDoubleAssociation_again", index: false
          end
        end

        with_model :ModelWithDoubleAssociation do
          table do |t|
            t.string "title"
          end

          model do
            include PgSearch::Model

            has_many :things,
              class_name: "DoublyAssociatedModel",
              foreign_key: "ModelWithDoubleAssociation_id"

            has_many :thingamabobs,
              class_name: "DoublyAssociatedModel",
              foreign_key: "ModelWithDoubleAssociation_again_id"

            pg_search_scope :with_associated, against: :title,
              associated_against: {things: :title, thingamabobs: :title}
          end
        end

        it "returns rows that match the query in either its own columns or the columns of the associated model" do
          included = [
            ModelWithDoubleAssociation.create!(title: "abcdef", things: [
              DoublyAssociatedModel.create!(title: "foo"),
              DoublyAssociatedModel.create!(title: "bar")
            ]),
            ModelWithDoubleAssociation.create!(title: "ghijkl", things: [
              DoublyAssociatedModel.create!(title: "foo bar"),
              DoublyAssociatedModel.create!(title: "mnopqr")
            ]),
            ModelWithDoubleAssociation.create!(title: "foo bar"),
            ModelWithDoubleAssociation.create!(title: "qwerty", thingamabobs: [
              DoublyAssociatedModel.create!(title: "foo bar")
            ])
          ]
          excluded = [
            ModelWithDoubleAssociation.create!(title: "stuvwx", things: [
              DoublyAssociatedModel.create!(title: "abcdef")
            ]),
            ModelWithDoubleAssociation.create!(title: "qwerty", thingamabobs: [
              DoublyAssociatedModel.create!(title: "uiop")
            ])
          ]

          results = ModelWithDoubleAssociation.with_associated("foo bar")
          expect(results.map(&:title)).to match_array(included.map(&:title))
          excluded.each { |object| expect(results).not_to include(object) }
        end
      end
    end

    context "when against multiple attributes on one association" do
      with_model :AssociatedModel do
        table do |t|
          t.string "title"
          t.text "author"
        end
      end

      with_model :ModelWithAssociation do
        table do |t|
          t.belongs_to "another_model", index: false
        end

        model do
          include PgSearch::Model

          belongs_to :another_model, class_name: "AssociatedModel"

          pg_search_scope :with_associated, associated_against: {another_model: %i[title author]}
        end
      end

      it "joins only once" do
        included = [
          ModelWithAssociation.create!(
            another_model: AssociatedModel.create!(
              title: "foo",
              author: "bar"
            )
          ),
          ModelWithAssociation.create!(
            another_model: AssociatedModel.create!(
              title: "foo bar",
              author: "baz"
            )
          )
        ]
        excluded = [
          ModelWithAssociation.create!(
            another_model: AssociatedModel.create!(
              title: "foo",
              author: "baz"
            )
          )
        ]

        results = ModelWithAssociation.with_associated("foo bar")

        expect(results.to_sql.scan("INNER JOIN #{AssociatedModel.quoted_table_name}").length).to eq(1)
        included.each { |object| expect(results).to include(object) }
        excluded.each { |object| expect(results).not_to include(object) }
      end
    end

    context "when against non-text columns" do
      with_model :AssociatedModel do
        table do |t|
          t.integer "number"
        end
      end

      with_model :Model do
        table do |t|
          t.integer "number"
          t.belongs_to "another_model", index: false
        end

        model do
          include PgSearch::Model

          belongs_to :another_model, class_name: "AssociatedModel"

          pg_search_scope :with_associated, associated_against: {another_model: :number}
        end
      end

      it "casts the columns to text" do
        associated = AssociatedModel.create!(number: 123)
        included = [
          Model.create!(number: 123, another_model: associated),
          Model.create!(number: 456, another_model: associated)
        ]
        excluded = [
          Model.create!(number: 123)
        ]

        results = Model.with_associated("123")
        expect(results.map(&:number)).to match_array(included.map(&:number))
        expect(results).not_to include(excluded)
      end
    end

    context "when including the associated model" do
      with_model :Parent do
        table do |t|
          t.text :name
        end

        model do
          has_many :children
          include PgSearch::Model

          pg_search_scope :search_name, against: :name
        end
      end

      with_model :Child do
        table do |t|
          t.belongs_to :parent
        end

        model do
          belongs_to :parent
        end
      end

      # https://github.com/Casecommons/pg_search/issues/14
      it "supports queries with periods" do
        included = Parent.create!(name: "bar.foo")
        excluded = Parent.create!(name: "foo.bar")

        results = Parent.search_name("bar.foo").includes(:children)
        results.to_a

        expect(results).to include(included)
        expect(results).not_to include(excluded)
      end
    end
  end

  context "when merging a pg_search_scope into another model's scope" do
    with_model :ModelWithAssociation do
      model do
        has_many :associated_models
      end
    end

    with_model :AssociatedModel do
      table do |t|
        t.string :content
        t.belongs_to :model_with_association, index: false
      end

      model do
        include PgSearch::Model

        belongs_to :model_with_association

        pg_search_scope :search_content, against: :content
      end
    end

    it "finds records of the other model" do
      included_associated_1 = AssociatedModel.create!(content: "foo bar")
      included_associated_2 = AssociatedModel.create!(content: "foo baz")
      excluded_associated_1 = AssociatedModel.create!(content: "baz quux")
      excluded_associated_2 = AssociatedModel.create!(content: "baz bar")

      included = [
        ModelWithAssociation.create(associated_models: [included_associated_1]),
        ModelWithAssociation.create(associated_models: [included_associated_2, excluded_associated_1])
      ]

      excluded = [
        ModelWithAssociation.create(associated_models: [excluded_associated_2]),
        ModelWithAssociation.create(associated_models: [])
      ]

      relation = AssociatedModel.search_content("foo")

      results = ModelWithAssociation.joins(:associated_models).merge(relation)

      expect(results).to include(*included)
      expect(results).not_to include(*excluded)
    end
  end

  context "when chained onto a has_many association" do
    with_model :Company do
      model do
        has_many :positions
      end
    end

    with_model :Position do
      table do |t|
        t.string :title
        t.belongs_to :company
      end

      model do
        include PgSearch::Model

        pg_search_scope :search, against: :title, using: %i[tsearch trigram]
      end
    end

    # https://github.com/Casecommons/pg_search/issues/106
    it "handles numbers in a trigram query properly" do
      company = Company.create!
      another_company = Company.create!

      included = [
        Position.create!(company_id: company.id, title: "teller 1"),
        Position.create!(company_id: company.id, title: "teller 2") # close enough
      ]

      excluded = [
        Position.create!(company_id: nil, title: "teller 1"),
        Position.create!(company_id: another_company.id, title: "teller 1"),
        Position.create!(company_id: company.id, title: "penn 1")
      ]

      results = company.positions.search("teller 1")

      expect(results).to include(*included)
      expect(results).not_to include(*excluded)
    end
  end
end
# standard:enable RSpec/NestedGroups
