# frozen_string_literal: true

require "spec_helper"

# For AR 5 and greater, the association reflection's cache needs be cleared
# because we're stubbing the related constants.
class << PgSearch::Document
  if ActiveRecord::VERSION::MAJOR >= 5
    def clear_searchable_cache
      reflect_on_association(:searchable).clear_association_scope_cache
    end
  else
    def clear_searchable_cache
    end
  end
end

describe PgSearch do
  describe ".multisearch" do
    with_table "pg_search_documents", {}, &DOCUMENTS_SCHEMA

    describe "delegation to PgSearch::Document.search" do
      subject { PgSearch.multisearch(query) }

      let(:query) { double(:query) }
      let(:relation) { double(:relation) }
      before do
        expect(PgSearch::Document).to receive(:search).with(query).and_return(relation)
      end

      it { is_expected.to eq(relation) }
    end

    context "with PgSearch.multisearch_options set to a Hash" do
      before { allow(PgSearch).to receive(:multisearch_options).and_return(using: :dmetaphone) }
      subject do
        PgSearch::Document.clear_searchable_cache
        PgSearch.multisearch(query).map(&:searchable)
      end

      with_model :MultisearchableModel do
        table do |t|
          t.string :title
        end
        model do
          include PgSearch
          multisearchable against: :title
        end
      end

      let!(:soundalike_record) { MultisearchableModel.create!(title: 'foning') }
      let(:query) { "Phoning" }
      it { is_expected.to include(soundalike_record) }
    end

    context "with PgSearch.multisearch_options set to a Proc" do
      subject do
        PgSearch::Document.clear_searchable_cache
        PgSearch.multisearch(query, soundalike).map(&:searchable)
      end

      before do
        allow(PgSearch).to receive(:multisearch_options) do
          lambda do |query, soundalike|
            if soundalike
              { using: :dmetaphone, query: query }
            else
              { query: query }
            end
          end
        end
      end

      with_model :MultisearchableModel do
        table do |t|
          t.string :title
        end
        model do
          include PgSearch
          multisearchable against: :title
        end
      end

      let!(:soundalike_record) { MultisearchableModel.create!(title: 'foning') }
      let(:query) { "Phoning" }

      context "with soundalike true" do
        let(:soundalike) { true }
        it { is_expected.to include(soundalike_record) }
      end

      context "with soundalike false" do
        let(:soundalike) { false }
        it { is_expected.not_to include(soundalike_record) }
      end
    end

    context "on an STI subclass" do
      context "with standard type column" do
        with_model :SuperclassModel do
          table do |t|
            t.text 'content'
            t.string 'type'
          end
        end

        before do
          searchable_subclass_model = Class.new(SuperclassModel) do
            include PgSearch
            multisearchable against: :content
          end
          stub_const("SearchableSubclassModel", searchable_subclass_model)
          stub_const("AnotherSearchableSubclassModel", searchable_subclass_model)
          stub_const("NonSearchableSubclassModel", Class.new(SuperclassModel))
        end

        it "returns only results for that subclass" do
          included = SearchableSubclassModel.create!(content: "foo bar")

          SearchableSubclassModel.create!(content: "baz")
          SuperclassModel.create!(content: "foo bar")
          SuperclassModel.create!(content: "baz")
          NonSearchableSubclassModel.create!(content: "foo bar")
          NonSearchableSubclassModel.create!(content: "baz")

          expect(SuperclassModel.count).to be 6
          expect(SearchableSubclassModel.count).to be 2

          expect(PgSearch::Document.count).to be 2

          results = PgSearch.multisearch("foo bar")

          expect(results).to eq [included.pg_search_document]
        end

        it "updates an existing STI model does not create a new pg_search document" do
          model = SearchableSubclassModel.create!(content: "foo bar")
          expect(SearchableSubclassModel.count).to eq(1)
          # We fetch the model from the database again otherwise
          # the pg_search_document from the cache is used.
          model = SearchableSubclassModel.find(model.id)
          model.content = "foo"
          model.save!
          results = PgSearch.multisearch("foo")
          expect(results.size).to eq(SearchableSubclassModel.count)
        end

        it "reindexing works" do
          NonSearchableSubclassModel.create!(content: "foo bar")
          NonSearchableSubclassModel.create!(content: "baz")
          expected = SearchableSubclassModel.create!(content: "baz")
          SuperclassModel.create!(content: "foo bar")
          SuperclassModel.create!(content: "baz")
          SuperclassModel.create!(content: "baz2")

          expect(SuperclassModel.count).to be 6
          expect(NonSearchableSubclassModel.count).to be 2
          expect(SearchableSubclassModel.count).to be 1

          expect(PgSearch::Document.count).to be 1

          PgSearch::Multisearch.rebuild(SearchableSubclassModel)

          PgSearch::Document.clear_searchable_cache
          expect(PgSearch::Document.count).to be 1
          expect(PgSearch::Document.first.searchable.class).to be SearchableSubclassModel
          expect(PgSearch::Document.first.searchable).to eq expected
        end

        it "reindexing searchable STI doesn't clobber other related STI models" do
          SearchableSubclassModel.create!(content: "baz")
          AnotherSearchableSubclassModel.create!(content: "baz")

          expect(PgSearch::Document.count).to be 2
          PgSearch::Multisearch.rebuild(SearchableSubclassModel)
          expect(PgSearch::Document.count).to be 2

          PgSearch::Document.clear_searchable_cache
          classes = PgSearch::Document.all.collect { |d| d.searchable.class }
          expect(classes).to include SearchableSubclassModel
          expect(classes).to include AnotherSearchableSubclassModel
        end
      end

      context "with custom type column" do
        with_model :SuperclassModel do
          table do |t|
            t.text 'content'
            t.string 'inherit'
          end

          model do
            self.inheritance_column = 'inherit'
          end
        end

        before do
          searchable_subclass_model = Class.new(SuperclassModel) do
            include PgSearch
            multisearchable against: :content
          end
          stub_const("SearchableSubclassModel", searchable_subclass_model)
          stub_const("AnotherSearchableSubclassModel", searchable_subclass_model)
          stub_const("NonSearchableSubclassModel", Class.new(SuperclassModel))
        end

        it "returns only results for that subclass" do
          included = SearchableSubclassModel.create!(content: "foo bar")

          SearchableSubclassModel.create!(content: "baz")
          SuperclassModel.create!(content: "foo bar")
          SuperclassModel.create!(content: "baz")
          NonSearchableSubclassModel.create!(content: "foo bar")
          NonSearchableSubclassModel.create!(content: "baz")

          expect(SuperclassModel.count).to be 6
          expect(SearchableSubclassModel.count).to be 2

          expect(PgSearch::Document.count).to be 2

          results = PgSearch.multisearch("foo bar")

          expect(results).to eq [included.pg_search_document]
        end
      end
    end
  end

  describe ".disable_multisearch" do
    it "should temporarily disable multisearch" do
      @multisearch_enabled_before = PgSearch.multisearch_enabled?
      PgSearch.disable_multisearch do
        @multisearch_enabled_inside = PgSearch.multisearch_enabled?
      end
      @multisearch_enabled_after = PgSearch.multisearch_enabled?

      expect(@multisearch_enabled_before).to be(true)
      expect(@multisearch_enabled_inside).to be(false)
      expect(@multisearch_enabled_after).to be(true)
    end

    it "should reenable multisearch after an error" do
      @multisearch_enabled_before = PgSearch.multisearch_enabled?
      begin
        PgSearch.disable_multisearch do
          @multisearch_enabled_inside = PgSearch.multisearch_enabled?
          raise
        end
      rescue StandardError
      end

      @multisearch_enabled_after = PgSearch.multisearch_enabled?

      expect(@multisearch_enabled_before).to be(true)
      expect(@multisearch_enabled_inside).to be(false)
      expect(@multisearch_enabled_after).to be(true)
    end

    it "should not disable multisearch on other threads" do
      values = Queue.new
      sync = Queue.new
      Thread.new do
        values.push PgSearch.multisearch_enabled?
        sync.pop # wait
        values.push PgSearch.multisearch_enabled?
        sync.pop # wait
        values.push PgSearch.multisearch_enabled?
      end

      @multisearch_enabled_before = values.pop
      PgSearch.disable_multisearch do
        sync.push :go
        @multisearch_enabled_inside = values.pop
      end
      sync.push :go
      @multisearch_enabled_after = values.pop

      expect(@multisearch_enabled_before).to be(true)
      expect(@multisearch_enabled_inside).to be(true)
      expect(@multisearch_enabled_after).to be(true)
    end
  end
end
