# frozen_string_literal: true

require 'test_helper'

class TestDeepCloneable < Minitest::Test
  def setup
    @jack  = Pirate.create(:name => 'Jack Sparrow', :nick_name => 'Captain Jack', :age => 30)
    @polly = Parrot.create(:name => 'Polly', :age => 2, :pirate => @jack)
    @john = Matey.create(:name => 'John', :pirate => @jack)
    @treasure = Treasure.create(:found_at => 'Isla del Muerte', :pirate => @jack, :matey => @john)
    @gold_piece = GoldPiece.create(:treasure => @treasure)
    @ship = BattleShip.create(:name => 'Black Pearl', :pirates => [@jack])
  end

  def test_single_deep_clone_exception
    deep_clone = @jack.deep_clone(:except => :name)
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal @jack.name, @jack.deep_clone.name
    assert_nil deep_clone.name
    assert_equal @jack.nick_name, deep_clone.nick_name
  end

  def test_multiple_deep_clone_exception
    deep_clone = @jack.deep_clone(:except => [:name, :nick_name])
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_nil deep_clone.name
    assert_equal 'no nickname', deep_clone.nick_name
    assert_equal @jack.age, deep_clone.age
  end

  def test_single_deep_clone_onliness
    deep_clone = @jack.deep_clone(:only => :name)
    assert deep_clone.new_record?
    assert deep_clone.piastres, []
    assert deep_clone.save
    assert_equal @jack.name, deep_clone.name
    assert_equal 'no nickname', deep_clone.nick_name
    assert_nil deep_clone.age
    assert_nil deep_clone.ship_id
    assert_nil deep_clone.ship_type
  end

  def test_multiple_deep_clone_onliness
    deep_clone = @jack.deep_clone(:only => [:name, :nick_name])
    assert deep_clone.new_record?
    assert deep_clone.piastres, []
    assert deep_clone.save
    assert_equal @jack.name, deep_clone.name
    assert_equal @jack.nick_name, deep_clone.nick_name
    assert_nil deep_clone.age
    assert_nil deep_clone.ship_id
    assert_nil deep_clone.ship_type
  end

  def test_single_include_association
    deep_clone = @jack.deep_clone(:include => :mateys)
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.mateys.size
  end

  def test_single_include_belongs_to_polymorphic_association
    deep_clone = @jack.deep_clone(:include => :ship)
    assert deep_clone.new_record?
    assert deep_clone.save
    refute_nil deep_clone.ship
    refute_equal @jack.ship, deep_clone.ship
  end

  def test_single_include_has_many_polymorphic_association
    deep_clone = @ship.deep_clone(:include => :pirates)
    assert deep_clone.new_record?
    assert deep_clone.save
    assert deep_clone.pirates.any?
  end

  def test_multiple_include_association
    deep_clone = @jack.deep_clone(:include => [:mateys, :treasures])
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.mateys.size
    assert_equal 1, deep_clone.treasures.size
  end

  def test_deep_include_association
    deep_clone = @jack.deep_clone(:include => { :treasures => :gold_pieces })
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.treasures.size
    assert_equal 1, deep_clone.gold_pieces.size
  end

  def test_include_association_assignments
    deep_clone = @jack.deep_clone(:include => :treasures)
    assert deep_clone.new_record?

    deep_clone.treasures.each do |treasure|
      assert_equal deep_clone, treasure.pirate
    end
  end

  def test_include_association_assignments_with_inverse_of
    @jack.pirate_cages.build
    deep_clone = @jack.deep_clone(:include => :pirate_cages)
    assert deep_clone.new_record?
    deep_clone.pirate_cages.each do |cage|
      assert_equal deep_clone, cage.pirate
    end
  end

  def test_multiple_and_deep_include_association
    deep_clone = @jack.deep_clone(:include => { :treasures => :gold_pieces, :mateys => {} })
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.treasures.size
    assert_equal 1, deep_clone.gold_pieces.size
    assert_equal 1, deep_clone.mateys.size
  end

  def test_multiple_and_deep_include_association_with_array
    deep_clone = @jack.deep_clone(:include => [{ :treasures => :gold_pieces }, :mateys])
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.treasures.size
    assert_equal 1, deep_clone.gold_pieces.size
    assert_equal 1, deep_clone.mateys.size
  end

  def test_multiple_and_deep_include_association_with_array_and_multikey_hash
    deep_clone = @jack.deep_clone(:include => [:parrot, { :treasures => :gold_pieces, :mateys => {} }])
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.treasures.size
    assert_equal 1, deep_clone.gold_pieces.size
    assert_equal 1, deep_clone.mateys.size
  end

  def test_with_belongs_to_relation
    deep_clone = @jack.deep_clone(:include => :parrot)
    assert deep_clone.new_record?
    assert deep_clone.save
    refute_equal deep_clone.parrot, @jack.parrot
  end

  def test_should_pass_nested_exceptions
    deep_clone = @jack.deep_clone(:include => :parrot, :except => [:name, { :parrot => [:name] }])
    assert deep_clone.new_record?
    assert deep_clone.save
    refute_equal deep_clone.parrot, @jack.parrot
    assert_equal deep_clone.parrot.age, @jack.parrot.age
    refute_nil @jack.parrot.name
    assert_nil deep_clone.parrot.name
  end

  def test_should_pass_nested_exception_with_hash_value
    deep_clone = @jack.deep_clone(:include => :parrot, :except => { :parrot => [:name] })
    assert deep_clone.new_record?
    assert deep_clone.save
    refute_equal deep_clone.parrot, @jack.parrot
    assert_equal deep_clone.parrot.age, @jack.parrot.age
    refute_nil @jack.parrot.name
    assert_nil deep_clone.parrot.name
  end

  def test_should_pass_nested_onlinesses
    deep_clone = @jack.deep_clone(:include => :parrot, :only => [:name, { :parrot => [:name] }])
    assert deep_clone.new_record?
    assert deep_clone.piastres, []
    assert deep_clone.save
    refute_equal deep_clone.parrot, @jack.parrot
    assert_equal deep_clone.parrot.name, @jack.parrot.name
    assert_nil deep_clone.parrot.age
  end

  def test_should_pass_nested_onliness_with_hash_value
    deep_clone = @jack.deep_clone(:include => :parrot, :only => { :parrot => [:name] })
    assert deep_clone.new_record?
    assert deep_clone.piastres, []
    assert deep_clone.save
    refute_equal deep_clone.parrot, @jack.parrot
    assert_equal deep_clone.parrot.name, @jack.parrot.name
    assert_nil deep_clone.parrot.age
  end

  def test_should_not_double_deep_clone_when_using_dictionary
    current_matey_count = Matey.count
    deep_clone = @jack.deep_clone(:include => [:mateys, { :treasures => :matey }], :use_dictionary => true)
    assert deep_clone.new_record?
    deep_clone.save!

    assert_equal current_matey_count + 1, Matey.count
  end

  def test_should_not_double_deep_clone_when_using_manual_dictionary
    current_matey_count = Matey.count

    dict = { :mateys => {} }
    @jack.mateys.each { |m| dict[:mateys][m] = m.deep_clone }

    deep_clone = @jack.deep_clone(:include => [:mateys, { :treasures => :matey }], :dictionary => dict)
    assert deep_clone.new_record?
    deep_clone.save!

    assert_equal current_matey_count + 1, Matey.count
  end

  def test_should_support_ar_class_under_module
    @human = Animal::Human.create :name => 'Michael'
    @pig = Animal::Pig.create :human => @human, :name => 'big pig'

    deep_clone_human = @human.deep_clone(:include => [:pigs])
    assert deep_clone_human.new_record?
    assert deep_clone_human.save
    assert_equal 1, deep_clone_human.pigs.count

    @human2 = Animal::Human.create :name => 'John'
    @pig2 = @human2.pigs.create :name => 'small pig'

    deep_clone_human2 = @human.deep_clone(:include => [:pigs])
    assert deep_clone_human2.new_record?
    assert deep_clone_human2.save
    assert_equal 1, deep_clone_human2.pigs.count
  end

  def test_should_deep_clone_many_to_many_associations
    @human = Animal::Human.create :name => 'Michael'
    @human2 = Animal::Human.create :name => 'Jack'
    @chicken1 = Animal::Chicken.create :name => 'Chick1'
    @chicken2 = Animal::Chicken.create :name => 'Chick2'
    @human.chickens << [@chicken1, @chicken2]
    @human2.chickens << [@chicken1, @chicken2]

    deep_clone_human = @human.deep_clone(:include => :ownerships)
    assert deep_clone_human.new_record?
    assert deep_clone_human.save
    assert_equal 2, deep_clone_human.chickens.count
  end

  def test_should_skip_missing_associations
    @earth = Animal::Planet.create :name => 'Earth'
    @human = Animal::Human.create :name => 'Michael'
    @chicken = Animal::Chicken.create :name => 'Chick', :humans => [@human], :planet => @earth
    @dove = Animal::Dove.create :name => 'Dovey', :planet => @earth

    assert_raises ::DeepCloneable::AssociationNotFoundException do
      @earth.deep_clone(:include => { :birds => :ownerships })
    end

    deep_clone_earth = @earth.deep_clone(:include => { :birds => :ownerships }, :skip_missing_associations => true)
    assert_equal 2, deep_clone_earth.birds.size
    assert deep_clone_earth.birds.detect { |bird| bird.is_a?(Animal::Chicken) }.ownerships.any?
  end

  def test_should_deep_clone_with_block
    deep_clone = @jack.deep_clone(:include => :parrot) do |original, kopy|
      kopy.cloned_from_id = original.id
    end

    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal @jack.id, deep_clone.cloned_from_id
    assert_equal @jack.parrot.id, deep_clone.parrot.cloned_from_id
  end

  def test_should_deep_clone_with_preprocessor
    deep_clone = @jack.deep_clone(:include => :parrot, :preprocessor => lambda { |original, kopy| kopy.cloned_from_id = original.id })

    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal @jack.id, deep_clone.cloned_from_id
    assert_equal @jack.parrot.id, deep_clone.parrot.cloned_from_id
  end

  def test_should_deep_clone_with_postprocessor
    deep_clone = @jack.deep_clone(:include => :parrot, :postprocessor => lambda { |original, kopy| kopy.cloned_from_id = original.id })

    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal @jack.id, deep_clone.cloned_from_id
    assert_equal @jack.parrot.id, deep_clone.parrot.cloned_from_id
  end

  def test_should_deep_clone_habtm_associations
    @person1 = Person.create :name => 'Bill'
    @person2 = Person.create :name => 'Ted'
    @car1 = Car.create :name => 'Mustang'
    @car2 = Car.create :name => 'Camaro'
    @person1.cars << [@car1, @car2]
    @person2.cars << [@car1, @car2]

    deep_clone_person = @person1.deep_clone :include => :cars

    assert deep_clone_person.new_record?
    assert_equal [@person1, @person2, deep_clone_person], @car1.people
    assert_equal [@person1, @person2, deep_clone_person], @car2.people

    assert deep_clone_person.save

    # did NOT deep_clone the Car instances
    assert_equal 2, Car.all.count

    # did deep_clone the correct join table rows
    assert_equal @person1.cars, deep_clone_person.cars
    assert_equal 2, deep_clone_person.cars.count
  end

  def test_should_deep_clone_habtm_associations_with_missing_reverse_association
    @coin = Coin.create :value => 1
    @person = Person.create :name => 'Bill'
    @coin.people << @person

    deep_clone = @coin.deep_clone :include => :people
    assert deep_clone.new_record?
    assert_equal [@person], @coin.people
    assert deep_clone.save
  end

  def test_should_deep_clone_joined_association
    subject1 = Subject.create(:name => 'subject 1')
    subject2 = Subject.create(:name => 'subject 2')
    student = Student.create(:name => 'Parent', :subjects => [subject1, subject2])

    deep_clone = student.deep_clone :include => { :student_assignments => :subject }
    deep_clone.save # Subjects will have been set after save
    assert_equal 2, deep_clone.subjects.size
    [subject1, subject2].each { |subject| assert !deep_clone.subjects.include?(subject) }
  end

  def test_parent_validations_run_on_save_after_clone
    child = ChildWithValidation.create :name => 'Jimmy'
    parent = ParentWithValidation.new :children => [child]
    parent.save :validate => false

    deep_clone_parent = parent.deep_clone :include => :children

    assert !deep_clone_parent.save
    assert deep_clone_parent.new_record?
    assert !deep_clone_parent.valid?
    assert deep_clone_parent.children.first.valid?
    assert_equal deep_clone_parent.errors.messages, :name => ["can't be blank"]
  end

  def test_parent_validations_dont_run_on_save_after_clone
    child = ChildWithValidation.create :name => 'Jimmy'
    parent = ParentWithValidation.new :children => [child]
    parent.save :validate => false

    deep_clone_parent = parent.deep_clone :include => :children, :validate => false

    assert deep_clone_parent.save
    assert !deep_clone_parent.new_record?
    assert !deep_clone_parent.valid?
    assert deep_clone_parent.children.first.valid?
    assert_equal deep_clone_parent.errors.messages, :name => ["can't be blank"]
  end

  def test_child_validations_run_on_save_after_clone
    child = ChildWithValidation.new
    child.save :validate => false
    parent = ParentWithValidation.create :name => 'John', :children => [child]

    deep_clone_parent = parent.deep_clone :include => :children

    assert !deep_clone_parent.save
    assert deep_clone_parent.new_record?
    assert !deep_clone_parent.valid?
    assert !deep_clone_parent.children.first.valid?
    assert_equal deep_clone_parent.errors.messages, :children => ['is invalid']
  end

  def test_child_validations_dont_run_on_save_after_clone_without_validation
    child = ChildWithValidation.new
    child.save :validate => false
    parent = ParentWithValidation.create :name => 'John', :children => [child]

    deep_clone_parent = parent.deep_clone :include => :children, :validate => false
    deep_clone_child = deep_clone_parent.children.first

    assert deep_clone_parent.save
    assert !deep_clone_parent.new_record?
    assert !deep_clone_child.new_record?
    assert !deep_clone_child.valid?
    assert_equal deep_clone_child.errors.messages, :name => ["can't be blank"]
  end

  def test_self_join_has_many
    parent_part = Part.create(:name => 'Parent')
    Part.create(:name => 'Child 1', :parent_part_id => parent_part.id)
    Part.create(:name => 'Child 2', :parent_part_id => parent_part.id)

    deep_clone_part = parent_part.deep_clone :include => :child_parts
    assert deep_clone_part.save
    assert_equal 2, deep_clone_part.child_parts.size
  end

  def test_should_include_has_many_through_associations
    subject1 = Subject.create(:name => 'subject 1')
    subject2 = Subject.create(:name => 'subject 2')
    student = Student.create(:name => 'Parent', :subjects => [subject1, subject2])

    deep_clone = student.deep_clone :include => :subjects
    assert_equal 2, deep_clone.subjects.size
    assert_equal [[student, deep_clone], [student, deep_clone]], deep_clone.subjects.map(&:students)
  end

  def test_should_include_has_one_through_associations
    organization = Organization.create(:name => 'organization')
    contractor = Contractor.create(:name => 'contractor')
    Contract.create(:number => 12_345, :contractor => contractor, :organization => organization)

    deep_clone = organization.deep_clone(:include => :contractor)
    assert_equal organization.contractor, deep_clone.contractor
  end

  def test_should_deep_clone_unsaved_objects
    jack = Pirate.new(:name => 'Jack Sparrow', :nick_name => 'Captain Jack', :age => 30)
    jack.mateys.build(:name => 'John')

    deep_clone = jack.deep_clone(:include => :mateys)
    assert deep_clone.new_record?
    assert_equal 1, deep_clone.mateys.size
    assert_equal 'John', deep_clone.mateys.first.name
  end

  def test_should_reject_copies_if_conditionals_are_passed
    subject1 = Subject.create(:name => 'subject 1')
    subject2 = Subject.create(:name => 'subject 2')
    student = Student.create(:name => 'Parent', :subjects => [subject1, subject2])

    deep_clone = student.deep_clone :include => { :subjects => { :if => lambda { |subject| subject.name == 'subject 2' } } }
    assert_equal 1, deep_clone.subjects.size
    assert_equal 'subject 2', deep_clone.subjects.first.name

    deep_clone = @jack.deep_clone(:include => {
                                    :treasures => { :gold_pieces => { :unless => lambda { |piece| piece.is_a?(Parrot) } } },
                                    :mateys => { :if => lambda { |matey| matey.is_a?(GoldPiece) } }
                                  })

    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.treasures.size
    assert_equal 1, deep_clone.gold_pieces.size
    assert_equal 0, deep_clone.mateys.size
  end

  def test_should_properly_read_conditions_in_arrays
    subject1 = Subject.create(:name => 'subject 1')
    subject2 = Subject.create(:name => 'subject 2')
    student = Student.create(:name => 'Parent', :subjects => [subject1, subject2])

    deep_clone = student.deep_clone(:include => [:subjects => [:if => lambda { |_subject| false }]])
    assert deep_clone.subjects.none?

    deep_clone = student.deep_clone(:include => [:subjects => [:if => lambda { |_subject| true }]])
    assert_equal 2, deep_clone.subjects.size
  end

  def test_should_set_association_to_nil_if_conditionals_fail
    deep_clone = @jack.deep_clone(:include => { :ship => { :unless => lambda { |ship| ship.name == 'Black Pearl' } } })
    assert_nil deep_clone.ship
  end

  def test_should_reject_copies_if_conditionals_are_passed_with_associations
    deep_clone = @ship.deep_clone(:include => [:pirates => [:treasures, :mateys, { :unless => lambda { |pirate| pirate.name == 'Jack Sparrow' } }]])

    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 0, deep_clone.pirates.size

    deep_clone = @ship.deep_clone(:include => { pirates: [:mateys, treasures: :gold_pieces, if: lambda { |pirate| pirate.name == 'Jack Sparrow' }] })
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.pirates.size
    assert_equal 1, deep_clone.pirates.first.treasures.size
    assert_equal 1, deep_clone.pirates.first.treasures.first.gold_pieces.size
  end

  def test_should_reject_copies_if_conditionals_are_passed_with_nested_many_to_many_associations
    @user = User.create :name => 'Jack'
    @order1 = Order.create
    @order2 = Order.create
    @product1 = Product.create  :name => 'Paper'
    @product2 = Product.create  :name => 'Ink'
    @order2.products << [@product1, @product2]
    @user.orders << [@order1, @order2]

    deep_clone = @user.deep_clone(:include => [:orders => [:products => [{ :unless => lambda { |product| product.name == 'Ink' } }]]])

    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.orders.second.products.size

    deep_clone = @user.deep_clone(:include => [:orders => [:products => [{ :if => lambda { |product| product.name == 'Ink' } }]]])
    assert deep_clone.new_record?
    assert deep_clone.save
    assert_equal 1, deep_clone.orders.second.products.size
  end

  def test_should_find_in_dict_for_habtm
    apt = Apartment.create(:number => '101')
    contractor = Contractor.create(:name => 'contractor', :apartments => [apt])

    apt.contractors = [contractor]
    apt.save!

    building = Building.create(:name => 'Tall Building', :contractors => [contractor], :apartments => [apt])

    deep_clone = building.deep_clone(:include => [
                                       :apartments,
                                       {
                                         :contractors => [
                                           :apartments
                                         ]
                                       }
                                     ],
                                     :use_dictionary => true)

    deep_clone.save!

    assert_equal deep_clone.contractors.first.apartments.first.id, deep_clone.apartments.first.id
    assert_equal deep_clone.apartments.first.contractors.first.id, deep_clone.contractors.first.id
  end

  def test_should_not_make_attributes_dirty_for_exceptions
    deep_clone = @jack.deep_clone(:except => :name)
    assert_nil deep_clone.name
    refute deep_clone.name_changed?
  end

  def test_should_not_modify_the_initial_options_hash
    options = { :include => [:pirates => [:treasures, :mateys, { :if => lambda { |pirate| pirate.name == 'Jack Sparrow' } }]], :use_dictionary => true }.freeze
    assert @ship.deep_clone(options)

    options = { :include => [:pirates => [:if => lambda { |pirate| pirate.name == 'Jack Sparrow' }]] }.freeze
    assert @ship.deep_clone(options)
  end
end
