require 'spec_helper'

module Modifiers
  describe "Modifiers" do
    let(:page_class_with_compound_key) {
      Doc do
        key :_id,         Hash, :default => lambda { Hash['n', 42, 'i', BSON::ObjectId.new] }
        key :title,       String
        key :day_count,   Integer, :default => 0
        key :week_count,  Integer, :default => 0
        key :month_count, Integer, :default => 0
        key :tags,        Array
      end
    }

    let(:page_class_with_standard_key) {
      Doc do
        key :title,       String
        key :day_count,   Integer, :default => 0
        key :week_count,  Integer, :default => 0
        key :month_count, Integer, :default => 0
        key :tags,        Array
      end
    }

    def assert_page_counts(page, day_count, week_count, month_count)
      doc = page.collection.find({:_id => page.id}).first
      doc.should be_present, "Could not find document"
      doc.fetch('day_count').should == day_count
      doc.fetch('week_count').should == week_count
      doc.fetch('month_count').should == month_count
    end

    def assert_keys_removed(page, *keys)
      page.class.collection.find({:_id => page.id}).first.tap do |doc|
        doc.should be_present, "Could not find document"
        (doc.keys & keys).should be_empty, "Expected to not have keys #{keys.inspect}, got #{(keys & doc.keys).inspect}"
      end
    end

    context "ClassMethods" do
      let!(:page_class) { page_class_with_standard_key }
      let!(:page)  { page_class.create(:title => 'Home') }
      let!(:page2) { page_class.create(:title => 'Home') }

      context "unset" do
        let!(:page)  { page_class.create(:title => 'Home', :tags => %w(foo bar)) }

        it "should work with criteria and keys" do
          page_class.unset({:title => 'Home'}, :title, :tags)
          assert_keys_removed page, :title, :tags
          assert_keys_removed page2, :title, :tags
        end

        it "should work with ids and keys" do
          page_class.unset(page.id, page2.id, :title, :tags)
          assert_keys_removed page, :title, :tags
          assert_keys_removed page2, :title, :tags
        end

        context "additional options (upsert & safe)" do
          it "should be able to pass upsert option" do
            new_key_value = DateTime.now.to_s
            page_class.unset({:title => new_key_value, :tags => %w(foo bar)}, :tags, {:upsert => true})
            page_class.count(:title => new_key_value).should == 1
            page_class.first(:title => new_key_value).tags.should == []
          end

          it "should be able to pass safe option" do
            page_class.create(:title => "Better Be Safe than Sorry")

            Mongo::Collection.any_instance.should_receive(:update_many).with(
              {:title => "Better Be Safe than Sorry"},
              {'$unset' => {:tags => 1}},
              {:w => 1}
            )
            page_class.unset({:title => "Better Be Safe than Sorry"}, :tags, {:w => 1})
          end

          it "should be able to pass both safe and upsert options" do
            new_key_value = DateTime.now.to_s
            page_class.unset({:title => new_key_value, :tags => %w(foo bar)}, :tags, {:upsert => true, :safe => true})
            page_class.count(:title => new_key_value).should == 1
            page_class.first(:title => new_key_value).tags.should == []
          end
        end
      end

      context "increment" do
        it "should work with criteria and modifier hashes" do
          page_class.increment({:title => 'Home'}, :day_count => 1, :week_count => 2, :month_count => 3)

          assert_page_counts page, 1, 2, 3
          assert_page_counts page2, 1, 2, 3
        end

        it "should work with ids and modifier hash" do
          page_class.increment(page.id, page2.id, :day_count => 1, :week_count => 2, :month_count => 3)

          assert_page_counts page, 1, 2, 3
          assert_page_counts page2, 1, 2, 3
        end

        it "should work with ids given as strings" do
          page_class.increment(page.id.to_s, page2.id.to_s, :day_count => 1, :week_count => 2, :month_count => 3)

          assert_page_counts page, 1, 2, 3
          assert_page_counts page2, 1, 2, 3
        end
      end

      context "decrement" do
        let!(:page)  { page_class.create(:title => 'Home', :day_count => 1, :week_count => 2, :month_count => 3) }
        let!(:page2) { page_class.create(:title => 'Home', :day_count => 1, :week_count => 2, :month_count => 3) }

        it "should work with criteria and modifier hashes" do
          page_class.decrement({:title => 'Home'}, :day_count => 1, :week_count => 2, :month_count => 3)

          assert_page_counts page, 0, 0, 0
          assert_page_counts page2, 0, 0, 0
        end

        it "should work with ids and modifier hash" do
          page_class.decrement(page.id, page2.id, :day_count => 1, :week_count => 2, :month_count => 3)

          assert_page_counts page, 0, 0, 0
          assert_page_counts page2, 0, 0, 0
        end

        it "should decrement with positive or negative numbers" do
          page_class.decrement(page.id, page2.id, :day_count => -1, :week_count => 2, :month_count => -3)

          assert_page_counts page, 0, 0, 0
          assert_page_counts page2, 0, 0, 0
        end

        it "should work with ids given as strings" do
          page_class.decrement(page.id.to_s, page2.id.to_s, :day_count => -1, :week_count => 2, :month_count => -3)

          assert_page_counts page, 0, 0, 0
          assert_page_counts page2, 0, 0, 0
        end
      end

      context "set" do
        it "should work with criteria and modifier hashes" do
          page_class.set({:title => 'Home'}, :title => 'Home Revised')

          page.reload
          page.title.should == 'Home Revised'

          page2.reload
          page2.title.should == 'Home Revised'
        end

        it "should work with ids and modifier hash" do
          page_class.set(page.id, page2.id, :title => 'Home Revised')

          page.reload
          page.title.should == 'Home Revised'

          page2.reload
          page2.title.should == 'Home Revised'
        end

        it "should typecast values before querying" do
          page_class.key :tags, Set

          lambda {
            page_class.set(page.id, :tags => ['foo', 'bar'].to_set)
            page.reload
            page.tags.should == Set.new(['foo', 'bar'])
          }.should_not raise_error
        end

        it "should not typecast keys that are not defined in document and have no default typecasing" do
          lambda {
            page_class.set(page.id, :colors => ['red', 'green'].to_set)
          }.should raise_error(BSON::Error::UnserializableClass, /does not define its BSON serialized type/)
        end

        it "should set keys that are not defined in document" do
          page_class.set(page.id, :colors => %w[red green])
          page.reload
          page[:colors].should == %w[red green]
        end

        context "additional options (upsert & safe)" do
          it "should be able to pass upsert option" do
            new_key_value = DateTime.now.to_s
            page_class.set({:title => new_key_value}, {:day_count => 1}, {:upsert => true})
            page_class.count(:title => new_key_value).should == 1
            page_class.first(:title => new_key_value).day_count.should == 1
          end

          it "should be able to pass safe option" do
            page_class.create(:title => "Better Be Safe than Sorry")

            Mongo::Collection.any_instance.should_receive(:update_many).with(
              {:title => "Better Be Safe than Sorry"},
              {'$set' => {:title => "I like safety."}},
              {:w => 1}
            )
            page_class.set({:title => "Better Be Safe than Sorry"}, {:title => "I like safety."}, {:safe => true})
          end

          it "should be able to pass both safe and upsert options" do
            new_key_value = DateTime.now.to_s
            page_class.set({:title => new_key_value}, {:day_count => 1}, {:upsert => true, :safe => true})
            page_class.count(:title => new_key_value).should == 1
            page_class.first(:title => new_key_value).day_count.should == 1
          end
        end
      end

      context "push" do
        it "should work with criteria and modifier hashes" do
          page_class.push({:title => 'Home'}, :tags => 'foo')

          page.reload
          page.tags.should == %w(foo)

          page2.reload
          page2.tags.should == %w(foo)
        end

        it "should work with ids and modifier hash" do
          page_class.push(page.id, page2.id, :tags => 'foo')

          page.reload
          page.tags.should == %w(foo)

          page2.reload
          page2.tags.should == %w(foo)
        end
      end

      context "push_all" do
        let(:tags) { %w(foo bar) }

        before do
          Kernel.stub(:warn)
        end

        it "should issue a warning" do
          Kernel.should_receive(:warn).with("push_all no longer supported. use $push with $each")
          page_class.push_all({:title => 'Home'}, :tags => tags)
        end

        it "should work with criteria and modifier hashes" do
          page_class.push_all({:title => 'Home'}, :tags => tags)

          page.reload
          page.tags.should == tags

          page2.reload
          page2.tags.should == tags
        end

        it "should work with ids and modifier hash" do
          page_class.push_all(page.id, page2.id, :tags => tags)

          page.reload
          page.tags.should == tags

          page2.reload
          page2.tags.should == tags
        end
      end

      context "pull" do
        let(:page)  { page_class.create(:title => 'Home', :tags => %w(foo bar)) }
        let(:page2) { page_class.create(:title => 'Home', :tags => %w(foo bar)) }

        it "should work with criteria and modifier hashes" do
          page_class.pull({:title => 'Home'}, :tags => 'foo')

          page.reload
          page.tags.should == %w(bar)

          page2.reload
          page2.tags.should == %w(bar)
        end

        it "should be able to pull with ids and modifier hash" do
          page_class.pull(page.id, page2.id, :tags => 'foo')

          page.reload
          page.tags.should == %w(bar)

          page2.reload
          page2.tags.should == %w(bar)
        end
      end

      context "pull_all" do
        let(:page)  { page_class.create(:title => 'Home', :tags => %w(foo bar baz)) }
        let(:page2) { page_class.create(:title => 'Home', :tags => %w(foo bar baz)) }

        it "should work with criteria and modifier hashes" do
          page_class.pull_all({:title => 'Home'}, :tags => %w(foo bar))

          page.reload
          page.tags.should == %w(baz)

          page2.reload
          page2.tags.should == %w(baz)
        end

        it "should work with ids and modifier hash" do
          page_class.pull_all(page.id, page2.id, :tags => %w(foo bar))

          page.reload
          page.tags.should == %w(baz)

          page2.reload
          page2.tags.should == %w(baz)
        end
      end

      context "add_to_set" do
        let(:page) { page_class.create(:title => 'Home', :tags => 'foo') }

        it "should be able to add to set with criteria and modifier hash" do
          page_class.add_to_set({:title => 'Home'}, :tags => 'foo')

          page.reload
          page.tags.should == %w(foo)

          page2.reload
          page2.tags.should == %w(foo)
        end

        it "should be able to add to set with ids and modifier hash" do
          page_class.add_to_set(page.id, page2.id, :tags => 'foo')

          page.reload
          page.tags.should == %w(foo)

          page2.reload
          page2.tags.should == %w(foo)
        end
      end

      context "push_uniq" do
        let(:page) { page_class.create(:title => 'Home', :tags => 'foo') }

        it "should be able to push uniq with criteria and modifier hash" do
          page_class.push_uniq({:title => 'Home'}, :tags => 'foo')

          page.reload
          page.tags.should == %w(foo)

          page2.reload
          page2.tags.should == %w(foo)
        end

        it "should be able to push uniq with ids and modifier hash" do
          page_class.push_uniq(page.id, page2.id, :tags => 'foo')

          page.reload
          page.tags.should == %w(foo)

          page2.reload
          page2.tags.should == %w(foo)
        end
      end

      context "pop" do
        let(:page) { page_class.create(:title => 'Home', :tags => %w(foo bar)) }

        it "should be able to remove the last element the array" do
          page_class.pop(page.id, :tags => 1)
          page.reload
          page.tags.should == %w(foo)
        end

        it "should be able to remove the first element of the array" do
          page_class.pop(page.id, :tags => -1)
          page.reload
          page.tags.should == %w(bar)
        end
      end

      context "additional options (upsert & safe)" do
        it "should be able to pass upsert option" do
          new_key_value = DateTime.now.to_s
          page_class.increment({:title => new_key_value}, {:day_count => 1}, {:upsert => true})
          page_class.count(:title => new_key_value).should == 1
          page_class.first(:title => new_key_value).day_count.should == 1
        end

        it "should be able to pass safe option" do
          page_class.create(:title => "Better Be Safe than Sorry")

          # We are trying to increment a key of type string here which should fail
          lambda {
            page_class.increment({:title => "Better Be Safe than Sorry"}, {:title => 1}, {:safe => true})
          }.should raise_error(Mongo::Error::OperationFailure)
        end

        it "should be able to pass both safe and upsert options" do
          new_key_value = DateTime.now.to_s
          page_class.increment({:title => new_key_value}, {:day_count => 1}, {:upsert => true, :safe => true})
          page_class.count(:title => new_key_value).should == 1
          page_class.first(:title => new_key_value).day_count.should == 1
        end
      end

      context "upsert" do
        it "should insert document if not present" do
          lambda {
            page_class.upsert({:title => 'A new story'}, {:title => 'A new story', :day_count => 1})
          }.should change {page_class.count}
          page_class.first(title: 'A new story').day_count.should == 1
        end

        it "should update documents if present" do
          lambda {
            page_class.upsert({:_id => page2.id}, {tags: %w(foo bar)})
          }.should_not change {page_class.count}
          page2.reload.tags.should == %w(foo bar)
        end
      end

      context "find_and_modify" do
        it "should retrieve the document without the changes" do
          page_class.find_and_modify(query: {_id: page2.id}, update: {title: 'A new title'})['title'].should == 'Home'
          page2.reload.title.should == 'A new title'
        end

        it "should allow find_and_modify options" do
          page_class.find_and_modify(query: {_id: page2.id}, update: {title: 'A new title'}, return_document: :after)['title'].should == 'A new title'
        end
      end

    end

    context "compound keys" do
      it "should create a document" do
        lambda {
          page_class_with_compound_key.create(:title => 'Foo', :tags => %w(foo))
        }.should change { page_class_with_compound_key.count }.by(1)
        doc = page_class_with_compound_key.first
        page_class_with_compound_key.find(doc._id).should == doc
      end
    end

    context "instance methods" do
      before do
        Kernel.stub(:warn)
      end

      {
        :page_class_with_standard_key => "with standard key",
        :page_class_with_compound_key => "with compound key",
      }.each do |klass, description|
        context description do
          let!(:page_class) { send(klass) }

          it "should be able to unset with keys" do
            page = page_class.create(:title => 'Foo', :tags => %w(foo))
            page.unset(:title, :tags)
            assert_keys_removed page, :title, :tags
          end

          it "should be able to increment with modifier hashes" do
            page = page_class.create
            page.increment(:day_count => 1, :week_count => 2, :month_count => 3)

            assert_page_counts page, 1, 2, 3
          end

          it "should be able to increment with just the field name" do
            page = page_class.create
            page.increment :day_count

            assert_page_counts page, 1, 0, 0
          end

          it "should be able to decrement with modifier hashes" do
            page = page_class.create(:day_count => 1, :week_count => 2, :month_count => 3)
            page.decrement(:day_count => 1, :week_count => 2, :month_count => 3)

            assert_page_counts page, 0, 0, 0
          end

          it "should be able to decrement with just the field name" do
            page = page_class.create
            page.decrement :day_count

            assert_page_counts page, -1, 0, 0
          end

          it "should always decrement when decrement is called whether number is positive or negative" do
            page = page_class.create(:day_count => 1, :week_count => 2, :month_count => 3)
            page.decrement(:day_count => -1, :week_count => 2, :month_count => -3)
            assert_page_counts page, 0, 0, 0
          end

          it "should be able to set with modifier hashes" do
            page  = page_class.create(:title => 'Home')
            page.set(:title => 'Home Revised')

            page.reload
            page.title.should == 'Home Revised'
          end

          it "should be able to push with modifier hashes" do
            page = page_class.create
            page.push(:tags => 'foo')

            page.reload
            page.tags.should == %w(foo)
          end

          it "should be able to push_all with modifier hashes" do
            page = page_class.create
            page.push_all(:tags => %w(foo bar))

            page.reload
            page.tags.should == %w(foo bar)
          end

          it "should issue a warning with push_all" do
            Kernel.should_receive(:warn).with("push_all no longer supported. use $push with $each")

            page = page_class.create
            page.push_all(:tags => %w(foo bar))
          end

          it "should be able to pull with criteria and modifier hashes" do
            page = page_class.create(:tags => %w(foo bar))
            page.pull(:tags => 'foo')

            page.reload
            page.tags.should == %w(bar)
          end

          it "should be able to pull_all with criteria and modifier hashes" do
            page = page_class.create(:tags => %w(foo bar baz))
            page.pull_all(:tags => %w(foo bar))

            page.reload
            page.tags.should == %w(baz)
          end

          it "should be able to add_to_set with criteria and modifier hash" do
            page  = page_class.create(:tags => 'foo')
            page2 = page_class.create

            page.add_to_set(:tags => 'foo')
            page2.add_to_set(:tags => 'foo')

            page.reload
            page.tags.should == %w(foo)

            page2.reload
            page2.tags.should == %w(foo)
          end

          it "should be able to push uniq with criteria and modifier hash" do
            page  = page_class.create(:tags => 'foo')
            page2 = page_class.create

            page.push_uniq(:tags => 'foo')
            page2.push_uniq(:tags => 'foo')

            page.reload
            page.tags.should == %w(foo)

            page2.reload
            page2.tags.should == %w(foo)
          end

          it "should be able to pop with modifier hashes" do
            page = page_class.create(:tags => %w(foo bar))
            page.pop(:tags => 1)

            page.reload
            page.tags.should == %w(foo)
          end

          it "should be able to pass upsert option" do
            page = page_class.create(:title => "Upsert Page")
            page.increment({:new_count => 1}, {:upsert => true})

            page.reload
            page.new_count.should == 1
          end

          it "should be able to pass safe option" do
            page = page_class.create(:title => "Safe Page")

            # We are trying to increment a key of type string here which should fail
            lambda {
              page.increment({:title => 1}, {:safe => true})
            }.should raise_error(Mongo::Error::OperationFailure)
          end

          it "should be able to pass upsert and safe options" do
            page = page_class.create(:title => "Upsert and Safe Page")
            page.increment({:another_count => 1}, {:upsert => true, :safe => true})

            page.reload
            page.another_count.should == 1
          end
        end
      end
    end
  end
end
