require 'spec_helper'

module KeyOverride
  def other_child
    self[:other_child] || "special result"
  end

  def other_child=(value)
    super(value + " modified")
  end
end

describe "EmbeddedDocument" do
  context "EmbeddedDocuments" do
    before do
      class ::Grandparent
        include MongoMapper::EmbeddedDocument
        key :grandparent, String
      end

      class ::Parent < ::Grandparent
        include MongoMapper::EmbeddedDocument
        key :parent, String
      end

      class ::Child < ::Parent
        include MongoMapper::EmbeddedDocument
        key :child, String
      end

      class ::OtherChild < ::Parent
        include MongoMapper::EmbeddedDocument
        include KeyOverride

        key :other_child, String
      end

      class ::EDocWithAValidation
        include MongoMapper::EmbeddedDocument
        key :name, String, :required => true
      end

      class ::DocWithAValidation
        include MongoMapper::Document
        key :name, String, :required => true
        many :e_doc_with_a_validations
      end
    end

    after do
      Object.send :remove_const, 'Grandparent'         if defined?(::Grandparent)
      Object.send :remove_const, 'Parent'              if defined?(::Parent)
      Object.send :remove_const, 'Child'               if defined?(::Child)
      Object.send :remove_const, 'OtherChild'          if defined?(::OtherChild)
      Object.send :remove_const, 'EDocWithAValidation' if defined?(::EDocWithAValidation)
      Object.send :remove_const, 'DocWithAValidation'  if defined?(::DocWithAValidation)
    end

    context "Including MongoMapper::EmbeddedDocument in a class" do
      before do
        @klass = EDoc()
      end

      it "should add _id key" do
        @klass.keys['_id'].should_not be_nil
      end

      it "should know it is using object id" do
        @klass.using_object_id?.should be_truthy
      end

      it "should know it is not using object id if _id type is changed" do
        @klass.key :_id, String
        @klass.using_object_id?.should be_falsey
      end
    end

    context "Class Methods" do
      it "should include logger" do
        @klass = EDoc()
        @klass.logger.should == MongoMapper.logger
        @klass.logger.should be_instance_of(Logger)
      end

      it "should return false for embeddable" do
        EDoc().embeddable?.should be_truthy
      end

      context "#to_mongo" do
        before { @klass = EDoc() }

        it "should be nil if nil" do
          @klass.to_mongo(nil).should be_nil
        end

        it "should convert to_mongo for other values" do
          doc = @klass.new(:foo => 'bar')
          to_mongo = @klass.to_mongo(doc)
          to_mongo.is_a?(Hash).should be_truthy
          to_mongo['foo'].should == 'bar'
        end
      end

      context "#from_mongo" do
        before { @klass = EDoc() }

        it "should be nil if nil" do
          @klass.from_mongo(nil).should be_nil
        end

        it "should be instance if instance of class" do
          doc = @klass.new
          @klass.from_mongo(doc).should == doc
        end

        it "should be instance if hash of attributes" do
          doc = @klass.from_mongo({:foo => 'bar'})
          doc.instance_of?(@klass).should be_truthy
          doc.foo.should == 'bar'
        end
      end

      context "defining a key" do
        before do
          @document = EDoc()
        end

        it "should work with name" do
          key = @document.key(:name)
          key.name.should == 'name'
        end

        it "should work with name and type" do
          key = @document.key(:name, String)
          key.name.should == 'name'
          key.type.should == String
        end

        it "should work with name, type and options" do
          key = @document.key(:name, String, :required => true)
          key.name.should == 'name'
          key.type.should == String
          key.options[:required].should be_truthy
        end

        it "should work with name and options" do
          key = @document.key(:name, :required => true)
          key.name.should == 'name'
          key.options[:required].should be_truthy
        end

        it "should be tracked per document" do
          @document.key(:name, String)
          @document.key(:age, Integer)
          @document.keys['name'].name.should == 'name'
          @document.keys['name'].type.should == String
          @document.keys['age'].name.should == 'age'
          @document.keys['age'].type.should == Integer
        end

        it "should be redefinable" do
          @document.key(:foo, String)
          @document.keys['foo'].type.should == String
          @document.key(:foo, Integer)
          @document.keys['foo'].type.should == Integer
        end

        it "should create reader method" do
          @document.new.should_not respond_to(:foo)
          @document.key(:foo, String)
          @document.new.should respond_to(:foo)
        end

        it "should create reader before type cast method" do
          @document.new.should_not respond_to(:foo_before_type_cast)
          @document.key(:foo, String)
          @document.new.should respond_to(:foo_before_type_cast)
        end

        it "should create writer method" do
          @document.new.should_not respond_to(:foo=)
          @document.key(:foo, String)
          @document.new.should respond_to(:foo=)
        end

        it "should create boolean method" do
          @document.new.should_not respond_to(:foo?)
          @document.key(:foo, String)
          @document.new.should respond_to(:foo?)
        end
      end

      context "keys" do
        it "should be inherited" do
          Grandparent.keys.keys.sort.should == ['_id', '_type', 'grandparent']
          Parent.keys.keys.sort.should == ['_id', '_type', 'grandparent', 'parent']
          Child.keys.keys.sort.should  == ['_id', '_type', 'child', 'grandparent', 'parent']
        end

        it "should propogate to descendants if key added after class definition" do
          Grandparent.key :foo, String

          Grandparent.keys.keys.sort.should == ['_id', '_type', 'foo', 'grandparent']
          Parent.keys.keys.sort.should      == ['_id', '_type', 'foo', 'grandparent', 'parent']
          Child.keys.keys.sort.should       == ['_id', '_type', 'child', 'foo', 'grandparent', 'parent']
        end

        it "should not add anonymous objects to the ancestor tree" do
          OtherChild.ancestors.any? { |a| a.name.blank? }.should be_falsey
        end

        it "should not include descendant keys" do
          lambda { Parent.new.other_child }.should raise_error(NoMethodError)
        end
      end

      context "descendants" do
        it "should default to an empty array" do
          Child.descendants.should == []
        end

        it "should be recorded" do
          Grandparent.direct_descendants.should contain_exactly(Parent)
          Grandparent.descendants.to_set.should contain_exactly(Parent, Child, OtherChild)

          Parent.descendants.should contain_exactly(Child, OtherChild)
        end
      end
    end

    context "An instance of an embedded document" do
      before do
        @document = EDoc do
          key :name, String
          key :age, Integer
        end
      end

      it "should respond to cache_key" do
        @document.new.should respond_to(:cache_key)
      end

      it "should have access to class logger" do
        doc = @document.new
        doc.logger.should == @document.logger
        doc.logger.should be_instance_of(Logger)
      end

      it "should automatically have an _id key" do
        @document.keys.keys.should include('_id')
      end

      it "should create id during initialization" do
        @document.new._id.should be_instance_of(BSON::ObjectId)
      end

      it "should have id method returns _id" do
        id = BSON::ObjectId.new
        doc = @document.new(:_id => id)
        doc.id.should == id
      end

      it "should convert string object id to mongo object id when assigning id with _id object id type" do
        id = BSON::ObjectId.new
        doc = @document.new(:id => id.to_s)
        doc._id.should == id
        doc.id.should  == id
        doc = @document.new(:_id => id.to_s)
        doc._id.should == id
        doc.id.should  == id
      end

      context "_parent_document" do
        it "should default to nil" do
          @document.new._parent_document.should be_nil
          @document.new._root_document.should be_nil
        end

        it "should set _root_document when setting _parent_document" do
          root = Doc().new
          doc  = @document.new(:_parent_document => root)
          doc._parent_document.should be(root)
          doc._root_document.should be(root)
        end

        it "should set _root_document when setting _parent_document on embedded many" do
          root   = Doc().new
          klass  = EDoc { many :children }
          parent = klass.new(:_parent_document => root, :children => [{}])
          child  = parent.children.first
          child._parent_document.should be(parent)
          child._root_document.should   be(root)
        end
      end

      context "being initialized" do
        it "should accept a hash that sets keys and values" do
          doc = @document.new(:name => 'John', :age => 23)
          doc.attributes.keys.sort.should == ['_id', 'age', 'name']
          doc.attributes['name'].should == 'John'
          doc.attributes['age'].should == 23
        end

        it "should be able to assign keys dynamically" do
          doc = @document.new(:name => 'John', :skills => ['ruby', 'rails'])
          doc.name.should == 'John'
          doc.skills.should == ['ruby', 'rails']
        end

        it "should not throw error if initialized with nil" do
          lambda { @document.new(nil) }.should_not raise_error
        end
      end

      context "initialized when _type key present" do
        before do
          @klass = EDoc('FooBar') { key :_type, String }
        end

        it "should set _type to class name" do
          @klass.new._type.should == 'FooBar'
        end

        it "should ignore _type attribute and always use class" do
          @klass.new(:_type => 'Foo')._type.should == 'FooBar'
        end
      end

      context "attributes=" do
        it "should update values for keys provided" do
          doc = @document.new(:name => 'foobar', :age => 10)
          doc.attributes = {:name => 'new value', :age => 5}
          doc.attributes[:name].should == 'new value'
          doc.attributes[:age].should == 5
        end

        it "should not update values for keys that were not provided" do
          doc = @document.new(:name => 'foobar', :age => 10)
          doc.attributes = {:name => 'new value'}
          doc.attributes[:name].should == 'new value'
          doc.attributes[:age].should == 10
        end

        it "should work with pre-defined methods" do
          @document.class_eval do
            attr_writer :password

            def passwd
              @password
            end
          end

          doc = @document.new(:name => 'foobar', :password => 'secret')
          doc.passwd.should == 'secret'
        end

        it "should type cast key values" do
          doc = @document.new(:name => 1234, :age => '21')
          doc.name.should == '1234'
          doc.age.should == 21
        end
      end

      context "attributes" do
        it "should default to hash with all keys" do
          doc = @document.new
          doc.keys.keys.sort.should == ['_id', 'age', 'name']
          doc.attributes.keys.sort.should == ['_id']
        end

        it "should return all keys with values" do
          doc = @document.new(:name => 'string', :age => nil)
          doc.attributes.keys.sort.should == ['_id', 'name']
          doc.keys.keys.sort.should == ['_id', 'age', 'name']
          doc.attributes.values.should include('string')
          doc.attributes.values.should_not include(nil)
        end

        it "should have indifferent access" do
          doc = @document.new(:name => 'string')
          doc.attributes[:name].should == 'string'
          doc.attributes['name'].should == 'string'
        end
      end

      context "to_mongo" do
        it "should default to hash with _id key" do
          doc = @document.new
          doc.to_mongo.keys.sort.should == ['_id']
          doc.keys.keys.sort.should == ['_id', 'age', 'name']
        end

        it "should return all keys" do
          doc = @document.new(:name => 'string', :age => nil)
          doc.keys.keys.sort.should == ['_id', 'age', 'name']
          doc.to_mongo.keys.sort.should == ['_id','name']
          doc.to_mongo.values.should include('string')
          doc.to_mongo.values.should_not include(nil)
        end
      end

      context "key shorcut access" do
        context "[]" do
          it "should work when key found" do
            doc = @document.new(:name => 'string')
            doc[:name].should == 'string'
          end

          it "should return nil when not found" do
            doc = @document.new(:name => 'string')
            doc[:not_here].should be_nil
          end
        end

        context "[]=" do
          it "should write key value for existing key" do
            doc = @document.new
            doc[:name] = 'string'
            doc[:name].should == 'string'
          end

          it "should create key and write value for missing key" do
            doc = @document.new
            doc[:foo] = 'string'
            doc.class.keys.include?('foo').should be_truthy
            doc[:foo].should == 'string'
          end

           it "should share the new key with the class" do
             doc = @document.new
             doc[:foo] = 'string'
             @document.keys.should include('foo')
           end
        end
      end

      context "reading a key" do
        it "should work for defined keys" do
          doc = @document.new(:name => 'string')
          doc.name.should == 'string'
        end

        it "should raise no method error for undefined keys" do
          doc = @document.new
          lambda { doc.fart }.should raise_error(NoMethodError)
        end

        it "should be accessible for use in the model" do
          @document.class_eval do
            def name_and_age
              "#{self[:name]} (#{self[:age]})"
            end
          end

          doc = @document.new(:name => 'John', :age => 27)
          doc.name_and_age.should == 'John (27)'
        end

        it "should set instance variable" do
          @document.key :foo, Array
          doc = @document.new
          doc.instance_variable_get("@foo").should be_nil
          doc.foo
          doc.instance_variable_get("@foo").should == []
        end

        it "should be overrideable by modules" do
          @document = Doc do
            key :other_child, String
          end

          child = @document.new
          child.other_child.should be_nil

          @document.send :include, KeyOverride

          overriden_child = @document.new
          overriden_child.other_child.should == 'special result'
        end
      end

      context "reading a key before typcasting" do
        it "should work for defined keys" do
          doc = @document.new(:name => 12)
          doc.name_before_type_cast.should == 12
        end

        it "should raise no method error for undefined keys" do
          doc = @document.new
          lambda { doc.foo_before_type_cast }.should raise_error(NoMethodError)
        end

        it "should be accessible for use in a document" do
          doc = @document.new(:name => 12)
          doc.name.should == '12'
          doc.name_before_type_cast.should == 12
        end
      end

      context "writing a key" do
        it "should work for defined keys" do
          doc = @document.new
          doc.name = 'John'
          doc.name.should == 'John'
        end

        it "should raise no method error for undefined keys" do
          doc = @document.new
          lambda { doc.fart = 'poof!' }.should raise_error(NoMethodError)
        end

        it "should type cast value" do
          doc = @document.new
          doc.name = 1234
          doc.name.should == '1234'
          doc.age = '21'
          doc.age.should == 21
        end

        it "should be accessible for use in the model" do
          @document.class_eval do
            def name_and_age=(new_value)
              new_value.match(/([^\(\s]+) \((.*)\)/)
              write_key :name, $1
              write_key :age, $2
            end
          end

          doc = @document.new
          doc.name_and_age = 'Frank (62)'
          doc.name.should == 'Frank'
          doc.age.should == 62
        end

        it "should be overrideable by modules" do
          @document = Doc do
            key :other_child, String
          end

          child = @document.new(:other_child => 'foo')
          child.other_child.should == 'foo'

          @document.send :include, KeyOverride

          overriden_child = @document.new(:other_child => 'foo')
          overriden_child.other_child.should == 'foo modified'
        end
      end # writing a key

      context "checking if a keys value is present" do
        it "should work for defined keys" do
          doc = @document.new
          doc.name?.should be_falsey
          doc.name = 'John'
          doc.name?.should be_truthy
        end

        it "should raise no method error for undefined keys" do
          doc = @document.new
          lambda { doc.fart? }.should raise_error(NoMethodError)
        end
      end

      context "equality" do
        before do
          @oid = BSON::ObjectId.new
        end

        it "should delegate hash to _id" do
          doc = @document.new
          doc.hash.should == doc._id.hash
        end

        it "should delegate eql to ==" do
          doc = @document.new
          other = @document.new
          doc.eql?(other).should == (doc == other)
          doc.eql?(doc).should == (doc == doc)
        end

        it "should know if same object as another" do
          doc = @document.new
          doc.should equal(doc)
          doc.should_not equal(@document.new)
        end

        it "should allow set operations on array of documents" do
          doc = @document.new
          ([doc] & [doc]).should == [doc]
        end

        it "should be equal if id and class are the same" do
          (@document.new('_id' => @oid) == @document.new('_id' => @oid)).should be_truthy
        end

        it "should not be equal if class same but id different" do
          (@document.new('_id' => @oid) == @document.new('_id' => BSON::ObjectId.new)).should be_falsey
        end

        it "should not be equal if id same but class different" do
          another_document = Doc()
          (@document.new('_id' => @oid) == another_document.new('_id' => @oid)).should be_falsey
        end
      end

      context "reading keys with default values" do
        before do
          @document = EDoc do
            key :name,      String,   :default => 'foo'
            key :age,       Integer,  :default => 20
            key :net_worth, Float,    :default => 100.00
            key :active,    Boolean,  :default => true
            key :smart,     Boolean,  :default => false
            key :skills,    Array,    :default => [1]
            key :options,   Hash,     :default => {'foo' => 'bar'}
          end

          @doc = @document.new
        end

        it "should work for strings" do
          @doc.name.should == 'foo'
        end

        it "should work for integers" do
          @doc.age.should == 20
        end

        it "should work for floats" do
          @doc.net_worth.should == 100.00
        end

        it "should work for booleans" do
          @doc.active.should == true
          @doc.smart.should == false
        end

        it "should work for arrays" do
          @doc.skills.should == [1]
          @doc.skills << 2
          @doc.skills.should == [1, 2]
        end

        it "should work for hashes" do
          @doc.options['foo'].should == 'bar'
          @doc.options['baz'] = 'wick'
          @doc.options['baz'].should == 'wick'
        end
      end

      context "#save!" do
         before do
           @root = DocWithAValidation.create(:name => "Root")
           @doc = @root.e_doc_with_a_validations.build :name => "Embedded"
         end

         it "should should save when valid" do
           @doc.save!
           @root.reload.e_doc_with_a_validations.first.should == @doc
         end

         it "should should raise errors when invalid" do
           @doc.name = ''
           lambda { @doc.save! }.should raise_error(MongoMapper::DocumentNotValid)
         end

         it "should should raise errors when root document is invalid" do
           @root.name = ''
           @root.save(:validate => false)
           lambda { @doc.save! }.should raise_error(MongoMapper::DocumentNotValid)
         end
       end
    end # instance of a embedded document
  end
end
