# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. licenses this file to you under
# the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

require 'spec_helper'

describe 'Elasticsearch::Model::Adapter::ActiveRecord Associations' do
  before(:all) do
    ActiveRecord::Schema.define(version: 1) do
      create_table :categories do |t|
        t.string     :title
        t.timestamps null: false
      end

      create_table :categories_posts do |t|
        t.references :post, :category
      end

      create_table :authors do |t|
        t.string     :first_name, :last_name
        t.timestamps null: false
      end

      create_table :authorships do |t|
        t.string     :first_name, :last_name
        t.references :post
        t.references :author
        t.timestamps null: false
      end

      create_table :comments do |t|
        t.string     :text
        t.string     :author
        t.references :post
        t.timestamps null: false
      end

      add_index(:comments, :post_id) unless index_exists?(:comments, :post_id)

      create_table :posts do |t|
        t.string     :title
        t.text       :text
        t.boolean    :published
        t.timestamps null: false
      end
    end

    Comment.__send__ :include, Elasticsearch::Model
    Comment.__send__ :include, Elasticsearch::Model::Callbacks
  end

  before do
    clear_tables(:categories, :categories_posts, :authors, :authorships, :comments, :posts)
    clear_indices(Post)
    Post.__elasticsearch__.create_index!(force: true)
    Comment.__elasticsearch__.create_index!(force: true)
  end

  after do
    clear_tables(Post, Category)
    clear_indices(Post)
  end

  context 'when a document is created' do
    before do
      Post.create!(title: 'Test')
      Post.create!(title: 'Testing Coding')
      Post.create!(title: 'Coding')
      Post.__elasticsearch__.refresh_index!
    end

    let(:search_result) do
      Post.search('title:test')
    end

    it 'indexes the document' do
      expect(search_result.results.size).to eq(2)
      expect(search_result.results.first.title).to eq('Test')
      expect(search_result.records.size).to eq(2)
      expect(search_result.records.first.title).to eq('Test')
    end
  end

  describe 'has_many_and_belongs_to association' do
    context 'when an association is updated' do
      before do
        post.categories = [category_a,  category_b]
        Post.__elasticsearch__.refresh_index!
      end

      let(:category_a) do
        Category.where(title: "One").first_or_create!
      end

      let(:category_b) do
        Category.where(title: "Two").first_or_create!
      end

      let(:post) do
        Post.create! title: "First Post", text: "This is the first post..."
      end

      let(:search_result) do
        Post.search(query: {
                      bool: {
                        must: {
                          multi_match: {
                            fields: ['title'],
                            query: 'first'
                          }
                        },
                        filter: {
                          terms: {
                            categories: ['One']
                          }
                        }
                      }
                    } )
      end

      it 'applies the update with' do
        expect(search_result.results.size).to eq(1)
        expect(search_result.results.first.title).to eq('First Post')
        expect(search_result.records.size).to eq(1)
        expect(search_result.records.first.title).to eq('First Post')
      end
    end

    context 'when an association is deleted' do
      before do
        post.categories = [category_a,  category_b]
        post.categories = [category_b]
        Post.__elasticsearch__.refresh_index!
      end

      let(:category_a) do
        Category.where(title: "One").first_or_create!
      end

      let(:category_b) do
        Category.where(title: "Two").first_or_create!
      end

      let(:post) do
        Post.create! title: "First Post", text: "This is the first post..."
      end

      let(:search_result) do
        Post.search(query: {
                      bool: {
                        must: {
                          multi_match: {
                            fields: ['title'],
                            query: 'first'
                          }
                        },
                        filter: {
                          terms: {
                            categories: ['One']
                          }
                        }
                      }
                    } )
      end

      it 'applies the update with a reindex' do
        expect(search_result.results.size).to eq(0)
        expect(search_result.records.size).to eq(0)
      end
    end
  end

  describe 'has_many through association' do
    context 'when the association is updated' do
      before do
        author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create!
        author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create!
        author_c = Author.where(first_name: "Kobe", last_name: "Griss").first_or_create!

        # Create posts
        post_1 = Post.create!(title: "First Post", text: "This is the first post...")
        post_2 = Post.create!(title: "Second Post", text: "This is the second post...")
        post_3 = Post.create!(title: "Third Post", text: "This is the third post...")

        # Assign authors
        post_1.authors = [author_a,  author_b]
        post_2.authors = [author_a]
        post_3.authors = [author_c]

        Post.__elasticsearch__.refresh_index!
      end

      let(:search_result) do
        Post.search('authors.full_name:john')
      end

      it 'applies the update' do
        expect(search_result.results.size).to eq(2)
        expect(search_result.records.size).to eq(2)
      end
    end

    context 'when an association is added' do
      before do
        author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create!
        author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create!

        # Create posts
        post_1 = Post.create!(title: "First Post", text: "This is the first post...")

        # Assign authors
        post_1.authors = [author_a]
        post_1.authors << author_b
        Post.__elasticsearch__.refresh_index!
      end

      let(:search_result) do
        Post.search('authors.full_name:john')
      end

      it 'adds the association' do
        expect(search_result.results.size).to eq(1)
        expect(search_result.records.size).to eq(1)
      end
    end
  end

  describe 'has_many association' do
    context 'when an association is added' do
      before do
        # Create posts
        post_1 = Post.create!(title: "First Post", text: "This is the first post...")
        post_2 = Post.create!(title: "Second Post", text: "This is the second post...")

        # Add comments
        post_1.comments.create!(author: 'John', text: 'Excellent')
        post_1.comments.create!(author: 'Abby', text: 'Good')

        post_2.comments.create!(author: 'John', text: 'Terrible')

        post_1.comments.create!(author: 'John', text: 'Or rather just good...')
        Post.__elasticsearch__.refresh_index!
      end

      let(:search_result) do
        Post.search(query: {
                      nested: {
                        path: 'comments',
                        query: {
                          bool: {
                            must: [
                              { match: { 'comments.author' => 'john' } },
                              { match: { 'comments.text'   => 'good' } }
                            ]
                          }
                        }
                      }
                    })
      end

      it 'adds the association' do
        expect(search_result.results.size).to eq(1)
      end
    end
  end

  describe '#touch' do
    context 'when a touch callback is defined on the model' do
      before do
        # Create categories
        category_a = Category.where(title: "One").first_or_create!

        # Create post
        post = Post.create!(title: "First Post", text: "This is the first post...")

        # Assign category
        post.categories << category_a
        category_a.update_attribute(:title, "Updated")
        category_a.posts.each { |p| p.touch }

        Post.__elasticsearch__.refresh_index!
      end

      it 'executes the callback after #touch' do
        expect(Post.search('categories:One').size).to eq(0)
        expect(Post.search('categories:Updated').size).to eq(1)
      end
    end
  end

  describe '#includes' do
    before do
      post_1 = Post.create(title: 'One')
      post_2 = Post.create(title: 'Two')
      post_1.comments.create(text: 'First comment')
      post_2.comments.create(text: 'Second comment')

      Comment.__elasticsearch__.refresh_index!
    end

    let(:search_result) do
      Comment.search('first').records(includes: :post)
    end

    it 'eager loads associations' do
      expect(search_result.first.association(:post)).to be_loaded
      expect(search_result.first.post.title).to eq('One')
    end
  end
end
