require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper'))

describe DataMapper::Associations::OneToMany do

  load_models_for_metaphor :vehicles

  before do
    @class = Class.new do
      def self.name
        'User'
      end

      include DataMapper::Resource

      property :user_id, Integer, :key => true
    end
  end

  it 'should provide #has' do
    @class.should respond_to(:has)
  end

  describe '#has' do
    it 'should return a Relationship' do
      @class.has(@class.n, :orders).should be_kind_of(DataMapper::Associations::Relationship)
    end

    describe 'relationship' do
      before do
        @relationship = mock('relationship')
        DataMapper::Associations::Relationship.stub!(:new).and_return(@relationship)
      end

      it 'should receive the name' do
        DataMapper::Associations::Relationship.should_receive(:new) do |name,_,_,_,_|
          name.should == :orders
        end
        @class.has(@class.n, :orders)
      end

      it 'should receive the repository name' do
        DataMapper::Associations::Relationship.should_receive(:new) do |_,repository_name,_,_,_|
          repository_name.should == :mock
        end
        repository(:mock) do
          @class.has(@class.n, :orders)
        end
      end

      it 'should receive the child model name when passed in as class_name' do
        DataMapper::Associations::Relationship.should_receive(:new) do |_,_,child_model_name,_,_|
          child_model_name.should == 'Company::Order'
        end
        @class.has(@class.n, :orders, :class_name => 'Company::Order')
      end

      it 'should receive the child model name when class_name not passed in' do
        DataMapper::Associations::Relationship.should_receive(:new) do |_,_,child_model_name,_,_|
          child_model_name.should == 'Order'
        end
        @class.has(@class.n, :orders)
      end

      it 'should receive the parent model name' do
        DataMapper::Associations::Relationship.should_receive(:new) do |_,_,_,parent_model_name,_|
          parent_model_name.should == @class
        end
        @class.has(@class.n, :orders)
      end

      it 'should receive the parent model name' do
        options = { :min => 0, :max => 100 }
        DataMapper::Associations::Relationship.should_receive(:new) do |_,_,_,parent_model_name,_|
          options.object_id.should == options.object_id
        end
        @class.has(@class.n, :orders, options)
      end
    end

    it 'should add an accessor for the proxy' do
      @class.new.should_not respond_to(:orders)
      @class.has(@class.n, :orders)
      @class.new.should respond_to(:orders)
    end

    describe 'proxy accessor' do
      before :all do
        class User
          include DataMapper::Resource
        end

        class Order
          include DataMapper::Resource
        end
      end

      it 'should return a OneToMany::Proxy' do
        @class.has(@class.n, :orders)
        @class.new.orders.should be_kind_of(DataMapper::Associations::OneToMany::Proxy)
      end
    end
  end

  it 'should work with classes inside modules'
end

