
require 'spec_helper'

describe Mongoid::FullTextSearch do
  context 'with several config options defined' do
    let!(:abcdef) { AdvancedArtwork.create(title: 'abcdefg hijklmn') }
    let!(:cesar) { AccentlessArtwork.create(title: "C\u00e9sar Galicia") }
    let!(:julio) { AccentlessArtwork.create(title: 'Julio Cesar Morales') }

    it 'should recognize all options' do
      # AdvancedArtwork is defined with an ngram_width of 4 and a different alphabet (abcdefg)
      expect(AdvancedArtwork.fulltext_search('abc')).to eq([])
      expect(AdvancedArtwork.fulltext_search('abcd').first).to eq(abcdef)
      expect(AdvancedArtwork.fulltext_search('defg').first).to eq(abcdef)
      expect(AdvancedArtwork.fulltext_search('hijklmn')).to eq([])
      # AccentlessArtwork is just like BasicArtwork, except that we set :remove_accents to false,
      # so this behaves like the ``old'' version of fulltext_search
      expect(AccentlessArtwork.fulltext_search('cesar').first).to eq(julio)
    end
  end

  context 'with default settings' do
    let!(:flower_myth) { BasicArtwork.create(title: 'Flower Myth') }
    let!(:flowers)     { BasicArtwork.create(title: 'Flowers') }
    let!(:lowered)     { BasicArtwork.create(title: 'Lowered') }
    let!(:cookies)     { BasicArtwork.create(title: 'Cookies') }
    let!(:empty)       { BasicArtwork.create(title: '') }
    let!(:cesar)       { BasicArtwork.create(title: "C\u00e9sar Galicia") }
    let!(:julio)       { BasicArtwork.create(title: 'Julio Cesar Morales') }
    let!(:csar)        { BasicArtwork.create(title: 'Csar') }
    let!(:percent)     { BasicArtwork.create(title: 'Untitled (cal%desert)') }

    it 'returns empty for empties' do
      expect(BasicArtwork.fulltext_search(nil, max_results: 1)).to eq([])
      expect(BasicArtwork.fulltext_search('', max_results: 1)).to eq([])
    end

    it 'finds percents' do
      expect(BasicArtwork.fulltext_search('cal%desert'.force_encoding('ASCII-8BIT'), max_results: 1).first).to eq(percent)
      expect(BasicArtwork.fulltext_search('cal%desert'.force_encoding('UTF-8'), max_results: 1).first).to eq(percent)
    end

    it 'forgets accents' do
      expect(BasicArtwork.fulltext_search('cesar', max_results: 1).first).to eq(cesar)
      expect(BasicArtwork.fulltext_search('cesar g', max_results: 1).first).to eq(cesar)
      expect(BasicArtwork.fulltext_search("C\u00e9sar", max_results: 1).first).to eq(cesar)
      expect(BasicArtwork.fulltext_search("C\303\251sar".force_encoding('UTF-8'), max_results: 1).first).to eq(cesar)
      expect(BasicArtwork.fulltext_search(CGI.unescape('c%C3%A9sar'), max_results: 1).first).to eq(cesar)
      expect(BasicArtwork.fulltext_search(CGI.unescape('c%C3%A9sar'.encode('ASCII-8BIT')), max_results: 1).first).to eq(cesar)
    end

    it 'returns exact matches' do
      expect(BasicArtwork.fulltext_search('Flower Myth', max_results: 1).first).to eq(flower_myth)
      expect(BasicArtwork.fulltext_search('Flowers', max_results: 1).first).to eq(flowers)
      expect(BasicArtwork.fulltext_search('Cookies', max_results: 1).first).to eq(cookies)
      expect(BasicArtwork.fulltext_search('Lowered', max_results: 1).first).to eq(lowered)
    end

    it 'returns exact matches regardless of case' do
      expect(BasicArtwork.fulltext_search('fLOWER mYTH', max_results: 1).first).to eq(flower_myth)
      expect(BasicArtwork.fulltext_search('FLOWERS', max_results: 1).first).to eq(flowers)
      expect(BasicArtwork.fulltext_search('cOOkies', max_results: 1).first).to eq(cookies)
      expect(BasicArtwork.fulltext_search('lOWERED', max_results: 1).first).to eq(lowered)
    end

    it 'returns all relevant results, sorted by relevance' do
      expect(BasicArtwork.fulltext_search('Flowers')).to eq([flowers, flower_myth, lowered])
    end

    it 'prefers prefix matches' do
      expect([flowers, flower_myth]).to include(BasicArtwork.fulltext_search('Floweockies').first)
      expect(BasicArtwork.fulltext_search('Lowers').first).to eq(lowered)
      expect(BasicArtwork.fulltext_search('Cookilowers').first).to eq(cookies)
    end

    it 'returns an empty result set for an empty query' do
      expect(BasicArtwork.fulltext_search('').empty?).to be_truthy
    end

    it "returns an empty result set for a query that doesn't contain any characters in the alphabet" do
      expect(BasicArtwork.fulltext_search('_+=--@!##%#$%%').empty?).to be_truthy
    end

    it 'returns results for a query that contains only a single ngram' do
      expect(BasicArtwork.fulltext_search('coo').first).to eq(cookies)
      expect(BasicArtwork.fulltext_search('c!!!oo').first).to eq(cookies)
    end
  end

  context 'with default settings' do
    let!(:flower_myth) { Gallery::BasicArtwork.create(title: 'Flower Myth') }
    let!(:flowers)     { Gallery::BasicArtwork.create(title: 'Flowers') }
    let!(:lowered)     { Gallery::BasicArtwork.create(title: 'Lowered') }
    let!(:cookies)     { Gallery::BasicArtwork.create(title: 'Cookies') }
    let!(:empty)       { Gallery::BasicArtwork.create(title: '') }

    it 'returns exact matches for model within a module' do
      expect(Gallery::BasicArtwork.fulltext_search('Flower Myth', max_results: 1).first).to eq(flower_myth)
      expect(Gallery::BasicArtwork.fulltext_search('Flowers', max_results: 1).first).to eq(flowers)
      expect(Gallery::BasicArtwork.fulltext_search('Cookies', max_results: 1).first).to eq(cookies)
      expect(Gallery::BasicArtwork.fulltext_search('Lowered', max_results: 1).first).to eq(lowered)
    end
  end

  context 'with default settings' do
    let!(:yellow)             { BasicArtwork.create(title: 'Yellow') }
    let!(:yellow_leaves_2)    { BasicArtwork.create(title: 'Yellow Leaves 2') }
    let!(:yellow_leaves_3)    { BasicArtwork.create(title: 'Yellow Leaves 3') }
    let!(:yellow_leaves_20)   { BasicArtwork.create(title: 'Yellow Leaves 20') }
    let!(:yellow_cup)         { BasicArtwork.create(title: 'Yellow Cup') }

    it 'prefers the best prefix that matches a given string' do
      expect(BasicArtwork.fulltext_search('yellow').first).to eq(yellow)
      expect(BasicArtwork.fulltext_search('yellow leaves', max_results: 3).sort_by(&:title)).to eq( \
        [yellow_leaves_2, yellow_leaves_3, yellow_leaves_20].sort_by(&:title)
      )
      expect(BasicArtwork.fulltext_search('yellow cup').first).to eq(yellow_cup)
    end
  end

  context 'with default settings' do
    let!(:monet) { BasicArtwork.create(title: 'claude monet') }
    let!(:one_month_weather_permitting) { BasicArtwork.create(title: 'one month weather permitting monday') }

    it 'finds better matches within exact strings' do
      expect(BasicArtwork.fulltext_search('monet').first).to eq(monet)
    end
  end

  context 'with default settings' do
    let!(:abc)       { BasicArtwork.create(title: 'abc') }
    let!(:abcd)      { BasicArtwork.create(title: 'abcd') }
    let!(:abcde)     { BasicArtwork.create(title: 'abcde') }
    let!(:abcdef)    { BasicArtwork.create(title: 'abcdef') }
    let!(:abcdefg)   { BasicArtwork.create(title: 'abcdefg') }
    let!(:abcdefgh)  { BasicArtwork.create(title: 'abcdefgh') }

    it 'returns exact matches from a list of similar prefixes' do
      expect(BasicArtwork.fulltext_search('abc').first).to eq(abc)
      expect(BasicArtwork.fulltext_search('abcd').first).to eq(abcd)
      expect(BasicArtwork.fulltext_search('abcde').first).to eq(abcde)
      expect(BasicArtwork.fulltext_search('abcdef').first).to eq(abcdef)
      expect(BasicArtwork.fulltext_search('abcdefg').first).to eq(abcdefg)
      expect(BasicArtwork.fulltext_search('abcdefgh').first).to eq(abcdefgh)
    end
  end

  context 'with an index name specified' do
    let!(:pablo_picasso)       { ExternalArtist.create(full_name: 'Pablo Picasso') }
    let!(:portrait_of_picasso) { ExternalArtwork.create(title: 'Portrait of Picasso') }
    let!(:andy_warhol)         { ExternalArtist.create(full_name: 'Andy Warhol') }
    let!(:warhol)              { ExternalArtwork.create(title: 'Warhol') }
    let!(:empty)               { ExternalArtwork.create(title: '') }

    it 'returns results of different types from the same query' do
      results = ExternalArtwork.fulltext_search('picasso', max_results: 2).map { |result| result }
      expect(results.member?(portrait_of_picasso)).to be_truthy
      expect(results.member?(pablo_picasso)).to be_truthy
      results = ExternalArtist.fulltext_search('picasso', max_results: 2).map { |result| result }
      expect(results.member?(portrait_of_picasso)).to be_truthy
      expect(results.member?(pablo_picasso)).to be_truthy
    end

    it 'returns exact matches' do
      expect(ExternalArtwork.fulltext_search('Pablo Picasso', max_results: 1).first).to eq(pablo_picasso)
      expect(ExternalArtwork.fulltext_search('Portrait of Picasso', max_results: 1).first).to eq(portrait_of_picasso)
      expect(ExternalArtwork.fulltext_search('Andy Warhol', max_results: 1).first).to eq(andy_warhol)
      expect(ExternalArtwork.fulltext_search('Warhol', max_results: 1).first).to eq(warhol)
      expect(ExternalArtist.fulltext_search('Pablo Picasso', max_results: 1).first).to eq(pablo_picasso)
      expect(ExternalArtist.fulltext_search('Portrait of Picasso', max_results: 1).first).to eq(portrait_of_picasso)
      expect(ExternalArtist.fulltext_search('Andy Warhol', max_results: 1).first).to eq(andy_warhol)
      expect(ExternalArtist.fulltext_search('Warhol', max_results: 1).first).to eq(warhol)
    end

    it 'returns exact matches regardless of case' do
      expect(ExternalArtwork.fulltext_search('pABLO pICASSO', max_results: 1).first).to eq(pablo_picasso)
      expect(ExternalArtist.fulltext_search('PORTRAIT OF PICASSO', max_results: 1).first).to eq(portrait_of_picasso)
      expect(ExternalArtwork.fulltext_search('andy warhol', max_results: 1).first).to eq(andy_warhol)
      expect(ExternalArtwork.fulltext_search('wArHoL', max_results: 1).first).to eq(warhol)
    end

    it 'returns all relevant results, sorted by relevance' do
      expect(ExternalArtist.fulltext_search('Pablo Picasso')).to eq([pablo_picasso, portrait_of_picasso])
      expect(ExternalArtwork.fulltext_search('Pablo Picasso')).to eq([pablo_picasso, portrait_of_picasso])
      expect(ExternalArtist.fulltext_search('Portrait of Picasso')).to eq([portrait_of_picasso, pablo_picasso])
      expect(ExternalArtwork.fulltext_search('Portrait of Picasso')).to eq([portrait_of_picasso, pablo_picasso])
      expect(ExternalArtist.fulltext_search('Andy Warhol')).to eq([andy_warhol, warhol])
      expect(ExternalArtwork.fulltext_search('Andy Warhol')).to eq([andy_warhol, warhol])
      expect(ExternalArtist.fulltext_search('Warhol')).to eq([warhol, andy_warhol])
      expect(ExternalArtwork.fulltext_search('Warhol')).to eq([warhol, andy_warhol])
    end

    it 'prefers prefix matches' do
      expect(ExternalArtist.fulltext_search('PabloWarhol').first).to eq(pablo_picasso)
      expect(ExternalArtist.fulltext_search('AndyPicasso').first).to eq(andy_warhol)
    end

    it 'returns an empty result set for an empty query' do
      expect(ExternalArtist.fulltext_search('').empty?).to be_truthy
    end

    it "returns an empty result set for a query that doesn't contain any characters in the alphabet" do
      expect(ExternalArtwork.fulltext_search('#$%!$#*%*').empty?).to be_truthy
    end

    it 'returns results for a query that contains only a single ngram' do
      expect(ExternalArtist.fulltext_search('and').first).to eq(andy_warhol)
    end
  end

  context 'with an index name specified' do
    let!(:andy_warhol)         { ExternalArtist.create(full_name: 'Andy Warhol') }
    let!(:warhol)              { ExternalArtwork.create(title: 'Warhol') }

    it "doesn't blow up if garbage is in the index collection" do
      expect(ExternalArtist.fulltext_search('warhol')).to eq([warhol, andy_warhol])
      index_collection = ExternalArtist.collection.database[ExternalArtist.mongoid_fulltext_config.keys.first]
      index_collection.find('document_id' => warhol.id).each do |idef|
        if Mongoid::Compatibility::Version.mongoid3?
          index_collection.find('_id' => idef['_id']).update('document_id' => Moped::BSON::ObjectId.new)
        elsif Mongoid::Compatibility::Version.mongoid4?
          index_collection.find('_id' => idef['_id']).update('document_id' => BSON::ObjectId.new)
        else
          index_collection.find('_id' => idef['_id']).update_one('document_id' => BSON::ObjectId.new)
        end
      end
      # We should no longer be able to find warhol, but that shouldn't keep it from returning results
      expect(ExternalArtist.fulltext_search('warhol')).to eq([andy_warhol])
    end
  end

  context 'with an index name specified' do
    let!(:pop)                { ExternalArtwork.create(title: 'Pop') }
    let!(:pop_culture)        { ExternalArtwork.create(title: 'Pop Culture') }
    let!(:contemporary_pop)   { ExternalArtwork.create(title: 'Contemporary Pop') }
    let!(:david_poppie)       { ExternalArtist.create(full_name: 'David Poppie') }
    let!(:kung_fu_lollipop)   { ExternalArtwork.create(title: 'Kung-Fu Lollipop') }

    it 'prefers the best prefix that matches a given string' do
      expect(ExternalArtwork.fulltext_search('pop').first).to eq(pop)
      expect(ExternalArtwork.fulltext_search('poppie').first).to eq(david_poppie)
      expect(ExternalArtwork.fulltext_search('pop cult').first).to eq(pop_culture)
      expect(ExternalArtwork.fulltext_search('pop', max_results: 5)[4]).to eq(kung_fu_lollipop)
    end
  end
  context 'with an index name specified' do
    let!(:abc)       { ExternalArtwork.create(title: 'abc') }
    let!(:abcd)      { ExternalArtwork.create(title: 'abcd') }
    let!(:abcde)     { ExternalArtwork.create(title: 'abcde') }
    let!(:abcdef)    { ExternalArtwork.create(title: 'abcdef') }
    let!(:abcdefg)   { ExternalArtwork.create(title: 'abcdefg') }
    let!(:abcdefgh)  { ExternalArtwork.create(title: 'abcdefgh') }

    it 'returns exact matches from a list of similar prefixes' do
      expect(ExternalArtwork.fulltext_search('abc').first).to eq(abc)
      expect(ExternalArtwork.fulltext_search('abcd').first).to eq(abcd)
      expect(ExternalArtwork.fulltext_search('abcde').first).to eq(abcde)
      expect(ExternalArtwork.fulltext_search('abcdef').first).to eq(abcdef)
      expect(ExternalArtwork.fulltext_search('abcdefg').first).to eq(abcdefg)
      expect(ExternalArtwork.fulltext_search('abcdefgh').first).to eq(abcdefgh)
    end
  end

  context 'with an index name specified' do
    it "cleans up item from the index after they're destroyed" do
      foobar = ExternalArtwork.create(title: 'foobar')
      barfoo = ExternalArtwork.create(title: 'barfoo')
      expect(ExternalArtwork.fulltext_search('foobar')).to eq([foobar, barfoo])
      foobar.destroy
      expect(ExternalArtwork.fulltext_search('foobar')).to eq([barfoo])
      barfoo.destroy
      expect(ExternalArtwork.fulltext_search('foobar')).to eq([])
    end
  end

  context 'with an index name specified and no fields provided to index' do
    let!(:big_bang) { ExternalArtworkNoFieldsSupplied.create(title: 'Big Bang', artist: 'David Poppie', year: '2009') }

    it 'indexes the string returned by to_s' do
      expect(ExternalArtworkNoFieldsSupplied.fulltext_search('big bang').first).to eq(big_bang)
      expect(ExternalArtworkNoFieldsSupplied.fulltext_search('poppie').first).to eq(big_bang)
      expect(ExternalArtworkNoFieldsSupplied.fulltext_search('2009').first).to eq(big_bang)
    end
  end

  context 'with multiple indexes defined' do
    let!(:pop)                { MultiExternalArtwork.create(title: 'Pop', year: '1970', artist: 'Joe Schmoe') }
    let!(:pop_culture)        { MultiExternalArtwork.create(title: 'Pop Culture', year: '1977', artist: 'Jim Schmoe') }
    let!(:contemporary_pop)   { MultiExternalArtwork.create(title: 'Contemporary Pop', year: '1800', artist: 'Bill Schmoe') }
    let!(:kung_fu_lollipop)   { MultiExternalArtwork.create(title: 'Kung-Fu Lollipop', year: '2006', artist: 'Michael Anderson') }

    it 'allows searches to hit a particular index' do
      title_results = MultiExternalArtwork.fulltext_search('pop', index: 'mongoid_fulltext.titles').sort_by(&:title)
      expect(title_results).to eq([pop, pop_culture, contemporary_pop, kung_fu_lollipop].sort_by(&:title))
      year_results = MultiExternalArtwork.fulltext_search('197', index: 'mongoid_fulltext.years').sort_by(&:title)
      expect(year_results).to eq([pop, pop_culture].sort_by(&:title))
      all_results = MultiExternalArtwork.fulltext_search('1800 and', index: 'mongoid_fulltext.all').sort_by(&:title)
      expect(all_results).to eq([contemporary_pop, kung_fu_lollipop].sort_by(&:title))
    end

    it "should raise an error if you don't specify which index to search with" do
      expect { MultiExternalArtwork.fulltext_search('foobar') }.to raise_error(Mongoid::FullTextSearch::UnspecifiedIndexError)
    end
  end

  context 'with multiple fields indexed and the same index used by multiple models' do
    let!(:andy_warhol)         { MultiFieldArtist.create(full_name: 'Andy Warhol', birth_year: '1928') }
    let!(:warhol)              { MultiFieldArtwork.create(title: 'Warhol', year: '2010') }
    let!(:pablo_picasso)       { MultiFieldArtist.create(full_name: 'Pablo Picasso', birth_year: '1881') }
    let!(:portrait_of_picasso) { MultiFieldArtwork.create(title: 'Portrait of Picasso', year: '1912') }

    it 'allows searches across all models on both fields indexed' do
      expect(MultiFieldArtist.fulltext_search('2010').first).to eq(warhol)
      expect(MultiFieldArtist.fulltext_search('andy').first).to eq(andy_warhol)
      expect(MultiFieldArtist.fulltext_search('pablo').first).to eq(pablo_picasso)
      expect(MultiFieldArtist.fulltext_search('1881').first).to eq(pablo_picasso)
      expect(MultiFieldArtist.fulltext_search('portrait 1912').first).to eq(portrait_of_picasso)

      expect(MultiFieldArtwork.fulltext_search('2010').first).to eq(warhol)
      expect(MultiFieldArtwork.fulltext_search('andy').first).to eq(andy_warhol)
      expect(MultiFieldArtwork.fulltext_search('pablo').first).to eq(pablo_picasso)
      expect(MultiFieldArtwork.fulltext_search('1881').first).to eq(pablo_picasso)
      expect(MultiFieldArtwork.fulltext_search('portrait 1912').first).to eq(portrait_of_picasso)
    end
  end
  context 'with filters applied to multiple models' do
    let!(:foobar_artwork)    { FilteredArtwork.create(title: 'foobar') }
    let!(:barfoo_artwork)    { FilteredArtwork.create(title: 'barfoo') }
    let!(:foobar_artist)     { FilteredArtist.create(full_name: 'foobar') }
    let!(:barfoo_artist)     { FilteredArtist.create(full_name: 'barfoo') }

    it 'allows filtered searches' do
      expect(FilteredArtwork.fulltext_search('foobar', is_artwork: true)).to eq([foobar_artwork, barfoo_artwork])
      expect(FilteredArtist.fulltext_search('foobar', is_artwork: true)).to eq([foobar_artwork, barfoo_artwork])

      expect(FilteredArtwork.fulltext_search('foobar', is_artwork: true, is_foobar: true)).to eq([foobar_artwork])
      expect(FilteredArtwork.fulltext_search('foobar', is_artwork: true, is_foobar: false)).to eq([barfoo_artwork])
      expect(FilteredArtwork.fulltext_search('foobar', is_artwork: false, is_foobar: true)).to eq([foobar_artist])
      expect(FilteredArtwork.fulltext_search('foobar', is_artwork: false, is_foobar: false)).to eq([barfoo_artist])

      expect(FilteredArtist.fulltext_search('foobar', is_artwork: true, is_foobar: true)).to eq([foobar_artwork])
      expect(FilteredArtist.fulltext_search('foobar', is_artwork: true, is_foobar: false)).to eq([barfoo_artwork])
      expect(FilteredArtist.fulltext_search('foobar', is_artwork: false, is_foobar: true)).to eq([foobar_artist])
      expect(FilteredArtist.fulltext_search('foobar', is_artwork: false, is_foobar: false)).to eq([barfoo_artist])
    end
  end

  context 'with different filters applied to multiple models' do
    let!(:foo_artwork)    { FilteredArtwork.create(title: 'foo') }
    let!(:bar_artist)     { FilteredArtist.create(full_name: 'bar') }
    let!(:baz_other)      { FilteredOther.create(name: 'baz') }

    # These three models are all indexed by the same mongoid_fulltext index, but have different filters
    # applied. The index created on the mongoid_fulltext collection should include the ngram and score
    # fields as well as the union of all the filter fields to allow for efficient lookups.

    it 'creates a proper index for searching efficiently' do
      # see https://github.com/mongoid/mongoid_fulltext/pull/39
      pending unless Mongoid::Compatibility::Version.mongoid5_or_newer?
      [FilteredArtwork, FilteredArtist, FilteredOther].each(&:create_indexes)
      index_collection = FilteredArtwork.collection.database['mongoid_fulltext.artworks_and_artists']
      ngram_indexes = []
      index_collection.indexes.each { |idef| ngram_indexes << idef if idef['key'].key?('ngram') }
      expect(ngram_indexes.length).to eq(1)
      keys = ngram_indexes.first['key'].keys
      expected_keys = ['ngram', 'score', 'filter_values.is_fuzzy', 'filter_values.is_awesome',
                       'filter_values.is_foobar', 'filter_values.is_artwork', 'filter_values.is_artist', 'filter_values.colors?'].sort
      expect(keys.sort).to eq(expected_keys)
    end
  end

  context 'with partitions applied to a model' do
    let!(:artist_2) { PartitionedArtist.create(full_name: 'foobar', exhibitions: ['Art Basel 2011', 'Armory NY']) }
    let!(:artist_1) { PartitionedArtist.create(full_name: 'foobar', exhibitions: ['Art Basel 2011']) }
    let!(:artist_0) { PartitionedArtist.create(full_name: 'foobar', exhibitions: []) }

    it 'allows partitioned searches' do
      artists_by_exhibition_length = [artist_0, artist_1, artist_2].sort_by { |x| x.exhibitions.length }
      expect(PartitionedArtist.fulltext_search('foobar').sort_by { |x| x.exhibitions.length }).to eq(artists_by_exhibition_length)
      expect(PartitionedArtist.fulltext_search('foobar', exhibitions: ['Armory NY'])).to eq([artist_2])
      art_basel_only = PartitionedArtist.fulltext_search('foobar', exhibitions: ['Art Basel 2011']).sort_by { |x| x.exhibitions.length }
      expect(art_basel_only).to eq([artist_1, artist_2].sort_by { |x| x.exhibitions.length })
      expect(PartitionedArtist.fulltext_search('foobar', exhibitions: ['Art Basel 2011', 'Armory NY'])).to eq([artist_2])
    end
  end

  context 'using search options' do
    let!(:patterns)    { BasicArtwork.create(title: 'Flower Patterns') }
    let!(:flowers)     { BasicArtwork.create(title: 'Flowers') }

    it 'returns max_results' do
      expect(BasicArtwork.fulltext_search('flower', max_results: 1).length).to eq(1)
    end

    it 'returns scored results' do
      results = BasicArtwork.fulltext_search('flowers', return_scores: true)
      first_result = results[0]
      expect(first_result.is_a?(Array)).to be_truthy
      expect(first_result.size).to eq(2)
      expect(first_result[0]).to eq(flowers)
      expect(first_result[1].is_a?(Float)).to be_truthy
    end
  end

  context 'with various word separators' do
    let!(:hard_edged_painting)   { BasicArtwork.create(title: 'Hard-edged painting') }
    let!(:edgy_painting)         { BasicArtwork.create(title: 'Edgy painting') }
    let!(:hard_to_find_ledge)    { BasicArtwork.create(title: 'Hard to find ledge') }

    it 'should treat dashes as word separators, giving a score boost to each dash-separated word' do
      expect(BasicArtwork.fulltext_search('hard-edged').first).to eq(hard_edged_painting)
      expect(BasicArtwork.fulltext_search('hard edge').first).to eq(hard_edged_painting)
      expect(BasicArtwork.fulltext_search('hard edged').first).to eq(hard_edged_painting)
    end
  end

  context 'returning scores' do
    # Since we return scores, let's make some weak guarantees about what they actually mean

    let!(:mao_yan)      { ExternalArtist.create(full_name: 'Mao Yan') }
    let!(:mao)          { ExternalArtwork.create(title: 'Mao by Andy Warhol') }
    let!(:maox)         { ExternalArtwork.create(title: 'Maox by Randy Morehall') }
    let!(:somao)        { ExternalArtwork.create(title: 'Somao by Randy Morehall') }

    it "returns basic matches that don't match a whole word and aren't prefixes with score < 1" do
      %w[paox porehall].each do |query|
        results = ExternalArtist.fulltext_search(query, return_scores: true)
        expect(results.length).to be > 0
        expect(results.map { |result| result[-1] }.inject(true) { |accum, item| accum &= (item < 1) }).to be_truthy
      end
    end

    it 'returns prefix matches with a score >= 1 but < 2' do
      %w[warho rand].each do |query|
        results = ExternalArtist.fulltext_search(query, return_scores: true)
        expect(results.length).to be > 0
        expect(results.map { |result| result[-1] if result[0].to_s.starts_with?(query) }.compact.inject(true) { |accum, item| accum &= (item >= 1 && item < 2) }).to be_truthy
      end
    end

    it 'returns full-word matches with a score >= 2' do
      %w[andy warhol mao].each do |query|
        results = ExternalArtist.fulltext_search(query, return_scores: true)
        expect(results.length).to be > 0
        expect(results.map { |result| result[-1] if result[0].to_s.split(' ').member?(query) }.compact.inject(true) { |accum, item| accum &= (item >= 2) }).to be_truthy
      end
    end
  end

  context 'with stop words defined' do
    let!(:flowers)      { StopwordsArtwork.create(title: 'Flowers by Andy Warhol') }
    let!(:many_ands)    { StopwordsArtwork.create(title: 'Foo and bar and baz and foobar') }
    let!(:harry)        { StopwordsArtwork.create(title: 'Harry in repose by JK Rowling') }

    it "doesn't give a full-word score boost to stopwords" do
      expect(StopwordsArtwork.fulltext_search('andy').map(&:title)).to eq([flowers.title, many_ands.title])
      expect(StopwordsArtwork.fulltext_search('warhol and other stuff').map(&:title)).to eq([flowers.title, many_ands.title])
    end

    it 'allows searching on words that are more than one letter, less than the ngram length and not stopwords' do
      expect(StopwordsArtwork.fulltext_search('jk').map(&:title)).to eq([harry.title])
      expect(StopwordsArtwork.fulltext_search('by').map(&:title)).to eq([])
    end
  end

  context 'indexing short prefixes' do
    let!(:dimethyl_mercury)   { ShortPrefixesArtwork.create(title: 'Dimethyl Mercury by Damien Hirst') }
    let!(:volume)             { ShortPrefixesArtwork.create(title: 'Volume by Dadamaino') }
    let!(:damaged)            { ShortPrefixesArtwork.create(title: 'Damaged: Photographs from the Chicago Daily News 1902-1933 (Governor) by Lisa Oppenheim') }
    let!(:frozen)             { ShortPrefixesArtwork.create(title: 'Frozen Fountain XXX by Evelyn Rosenberg') }
    let!(:skull)              { ShortPrefixesArtwork.create(title: 'Skull by Andy Warhol') }

    it 'finds the most relevant items with prefix indexing' do
      expect(ShortPrefixesArtwork.fulltext_search('damien').first).to eq(dimethyl_mercury)
      expect(ShortPrefixesArtwork.fulltext_search('dami').first).to eq(dimethyl_mercury)
      expect(ShortPrefixesArtwork.fulltext_search('dama').first).to eq(damaged)
      expect(ShortPrefixesArtwork.fulltext_search('dam').first).not_to eq(volume)
      expect(ShortPrefixesArtwork.fulltext_search('dadamaino').first).to eq(volume)
      expect(ShortPrefixesArtwork.fulltext_search('kull').first).to eq(skull)
    end

    it "doesn't index prefixes of stopwords" do
      # damaged has the word "from" in it, which shouldn't get indexed.
      expect(ShortPrefixesArtwork.fulltext_search('fro')).to eq([frozen])
    end

    it 'does index prefixes that would be stopwords taken alone' do
      # skull has the word "andy" in it, which should get indexed as "and" even though "and" is a stopword
      expect(ShortPrefixesArtwork.fulltext_search('and')).to eq([skull])
    end
  end

  context 'remove_from_ngram_index' do
    let!(:flowers1)     { BasicArtwork.create(title: 'Flowers 1') }
    let!(:flowers2)     { BasicArtwork.create(title: 'Flowers 1') }

    it 'removes all records from the index' do
      BasicArtwork.remove_from_ngram_index
      expect(BasicArtwork.fulltext_search('flower').length).to eq(0)
    end

    it 'removes a single record from the index' do
      flowers1.remove_from_ngram_index
      expect(BasicArtwork.fulltext_search('flower').length).to eq(1)
    end
  end

  context 'update_ngram_index' do
    let!(:flowers1)     { BasicArtwork.create(title: 'Flowers 1') }
    let!(:flowers2)     { BasicArtwork.create(title: 'Flowers 2') }

    context 'when config[:update_if] exists' do
      let(:painting)          { BasicArtwork.new title: 'Painting' }
      let(:conditional_index) { BasicArtwork.mongoid_fulltext_config['mongoid_fulltext.index_conditional'] }

      before(:each) do
        BasicArtwork.class_eval do
          fulltext_search_in :title, index_name: 'mongoid_fulltext.index_conditional'
        end
      end

      after(:all) do
        # Moped 1.0.0rc raises an error when removing a collection that does not exist
        # Will be fixed soon.
        begin
          Mongoid.default_session['mongoid_fulltext.index_conditional'].drop
        rescue Moped::Errors::OperationFailure => e
        end
        BasicArtwork.mongoid_fulltext_config.delete 'mongoid_fulltext.index_conditional'
      end

      context 'and is a symbol' do
        before(:each) do
          conditional_index[:update_if] = :persisted?
        end

        context 'when sending the symbol to the document evaluates to false' do
          it "doesn't update the index for the document" do
            painting.update_ngram_index
            expect(BasicArtwork.fulltext_search('painting', index: 'mongoid_fulltext.index_conditional').length).to be 0
          end
        end
      end

      context 'and is a string' do
        before(:each) do
          conditional_index[:update_if] = 'false'
        end

        context "when evaluating the string within the document's instance evaluates to false" do
          it "doesn't update the index for the document" do
            painting.update_ngram_index
            expect(BasicArtwork.fulltext_search('painting', index: 'mongoid_fulltext.index_conditional').length).to be 0
          end
        end
      end

      context 'and is a proc' do
        before(:each) do
          conditional_index[:update_if] = proc { false }
        end

        context "when evaluating the string within the document's instance evaluates to false" do
          it "doesn't update the index for the document" do
            painting.update_ngram_index
            expect(BasicArtwork.fulltext_search('painting', index: 'mongoid_fulltext.index_conditional').length).to be 0
          end
        end
      end

      context 'and is not a symbol, string, or proc' do
        before(:each) do
          conditional_index[:update_if] = %w[this isn't a symbol string or proc]
        end

        it "doesn't update the index for the document" do
          painting.update_ngram_index
          expect(BasicArtwork.fulltext_search('painting', index: 'mongoid_fulltext.index_conditional').length).to be 0
        end
      end
    end

    context 'from scratch' do
      before(:each) do
        Mongoid.default_session['mongoid_fulltext.index_basicartwork_0'].drop
      end

      it 'updates index on a single record' do
        flowers1.update_ngram_index
        expect(BasicArtwork.fulltext_search('flower').length).to eq(1)
      end

      it 'updates index on all records' do
        BasicArtwork.update_ngram_index
        expect(BasicArtwork.fulltext_search('flower').length).to eq(2)
      end
    end

    context 'incremental' do
      it 'removes an existing record' do
        coll = Mongoid.default_session['mongoid_fulltext.index_basicartwork_0']
        if Mongoid::Compatibility::Version.mongoid5_or_newer?
          coll.find('document_id' => flowers1._id).delete_many
        else
          coll.find('document_id' => flowers1._id).remove_all
        end
        expect(coll.find('document_id' => flowers1._id).first).to be nil
        flowers1.update_ngram_index
      end
    end

    context 'mongoid indexes' do
      it 'can re-create dropped indexes' do
        # see https://github.com/mongoid/mongoid_fulltext/pull/39
        pending unless Mongoid::Compatibility::Version.mongoid5_or_newer?
        # there're no indexes by default as Mongoid.autocreate_indexes is set to false
        # but mongo will automatically attempt to index _id in the background
        expect(Mongoid.default_session['mongoid_fulltext.index_basicartwork_0'].indexes.count).to be <= 1
        BasicArtwork.create_indexes
        expected_indexes = %w[_id_ fts_index document_id_1].sort
        current_indexes = []
        Mongoid.default_session['mongoid_fulltext.index_basicartwork_0'].indexes.each do |idef|
          current_indexes << idef['name']
        end
        expect(current_indexes.sort).to eq(expected_indexes)
      end

      it "doesn't fail on models that don't have a fulltext index" do
        expect { HiddenDragon.create_indexes }.not_to raise_error
      end

      it "doesn't blow up when the Mongoid.logger is set to false" do
        Mongoid.logger = false
        BasicArtwork.create_indexes
      end
    end

    context 'using update index options' do
      it 'will not timeout' do
        expect(BasicArtwork).to receive_message_chain(:all, :no_timeout).and_return([])
        BasicArtwork.update_ngram_index(timeout: false)
      end

      it 'will timeout' do
        expect(BasicArtwork).to receive_message_chain(:all).and_return([])
        BasicArtwork.update_ngram_index(timeout: true)
      end
    end
  end

  context 'batched reindexing' do
    let!(:flowers1) { DelayedArtwork.create(title: 'Flowers 1') }

    it 'should not rebuild index until explicitly invoked' do
      expect(DelayedArtwork.fulltext_search('flowers').length).to eq(0)
      DelayedArtwork.update_ngram_index
      expect(DelayedArtwork.fulltext_search('flowers').length).to eq(1)
    end
  end

  # For =~ operator documentation
  # https://github.com/dchelimsky/rspec/blob/master/lib/spec/matchers/match_array.rb#L53

  context 'with artwork that returns an array of colors as a filter' do
    let!(:title) { 'title' }
    let!(:nomatch) { 'nomatch' }
    let!(:red) { 'red' }
    let!(:green) { 'green' }
    let!(:blue) { 'blue' }
    let!(:yellow) { 'yellow' }
    let!(:brown) { 'brown' }

    let!(:rgb_artwork) { FilteredArtwork.create(title: "#{title} rgb", colors: [red, green, blue]) }
    let!(:holiday_artwork) { FilteredArtwork.create(title: "#{title} holiday", colors: [red, green]) }
    let!(:aqua_artwork) { FilteredArtwork.create(title: "#{title} aqua", colors: [green, blue]) }

    context 'with a fulltext search passing red, green, and blue to the colors filter' do
      it 'should return the rgb artwork' do
        expect(FilteredArtwork.fulltext_search(title, colors?: [red, green, blue])).to eq([rgb_artwork])
      end
    end

    context 'with a fulltext search passing blue and red to the colors filter' do
      it 'should return the rgb artwork' do
        expect(FilteredArtwork.fulltext_search(title, colors?: [blue, red])).to eq([rgb_artwork])
      end
    end

    context 'with a fulltext search passing green to the colors filter' do
      it 'should return all artwork' do
        expect(FilteredArtwork.fulltext_search(title, colors?: [green])).to match_array([rgb_artwork, holiday_artwork, aqua_artwork])
      end
    end

    context 'with a fulltext search passing no colors to the filter' do
      it 'should return all artwork' do
        expect(FilteredArtwork.fulltext_search(title)).to match_array([rgb_artwork, holiday_artwork, aqua_artwork])
      end
    end

    context 'with a fulltext search passing green and yellow to the colors filter' do
      it 'should return no artwork' do
        expect(FilteredArtwork.fulltext_search(title, colors?: [green, yellow])).to eq([])
      end
    end

    context 'with the query operator overridden to use $in instead of the default $all' do
      context 'with a fulltext search passing green and yellow to the colors filter' do
        it 'should return all of the artwork' do
          expect(FilteredArtwork.fulltext_search(title, colors?: { any: [green, yellow] })).to match_array([rgb_artwork, holiday_artwork, aqua_artwork])
        end
      end

      context 'with a fulltext search passing brown and yellow to the colors filter' do
        it 'should return none of the artwork' do
          expect(FilteredArtwork.fulltext_search(title, colors?: { any: [brown, yellow] })).to eq([])
        end
      end

      context 'with a fulltext search passing blue to the colors filter' do
        it 'should return the rgb and aqua artwork' do
          expect(FilteredArtwork.fulltext_search(title, colors?: { any: [blue] })).to eq([rgb_artwork, aqua_artwork])
        end
      end

      context "with a fulltext search term that won't match" do
        it 'should return none of the artwork' do
          expect(FilteredArtwork.fulltext_search(nomatch, colors?: { any: [green, yellow] })).to eq([])
        end
      end
    end

    context 'with the query operator overridden to use $all' do
      context 'with a fulltext search passing red, green, and blue to the colors filter' do
        it 'should return the rgb artwork' do
          expect(FilteredArtwork.fulltext_search(title, colors?: { all: [red, green, blue] })).to eq([rgb_artwork])
        end
      end

      context 'with a fulltext search passing green to the colors filter' do
        it 'should return all artwork' do
          expect(FilteredArtwork.fulltext_search(title, colors?: { all: [green] })).to match_array([rgb_artwork, holiday_artwork, aqua_artwork])
        end
      end
    end

    context 'with an unknown query operator used to override the default $all' do
      context 'with a fulltext search passing red, green, and blue to the colors filter' do
        it 'should raise an error' do
          expect do
            FilteredArtwork.fulltext_search(title, colors?: { unknown: [red, green, blue] })
          end.to raise_error(Mongoid::FullTextSearch::UnknownFilterQueryOperator)
        end
      end
    end

    context 'should properly work with non-latin strings (i.e. cyrillic)' do
      let!(:morning) { RussianArtwork.create(title: 'Утро в сосновом лесу Шишкин Morning in a Pine Forest Shishkin') }

      it 'should find a match if query is non-latin string' do
        # RussianArtwork is just like BasicArtwork, except that we set :alphabet to
        # 'abcdefghijklmnopqrstuvwxyz0123456789абвгдежзиклмнопрстуфхцчшщъыьэюя'
        expect(RussianArtwork.fulltext_search('shishkin').first).to eq(morning)
        expect(RussianArtwork.fulltext_search('шишкин').first).to eq(morning)
      end
    end
  end
end
