require 'test_helper'

class ValidationsTest < Test::Unit::TestCase  
  context "Saving a new document that is invalid" do
    setup do
      @document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        key :name, String, :required => true
      end
      @document.collection.remove
    end
    
    should "not insert document" do
      doc = @document.new
      doc.save
      @document.count.should == 0
    end
    
    should "populate document's errors" do
      doc = @document.new
      doc.errors.size.should == 0
      doc.save
      doc.errors.full_messages.should == ["Name can't be empty"]
    end
  end

  context "Skipping validations when saving" do
    setup do
      @document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        key :name, String, :required => true
      end
      @document.collection.remove
    end

    should "insert document" do
      doc = @document.new
      doc.save(false)
      @document.count.should == 1
    end
  end

  context "Saving a document that is invalid (destructive)" do
    setup do
      @document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        key :name, String, :required => true
      end
      @document.collection.remove
    end
    
    should "raise error" do
      doc = @document.new
      lambda { doc.save! }.should raise_error(MongoMapper::DocumentNotValid)
    end
  end

  context "Creating a document that is invalid (destructive)" do
    setup do
      @document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        key :name, String, :required => true
      end
      @document.collection.remove
    end
    
    should "raise error" do
      lambda { @document.create! }.should raise_error(MongoMapper::DocumentNotValid)
    end

    should "create a new document" do
      instance = @document.create!(:name => "James")
      instance.new_record?.should be_false
    end
  end
  
  context "Saving an existing document that is invalid" do
    setup do
      @document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        key :name, String, :required => true
      end
      @document.collection.remove
      
      @doc = @document.create(:name => 'John Nunemaker')
    end
    
    should "not update document" do
      @doc.name = nil
      @doc.save
      @document.find(@doc.id).name.should == 'John Nunemaker'
    end
    
    should "populate document's errors" do
      @doc.name = nil
      @doc.save
      @doc.errors.full_messages.should == ["Name can't be empty"]
    end
  end
  
  context "Adding validation errors" do
    setup do
      @document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        
        key :action, String
        def action_present
          errors.add(:action, 'is invalid') if action.blank?
        end
      end
      @document.collection.remove
    end
    
    should "work with validate_on_create callback" do
      @document.validate_on_create :action_present
      
      doc = @document.new
      doc.action = nil
      doc.should have_error_on(:action)
      
      doc.action = 'kick'
      doc.should_not have_error_on(:action)
      doc.save
      
      doc.action = nil
      doc.should_not have_error_on(:action)
    end
    
    should "work with validate_on_update callback" do
      @document.validate_on_update :action_present
      
      doc = @document.new
      doc.action = nil
      doc.should_not have_error_on(:action)
      doc.save
      
      doc.action = nil
      doc.should have_error_on(:action)
      
      doc.action = 'kick'
      doc.should_not have_error_on(:action)
    end
  end
  
  context "validating uniqueness of" do
    setup do
      @document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        
        key :name, String
        validates_uniqueness_of :name
      end
      @document.collection.remove
    end

    should "not fail if object is new" do
      doc = @document.new
      doc.should_not have_error_on(:name)
    end

    should "not fail when new object is out of scope" do
      document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        
        key :name
        key :adult
        validates_uniqueness_of :name, :scope => :adult
      end
      doc = document.new("name" => "joe", :adult => true)
      doc.save.should be_true

      doc2 = document.new("name" => "joe", :adult => false)
      doc2.should be_valid

    end

    should "allow to update an object" do
      doc = @document.new("name" => "joe")
      doc.save.should be_true

      @document \
        .stubs(:first) \
        .with(:name => 'joe') \
        .returns(doc)

      doc.name = "joe"
      doc.valid?.should be_true
      doc.should_not have_error_on(:name)
    end

    should "fail if object name is not unique" do
      doc = @document.new("name" => "joe")
      doc.save.should be_true

      @document \
        .stubs(:first) \
        .with(:name => 'joe') \
        .returns(doc)

      doc2 = @document.new("name" => "joe")
      doc2.should have_error_on(:name)
    end
    
    should "allow multiple blank entries if :allow_blank => true" do
      document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        
        key :name
        validates_uniqueness_of :name, :allow_blank => :true
      end
      
      doc = document.new("name" => "")
      doc.save.should be_true

      document \
        .stubs(:first) \
        .with(:name => '') \
        .returns(doc)

      doc2 = document.new("name" => "")
      doc2.should_not have_error_on(:name)
    end

    should "allow entries that differ only in case by default" do
      document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'

        key :name
        validates_uniqueness_of :name
      end

      doc = document.new("name" => "BLAMMO")
      doc.save.should be_true

      doc2 = document.new("name" => "blammo")
      doc2.should_not have_error_on(:name)
    end

    should "fail on entries that differ only in case if :case_sensitive => false" do
      document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'

        key :name
        validates_uniqueness_of :name, :case_sensitive => false
      end

      doc = document.new("name" => "BLAMMO")
      doc.save.should be_true

      doc2 = document.new("name" => "blammo")
      doc2.should have_error_on(:name)
    end

    context "scoped by a single attribute" do
      setup do
        @document = Class.new do
          include MongoMapper::Document
          set_collection_name 'test'
          
          key :name, String
          key :scope, String
          validates_uniqueness_of :name, :scope => :scope
        end
        @document.collection.remove
      end

      should "fail if the same name exists in the scope" do
        doc = @document.new("name" => "joe", "scope" => "one")
        doc.save.should be_true
        
        @document \
          .stubs(:first) \
          .with(:name => 'joe', :scope => "one") \
          .returns(doc)

        doc2 = @document.new("name" => "joe", "scope" => "one")
        doc2.should have_error_on(:name)
      end

      should "pass if the same name exists in a different scope" do
        doc = @document.new("name" => "joe", "scope" => "one")
        doc.save.should be_true

        @document \
          .stubs(:first) \
          .with(:name => 'joe', :scope => 'two') \
          .returns(nil)

        doc2 = @document.new("name" => "joe", "scope" => "two")
        doc2.should_not have_error_on(:name)
      end
    end

    context "scoped by a multiple attributes" do
      setup do
        @document = Class.new do
          include MongoMapper::Document
          set_collection_name 'test'
          
          key :name, String
          key :first_scope, String
          key :second_scope, String
          validates_uniqueness_of :name, :scope => [:first_scope, :second_scope]
        end
        @document.collection.remove
      end
      
      should "fail if the same name exists in the scope" do
        doc = @document.new("name" => "joe", "first_scope" => "one", "second_scope" => "two")
        doc.save.should be_true
        
        @document \
          .stubs(:first) \
          .with(:name => 'joe', :first_scope => 'one', :second_scope => 'two') \
          .returns(doc)

        doc2 = @document.new("name" => "joe", "first_scope" => "one", "second_scope" => "two")
        doc2.should have_error_on(:name)
      end
      
      should "pass if the same name exists in a different scope" do
        doc = @document.new("name" => "joe", "first_scope" => "one", "second_scope" => "two")
        doc.save.should be_true
        
        @document \
          .stubs(:first) \
          .with(:name => 'joe', :first_scope => 'one', :second_scope => 'one') \
          .returns(nil)

        doc2 = @document.new("name" => "joe", "first_scope" => "one", "second_scope" => "one")
        doc2.should_not have_error_on(:name)
      end
    end
  end
  
  context "validates uniqueness of with :unique shortcut" do
    should "work" do
      @document = Class.new do
        include MongoMapper::Document
        set_collection_name 'test'
        
        key :name, String, :unique => true
      end
      @document.collection.remove
      
      doc = @document.create(:name => 'John')
      doc.should_not have_error_on(:name)
      
      @document \
        .stubs(:first) \
        .with(:name => 'John') \
        .returns(doc)
      
      second_john = @document.create(:name => 'John')
      second_john.should have_error_on(:name, 'has already been taken')
    end
  end
end