describe DataMapper::Associations::OneToMany::Proxy do
  before do
    @parent       = mock('parent', :new_record? => true, :kind_of? => true)
    @resource     = mock('resource', :null_object => true)
    @collection   = []
    @parent_key   = mock('parent key', :get => [])
    @repository   = mock('repository', :save => nil, :kind_of? => true)
    @relationship = mock('relationship', :get_children => @collection, :query => {}, :kind_of? => true, :child_key => [], :parent_key => @parent_key)
    @association  = DataMapper::Associations::OneToMany::Proxy.new(@relationship, @parent)
  end

  describe 'a method that relates the resource', :shared => true do
    it 'should add the resource to the collection' do
      @association.should_not include(@resource)
      do_add.should == return_value
      @association.should include(@resource)
    end

    it 'should not automatically save that the resource was added to the association' do
      @relationship.should_not_receive(:attach_parent)
      do_add.should == return_value
    end

    it 'should persist the addition after saving the association' do
      @relationship.should_receive(:with_repository).with(@resource).and_yield(@repository)
      do_add.should == return_value
      @relationship.should_receive(:attach_parent).with(@resource, @parent)
      @association.save
    end
  end

  describe 'a method that orphans the resource', :shared => true do
    before do
      @association << @resource
    end

    it 'should remove the resource from the collection' do
      @association.should include(@resource)
      do_remove.should == return_value
      @association.should_not include(@resource)
    end

    it 'should not automatically save that the resource was removed from the association' do
      @relationship.should_not_receive(:attach_parent)
      do_remove.should == return_value
    end

    it 'should persist the removal after saving the association' do
      @relationship.should_receive(:with_repository).with(@resource).and_yield(@repository)
      do_remove.should == return_value
      @relationship.should_receive(:attach_parent).with(@resource, nil)
      @association.save
    end
  end

  it 'should provide #<<' do
    @association.should respond_to(:<<)
  end

  describe '#<<' do
    def do_add
      @association << @resource
    end

    def return_value
      @association
    end

    it_should_behave_like 'a method that relates the resource'
  end

  it 'should provide #push' do
    @association.should respond_to(:push)
  end

  describe '#push' do
    def do_add
      @association.push(@resource)
    end

    def return_value
      @association
    end

    it_should_behave_like 'a method that relates the resource'
  end

  it 'should provide #unshift' do
    @association.should respond_to(:unshift)
  end

  describe '#unshift' do
    def do_add
      @association.unshift(@resource)
    end

    def return_value
      @association
    end

    it_should_behave_like 'a method that relates the resource'
  end

  it 'should provide #replace' do
    @association.should respond_to(:replace)
  end

  describe '#replace' do
    before do
      @children = [
        mock('child 1', :save => true),
        mock('child 2', :save => true),
      ]
      @collection << @resource
      @collection.stub!(:loaded?).and_return(true)
      @relationship.stub!(:attach_parent)
    end

    def do_replace
      @association.replace(@children)
    end

    def return_value
      @association
    end

    it 'should remove the resource from the collection' do
      @association.should include(@resource)
      do_replace.should == return_value
      @association.should_not include(@resource)
    end

    it 'should not automatically save that the resource was removed from the association' do
      @relationship.should_not_receive(:attach_parent)
      do_replace.should == return_value
    end

    it 'should persist the removal after saving the association' do
      do_replace.should == return_value
      @relationship.should_receive(:with_repository).exactly(3).times.and_yield(@repository)
      @relationship.should_receive(:attach_parent).with(@resource, nil)
      @association.save
    end

    it 'should not automatically save that the children were added to the association' do
      @relationship.should_not_receive(:attach_parent)
      do_replace.should == return_value
    end

    it 'should persist the addition after saving the association' do
      do_replace.should == return_value
      @relationship.should_receive(:with_repository).exactly(3).times.and_yield(@repository)
      @relationship.should_receive(:attach_parent).with(@children[0], @parent)
      @relationship.should_receive(:attach_parent).with(@children[1], @parent)
      @association.save
    end
  end

  it 'should provide #pop' do
    @association.should respond_to(:pop)
  end

  describe '#pop' do
    def do_remove
      @association.pop
    end

    def return_value
      @resource
    end

    it_should_behave_like 'a method that orphans the resource'
  end

  it 'should provide #shift' do
    @association.should respond_to(:shift)
  end

  describe '#shift' do
    def do_remove
      @association.shift
    end

    def return_value
      @resource
    end

    it_should_behave_like 'a method that orphans the resource'
  end

  it 'should provide #delete' do
    @association.should respond_to(:delete)
  end

  describe '#delete' do
    def do_remove
      @association.delete(@resource)
    end

    def return_value
      @resource
    end

    it_should_behave_like 'a method that orphans the resource'
  end

  it 'should provide #delete_at' do
    @association.should respond_to(:delete_at)
  end

  describe '#delete_at' do
    def do_remove
      @association.delete_at(0)
    end

    def return_value
      @resource
    end

    it_should_behave_like 'a method that orphans the resource'
  end

  it 'should provide #clear' do
    @association.should respond_to(:clear)
  end

  describe '#clear' do
    def do_remove
      @association.clear
    end

    def return_value
      @association
    end

    it_should_behave_like 'a method that orphans the resource'

    it 'should empty the collection' do
      @association << mock('other resource', :new_record? => false)
      @association.should have(2).entries
      do_remove
      @association.should be_empty
    end
  end

  it 'should provide #reload' do
    @association.should respond_to(:reload)
  end

  describe '#reload' do
    before do
      @children = [ mock('child 1', :save => true), mock('child 2', :save => true) ]
      @relationship.stub!(:get_children).and_return(@children)
    end

    it 'should set the @children ivar to nil' do
      @association.__send__(:children).should == @children # Sanity check.

      # We can't test the value of the @children instance variable since
      # #instance_variable_get will be run on @children (thanks to
      # Proxy#method_missing). Instead, test that Relationship#get_children is
      # run -- if @children wasn't set to nil, this expectation should fail.
      @relationship.should_receive(:get_children).once.and_return(@children)
      @association.reload

      # Trigger #get_children on the relationship.
      @association.__send__(:children).should == @children
    end

    it 'should return self' do
      @association.reload.should be_kind_of(DataMapper::Associations::OneToMany::Proxy)
      @association.reload.object_id.should == @association.object_id
    end
  end

  describe 'when deleting the parent' do
    it 'should delete all the children without calling destroy if relationship :dependent is :delete_all'

    it 'should destroy all the children if relationship :dependent is :destroy'

    it 'should set the parent key for each child to nil if relationship :dependent is :nullify'

    it 'should restrict the parent from being deleted if a child remains if relationship :dependent is restrict'

    it 'should be restrict by default if relationship :dependent is not specified'
  end
end
