# frozen_string_literal: true

require "cases/helper"
require "models/club"
require "models/member_type"
require "models/member"
require "models/membership"
require "models/sponsor"
require "models/organization"
require "models/member_detail"
require "models/minivan"
require "models/dashboard"
require "models/speedometer"
require "models/category"
require "models/author"
require "models/essay"
require "models/owner"
require "models/post"
require "models/comment"
require "models/categorization"
require "models/customer"
require "models/carrier"
require "models/shop_account"
require "models/customer_carrier"
require "models/cpk"

class HasOneThroughAssociationsTest < ActiveRecord::TestCase
  fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
           :dashboards, :speedometers, :authors, :author_addresses, :posts, :comments, :categories, :essays, :owners

  def setup
    @member = members(:groucho)
  end

  def test_has_one_through_with_has_one
    assert_equal clubs(:boring_club), @member.club
  end

  def test_has_one_through_executes_limited_query
    boring_club = clubs(:boring_club)
    assert_queries_match(/LIMIT|ROWNUM <=|FETCH FIRST/) do
      assert_equal boring_club, @member.general_club
    end
  end

  def test_creating_association_creates_through_record
    new_member = Member.create(name: "Chris")
    new_member.club = Club.create(name: "LRUG")
    assert_not_nil new_member.current_membership
    assert_not_nil new_member.club
  end

  def test_association_create_constructor_creates_through_record
    new_member = Member.create(name: "Chris")
    new_member.create_club
    assert_not_nil new_member.current_membership
    assert_not_nil new_member.club
  end

  def test_creating_association_builds_through_record
    new_member = Member.create(name: "Chris")
    new_club = new_member.association(:club).build
    assert new_member.current_membership
    assert_equal new_club, new_member.club
    assert_predicate new_club, :new_record?
    assert_predicate new_member.current_membership, :new_record?
    assert new_member.save
    assert_predicate new_club, :persisted?
    assert_predicate new_member.current_membership, :persisted?
  end

  def test_association_build_constructor_builds_through_record
    new_member = Member.create(name: "Chris")
    new_club = new_member.build_club
    assert new_member.current_membership
    assert_equal new_club, new_member.club
    assert_predicate new_club, :new_record?
    assert_predicate new_member.current_membership, :new_record?
    assert new_member.save
    assert_predicate new_club, :persisted?
    assert_predicate new_member.current_membership, :persisted?
  end

  def test_creating_association_builds_through_record_for_new
    new_member = Member.new(name: "Jane")
    new_member.club = clubs(:moustache_club)
    assert new_member.current_membership
    assert_equal clubs(:moustache_club), new_member.current_membership.club
    assert_equal clubs(:moustache_club), new_member.club
    assert new_member.save
    assert_equal clubs(:moustache_club), new_member.club
  end

  def test_building_multiple_associations_builds_through_record
    member_type = MemberType.create!
    member = Member.create!
    member_detail_with_one_association = MemberDetail.new(member_type: member_type)
    assert_predicate member_detail_with_one_association.member, :new_record?
    member_detail_with_two_associations = MemberDetail.new(member_type: member_type, admittable: member)
    assert_predicate member_detail_with_two_associations.member, :new_record?
  end

  def test_building_works_with_has_one_through_belongs_to
    new_member = Member.create!(name: "Joe")
    new_member.create_current_membership!
    new_club = new_member.build_club

    assert_equal(new_member.club, new_club)
  end

  def test_creating_multiple_associations_creates_through_record
    member_type = MemberType.create!
    member = Member.create!
    member_detail_with_one_association = MemberDetail.create!(member_type: member_type)
    assert_not_predicate member_detail_with_one_association.member, :new_record?
    member_detail_with_two_associations = MemberDetail.create!(member_type: member_type, admittable: member)
    assert_not_predicate member_detail_with_two_associations.member, :new_record?
  end

  def test_creating_association_sets_both_parent_ids_for_new
    member = Member.new(name: "Sean Griffin")
    club = Club.new(name: "Da Club")

    member.club = club

    member.save!

    assert member.id
    assert club.id
    assert_equal member.id, member.current_membership.member_id
    assert_equal club.id, member.current_membership.club_id
  end

  def test_replace_target_record
    new_club = Club.create(name: "Marx Bros")
    @member.club = new_club
    @member.reload
    assert_equal new_club, @member.club
  end

  def test_replacing_target_record_deletes_old_association
    assert_no_difference "Membership.count" do
      new_club = Club.create(name: "Bananarama")
      @member.club = new_club
      @member.reload
    end
  end

  def test_set_record_to_nil_should_delete_association
    @member.club = nil
    @member.reload
    assert_nil @member.current_membership
    assert_nil @member.club
  end

  def test_set_record_after_delete_association
    @member.club = nil
    @member.club = clubs(:moustache_club)
    @member.reload
    assert_equal clubs(:moustache_club), @member.club
  end

  def test_has_one_through_polymorphic
    assert_equal clubs(:moustache_club), @member.sponsor_club
  end

  def test_has_one_through_eager_loading
    members = assert_queries_count(3) do # base table, through table, clubs table
      Member.all.merge!(includes: :club, where: ["name = ?", "Groucho Marx"]).to_a
    end
    assert_equal 1, members.size
    assert_not_nil assert_no_queries { members[0].club }
  end

  def test_has_one_through_eager_loading_through_polymorphic
    members = assert_queries_count(3) do # base table, through table, clubs table
      Member.all.merge!(includes: :sponsor_club, where: ["name = ?", "Groucho Marx"]).to_a
    end
    assert_equal 1, members.size
    assert_not_nil assert_no_queries { members[0].sponsor_club }
  end

  def test_has_one_through_with_conditions_eager_loading
    # conditions on the through table
    assert_equal clubs(:moustache_club), Member.all.merge!(includes: :favorite_club).find(@member.id).favorite_club
    memberships(:membership_of_favorite_club).update_columns(favorite: false)
    assert_nil Member.all.merge!(includes: :favorite_club).find(@member.id).reload.favorite_club

    # conditions on the source table
    assert_equal clubs(:moustache_club), Member.all.merge!(includes: :hairy_club).find(@member.id).hairy_club
    clubs(:moustache_club).update_columns(name: "Association of Clean-Shaven Persons")
    assert_nil Member.all.merge!(includes: :hairy_club).find(@member.id).reload.hairy_club
  end

  def test_has_one_through_polymorphic_with_source_type
    assert_equal members(:groucho), clubs(:moustache_club).sponsored_member
  end

  def test_eager_has_one_through_polymorphic_with_source_type
    clubs = Club.all.merge!(includes: :sponsored_member, where: ["name = ?", "Moustache and Eyebrow Fancier Club"]).to_a
    # Only the eyebrow fanciers club has a sponsored_member
    assert_not_nil assert_no_queries { clubs[0].sponsored_member }
  end

  def test_has_one_through_nonpreload_eagerloading
    members = assert_queries_count(1) do
      Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
    end
    assert_equal 1, members.size
    assert_not_nil assert_no_queries { members[0].club }
  end

  def test_has_one_through_nonpreload_eager_loading_through_polymorphic
    members = assert_queries_count(1) do
      Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
    end
    assert_equal 1, members.size
    assert_not_nil assert_no_queries { members[0].sponsor_club }
  end

  def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
    Sponsor.new(sponsor_club: clubs(:outrageous_club), sponsorable: members(:groucho)).save!
    members = assert_queries_count(1) do
      Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a # force fallback
    end
    assert_equal 1, members.size
    assert_not_nil assert_no_queries { members[0].sponsor_club }
    assert_equal clubs(:outrageous_club), members[0].sponsor_club
  end

  def test_uninitialized_has_one_through_should_return_nil_for_unsaved_record
    assert_nil Member.new.club
  end

  def test_assigning_association_correctly_assigns_target
    new_member = Member.create(name: "Chris")
    new_member.club = new_club = Club.create(name: "LRUG")
    assert_equal new_club, new_member.association(:club).target
  end

  def test_has_one_through_proxy_should_not_respond_to_private_methods
    assert_raise(NoMethodError) { clubs(:moustache_club).private_method }
    assert_raise(NoMethodError) { @member.club.private_method }
  end

  def test_has_one_through_proxy_should_respond_to_private_methods_via_send
    clubs(:moustache_club).send(:private_method)
    assert_nothing_raised do
      @member.club.send(:private_method)
    end
  end

  def test_assigning_to_has_one_through_preserves_decorated_join_record
    @organization = organizations(:nsa)
    assert_difference "MemberDetail.count", 1 do
      @member_detail = MemberDetail.new(extra_data: "Extra")
      @member.member_detail = @member_detail
      @member.organization = @organization
    end
    assert_equal @organization, @member.organization
    assert_includes @organization.members, @member
    assert_equal "Extra", @member.member_detail.extra_data
  end

  def test_reassigning_has_one_through
    @organization = organizations(:nsa)
    @new_organization = organizations(:discordians)

    assert_difference "MemberDetail.count", 1 do
      @member_detail = MemberDetail.new(extra_data: "Extra")
      @member.member_detail = @member_detail
      @member.organization = @organization
    end
    assert_equal @organization, @member.organization
    assert_equal "Extra", @member.member_detail.extra_data
    assert_includes @organization.members, @member
    assert_not_includes @new_organization.members, @member

    assert_no_difference "MemberDetail.count" do
      @member.organization = @new_organization
    end
    assert_equal @new_organization, @member.organization
    assert_equal "Extra", @member.member_detail.extra_data
    assert_not_includes @organization.members, @member
    assert_includes @new_organization.members, @member
  end

  def test_preloading_has_one_through_on_belongs_to
    MemberDetail.delete_all
    assert_not_nil @member.member_type
    @organization = organizations(:nsa)
    @member_detail = MemberDetail.new
    @member.member_detail = @member_detail
    @member.organization = @organization
    @member_details = assert_queries_count(3) do
      MemberDetail.all.merge!(includes: :member_type).to_a
    end
    @new_detail = @member_details[0]
    assert_predicate @new_detail.send(:association, :member_type), :loaded?
    assert_no_queries { @new_detail.member_type }
  end

  def test_save_of_record_with_loaded_has_one_through
    @club = @member.club
    assert_not_nil @club.sponsored_member

    assert_nothing_raised do
      Club.find(@club.id).save!
      Club.all.merge!(includes: :sponsored_member).find(@club.id).save!
    end

    @club.sponsor.destroy

    assert_nothing_raised do
      Club.find(@club.id).save!
      Club.all.merge!(includes: :sponsored_member).find(@club.id).save!
    end
  end

  def test_through_belongs_to_after_destroy
    @member_detail = MemberDetail.new(extra_data: "Extra")
    @member.member_detail = @member_detail
    @member.save!

    assert_not_nil @member_detail.member_type
    @member_detail.destroy
    assert_queries_count(1) do
      @member_detail.association(:member_type).reload
      assert_not_nil @member_detail.member_type
    end

    @member_detail.member.destroy
    assert_queries_count(1) do
      @member_detail.association(:member_type).reload
      assert_nil @member_detail.member_type
    end
  end

  def test_value_is_properly_quoted
    minivan = Minivan.find("m1")
    assert_nothing_raised do
      minivan.dashboard
    end
  end

  def test_has_one_through_polymorphic_with_primary_key_option
    assert_equal categories(:general), authors(:david).essay_category

    authors = Author.joins(:essay_category).where("categories.id" => categories(:general).id)
    assert_equal authors(:david), authors.first

    assert_equal owners(:blackbeard), authors(:david).essay_owner

    authors = Author.joins(:essay_owner).where("owners.name = 'blackbeard'")
    assert_equal authors(:david), authors.first
  end

  def test_has_one_through_with_primary_key_option
    assert_equal categories(:general), authors(:david).essay_category_2

    authors = Author.joins(:essay_category_2).where("categories.id" => categories(:general).id)
    assert_equal authors(:david), authors.first
  end

  def test_has_one_through_with_default_scope_on_join_model
    assert_equal posts(:welcome).comments.order("id").first, authors(:david).comment_on_first_post
  end

  def test_has_one_through_many_raises_exception
    assert_raise(ActiveRecord::HasOneThroughCantAssociateThroughCollection) do
      members(:groucho).club_through_many
    end
  end

  def test_has_one_through_polymorphic_association
    assert_raise(ActiveRecord::HasOneAssociationPolymorphicThroughError) do
      @member.premium_club
    end
  end

  def test_has_one_through_belongs_to_should_update_when_the_through_foreign_key_changes
    minivan = minivans(:cool_first)

    minivan.dashboard
    proxy = minivan.send(:association_instance_get, :dashboard)

    assert_not_predicate proxy, :stale_target?
    assert_equal dashboards(:cool_first), minivan.dashboard

    minivan.speedometer_id = speedometers(:second).id

    assert_predicate proxy, :stale_target?
    assert_equal dashboards(:second), minivan.dashboard
  end

  def test_has_one_through_belongs_to_setting_belongs_to_foreign_key_after_nil_target_loaded
    minivan = Minivan.new

    minivan.dashboard
    proxy = minivan.send(:association_instance_get, :dashboard)

    minivan.speedometer_id = speedometers(:second).id

    assert_predicate proxy, :stale_target?
    assert_equal dashboards(:second), minivan.dashboard
  end

  def test_assigning_has_one_through_belongs_to_with_new_record_owner
    minivan   = Minivan.new
    dashboard = dashboards(:cool_first)

    minivan.dashboard = dashboard

    assert_equal dashboard, minivan.dashboard
    assert_equal dashboard, minivan.speedometer.dashboard
  end

  def test_has_one_through_with_custom_select_on_join_model_default_scope
    assert_equal clubs(:boring_club), members(:groucho).selected_club
  end

  def test_has_one_through_relationship_cannot_have_a_counter_cache
    assert_raise(ArgumentError) do
      Class.new(ActiveRecord::Base) do
        has_one :thing, through: :other_thing, counter_cache: true
      end
    end
  end

  def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
    customer = Customer.create!
    carrier = Carrier.create!
    customer_carrier = CustomerCarrier.create!(
      customer: customer,
      carrier: carrier,
    )
    account = ShopAccount.create!(customer_carrier: customer_carrier)

    CustomerCarrier.current_customer = customer

    account_carrier = account.carrier
    assert_equal carrier, account_carrier

    CustomerCarrier.current_customer = nil

    other_carrier = Carrier.create!
    other_customer = Customer.create!
    other_customer_carrier = CustomerCarrier.create!(
      customer: other_customer,
      carrier: other_carrier,
    )
    other_account = ShopAccount.create!(customer_carrier: other_customer_carrier)

    account_carrier = other_account.carrier
    assert_equal other_carrier, account_carrier
  ensure
    CustomerCarrier.current_customer = nil
  end

  def test_loading_cpk_association_with_unpersisted_owner
    order = Cpk::Order.create!(shop_id: 1)
    book = Cpk::BookWithOrderAgreements.new(id: [1, 2], order: order)
    order_agreement = Cpk::OrderAgreement.create!(order: order)

    assert_equal(order_agreement, book.order_agreement)
  end

  def test_cpk_stale_target
    order = Cpk::Order.create!(shop_id: 1)
    book = Cpk::BookWithOrderAgreements.create!(id: [1, 2], order: order)
    Cpk::OrderAgreement.create!(order: order)

    book.order_agreement
    book.order = Cpk::Order.new

    assert_predicate(book.association(:order_agreement), :stale_target?)
  end
end
