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

GOOD_OPTIONS = [
  [ :reload,   false     ],
  [ :reload,   true      ],
  [ :offset,   0         ],
  [ :offset,   1         ],
  [ :limit,    1         ],
  [ :limit,    2         ],
  [ :order,    [ DataMapper::Query::Direction.new(Article.properties[:created_at], :desc) ] ],
  [ :fields,   Article.properties.defaults.to_a ], # TODO: fill in allowed default value
  #[ :links,    [ :stub ] ], # TODO: fill in allowed default value
  [ :includes, [ :stub ] ], # TODO: fill in allowed default value
]

BAD_OPTIONS = {
  :reload     => 'true',
  :offset     => -1,
  :limit      => 0,
#  :order      => [],  # TODO: spec conditions where :order may be empty
#  :fields     => [],  # TODO: spec conditions where :fields may be empty
  :links      => [],
  :includes   => [],
  :conditions => [],
}

# flatten GOOD_OPTIONS into a Hash to remove default values, since
# default value, when defined, is always listed first in GOOD_OPTIONS
UPDATED_OPTIONS = GOOD_OPTIONS.inject({}) do |options,(attribute,value)|
  options.update attribute => value
end

UPDATED_OPTIONS.merge!({ :fields => [ :id, :author ]})

describe DataMapper::Query do
  before do
    @adapter    = mock('adapter')
    @repository = mock('repository', :kind_of? => true, :name => 'mock', :adapter => @adapter)

    @query = DataMapper::Query.new(@repository, Article)
  end

  it 'should provide .new' do
    DataMapper::Query.should respond_to(:new)
  end

  describe '.new' do
    describe 'should set the attribute' do
      it '#model with model' do
        query = DataMapper::Query.new(@repository, Article)
        query.model.should == Article
      end

      GOOD_OPTIONS.each do |(attribute,value)|
        it "##{attribute} with options[:#{attribute}] if it is #{value.inspect}" do
          query = DataMapper::Query.new(@repository, Article, attribute => value)
          query.send(attribute == :reload ? :reload? : attribute).should == value
        end
      end

      describe ' #conditions with options[:conditions]' do
        it 'when they are a Hash' do
          query = DataMapper::Query.new(@repository, Article, :conditions => { :author => 'dkubb' })
          query.conditions.should == [ [ :eql, Article.properties[:author], 'dkubb' ] ]
        end

        it 'when they have a one element Array' do
          query = DataMapper::Query.new(@repository, Article, :conditions => [ 'name = "dkubb"' ])
          query.conditions.should == [ [ :raw, 'name = "dkubb"' ] ]
          query.bind_values.should == []
        end

        it 'when they have a two or more element Array' do
          bind_values = %w[ dkubb ]
          query = DataMapper::Query.new(@repository, Article, :conditions => [ 'name = ?', *bind_values ])
          query.conditions.should == [ [ :raw, 'name = ?', bind_values ] ]
          query.bind_values.should == bind_values

          bind_values = [ 'dkubb', 32 ]
          query = DataMapper::Query.new(@repository, Article, :conditions => [ 'name = ? OR age = ?', *bind_values ])
          query.conditions.should == [ [ :raw, 'name = ? OR age = ?', bind_values ] ]
          query.bind_values.should == bind_values

          bind_values = [ %w[ dkubb ssmoot ] ]
          query = DataMapper::Query.new(@repository, Article, :conditions => [ 'name IN ?', *bind_values ])
          query.conditions.should == [ [ :raw, 'name IN ?', bind_values ] ]
          query.bind_values.should == bind_values
        end

        it 'when they have another DM:Query as the value of sub-select' do
          class Acl
            include DataMapper::Resource
            property :id, Integer
            property :resource_id, Integer
          end

          acl_query = DataMapper::Query.new(@repository, Acl, :fields=>[:resource_id]) #this would normally have conditions
          query = DataMapper::Query.new(@repository, Article, :id.in => acl_query)
          query.conditions.each do |operator, property, value|
            operator.should == :in
            property.name.should == :id
            value.should == acl_query
          end
        end
      end

      describe ' #conditions with unknown options' do
        it 'when a Symbol object is a key' do
          query = DataMapper::Query.new(@repository, Article, :author => 'dkubb')
          query.conditions.should == [ [ :eql, Article.properties[:author], 'dkubb' ] ]
        end

        it 'when a Query::Operator object is a key' do
          query = DataMapper::Query.new(@repository, Article, :author.like => /\Ad(?:an\.)kubb\z/)
          query.conditions.should == [ [ :like, Article.properties[:author], /\Ad(?:an\.)kubb\z/ ] ]
        end
      end

      it '#order with model.default_order if none provided' do
        query = DataMapper::Query.new(@repository, Article)
        query.order.should == [ DataMapper::Query::Direction.new(Article.properties[:id], :asc) ]
      end
    end

    describe 'should raise an ArgumentError' do
      it 'when repository is nil' do
        lambda {
          DataMapper::Query.new(nil, NormalClass)
        }.should raise_error(ArgumentError)
      end

      it 'when model is nil' do
        lambda {
          DataMapper::Query.new(@repository, nil)
        }.should raise_error(ArgumentError)
      end

      it 'when model is a Class that does not include DataMapper::Resource' do
        lambda {
          DataMapper::Query.new(@repository, NormalClass)
        }.should raise_error(ArgumentError)
      end

      it 'when options is not a Hash' do
        lambda {
          DataMapper::Query.new(@repository, Article, nil)
        }.should raise_error(ArgumentError)
      end

      BAD_OPTIONS.each do |attribute,value|
        it "when options[:#{attribute}] is nil" do
          lambda {
            DataMapper::Query.new(@repository, Article, attribute => nil)
          }.should raise_error(ArgumentError)
        end

        it "when options[:#{attribute}] is #{value.kind_of?(Array) && value.empty? ? 'an empty Array' : value.inspect}" do
          lambda {
            DataMapper::Query.new(@repository, Article, attribute => value)
          }.should raise_error(ArgumentError)
        end
      end

      it 'when unknown options use something that is not a Query::Operator, Symbol or String is a key' do
        lambda {
          DataMapper::Query.new(@repository, Article, nil => nil)
        }.should raise_error(ArgumentError)
      end
    end

    describe 'should normalize' do
      it '#fields' do
        DataMapper::Query.new(@repository, Article, :fields => [:id]).fields.should == Article.properties.slice(:id)
      end
    end

    describe 'should translate custom types' do
      before(:each) do
        class Acl
          include DataMapper::Resource
          property :id, Integer
          property :is_custom_type, DM::Boolean
        end
      end
      it "should call Boolean#dump for :is_custom_type options" do
        DM::Boolean.should_receive(:dump).with(:false, Acl.properties[:is_custom_type])
        DataMapper::Query.new(@repository, Acl, :is_custom_type => :false)
      end
    end
  end

  it 'should provide #update' do
    @query.should respond_to(:update)
  end

  describe '#update' do
    before do
      @query = DataMapper::Query.new(@repository, Article, UPDATED_OPTIONS)
    end

    it 'should instantiate a DataMapper::Query object from other when it is a Hash' do
      other = { :reload => :true }

      @query.should_receive(:class).with(no_args).exactly(3).times.ordered.and_return(DataMapper::Query)
      DataMapper::Query.should_receive(:new).with(@repository, @query.model, other).ordered.and_return(@query)

      @query.update(other)
    end

    it 'should raise an ArgumentError if other query model is different' do
      lambda {
        other = DataMapper::Query.new(@repository, Comment)
        @query.update(other)
      }.should raise_error(ArgumentError)
    end

    it 'should return self' do
      other = DataMapper::Query.new(@repository, Article)
      @query.update(other).should == @query
    end

    describe 'should overwrite the attribute' do
      it '#reload? with other reload?' do
        other = DataMapper::Query.new(@repository, Article, :reload => true)
        @query.update(other).reload?.should == true
      end

      it '#offset with other offset when it is not equal to 0' do
        other = DataMapper::Query.new(@repository, Article, :offset => 1)
        @query.update(other).offset.should == 1
      end

      it '#limit with other limit when it is not nil' do
        other = DataMapper::Query.new(@repository, Article, :limit => 1)
        @query.update(other).limit.should == 1
      end

      it '#the operator if condition is the same and operater is changed (:not / :eql)' do
        # especially needed for collection#update where you might do something like:
        # all(:name.not => "John").update(:name => "John")
        pending do
          other = DataMapper::Query.new(@repository, Article, :author.not => "dkubb")
          @query.update(other).conditions.should == [ [ :not, Article.properties[:author], 'dkubb' ] ]
          @query.update(:author => "dkubb").conditions.should == [ [ :eql, Article.properties[:author], 'dkubb' ] ]
        end
      end

      [ :eql, :like ].each do |operator|
        it "#conditions with other conditions when updating the '#{operator}' clause to a different value than in self" do
          # set the initial conditions
          @query.update(:author.send(operator) => 'ssmoot')

          # update the conditions, and overwrite with the new value
          other = DataMapper::Query.new(@repository, Article, :author.send(operator) => 'dkubb')
          @query.update(other).conditions.should == [ [ operator, Article.properties[:author], 'dkubb' ] ]
        end
      end

      [ :gt, :gte ].each do |operator|
        it "#conditions with other conditions when updating the '#{operator}' clause to a value less than in self" do
          # set the initial conditions
          @query.update(:created_at.send(operator) => Time.at(1))

          # update the conditions, and overwrite with the new value is less
          other = DataMapper::Query.new(@repository, Article, :created_at.send(operator) => Time.at(0))
          @query.update(other).conditions.should == [ [ operator, Article.properties[:created_at], Time.at(0) ] ]
        end
      end

      [ :lt, :lte ].each do |operator|
        it "#conditions with other conditions when updating the '#{operator}' clause to a value greater than in self" do
          # set the initial conditions
          @query.update(:created_at.send(operator) => Time.at(0))

          # update the conditions, and overwrite with the new value is more
          other = DataMapper::Query.new(@repository, Article, :created_at.send(operator) => Time.at(1))
          @query.update(other).conditions.should == [ [ operator, Article.properties[:created_at], Time.at(1) ] ]
        end
      end

      it "#order with other order unique values" do
        order = [
          DataMapper::Query::Direction.new(Article.properties[:created_at], :desc),
          DataMapper::Query::Direction.new(Article.properties[:author],     :desc),
          DataMapper::Query::Direction.new(Article.properties[:title],      :desc),
        ]

        other = DataMapper::Query.new(@repository, Article, :order => order)
        @query.update(other).order.should == order
      end

      # dkubb: I am not sure i understand the intent here. link now needs to be
      #       a DM::Assoc::Relationship or the name (Symbol or String) of an
      #       association on the Resource -- thx guyvdb
      #
      # NOTE: I have commented out :links in the GOOD_OPTIONS above
      #
      [ :links, :includes ].each do |attribute|
        it "##{attribute} with other #{attribute} unique values" do
          pending 'DataMapper::Query::Path not ready'
          other = DataMapper::Query.new(@repository, Article, attribute => [ :stub, :other, :new ])
          @query.update(other).send(attribute).should == [ :stub, :other, :new ]
        end
      end

      it "#fields with other fields unique values" do
        other = DataMapper::Query.new(@repository, Article, :fields => [ :blog_id ])
        @query.update(other).fields.should == Article.properties.slice(:blog_id)
      end

      it '#conditions with other conditions when they are unique' do
        # set the initial conditions
        @query.update(:title => 'On DataMapper')

        # update the conditions, but merge the conditions together
        other = DataMapper::Query.new(@repository, Article, :author => 'dkubb')
        @query.update(other).conditions.should == [ [ :eql, Article.properties[:title], 'On DataMapper' ], [ :eql, Article.properties[:author], 'dkubb' ] ]
      end

      [ :not, :in ].each do |operator|
        it "#conditions with other conditions when updating the '#{operator}' clause" do
          # set the initial conditions
          @query.update(:created_at.send(operator) => [ Time.at(0) ])

          # update the conditions, and overwrite with the new value is more
          other = DataMapper::Query.new(@repository, Article, :created_at.send(operator) => [ Time.at(1) ])
          @query.update(other).conditions.should == [ [ operator, Article.properties[:created_at], [ Time.at(0), Time.at(1) ] ] ]
        end
      end

      it '#conditions with other conditions when they have a one element condition' do
        # set the initial conditions
        @query.update(:title => 'On DataMapper')

        # update the conditions, but merge the conditions together
        other = DataMapper::Query.new(@repository, Article, :conditions => [ 'author = "dkubb"' ])
        @query.update(other).conditions.should == [ [ :eql, Article.properties[:title], 'On DataMapper' ], [ :raw, 'author = "dkubb"' ] ]
      end

      it '#conditions with other conditions when they have a two or more element condition' do
        # set the initial conditions
        @query.update(:title => 'On DataMapper')

        # update the conditions, but merge the conditions together
        other = DataMapper::Query.new(@repository, Article, :conditions => [ 'author = ?', 'dkubb' ])
        @query.update(other).conditions.should == [ [ :eql, Article.properties[:title], 'On DataMapper' ], [ :raw, 'author = ?', [ 'dkubb' ] ] ]
      end
    end

    describe 'should not update the attribute' do
      it '#offset when other offset is equal to 0' do
        other = DataMapper::Query.new(@repository, Article, :offset => 0)
        other.offset.should == 0
        @query.update(other).offset.should == 1
      end

      it '#limit when other limit is nil' do
        other = DataMapper::Query.new(@repository, Article)
        other.limit.should be_nil
        @query.update(other).offset.should == 1
      end

      [ :gt, :gte ].each do |operator|
        it "#conditions with other conditions when they have a '#{operator}' clause with a value greater than in self" do
          # set the initial conditions
          @query.update(:created_at.send(operator) => Time.at(0))

          # do not overwrite with the new value if it is more
          other = DataMapper::Query.new(@repository, Article, :created_at.send(operator) => Time.at(1))
          @query.update(other).conditions.should == [ [ operator, Article.properties[:created_at], Time.at(0) ] ]
        end
      end

      [ :lt, :lte ].each do |operator|
        it "#conditions with other conditions when they have a '#{operator}' clause with a value less than in self" do
          # set the initial conditions
          @query.update(:created_at.send(operator) => Time.at(1))

          # do not overwrite with the new value if it is less
          other = DataMapper::Query.new(@repository, Article, :created_at.send(operator) => Time.at(0))
          @query.update(other).conditions.should == [ [ operator, Article.properties[:created_at], Time.at(1) ] ]
        end
      end
    end
  end

  it 'should provide #merge' do
    @query.should respond_to(:merge)
  end

  describe '#merge' do
    it 'should pass arguments as-is to duplicate object\'s #update method' do
      dupe_query = @query.dup
      @query.should_receive(:dup).with(no_args).ordered.and_return(dupe_query)
      dupe_query.should_receive(:update).with(:author => 'dkubb').ordered
      @query.merge(:author => 'dkubb')
    end

    it 'should return the duplicate object' do
      dupe_query = @query.merge(:author => 'dkubb')
      @query.object_id.should_not == dupe_query.object_id
      @query.merge(:author => 'dkubb').should == dupe_query
    end
  end

  it 'should provide #==' do
    @query.should respond_to(:==)
  end

  describe '#==' do
    describe 'should be equal' do
      it 'when other is same object' do
        @query.update(:author => 'dkubb').should == @query
      end

      it 'when other has the same attributes' do
        other = DataMapper::Query.new(@repository, Article)
        @query.object_id.should_not == other.object_id
        @query.should == other
      end

      it 'when other has the same conditions sorted differently' do
        @query.update(:author => 'dkubb')
        @query.update(:title  => 'On DataMapper')

        other = DataMapper::Query.new(@repository, Article, :title => 'On DataMapper')
        other.update(:author => 'dkubb')

        # query conditions are in different order
        @query.conditions.should == [ [ :eql, Article.properties[:author], 'dkubb'         ], [ :eql, Article.properties[:title],  'On DataMapper' ] ]
        other.conditions.should  == [ [ :eql, Article.properties[:title],  'On DataMapper' ], [ :eql, Article.properties[:author], 'dkubb'         ] ]

        @query.should == other
      end
    end

    describe 'should be different' do
      it 'when other model is different than self.model' do
        @query.should_not == DataMapper::Query.new(@repository, Comment)
      end

      UPDATED_OPTIONS.each do |attribute,value|
        it "when other #{attribute} is different than self.#{attribute}" do
          @query.should_not == DataMapper::Query.new(@repository, Article, attribute => value)
        end
      end

      it 'when other conditions are different than self.conditions' do
        @query.should_not == DataMapper::Query.new(@repository, Article, :author => 'dkubb')
      end
    end
  end

  it 'should provide #reverse' do
    @query.should respond_to(:reverse)
  end

  describe '#reverse' do
    it 'should create a duplicate query and delegate to #reverse!' do
      copy = @query.dup
      copy.should_receive(:reverse!).with(no_args).and_return(@query)
      @query.should_receive(:dup).with(no_args).and_return(copy)

      @query.reverse.should == @query
    end
  end

  it 'should provide #reverse!' do
    @query.should respond_to(:reverse!)
  end

  describe '#reverse!' do
    it 'should update the query with the reverse order' do
      normal_order  = Article.key.map { |p| DataMapper::Query::Direction.new(p, :asc) }
      reverse_order = Article.key.map { |p| DataMapper::Query::Direction.new(p, :desc) }

      normal_order.should_not be_empty
      reverse_order.should_not be_empty

      @query.order.should == normal_order
      @query.should_receive(:update).with(:order => reverse_order)
      @query.reverse!.object_id.should == @query.object_id
    end
  end
end

describe DataMapper::Query::Operator do
  before do
    @operator = :thing.gte
  end

  it 'should provide #==' do
    @operator.should respond_to(:==)
  end

  describe '#==' do
    describe 'should be equal' do
      it 'when other is same object' do
        @operator.should == @operator
      end

      it 'when other has the same target and operator' do
        other = :thing.gte
        @operator.target.should == other.target
        @operator.operator.should == other.operator
        @operator.should == other
      end
    end

    describe 'should be different' do
      it 'when other class is not a descendant of self.class' do
        other = :thing
        other.class.should_not be_kind_of(@operator.class)
        @operator.should_not == other
      end

      it 'when other has a different target' do
        other = :other.gte
        @operator.target.should_not == other.target
        @operator.should_not == other
      end

      it 'when other has a different operator' do
        other = :thing.gt
        @operator.operator.should_not == other.operator
        @operator.should_not == other
      end
    end
  end
end
