require 'spec_helper'

RSpec.describe 'HasMany' do
  context 'on builder' do
    let(:builder) { ActiveRecord::Associations::Builder::HasMany }

    it 'adds the array option' do
      expect(builder.send(:valid_options, {})).to include(:array)
    end
  end

  context 'on original' do
    let(:other) { Text }
    let(:key) { :texts }

    before { User.has_many :texts }
    subject { User.create(name: 'User 1') }
    after { User._reflections = {} }

    it 'has the method' do
      expect(subject).to respond_to(:texts)
      expect(subject._reflections).to include(key)
    end

    it 'has correct foreign key' do
      item = subject._reflections[key]
      expect(item.foreign_key).to be_eql('user_id')
    end

    it 'loads associated records' do
      expect(subject.texts.to_sql).to match(Regexp.new(<<-SQL.squish))
        SELECT "texts"\\.\\* FROM "texts" WHERE \\(?"texts"\\."user_id" = #{subject.id}\\)?
      SQL

      expect(subject.texts.load).to be_a(ActiveRecord::Associations::CollectionProxy)
      expect(subject.texts.to_a).to be_eql([])
    end

    it 'can be marked as loaded' do
      expect(subject.texts.loaded?).to be_eql(false)
      expect(subject.texts).to respond_to(:load_target)
      expect(subject.texts.load_target).to be_eql([])
      expect(subject.texts.loaded?).to be_eql(true)
    end

    it 'can find specific records' do
      records = FactoryBot.create_list(:text, 10, user_id: subject.id)
      ids = records.map(&:id).sample(5)

      expect(subject.texts).to respond_to(:find)
      records = subject.texts.find(*ids)

      expect(records.size).to be_eql(5)
      expect(records.map(&:id).sort).to be_eql(ids.sort)
    end

    it 'can return last n records' do
      records = FactoryBot.create_list(:text, 10, user_id: subject.id)
      ids = records.map(&:id).last(5)

      expect(subject.texts).to respond_to(:last)
      records = subject.texts.last(5)

      expect(records.size).to be_eql(5)
      expect(records.map(&:id).sort).to be_eql(ids.sort)
    end

    it 'can return first n records' do
      records = FactoryBot.create_list(:text, 10, user_id: subject.id)
      ids = records.map(&:id).first(5)

      expect(subject.texts).to respond_to(:take)
      records = subject.texts.take(5)

      expect(records.size).to be_eql(5)
      expect(records.map(&:id).sort).to be_eql(ids.sort)
    end

    it 'can build an associated record' do
      record = subject.texts.build(content: 'Test')
      expect(record).to be_a(other)
      expect(record).not_to be_persisted
      expect(record.content).to be_eql('Test')
      expect(record.user_id).to be_eql(subject.id)

      expect(subject.save).to be_truthy
      expect(subject.texts.size).to be_eql(1)
    end

    it 'can create an associated record' do
      record = subject.texts.create(content: 'Test')
      expect(subject.texts).to respond_to(:create!)

      expect(record).to be_a(other)
      expect(record).to be_persisted
      expect(record.content).to be_eql('Test')
      expect(record.user_id).to be_eql(subject.id)
    end

    it 'can concat records' do
      FactoryBot.create(:text, user_id: subject.id)
      expect(subject.texts.size).to be_eql(1)

      subject.texts.concat(other.new(content: 'Test'))
      expect(subject.texts.size).to be_eql(2)
      expect(subject.texts.last.content).to be_eql('Test')
    end

    it 'can replace records' do
      FactoryBot.create(:text, user_id: subject.id)
      expect(subject.texts.size).to be_eql(1)

      subject.texts.replace([other.new(content: 'Test 1'), other.new(content: 'Test 2')])
      expect(subject.texts.size).to be_eql(2)
      expect(subject.texts[0].content).to be_eql('Test 1')
      expect(subject.texts[1].content).to be_eql('Test 2')
    end

    it 'can delete all records' do
      FactoryBot.create_list(:text, 5, user_id: subject.id)
      expect(subject.texts.size).to be_eql(5)

      subject.texts.delete_all
      expect(subject.texts.size).to be_eql(0)
    end

    it 'can destroy all records' do
      FactoryBot.create_list(:text, 5, user_id: subject.id)
      expect(subject.texts.size).to be_eql(5)

      subject.texts.destroy_all
      expect(subject.texts.size).to be_eql(0)
    end

    it 'can have sum operations' do
      result = FactoryBot.create_list(:text, 5, user_id: subject.id).map(&:id).reduce(:+)
      expect(subject.texts).to respond_to(:sum)
      expect(subject.texts.sum(:id)).to be_eql(result)
    end

    it 'can have a pluck operation' do
      result = FactoryBot.create_list(:text, 5, user_id: subject.id).map(&:content).sort
      expect(subject.texts).to respond_to(:pluck)
      expect(subject.texts.pluck(:content).sort).to be_eql(result)
    end

    it 'can be markes as empty' do
      expect(subject.texts).to respond_to(:empty?)
      expect(subject.texts.empty?).to be_truthy

      FactoryBot.create(:text, user_id: subject.id)
      expect(subject.texts.empty?).to be_falsey
    end

    it 'can check if a record is included on the list' do
      inside = FactoryBot.create(:text, user_id: subject.id)
      outside = FactoryBot.create(:text)

      expect(subject.texts).to respond_to(:include?)
      expect(subject.texts.include?(inside)).to be_truthy
      expect(subject.texts.include?(outside)).to be_falsey
    end

    it 'can append records' do
      FactoryBot.create(:text, user_id: subject.id)
      expect(subject.texts.size).to be_eql(1)

      subject.texts << other.new(content: 'Test')
      expect(subject.texts.size).to be_eql(2)
      expect(subject.texts.last.content).to be_eql('Test')
    end

    it 'can clear records' do
      FactoryBot.create(:text, user_id: subject.id)
      expect(subject.texts.size).to be_eql(1)

      subject.texts.clear
      expect(subject.texts.size).to be_eql(0)
    end

    it 'can reload records' do
      expect(subject.texts.size).to be_eql(0)
      FactoryBot.create(:text, user_id: subject.id)

      expect(subject.texts.size).to be_eql(0)

      subject.texts.reload
      expect(subject.texts.size).to be_eql(1)
    end

    it 'can preload records' do
      FactoryBot.create_list(:text, 5, user_id: subject.id)
      entries = User.all.includes(:texts).load

      expect(entries.size).to be_eql(1)
      expect(entries.first.texts).to be_loaded
      expect(entries.first.texts.size).to be_eql(5)
    end

    it 'can joins records' do
      query = User.all.joins(:texts)
      expect(query.to_sql).to match(/INNER JOIN "texts"/)
      expect { query.load }.not_to raise_error
    end

    context 'with query constraint' do
      let(:activity) { Activity.create! }

      before do
        skip('Only Rails 7.1 onwards') unless Post.respond_to?(:query_constraints)

        Post.query_constraints :author_id, :id
        Activity.query_constraints :author_id, :id
        Activity.has_many :posts
      end

      after do
        Post.instance_variable_set(:@has_query_constraints, false)
        Post.instance_variable_set(:@query_constraints_list, nil)
        Post.instance_variable_set(:@_query_constraints_list, nil)
        Activity.instance_variable_set(:@has_query_constraints, false)
        Activity.instance_variable_set(:@query_constraints_list, nil)
        Activity.instance_variable_set(:@_query_constraints_list, nil)
      end

      it 'properly preload records' do
        FactoryBot.create_list(:post, 5, activity: activity)
        entries = Activity.all.includes(:posts).load

        expect(entries.size).to be_eql(1)
        expect(entries.first.posts).to be_loaded
        expect(entries.first.posts.size).to be_eql(5)
      end

      it 'properly preload records using preloader' do
        FactoryBot.create_list(:post, 5, activity: activity)
        entries = ActiveRecord::Associations::Preloader.new(
          records: Activity.all,
          associations: [:posts],
        ).call.first.records_by_owner

        expect(entries.size).to be_eql(1)
        expect(entries.values.first.size).to be_eql(5)
      end
    end
  end

  context 'on array' do
    let(:other) { Video }
    let(:key) { :videos }

    before { Tag.has_many :videos, array: true }
    subject { Tag.create(name: 'A') }
    after { Tag._reflections = {} }

    it 'has the method' do
      expect(subject).to respond_to(:videos)
      expect(subject._reflections).to include(key)
    end

    it 'has correct foreign key' do
      item = subject._reflections[key]
      expect(item.foreign_key).to be_eql('tag_ids')
    end

    it 'loads associated records' do
      expect(subject.videos.to_sql).to eq(<<~SQL.squish)
        SELECT "videos".* FROM "videos" WHERE #{subject.id} = ANY("videos"."tag_ids")
      SQL

      expect(subject.videos.load).to be_a(ActiveRecord::Associations::CollectionProxy)
      expect(subject.videos.to_a).to be_eql([])
    end

    it 'uses binds instead of the literal value' do
      query = subject.videos
      sql, binds = get_query_with_binds { query.load }
      expect(sql).to include('WHERE $1 = ANY("videos"."tag_ids")')
      expect(binds.first.value).to eq(subject.id)
    end

    it 'can be marked as loaded' do
      expect(subject.videos.loaded?).to be_eql(false)
      expect(subject.videos).to respond_to(:load_target)
      expect(subject.videos.load_target).to be_eql([])
      expect(subject.videos.loaded?).to be_eql(true)
    end

    it 'can find specific records' do
      records = FactoryBot.create_list(:video, 10, tag_ids: [subject.id])
      ids = records.map(&:id).sample(5)

      expect(subject.videos).to respond_to(:find)
      records = subject.videos.find(*ids)

      expect(records.size).to be_eql(5)
      expect(records.map(&:id).sort).to be_eql(ids.sort)
    end

    it 'can return last n records' do
      records = FactoryBot.create_list(:video, 10, tag_ids: [subject.id])
      ids = records.map(&:id).last(5)

      expect(subject.videos).to respond_to(:last)
      records = subject.videos.last(5)

      expect(records.size).to be_eql(5)
      expect(records.map(&:id).sort).to be_eql(ids.sort)
    end

    it 'can return first n records' do
      records = FactoryBot.create_list(:video, 10, tag_ids: [subject.id])
      ids = records.map(&:id).first(5)

      expect(subject.videos).to respond_to(:take)
      records = subject.videos.take(5)

      expect(records.size).to be_eql(5)
      expect(records.map(&:id).sort).to be_eql(ids.sort)
    end

    it 'can build an associated record' do
      record = subject.videos.build(title: 'Test')
      expect(record).to be_a(other)
      expect(record).not_to be_persisted
      expect(record.title).to be_eql('Test')

      expect(subject.save).to be_truthy
      expect(record.tag_ids).to be_eql([subject.id])
      expect(subject.videos.size).to be_eql(1)
    end

    it 'can create an associated record' do
      record = subject.videos.create(title: 'Test')
      expect(subject.videos).to respond_to(:create!)

      expect(record).to be_a(other)
      expect(record).to be_persisted
      expect(record.title).to be_eql('Test')
      expect(record.tag_ids).to be_eql([subject.id])
    end

    it 'can perist after accessed in after_create' do
      other.belongs_to_many(:tags)
      other.after_create { self.tags.to_a }

      video = FactoryBot.create(:video)
      subject.videos << video

      expect(subject.reload.videos.size).to eql(1)
      expect(video.reload.tags.size).to eql(1)

      other.reset_callbacks(:create)
      other._reflections = {}
    end

    it 'can concat records' do
      FactoryBot.create(:video, tag_ids: [subject.id])
      expect(subject.videos.size).to be_eql(1)

      subject.videos.concat(other.new(title: 'Test'))
      expect(subject.videos.size).to be_eql(2)
      expect(subject.videos.last.title).to be_eql('Test')
    end

    it 'can replace records' do
      FactoryBot.create(:video, tag_ids: [subject.id])
      expect(subject.videos.size).to be_eql(1)

      subject.videos.replace([other.new(title: 'Test 1'), other.new(title: 'Test 2')])
      expect(subject.videos.size).to be_eql(2)
      expect(subject.videos[0].title).to be_eql('Test 1')
      expect(subject.videos[1].title).to be_eql('Test 2')
    end

    it 'can delete all records' do
      FactoryBot.create_list(:video, 5, tag_ids: [subject.id])
      expect(subject.videos.size).to be_eql(5)

      subject.videos.delete_all
      expect(subject.videos.size).to be_eql(0)
    end

    it 'can destroy all records' do
      FactoryBot.create_list(:video, 5, tag_ids: [subject.id])
      expect(subject.videos.size).to be_eql(5)

      subject.videos.destroy_all
      expect(subject.videos.size).to be_eql(0)
    end

    it 'can have sum operations' do
      result = FactoryBot.create_list(:video, 5, tag_ids: [subject.id]).map(&:id).reduce(:+)
      expect(subject.videos).to respond_to(:sum)
      expect(subject.videos.sum(:id)).to be_eql(result)
    end

    it 'can have a pluck operation' do
      result = FactoryBot.create_list(:video, 5, tag_ids: [subject.id]).map(&:title).sort
      expect(subject.videos).to respond_to(:pluck)
      expect(subject.videos.pluck(:title).sort).to be_eql(result)
    end

    it 'can be markes as empty' do
      expect(subject.videos).to respond_to(:empty?)
      expect(subject.videos.empty?).to be_truthy

      FactoryBot.create(:video, tag_ids: [subject.id])
      expect(subject.videos.empty?).to be_falsey
    end

    it 'can check if a record is included on the list' do
      inside = FactoryBot.create(:video, tag_ids: [subject.id])
      outside = FactoryBot.create(:video)

      expect(subject.videos).to respond_to(:include?)
      expect(subject.videos.include?(inside)).to be_truthy
      expect(subject.videos.include?(outside)).to be_falsey
    end

    it 'can append records' do
      FactoryBot.create(:video, tag_ids: [subject.id])
      expect(subject.videos.size).to be_eql(1)

      subject.videos << other.new(title: 'Test')
      expect(subject.videos.size).to be_eql(2)
      expect(subject.videos.last.title).to be_eql('Test')
    end

    it 'can clear records' do
      FactoryBot.create(:video, tag_ids: [subject.id])
      expect(subject.videos.size).to be_eql(1)

      subject.videos.clear
      expect(subject.videos.size).to be_eql(0)
    end

    it 'can reload records' do
      expect(subject.videos.size).to be_eql(0)
      FactoryBot.create(:video, tag_ids: [subject.id])

      expect(subject.videos.size).to be_eql(0)

      subject.videos.reload
      expect(subject.videos.size).to be_eql(1)
    end

    it 'can preload records' do
      FactoryBot.create_list(:video, 5, tag_ids: [subject.id])
      entries = Tag.all.includes(:videos).load

      expect(entries.size).to be_eql(1)
      expect(entries.first.videos).to be_loaded
      expect(entries.first.videos.size).to be_eql(5)
    end

    it 'can joins records' do
      query = Tag.all.joins(:videos)
      expect(query.to_sql).to match(/INNER JOIN "videos"/)
      expect { query.load }.not_to raise_error
    end
  end

  context 'using uuid' do
    let(:connection) { ActiveRecord::Base.connection }
    let(:game) { Class.new(ActiveRecord::Base) }
    let(:player) { Class.new(ActiveRecord::Base) }

    # TODO: Set as a shred example
    before do
      connection.create_table(:players, id: :uuid) { |t| t.string :name }
      connection.create_table(:games, id: :uuid) { |t| t.uuid :player_ids, array: true }

      game.table_name = 'games'
      player.table_name = 'players'
      player.has_many :games, array: true, anonymous_class: game,
        inverse_of: false, foreign_key: :player_ids
    end

    subject { player.create }

    it 'loads associated records' do
      expect(subject.games.to_sql).to eq(<<~SQL.squish)
        SELECT "games".* FROM "games"
        WHERE '#{subject.id}' = ANY("games"."player_ids")
      SQL

      expect(subject.games.load).to be_a(ActiveRecord::Associations::CollectionProxy)
      expect(subject.games.to_a).to be_eql([])
    end

    it 'uses binds instead of the literal value' do
      query = subject.games
      sql, binds = get_query_with_binds { query.load }
      expect(sql).to include('WHERE $1 = ANY("games"."player_ids")')
      expect(binds.first.value).to eq(subject.id)
    end

    it 'can preload records' do
      5.times { game.create(player_ids: [subject.id]) }
      entries = player.all.includes(:games).load

      expect(entries.size).to be_eql(1)
      expect(entries.first.games).to be_loaded
      expect(entries.first.games.size).to be_eql(5)
    end

    it 'can joins records' do
      query = player.all.joins(:games)
      expect(query.to_sql).to match(/INNER JOIN "games"/)
      expect { query.load }.not_to raise_error
    end
  end
end
