# encoding: UTF-8

require 'spec_helper'

describe Goldiloader do
  let!(:author1) do
    User.create!(name: 'author1') { |u| u.address = Address.new(city: 'author1-city') }
  end

  let!(:author2) do
    User.create!(name: 'author2') { |u| u.address = Address.new(city: 'author2-city') }
  end

  let!(:author3) do
    User.create!(name: 'author3') { |u| u.address = Address.new(city: 'author3-city') }
  end

  let!(:group1) { Group.create!(name: 'group1') }

  let!(:parent_tag1) { Tag.create!(name: 'parent1') { |t| t.owner = group1 } }
  let!(:child_tag1) { parent_tag1.children.create!(name: 'parent1-child1') { |t| t.owner = author1 } }
  let!(:child_tag2) { parent_tag1.children.create!(name: 'parent1-child2') { |t| t.owner = group1 } }
  let!(:parent_tag2) { Tag.create!(name: 'parent2') { |t| t.owner = group1 } }
  let!(:child_tag3) { parent_tag2.children.create!(name: 'parent2-child1') { |t| t.owner = author2 } }

  let!(:blog1) do
    blog1 = Blog.create!(name: 'blog1')

    blog1.posts.create!(title: 'blog1-post1') do |post|
      post.author = author1
      post.tags << child_tag1 << child_tag2
    end

    blog1.posts.create!(title: 'blog1-post2') do |post|
      post.author = author2
      post.tags << child_tag1
    end

    blog1
  end

  let!(:blog2) do
    blog2 = Blog.create!(name: 'blog2')

    blog2.posts.create!(title: 'blog2-post1') do |post|
      post.author = author3
      post.tags << child_tag1
    end

    blog2.posts.create!(title: 'blog2-post2') do |post|
      post.author = author1
      post.tags << child_tag3
    end

    blog2
  end

  before do
    [Address, Blog, Post, Tag, User, Group].each do |klass|
      allow(klass).to receive(:find_by_sql).and_call_original
    end

    ActiveRecord::Base.logger.info('Test setup complete')
  end

  it "auto eager loads has_many associations" do
    blogs = Blog.order(:name).to_a

    # Sanity check that associations aren't loaded yet
    blogs.each do |blog|
      expect(blog.association(:posts)).to_not be_loaded
    end

    # Force the first blogs first post to load
    blogs.first.posts.to_a

    blogs.each do |blog|
      expect(blog.association(:posts)).to be_loaded
    end

    expect(blogs.first.posts.map(&:title)).to match_array(['blog1-post1', 'blog1-post2'])
    expect(blogs.second.posts.map(&:title)).to match_array(['blog2-post1', 'blog2-post2'])

    expect(Post).to have_received(:find_by_sql).once
  end

  it "auto eager loads belongs_to associations" do
    posts = Post.order(:title).to_a

    # Sanity check that associations aren't loaded yet
    posts.each do |blog|
      expect(blog.association(:blog)).to_not be_loaded
    end

    # Force the first post's blog to load
    posts.first.blog

    posts.each do |blog|
      expect(blog.association(:blog)).to be_loaded
    end

    expect(posts.map(&:blog).map(&:name)).to eq(['blog1', 'blog1', 'blog2', 'blog2'])
    expect(Blog).to have_received(:find_by_sql).once
  end

  it "auto eager loads has_one associations" do
    users = User.order(:name).to_a

    # Sanity check that associations aren't loaded yet
    users.each do |user|
      expect(user.association(:address)).to_not be_loaded
    end

    # Force the first user's address to load
    users.first.address

    users.each do |blog|
      expect(blog.association(:address)).to be_loaded
    end

    expect(users.map(&:address).map(&:city)).to match_array(['author1-city', 'author2-city', 'author3-city'])
    expect(Address).to have_received(:find_by_sql).once
  end

  it "auto eager loads nested associations" do
    blogs = Blog.order(:name).to_a
    blogs.first.posts.to_a.first.author

    blogs.flat_map(&:posts).each do |blog|
      expect(blog.association(:author)).to be_loaded
    end

    expect(blogs.first.posts.first.author).to eq author1
    expect(blogs.first.posts.second.author).to eq author2
    expect(blogs.second.posts.first.author).to eq author3
    expect(blogs.second.posts.second.author).to eq author1
    expect(Post).to have_received(:find_by_sql).once
  end

  it "auto eager loads has_many through associations" do
    blogs = Blog.order(:name).to_a
    blogs.first.authors.to_a

    blogs.each do |blog|
      expect(blog.association(:authors)).to be_loaded
    end

    expect(blogs.first.authors).to match_array([author1, author2])
    expect(blogs.second.authors).to match_array([author3, author1])
    expect(User).to have_received(:find_by_sql).once
  end

  it "auto eager loads associations when the model is loaded via find" do
    blog = Blog.find(blog1.id)
    blog.posts.to_a.first.author

    blog.posts.each do |blog|
      expect(blog.association(:author)).to be_loaded
    end
  end

  it "auto eager loads polymorphic associations" do
    tags = Tag.where('parent_id IS NOT NULL').order(:name).to_a
    tags.first.owner

    tags.each do |tag|
      expect(tag.association(:owner)).to be_loaded
    end

    expect(tags.first.owner).to eq author1
    expect(tags.second.owner).to eq group1
    expect(tags.third.owner).to eq author2
  end

  it "auto eager loads associations of polymorphic associations" do
    tags = Tag.where('parent_id IS NOT NULL').order(:name).to_a
    users = tags.map(&:owner).select {|owner| owner.is_a?(User) }.sort_by(&:name)
    users.first.posts.to_a

    users.each do |user|
      expect(user.association(:posts)).to be_loaded
    end

    expect(users.first.posts).to eq Post.where(author_id: author1.id)
    expect(users.second.posts).to eq Post.where(author_id: author2.id)
  end

  it "only auto eager loads associations loaded through the same path" do
    root_tags = Tag.where(parent_id: nil).order(:name).to_a
    root_tags.first.children.to_a

    # Make sure we loaded all child tags
    root_tags.each do |tag|
      expect(tag.association(:children)).to be_loaded
    end

    # Force a load of a root tag's owner
    root_tags.first.owner

    # All root tag owners should be loaded
    root_tags.each do |tag|
      expect(tag.association(:owner)).to be_loaded
    end

    # Child tag owners should not be loaded
    child_tags = root_tags.flat_map(&:children)
    child_tags.each do |tag|
      expect(tag.association(:owner)).to_not be_loaded
    end
  end

  context "when a has_many association has in-memory changes" do
    let!(:blogs) { Blog.order(:name).to_a }
    let(:blog) { blogs.first }
    let(:other_blog) { blogs.last }

    before do
      blog.posts.create(title: 'blog1-new-post')
    end

    it "returns the correct models for the modified has_many association" do
      expect(blog.posts).to match_array Post.where(blog_id: blog.id)
    end

    it "doesn't auto eager load peers when accessing the modified has_many association" do
      blog.posts.to_a
      expect(other_blog.association(:posts)).to_not be_loaded
    end

    it "returns the correct models for the modified has_many association when accessing a peer" do
      other_blog.posts.to_a
      expect(blog.posts).to match_array Post.where(blog_id: blog.id)
    end
  end

  context "when a has_many through association has in-memory changes" do
    let!(:posts) { Post.order(:title).to_a }
    let(:post) { posts.first }
    let(:other_post) { posts.last }

    before do
      tag = Tag.create(name: 'new-tag')
      post.post_tags.create(tag: tag)
    end

    it "returns the correct models for the modified has_many through association" do
      expect(post.tags).to match_array PostTag.where(post_id: post.id).includes(:tag).map(&:tag)
    end

    it "doesn't auto eager load peers when accessing the modified has_many through association" do
      post.tags.to_a
      expect(other_post.association(:tags)).to_not be_loaded
    end

    it "returns the correct models for the modified has_many through association when accessing a peer" do
      other_post.tags.to_a
      expect(post.tags).to match_array PostTag.where(post_id: post.id).includes(:tag).map(&:tag)
    end
  end

  context "with fully_load false" do

    it "doesn't auto eager loads a has_many association when size is called" do
      blogs = Blog.order(:name).to_a
      blogs.first.posts.size

      blogs.each do |blog|
        expect(blog.association(:posts)).to_not be_loaded
      end
    end

    it "doesn't auto eager loads a has_many association when exists? is called" do
      blogs = Blog.order(:name).to_a
      blogs.first.posts.exists?

      blogs.each do |blog|
        expect(blog.association(:posts)).to_not be_loaded
      end
    end

    it "doesn't auto eager loads a has_many association when last is called" do
      blogs = Blog.order(:name).to_a
      blogs.first.posts.last

      blogs.each do |blog|
        expect(blog.association(:posts)).to_not be_loaded
      end
    end

    it "doesn't auto eager loads a has_many association when ids is called" do
      blogs = Blog.order(:name).to_a
      blogs.first.post_ids

      blogs.each do |blog|
        expect(blog.association(:posts)).to_not be_loaded
      end
    end
  end

  context "with fully_load true" do

    it "auto eager loads a has_many association when size is called" do
      blogs = Blog.order(:name).to_a
      blogs.first.posts_fully_load.size

      blogs.each do |blog|
        expect(blog.association(:posts_fully_load)).to be_loaded
      end
    end

    it "auto eager loads a has_many association when exists? is called" do
      blogs = Blog.order(:name).to_a
      blogs.first.posts_fully_load.exists?

      blogs.each do |blog|
        expect(blog.association(:posts_fully_load)).to be_loaded
      end
    end

    it "auto eager loads a has_many association when last is called" do
      blogs = Blog.order(:name).to_a
      blogs.first.posts_fully_load.last

      blogs.each do |blog|
        expect(blog.association(:posts_fully_load)).to be_loaded
      end
    end

    it "auto eager loads a has_many association when ids is called" do
      blogs = Blog.order(:name).to_a
      blogs.first.posts_fully_load_ids

      blogs.each do |blog|
        expect(blog.association(:posts_fully_load)).to be_loaded
      end
    end

  end

  context "with auto_include disabled" do

    it "doesn't auto eager load has_many associations" do
      blogs = Blog.order(:name).to_a

      # Force the first blogs first post to load
      posts = blogs.first.posts_without_auto_include.to_a
      expect(posts).to match_array Post.where(blog_id: blogs.first.id)

      blogs.drop(1).each do |blog|
        expect(blog.association(:posts_without_auto_include)).to_not be_loaded
      end
    end

    it "doesn't auto eager load has_one associations" do
      users = User.order(:name).to_a

      # Force the first user's address to load
      user = users.first
      address = user.address_without_auto_include
      expect(address).to eq Address.where(user_id: user.id).first

      users.drop(1).each do |blog|
        expect(blog.association(:address_without_auto_include)).to_not be_loaded
      end
    end

    it "doesn't auto eager load belongs_to associations" do
      posts = Post.order(:title).to_a
      # Force the first post's blog to load
      post = posts.first
      blog = post.blog_without_auto_include
      expect(blog).to eq Blog.where(id: post.blog_id).first

      posts.drop(1).each do |blog|
        expect(blog.association(:blog_without_auto_include)).to_not be_loaded
      end
    end

    it "still auto eager loads nested associations" do
      posts = Post.order(:title).to_a
      # Force the first post's blog to load
      blog = posts.first.blog_without_auto_include

      # Load another blogs posts
      other_blog = posts.last.blog_without_auto_include
      other_blog.posts.to_a

      blog.posts.to_a.first.tags.to_a

      blog.posts.each do |post|
        expect(post.association(:tags)).to be_loaded
      end

      other_blog.posts.each do |post|
        expect(post.association(:tags)).to_not be_loaded
      end
    end
  end
end