require 'spec_helper'

describe ActiveModel::Associations do
  context "When included Comment class" do
    class Comment
      include ActiveModel::Model
      include ActiveModel::Associations

      attr_accessor :body, :user_id

      belongs_to :user

      def [](attr)
        self.send(attr)
      end

      def []=(attr, value)
        self.send("#{attr}=", value)
      end
    end

    it "extends constructor" do
      comment = Comment.new(body: "foo")
      expect(comment.body).to eq "foo"
      expect(comment.instance_variable_get("@association_cache")).to eq({})
    end

    describe "belongs_to" do
      it "Add belongs_to macro" do
        expect(Comment).to be_respond_to(:belongs_to)
      end

      describe ".belongs_to" do
        let(:comment) { Comment.new }

        it "defines association accessor" do
          expect(comment).to be_respond_to(:user)
          expect(comment).to be_respond_to(:user=)
        end

        describe "defined accessor" do
          let(:user) { User.create(name: "joker1007") }
          let(:comment) { Comment.new(user_id: user.id) }

          it "defined accessor loads target ActiveRecord instance" do
            expect(comment.user).to eq user
          end

          it "receives target ActiveRecord instance, and set foreign_key attributes" do
            other_user = User.create(name: "kakyoin")
            expect { comment.user = other_user }.to change { [comment.user, comment.user_id] }
              .from([user, user.id]).to([other_user, other_user.id])
          end

          it "can validate" do
            expect(comment.valid?).to be_truthy
          end
        end

        describe "defined builder" do
          it "sets foreign_key" do
            comment.create_user(name: "joker1007")
            expect(comment.user).to be_a(User)
            expect(comment.user).to be_persisted
            expect(comment.user_id).not_to be_nil
          end
        end

        context "When set foreign_key manually" do
          let!(:user) { User.create(name: "joker1007") }
          let(:comment) { Comment.new }

          it "can access target ActiveRecord instance" do
            expect { comment.user_id = user.id }.to change { comment.user }
              .from(nil).to(user)
          end
        end

        it "can define polymorphic association" do
          class PolymorhicBelongsToComment < Comment
            attr_accessor :commenter_id, :commenter_type
            belongs_to :commenter, polymorphic: true
          end

          user = User.create(name: "joker1007")
          comment = PolymorhicBelongsToComment.new(commenter_id: user.id, commenter_type: "User")
          expect(comment.commenter).to eq user
        end

        it "can define different class_name association" do
          class DiffClassNameBelongsToComment < Comment
            attr_accessor :commenter_id
            belongs_to :commenter, class_name: "User"
          end

          user = User.create(name: "joker1007")
          comment = DiffClassNameBelongsToComment.new(commenter: user)
          expect(comment.commenter_id).to eq user.id
        end
      end
    end

    describe "has_many" do
      class Group
        include ActiveModel::Model
        include ActiveModel::Associations

        attr_accessor :name
        attr_reader :user_ids

        has_many :users

        def [](attr)
          self.send(attr)
        end

        def []=(attr, value)
          self.send("#{attr}=", value)
        end
      end

      let(:group) { Group.new }

      it "Add has_many macro" do
        expect(Group).to be_respond_to(:has_many)
      end

      describe ".has_many" do
        it "defines association accessor" do
          expect(group).to be_respond_to(:users)
          expect(group).to be_respond_to(:users=)
        end

        describe "defined accessor" do
          let!(:user1) { User.create(name: "joker1007") }
          let!(:user2) { User.create(name: "kakyoin") }
          let(:group) { Group.new(user_ids: [user1.id, user2.id]) }

          it "returns ActiveRecord CollectionProxy of target class" do
            expect(group.users).to eq [user1, user2]
            expect(group.users.find_by(id: user1.id)).to eq user1
          end

          it "receives target ActiveRecord instance Array, and set target_ids attributes" do
            group = Group.new
            expect(group.users).to be_empty
            group.users = [user1, user2]
            expect(group.users).to eq [user1, user2]
          end

          it "replace target, and set target_ids attributes" do
            expect(group.users).to eq [user1, user2]
            group.users = [user1]
            expect(group.users).to eq [user1]
            group.users = []
            expect(group.users).to be_empty
          end

          it "receives target ActiveRecord CollectionProxy, and set target_ids attributes" do
            group = Group.new
            expect(group.users).to be_empty
            group.users = User.all
            expect(group.users).to eq [user1, user2]
          end

          it "can replace having association" do
            user3 = User.create(name: "jotaro")
            group = Group.new
            expect(group.users).to be_empty
            group.users = [user1, user2]
            group.users = [user3]
            expect(group.users).to eq [user3]
          end

          it "can concat records" do
            expect(group.users).to eq [user1, user2]
            user3 = User.create(name: "jotaro")
            group.users << user3
            expect(group.users).to eq [user1, user2, user3]
          end

          it "can validate" do
            expect(group.valid?).to be_truthy
          end
        end

        context "When set target_ids manually" do
          let!(:user) { User.create(name: "joker1007") }
          let(:group) { Group.new }

          it "can access target ActiveRecord instance" do
            expect(group.users).to be_empty
            group.user_ids = [user.id]
            expect(group.users).to eq [user]
          end
        end

        context "When ids attribute is nil" do
          let!(:user) { User.create(name: "joker1007") }
          let(:group) { Group.new }

          it "can concat" do
            expect(group.users).to be_empty
            group.users << user
            expect(group.users).to eq [user]
          end
        end

        it "can define different class_name association" do
          class DiffClassNameHasManyGroup < Group
            attr_reader :member_ids
            has_many :members, class_name: "User"
          end

          user = User.create(name: "joker1007")
          group = DiffClassNameHasManyGroup.new(members: [user])
          expect(group.member_ids).to eq [user.id]
          expect(group.members).to eq [user]
        end

        %i(through dependent source source_type counter_cache as).each do |option_name|
          it "#{option_name} option is unsupported" do
            expect {
              eval <<-CODE
                class #{option_name.to_s.classify}Group
                  include ActiveModel::Model
                  include ActiveModel::Associations

                  has_many :users, #{option_name}: true
                end
              CODE
            }.to raise_error(ArgumentError)
          end
        end
      end
    end
  end
end