require 'test_helper'

class QueryingTesting < Test::Unit::TestCase
  def setup
    @document = Doc do
      key :first_name, String
      key :last_name, String
      key :age, Integer
      key :date, Date
    end
  end

  context ".query" do
    setup do
      @query = @document.query
    end

    should "set model to self" do
      @query.model.should == @document
    end

    should "always return new instance" do
      @document.query.should_not equal(@query)
    end

    should "apply options" do
      @document.query(:foo => 'bar')[:foo].should == 'bar'
    end
  end

  context ".criteria_hash" do
    setup do
      @hash = @document.criteria_hash
    end

    should "set object id keys on hash" do
      @hash.object_ids.should == [:_id]
    end

    should "always return new instance" do
      @document.criteria_hash.should_not equal(@hash)
    end

    should "apply provided criteria" do
      @document.criteria_hash(:foo => 'bar')[:foo].should == 'bar'
    end
  end

  context ".create (single document)" do
    setup do
      @doc = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
    end

    should "create a document in correct collection" do
      @document.count.should == 1
    end

    should "automatically set id" do
      @doc.id.should be_instance_of(BSON::ObjectId)
      @doc._id.should be_instance_of(BSON::ObjectId)
    end

    should "no longer be new?" do
      @doc.new?.should be_false
    end

    should "return instance of document" do
      @doc.should be_instance_of(@document)
      @doc.first_name.should == 'John'
      @doc.last_name.should == 'Nunemaker'
      @doc.age.should == 27
    end

    should "not fail if no attributes provided" do
      document = Doc()
      lambda { document.create }.should change { document.count }.by(1)
    end
  end

  context ".create (multiple documents)" do
    setup do
      @docs = @document.create([
        {:first_name => 'John', :last_name => 'Nunemaker', :age => '27'},
        {:first_name => 'Steve', :last_name => 'Smith', :age => '28'},
      ])
    end

    should "create multiple documents" do
      @document.count.should == 2
    end

    should "return an array of doc instances" do
      @docs.map do |doc|
        doc.should be_instance_of(@document)
      end
    end
  end

  context ".update (single document)" do
    setup do
      doc = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc = @document.update(doc._id, {:age => 40})
    end

    should "update attributes provided" do
      @doc.age.should == 40
    end

    should "not update existing attributes that were not set to update" do
      @doc.first_name.should == 'John'
      @doc.last_name.should == 'Nunemaker'
    end

    should "not create new document" do
      @document.count.should == 1
    end

    should "raise error if not provided id" do
      doc = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      lambda { @document.update }.should raise_error(ArgumentError)
    end

    should "raise error if not provided attributes" do
      doc = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      lambda { @document.update(doc._id) }.should raise_error(ArgumentError)
      lambda { @document.update(doc._id, [1]) }.should raise_error(ArgumentError)
    end
  end

  context ".update (multiple documents)" do
    setup do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})

      @docs = @document.update({
        @doc1._id => {:age => 30},
        @doc2._id => {:age => 30},
      })
    end

    should "not create any new documents" do
      @document.count.should == 2
    end

    should "should return an array of doc instances" do
      @docs.map do |doc|
        doc.should be_instance_of(@document)
      end
    end

    should "update the documents" do
      @document.find(@doc1._id).age.should == 30
      @document.find(@doc2._id).age.should == 30
    end

    should "raise error if not a hash" do
      lambda { @document.update([1, 2]) }.should raise_error(ArgumentError)
    end
  end

  context ".find" do
    setup do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @doc3 = @document.create({:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26'})
    end

    should "return nil if nothing provided for find" do
      @document.find.should be_nil
    end

    should "raise document not found if nothing provided for find!" do
      assert_raises(MongoMapper::DocumentNotFound) do
        @document.find!
      end
    end

    context "(with a single id)" do
      should "work" do
        @document.find(@doc1._id).should == @doc1
      end

      should "return nil if document not found with find" do
        @document.find(123).should be_nil
      end

      should "raise error if document not found with find!" do
        assert_raises(MongoMapper::DocumentNotFound) { @document.find!(123) }
      end
    end

    context "(with multiple id's)" do
      should "work as arguments" do
        @document.find(@doc1._id, @doc2._id).should == [@doc1, @doc2]
      end

      should "work as arguments with string ids" do
        @document.find(@doc1._id.to_s, @doc2._id.to_s).should == [@doc1, @doc2]
      end

      should "work as array" do
        @document.find([@doc1._id, @doc2._id]).should == [@doc1, @doc2]
      end

      should "work as array with string ids" do
        @document.find([@doc1._id.to_s, @doc2._id.to_s]).should == [@doc1, @doc2]
      end

      should "compact not found when using find" do
        @document.find(@doc1._id, BSON::ObjectId.new.to_s).should == [@doc1]
      end

      should "raise error if not all found when using find!" do
        assert_raises(MongoMapper::DocumentNotFound) do
          @document.find!(@doc1._id, BSON::ObjectId.new.to_s)
        end
      end

      should "return array if array with one element" do
        @document.find([@doc1._id]).should == [@doc1]
      end
    end

    should "be able to find using condition auto-detection" do
      @document.first(:first_name => 'John').should == @doc1
      @document.all(:last_name => 'Nunemaker', :order => 'age desc').should == [@doc1, @doc3]
    end

    context "#all" do
      should "find all documents with options" do
        @document.all(:order => 'first_name').should == [@doc1, @doc3, @doc2]
        @document.all(:last_name => 'Nunemaker', :order => 'age desc').should == [@doc1, @doc3]
      end
    end

    context "#first" do
      should "find first document with options" do
        @document.first(:order => 'first_name').should == @doc1
        @document.first(:age => 28).should == @doc2
      end
    end

    context "#last" do
      should "find last document with options" do
        @document.last(:order => 'age').should == @doc2
        @document.last(:order => 'age', :age => 28).should == @doc2
      end
    end

    context "#find_each" do
      should "yield all documents found based on options" do
        yield_documents = []
        @document.find_each(:order => "first_name") {|doc| yield_documents << doc }
        yield_documents.should == [@doc1, @doc3, @doc2]

        yield_documents = []
        @document.find_each(:last_name => 'Nunemaker', :order => 'age desc') {|doc| yield_documents << doc }
        yield_documents.should == [@doc1, @doc3]
      end
    end
  end # finding documents

  context ".find_by_id" do
    setup do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
    end

    should "be able to find by id" do
      @document.find_by_id(@doc1._id).should == @doc1
      @document.find_by_id(@doc2._id).should == @doc2
    end

    should "return nil if document not found" do
      @document.find_by_id(1234).should be_nil
    end
  end

  context ".first_or_create" do
    should "find if exists" do
      created = @document.create(:first_name => 'John', :last_name => 'Nunemaker')
      lambda {
        found = @document.first_or_create(:first_name => 'John', :last_name => 'Nunemaker')
        found.should == created
      }.should_not change { @document.count }
    end

    should "create if not found" do
      lambda {
        created = @document.first_or_create(:first_name => 'John', :last_name => 'Nunemaker')
        created.first_name.should == 'John'
        created.last_name.should == 'Nunemaker'
      }.should change { @document.count }.by(1)
    end

    should "disregard non-keys when creating, but use them in the query" do
      assert_nothing_raised do
        @document.create(:first_name => 'John', :age => 9)
        lambda {
          @document.first_or_create(:first_name => 'John', :age.gt => 10).first_name.should == 'John'
        }.should change { @document.count }.by(1)
      end
    end
  end

  context ".first_or_new" do
    should "find if exists" do
      created = @document.create(:first_name => 'John', :last_name => 'Nunemaker')
      lambda {
        found = @document.first_or_new(:first_name => 'John', :last_name => 'Nunemaker')
        found.should == created
      }.should_not change { @document.count }
    end

    should "initialize if not found" do
      lambda {
        created = @document.first_or_new(:first_name => 'John', :last_name => 'Nunemaker')
        created.first_name.should == 'John'
        created.last_name.should == 'Nunemaker'
        created.should be_new
      }.should_not change { @document.count }
    end

    should "disregard non-keys when initializing, but use them in the query" do
      assert_nothing_raised do
        @document.create(:first_name => 'John', :age => 9)
        @document.first_or_new(:first_name => 'John', :age.gt => 10).first_name.should == 'John'
      end
    end
  end

  context ".delete (single document)" do
    setup do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @document.delete(@doc1._id)
    end

    should "remove document from collection" do
      @document.count.should == 1
    end

    should "not remove other documents" do
      @document.find(@doc2._id).should_not be(nil)
    end
  end

  context ".delete (multiple documents)" do
    should "work with multiple arguments" do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @doc3 = @document.create({:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26'})
      @document.delete(@doc1._id, @doc2._id)

      @document.count.should == 1
    end

    should "work with array as argument" do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @doc3 = @document.create({:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26'})
      @document.delete([@doc1._id, @doc2._id])

      @document.count.should == 1
    end
  end

  context ".delete_all" do
    setup do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @doc3 = @document.create({:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26'})
    end

    should "remove all documents when given no conditions" do
      @document.delete_all
      @document.count.should == 0
    end

    should "only remove matching documents when given conditions" do
      @document.delete_all({:first_name => 'John'})
      @document.count.should == 2
    end

    should "convert the conditions to mongo criteria" do
      @document.delete_all(:age => [26, 27])
      @document.count.should == 1
    end
  end

  context ".destroy (single document)" do
    setup do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @document.destroy(@doc1._id)
    end

    should "remove document from collection" do
      @document.count.should == 1
    end

    should "not remove other documents" do
      @document.find(@doc2._id).should_not be(nil)
    end
  end

  context ".destroy (multiple documents)" do
    should "work with multiple arguments" do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @doc3 = @document.create({:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26'})
      @document.destroy(@doc1._id, @doc2._id)

      @document.count.should == 1
    end

    should "work with array as argument" do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @doc3 = @document.create({:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26'})
      @document.destroy([@doc1._id, @doc2._id])

      @document.count.should == 1
    end
  end

  context ".destroy_all" do
    setup do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @doc3 = @document.create({:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26'})
    end

    should "remove all documents when given no conditions" do
      @document.destroy_all
      @document.count.should == 0
    end

    should "only remove matching documents when given conditions" do
      @document.destroy_all(:first_name => 'John')
      @document.count.should == 2
      @document.destroy_all(:age => 26)
      @document.count.should == 1
    end

    should "convert the conditions to mongo criteria" do
      @document.destroy_all(:age => [26, 27])
      @document.count.should == 1
    end
  end

  context ".count" do
    setup do
      @doc1 = @document.create({:first_name => 'John', :last_name => 'Nunemaker', :age => '27'})
      @doc2 = @document.create({:first_name => 'Steve', :last_name => 'Smith', :age => '28'})
      @doc3 = @document.create({:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26'})
    end

    should "count all with no arguments" do
      @document.count.should == 3
    end

    should "return 0 if there are no documents in the collection" do
      @document.delete_all
      @document.count.should == 0
    end

    should "return 0 if the collection does not exist" do
      klass = Doc do
        set_collection_name 'foobarbazwickdoesnotexist'
      end

      klass.count.should == 0
    end

    should "return count for matching documents if conditions provided" do
      @document.count(:age => 27).should == 1
    end

    should "convert the conditions to mongo criteria" do
      @document.count(:age => [26, 27]).should == 2
    end
  end

  context ".exists?" do
    setup do
      @doc = @document.create(:first_name => "James", :age => 27)
    end

    should "be true when at least one document exists" do
      @document.exists?.should == true
    end

    should "be false when no documents exist" do
      @doc.destroy
      @document.exists?.should == false
    end

    should "be true when at least one document exists that matches the conditions" do
      @document.exists?(:first_name => "James").should == true
    end

    should "be false when no documents exist with the provided conditions" do
      @document.exists?(:first_name => "Jean").should == false
    end
  end

  context ".where" do
    setup do
      @doc1 = @document.create(:first_name => 'John',  :last_name => 'Nunemaker', :age => '27')
      @doc2 = @document.create(:first_name => 'Steve', :last_name => 'Smith',     :age => '28')
      @doc3 = @document.create(:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26')
      @query = @document.where(:last_name => 'Nunemaker')
    end

    should "fetch documents when kicker called" do
      docs = @query.all
      docs.should include(@doc1)
      docs.should include(@doc3)
      docs.should_not include(@doc2)
    end

    should "be chainable" do
      @query.sort(:age).first.should == @doc3
    end
  end

  context ".fields" do
    setup do
      @doc1 = @document.create(:first_name => 'John',  :last_name => 'Nunemaker', :age => '27')
      @doc2 = @document.create(:first_name => 'Steve', :last_name => 'Smith',     :age => '28')
      @doc3 = @document.create(:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26')
      @query = @document.fields(:age)
    end

    should "fetch documents when kicker called" do
      docs = @query.all
      docs.should include(@doc1)
      docs.should include(@doc3)
      docs.should include(@doc2)
      docs.each do |doc|
        doc.age.should_not    be_nil
        doc.first_name.should be_nil # key was not loaded
        doc.last_name.should  be_nil # key was not loaded
      end
    end

    should "be chainable" do
      @query.sort(:age).all.map(&:age).should == [26, 27, 28]
    end
  end

  context ".limit" do
    setup do
      @doc1 = @document.create(:first_name => 'John',  :last_name => 'Nunemaker', :age => '27')
      @doc2 = @document.create(:first_name => 'Steve', :last_name => 'Smith',     :age => '28')
      @doc3 = @document.create(:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26')
      @query = @document.limit(2)
    end

    should "fetch documents when kicker called" do
      docs = @query.all
      docs.size.should == 2
    end

    should "be chainable" do
      result = [26, 27]
      @query.sort(:age).all.map(&:age).should == result
      @query.count.should > result.size
    end
  end

  context ".skip" do
    setup do
      @doc1 = @document.create(:first_name => 'John',  :last_name => 'Nunemaker', :age => '27')
      @doc2 = @document.create(:first_name => 'Steve', :last_name => 'Smith',     :age => '28')
      @doc3 = @document.create(:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26')
      @query = @document.skip(1)
    end

    should "fetch documents when kicker called" do
      docs = @query.all
      docs.size.should == 2 # skipping 1 out of 3
    end

    should "be chainable" do
      result = [27, 28]
      @query.sort(:age).all.map(&:age).should == result
      @query.count.should > result.size
    end
  end

  context ".sort" do
    setup do
      @doc1 = @document.create(:first_name => 'John',  :last_name => 'Nunemaker', :age => '27')
      @doc2 = @document.create(:first_name => 'Steve', :last_name => 'Smith',     :age => '28')
      @doc3 = @document.create(:first_name => 'Steph', :last_name => 'Nunemaker', :age => '26')
      @query = @document.sort(:age)
    end

    should "fetch documents when kicker called" do
      @query.all.should == [@doc3, @doc1, @doc2]
    end

    should "be chainable" do
      result = [28]
      @query.skip(2).all.map(&:age).should == result
      @query.count.should > result.size
    end
  end

  context "#update_attributes (new document)" do
    setup do
      @doc = @document.new(:first_name => 'John', :age => '27')
      @doc.update_attributes(:first_name => 'Johnny', :age => 30)
    end

    should "insert document into the collection" do
      @document.count.should == 1
    end

    should "assign an id for the document" do
      @doc.id.should be_instance_of(BSON::ObjectId)
    end

    should "save attributes" do
      @doc.first_name.should == 'Johnny'
      @doc.age.should == 30
    end

    should "update attributes in the database" do
      doc = @doc.reload
      doc.should == @doc
      doc.first_name.should == 'Johnny'
      doc.age.should == 30
    end

    should "allow updating custom attributes" do
      @doc.update_attributes(:gender => 'mALe')
      @doc.reload.gender.should == 'mALe'
    end
  end

  context "#update_attributes (existing document)" do
    setup do
      @doc = @document.create(:first_name => 'John', :age => '27')
      @doc.update_attributes(:first_name => 'Johnny', :age => 30)
    end

    should "not insert document into collection" do
      @document.count.should == 1
    end

    should "update attributes" do
      @doc.first_name.should == 'Johnny'
      @doc.age.should == 30
    end

    should "update attributes in the database" do
      doc = @doc.reload
      doc.first_name.should == 'Johnny'
      doc.age.should == 30
    end
  end

  context "#update_attributes (return value)" do
    setup do
      @document.key :foo, String, :required => true
    end

    should "be true if document valid" do
      @document.new.update_attributes(:foo => 'bar').should be_true
    end

    should "be false if document not valid" do
      @document.new.update_attributes({}).should be_false
    end
  end

  context "#save (new document)" do
    setup do
      @doc = @document.new(:first_name => 'John', :age => '27')
      @doc.save
    end

    should "insert document into the collection" do
      @document.count.should == 1
    end

    should "assign an id for the document" do
      @doc.id.should be_instance_of(BSON::ObjectId)
    end

    should "save attributes" do
      @doc.first_name.should == 'John'
      @doc.age.should == 27
    end

    should "update attributes in the database" do
      doc = @doc.reload
      doc.should == @doc
      doc.first_name.should == 'John'
      doc.age.should == 27
    end

    should "allow to add custom attributes to the document" do
      @doc = @document.new(:first_name => 'David', :age => '26', :gender => 'male', :tags => [1, "2"])
      @doc.save
      doc = @doc.reload
      doc.gender.should == 'male'
      doc.tags.should == [1, "2"]
    end

    should "allow to use custom methods to assign properties" do
      klass = Doc do
        key :name, String

        def realname=(value)
          self.name = value
        end
      end

      person = klass.new(:realname => 'David')
      person.save
      person.reload.name.should == 'David'
    end

    context "with key of type date" do
      should "save the date value as a Time object" do
        doc = @document.new(:first_name => 'John', :age => '27', :date => "2009-12-01")
        doc.save
        doc.date.should == Date.new(2009, 12, 1)
      end
    end
  end

  context "#save (existing document)" do
    setup do
      @doc = @document.create(:first_name => 'John', :age => '27')
      @doc.first_name = 'Johnny'
      @doc.age = 30
      @doc.save
    end

    should "not insert document into collection" do
      @document.count.should == 1
    end

    should "update attributes" do
      @doc.first_name.should == 'Johnny'
      @doc.age.should == 30
    end

    should "update attributes in the database" do
      doc = @doc.reload
      doc.first_name.should == 'Johnny'
      doc.age.should == 30
    end

    should "allow updating custom attributes" do
      @doc = @document.new(:first_name => 'David', :age => '26', :gender => 'male')
      @doc.gender = 'Male'
      @doc.save
      @doc.reload.gender.should == 'Male'
    end
  end

  context "#save (with validations off)" do
    setup do
      @document = Doc do
        key :name, String, :required => true
      end
    end

    should "insert invalid document" do
      doc = @document.new
      doc.expects(:valid?).never
      doc.save(:validate => false)
      @document.count.should == 1
    end
  end

  context "#save (with options)" do
    setup do
      @document = Doc do
        key :name, String
      end
      @document.ensure_index :name, :unique => true
    end
    teardown { drop_indexes(@document) }

    should "allow passing safe" do
      @document.create(:name => 'John')
      assert_raises(Mongo::OperationFailure) do
        @document.new(:name => 'John').save(:safe => true)
      end
    end

    should "raise argument error if options has unsupported key" do
      assert_raises(ArgumentError) do
        @document.new.save(:foo => true)
      end
    end
  end

  context "#save! (with options)" do
    setup do
      @document = Doc { key :name, String }
      @document.ensure_index :name, :unique => true
    end
    teardown { drop_indexes(@document) }

    should "allow passing safe" do
      @document.create(:name => 'John')
      assert_raises(Mongo::OperationFailure) do
        @document.new(:name => 'John').save!(:safe => true)
      end
    end

    should "raise argument error if options has unsupported key" do
      assert_raises(ArgumentError) do
        @document.new.save!(:foo => true)
      end
    end

    should "raise argument error if using validate as that would be pointless with save!" do
      assert_raises(ArgumentError) do
        @document.new.save!(:validate => false)
      end
    end
  end

  context "#destroy" do
    setup do
      @doc = @document.create(:first_name => 'John', :age => '27')
      @doc.destroy
    end

    should "remove the document from the collection" do
      @document.count.should == 0
    end
  end

  context "#delete" do
    setup do
      @doc1 = @document.create(:first_name => 'John', :last_name => 'Nunemaker', :age => '27')
      @doc2 = @document.create(:first_name => 'Steve', :last_name => 'Smith', :age => '28')

      @document.class_eval do
        before_destroy :before_destroy_callback
        after_destroy :after_destroy_callback

        def history; @history ||= [] end
        def before_destroy_callback; history << :after_destroy end
        def after_destroy_callback;  history << :after_destroy end
      end

      @doc1.delete
    end

    should "remove document from collection" do
      @document.count.should == 1
    end

    should "not remove other documents" do
      @document.find(@doc2.id).should_not be(nil)
    end

    should "not call before/after destroy callbacks" do
      @doc1.history.should == []
    end
  end
end