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

describe 'Search' do
  it 'should search by keywords from DSL' do
    session.search Post do
      keywords 'keyword search'
    end
    connection.should have_last_search_with(:q => 'keyword search')
  end

  it 'should search by keywords from options' do
    session.search Post, :keywords => 'keyword search'
    connection.should have_last_search_with(:q => 'keyword search')
  end

  it 'should set default query parser to dismax when keywords used' do
    session.search Post do
      keywords 'keyword search'
    end
    connection.should have_last_search_with(:defType => 'dismax')
  end

  it 'should search types in filter query if keywords used' do
    session.search Post do
      keywords 'keyword search'
    end
    connection.should have_last_search_with(:fq => 'type:Post')
  end

  it 'should search types in main query if keywords not used' do
    session.search Post
    connection.should have_last_search_with(:q => 'type:Post')
  end

  it 'should search type of subclass when superclass is configured' do
    session.search PhotoPost
    connection.should have_last_search_with(:q => 'type:PhotoPost')
  end

  it 'should search all text fields for searched class' do
    session.search Post do
      keywords 'keyword search'
    end
    connection.searches.last[:qf].split(' ').sort.should == %w(backwards_title_text body_text title_text)
  end

  it 'should search only specified text fields when specified' do
    session.search Post do
      keywords 'keyword search', :fields => [:title, :body]
    end
    connection.searches.last[:qf].split(' ').sort.should == %w(body_text title_text)
  end

  it 'should request score when keywords used' do
    session.search Post, :keywords => 'keyword search'
    connection.should have_last_search_with(:fl => '* score')
  end

  it 'should not request score when keywords not used' do
    session.search Post
    connection.should_not have_last_search_with(:fl)
  end

  it 'should scope by exact match with a string from DSL' do
    session.search Post do
      with :title, 'My Pet Post'
    end
    connection.should have_last_search_with(:fq => ['title_ss:My\ Pet\ Post'])
  end

  it 'should scope by exact match with a string from options' do
    session.search Post, :conditions => { :title => 'My Pet Post' }
    connection.should have_last_search_with(:fq => ['title_ss:My\ Pet\ Post'])
  end

  it 'should ignore nonexistant fields in hash scope' do
    session.search Post, :conditions => { :bogus => 'Field' }
    connection.should_not have_last_search_with(:fq)
  end

  it 'should scope by exact match with time' do
    time = Time.parse('1983-07-08 05:00:00 -0400')
    session.search Post do
      with :published_at, time
    end
    connection.should have_last_search_with(
      :fq => ['published_at_d:1983\-07\-08T09\:00\:00Z']
    )
  end

  it 'should scope by exact match with date' do
    date = Date.new(1983, 7, 8)
    session.search Post do
      with :expire_date, date
    end
    connection.should have_last_search_with(
      :fq => ['expire_date_d:1983\-07\-08T00\:00\:00Z']
    )
  end
  
  it 'should scope by exact match with boolean' do
    session.search Post do
      with :featured, false
    end
    connection.should have_last_search_with(:fq => ['featured_b:false'])
  end

  it 'should scope by less than match with float' do
    session.search Post do
      with(:average_rating).less_than 3.0
    end
    connection.should have_last_search_with(:fq => ['average_rating_f:[* TO 3\.0]'])
  end

  it 'should scope by greater than match with float' do
    session.search Post do
      with(:average_rating).greater_than 3.0
    end
    connection.should have_last_search_with(:fq => ['average_rating_f:[3\.0 TO *]'])
  end

  it 'should scope by short-form between match with integers' do
    session.search Post do
      with :blog_id, 2..4
    end
    connection.should have_last_search_with(:fq => ['blog_id_i:[2 TO 4]'])
  end

  it 'should scope by between match with float' do
    session.search Post do
      with(:average_rating).between 2.0..4.0
    end
    connection.should have_last_search_with(:fq => ['average_rating_f:[2\.0 TO 4\.0]'])
  end

  it 'should scope by any match with integer using DSL' do
    session.search Post do
      with(:category_ids).any_of [2, 7, 12]
    end
    connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
  end

  it 'should scope by any match with integer using options' do
    session.search Post, :conditions => { :category_ids => [2, 7, 12] }
    connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
  end

  it 'should scope by short-form any-of match with integers' do
    session.search Post do
      with :category_ids, [2, 7, 12]
    end
    connection.should have_last_search_with(:fq => ['category_ids_im:(2 OR 7 OR 12)'])
  end

  it 'should scope by all match with integer' do
    session.search Post do
      with(:category_ids).all_of [2, 7, 12]
    end
    connection.should have_last_search_with(:fq => ['category_ids_im:(2 AND 7 AND 12)'])
  end

  it 'should scope by not equal match with string' do
    session.search Post do
      without :title, 'Bad Post'
    end
    connection.should have_last_search_with(:fq => ['-title_ss:Bad\ Post'])
  end

  it 'should scope by not less than match with float' do
    session.search Post do
      without(:average_rating).less_than 3.0
    end
    connection.should have_last_search_with(:fq => ['-average_rating_f:[* TO 3\.0]'])
  end

  it 'should scope by not greater than match with float' do
    session.search Post do
      without(:average_rating).greater_than 3.0
    end
    connection.should have_last_search_with(:fq => ['-average_rating_f:[3\.0 TO *]'])
  end
  
  it 'should scope by not between match with shorthand' do
    session.search Post do
      without(:blog_id, 2..4)
    end
    connection.should have_last_search_with(:fq => ['-blog_id_i:[2 TO 4]'])
  end

  it 'should scope by not between match with float' do
    session.search Post do
      without(:average_rating).between 2.0..4.0
    end
    connection.should have_last_search_with(:fq => ['-average_rating_f:[2\.0 TO 4\.0]'])
  end

  it 'should scope by not any match with integer' do
    session.search Post do
      without(:category_ids).any_of [2, 7, 12]
    end
    connection.should have_last_search_with(:fq => ['-category_ids_im:(2 OR 7 OR 12)'])
  end


  it 'should scope by not all match with integer' do
    session.search Post do
      without(:category_ids).all_of [2, 7, 12]
    end
    connection.should have_last_search_with(:fq => ['-category_ids_im:(2 AND 7 AND 12)'])
  end

  it 'should scope by empty field' do
    session.search Post do
      with :average_rating, nil
    end
    connection.should have_last_search_with(:fq => ['-average_rating_f:[* TO *]'])
  end

  it 'should scope by non-empty field' do
    session.search Post do
      without :average_rating, nil
    end
    connection.should have_last_search_with(:fq => ['average_rating_f:[* TO *]'])
  end

  it 'should exclude by object identity' do
    post = Post.new
    session.search Post do
      without post
    end
    connection.should have_last_search_with(:fq => ["-id:Post\\ #{post.id}"])
  end

  it 'should exclude multiple objects passed as varargs by object identity' do
    post1, post2 = Post.new, Post.new
    session.search Post do
      without post1, post2
    end
    connection.should have_last_search_with(
      :fq => ["-id:Post\\ #{post1.id}", "-id:Post\\ #{post2.id}"]
    )
  end

  it 'should exclude multiple objects passed as array by object identity' do
    posts = [Post.new, Post.new]
    session.search Post do
      without posts
    end
    connection.should have_last_search_with(
      :fq => ["-id:Post\\ #{posts.first.id}", "-id:Post\\ #{posts.last.id}"]
    )
  end

  it 'should create a disjunction between two restrictions' do
    session.search Post do
      any_of do
        with :category_ids, 1
        with :blog_id, 2
      end
    end
    connection.should have_last_search_with(
      :fq => '(category_ids_im:1 OR blog_id_i:2)'
    )
  end

  it 'should create a conjunction inside of a disjunction' do
    session.search Post do
      any_of do
        with :blog_id, 2
        all_of do
          with :category_ids, 1
          with(:average_rating).greater_than(3.0)
        end
      end
    end
    connection.should have_last_search_with(
      :fq => '(blog_id_i:2 OR (category_ids_im:1 AND average_rating_f:[3\.0 TO *]))'
    )
  end

  it 'should do nothing special if #all_of called from the top level' do
    session.search Post do
      all_of do
        with :blog_id, 2
        with :category_ids, 1
      end
    end
    connection.should have_last_search_with(
      :fq => ['blog_id_i:2', 'category_ids_im:1']
    )
  end

  it 'should create a disjunction with negated restrictions' do
    session.search Post do
      any_of do
        with :category_ids, 1
        without(:average_rating).greater_than(3.0)
      end
    end
    connection.should have_last_search_with(
      :fq => '-(-category_ids_im:1 AND average_rating_f:[3\.0 TO *])'
    )
  end

  it 'should create a disjunction with nested conjunction with negated restrictions' do
    session.search Post do
      any_of do
        with :category_ids, 1
        all_of do
          without(:average_rating).greater_than(3.0)
          with(:blog_id, 1)
        end
      end
    end
    connection.should have_last_search_with(
      :fq => '(category_ids_im:1 OR (-average_rating_f:[3\.0 TO *] AND blog_id_i:1))'
    )
  end

  it 'should create a disjunction with nested conjunction with nested disjunction with negated restriction' do
    session.search(Post) do
      any_of do
        with(:title, 'Yes')
        all_of do
          with(:blog_id, 1)
          any_of do
            with(:category_ids, 4)
            without(:average_rating, 2.0)
          end
        end
      end
    end
    connection.should have_last_search_with(
      :fq => '(title_ss:Yes OR (blog_id_i:1 AND -(-category_ids_im:4 AND average_rating_f:2\.0)))'
    )
  end

  it 'should create a disjunction with a negated restriction and a nested disjunction in a conjunction with a negated restriction' do
    session.search(Post) do
      any_of do
        without(:title, 'Yes')
        all_of do
          with(:blog_id, 1)
          any_of do
            with(:category_ids, 4)
            without(:average_rating, 2.0)
          end
        end
      end
    end
    connection.should have_last_search_with(
      :fq => '-(title_ss:Yes AND -(blog_id_i:1 AND -(-category_ids_im:4 AND average_rating_f:2\.0)))'
    )
  end

  #
  # This is important because if a disjunction could be nested in another
  # disjunction, then the inner disjunction could denormalize (and thus
  # become negated) after the outer disjunction denormalized (checking to
  # see if the inner one is negated). Since conjunctions never need to
  # denormalize, if a disjunction can only contain conjunctions or restrictions,
  # we can guarantee that the negation state of a disjunction's components will
  # not change when #to_params is called on them.
  #
  # Since disjunction is associative, this behavior has no effect on the actual
  # logical semantics of the disjunction.
  #
  it 'should create a single disjunction when disjunctions nested' do
    session.search(Post) do
      any_of do
        with(:title, 'Yes')
        any_of do
          with(:blog_id, 1)
          with(:category_ids, 4)
        end
      end
    end
    connection.should have_last_search_with(
      :fq => '(title_ss:Yes OR blog_id_i:1 OR category_ids_im:4)'
    )
  end

  it 'should create a disjunction with instance exclusion' do
    post = Post.new
    session.search Post do
      any_of do
        without(post)
        with(:category_ids, 1)
      end
    end
    connection.should have_last_search_with(
      :fq => "-(id:Post\\ #{post.id} AND -category_ids_im:1)"
    )
  end

  it 'should create a disjunction with empty restriction' do
    session.search Post do
      any_of do
        with(:average_rating, nil)
        with(:average_rating).greater_than(3.0)
      end
    end
    connection.should have_last_search_with(
      :fq => '-(average_rating_f:[* TO *] AND -average_rating_f:[3\.0 TO *])'
    )
  end

  it 'should restrict by dynamic string field with equality restriction' do
    session.search Post do
      dynamic :custom_string do
        with :test, 'string'
      end
    end
    connection.should have_last_search_with(:fq => ['custom_string\:test_s:string'])
  end

  it 'should restrict by dynamic integer field with less than restriction' do
    session.search Post do
      dynamic :custom_integer do
        with(:test).less_than(1)
      end
    end
    connection.should have_last_search_with(:fq => ['custom_integer\:test_i:[* TO 1]'])
  end

  it 'should restrict by dynamic float field with between restriction' do
    session.search Post do
      dynamic :custom_float do
        with(:test).between(2.2..3.3)
      end
    end
    connection.should have_last_search_with(:fq => ['custom_float\:test_fm:[2\.2 TO 3\.3]'])
  end

  it 'should restrict by dynamic time field with any of restriction' do
    session.search Post do
      dynamic :custom_time do
        with(:test).any_of([Time.parse('2009-02-10 14:00:00 UTC'),
                            Time.parse('2009-02-13 18:00:00 UTC')])
      end
    end
    connection.should have_last_search_with(:fq => ['custom_time\:test_d:(2009\-02\-10T14\:00\:00Z OR 2009\-02\-13T18\:00\:00Z)'])
  end

  it 'should restrict by dynamic boolean field with equality restriction' do
    session.search Post do
      dynamic :custom_boolean do
        with :test, false
      end
    end
    connection.should have_last_search_with(:fq => ['custom_boolean\:test_b:false'])
  end

  it 'should negate a dynamic field restriction' do
    session.search Post do
      dynamic :custom_string do
        without :test, 'foo'
      end
    end
    connection.should have_last_search_with(:fq => ['-custom_string\:test_s:foo'])
  end

  it 'should search by a dynamic field inside a disjunction' do
    session.search Post do
      any_of do
        dynamic :custom_string do
          with :test, 'foo'
        end
        with :title, 'bar'
      end
    end
    connection.should have_last_search_with(
      :fq => '(custom_string\:test_s:foo OR title_ss:bar)'
    )
  end

  it 'should throw an UnrecognizedFieldError if an unknown dynamic field is searched by' do
    lambda do
      session.search Post do
        dynamic(:bogus) { with :some, 'value' }
      end
    end.should raise_error(Sunspot::UnrecognizedFieldError)
  end

  it 'should throw a NoMethodError if pagination is attempted in a dynamic query' do
    lambda do
      session.search Post do
        dynamic :custom_string do
          paginate 3, 10
        end
      end
    end.should raise_error(NoMethodError)
  end

  it 'should paginate using default per_page when page not provided' do
    session.search Post
    connection.should have_last_search_with(:rows => 30)
  end

  it 'should paginate using default per_page when page provided in DSL' do
    session.search Post do
      paginate :page => 2
    end
    connection.should have_last_search_with(:rows => 30, :start => 30)
  end

  it 'should paginate using default per_page when page provided in options' do
    session.search Post, :page => 2
    connection.should have_last_search_with(:rows => 30, :start => 30)
  end

  it 'should paginate using provided per_page in DSL' do
    session.search Post do
      paginate :page => 4, :per_page => 15
    end
    connection.should have_last_search_with(:rows => 15, :start => 45)
  end

  it 'should paginate using provided per_page in options' do
    session.search Post, :page => 4, :per_page => 15
    connection.should have_last_search_with(:rows => 15, :start => 45)
  end

  it 'should order in DSL' do
    session.search Post do
      order_by :average_rating, :desc
    end
    connection.should have_last_search_with(:sort => 'average_rating_f desc')
  end

  it 'should order in keywords' do
    session.search Post, :order => 'average_rating desc'
    connection.should have_last_search_with(:sort => 'average_rating_f desc')
  end

  it 'should order by multiple fields in DSL' do
    session.search Post do
      order_by :average_rating, :desc
      order_by :sort_title, :asc
    end
    connection.should have_last_search_with(:sort => 'average_rating_f desc, sort_title_s asc')
  end

  it 'should order by multiple fields in options' do
    session.search Post, :order => ['average_rating desc', 'sort_title asc']
    connection.should have_last_search_with(:sort => 'average_rating_f desc, sort_title_s asc')
  end

  it 'should order by a dynamic field' do
    session.search Post do
      dynamic :custom_integer do
        order_by :test, :desc
      end
    end
    connection.should have_last_search_with(:sort => 'custom_integer:test_i desc')
  end

  it 'should order by a dynamic field and static field, with given precedence' do
    session.search Post do
      dynamic :custom_integer do
        order_by :test, :desc
      end
      order_by :sort_title, :asc
    end
    connection.should have_last_search_with(:sort => 'custom_integer:test_i desc, sort_title_s asc')
  end

  it 'should order by random' do
    session.search Post do
      order_by_random
    end
    connection.searches.last[:sort].should =~ /^random_\d+ asc$/
  end

  it 'should throw an ArgumentError if a bogus order direction is given' do
    lambda do
      session.search Post do
        order_by :sort_title, :sideways
      end
    end.should raise_error(ArgumentError)
  end

  it 'should not allow ordering by multiple-value fields' do
    lambda do
      session.search Post do
        order_by :category_ids
      end
    end.should raise_error(ArgumentError)
  end

  it 'should not turn faceting on if no facet requested' do
    session.search(Post)
    connection.should_not have_last_search_with('facet')
  end

  it 'should turn faceting on if facet is requested' do
    session.search Post do
      facet :category_ids
    end
    connection.should have_last_search_with(:facet => 'true')
  end

  it 'should request single field facet' do
    session.search Post do
      facet :category_ids
    end
    connection.should have_last_search_with(:"facet.field" => %w(category_ids_im))
  end

  it 'should request multiple field facets' do
    session.search Post do
      facet :category_ids, :blog_id
    end
    connection.should have_last_search_with(:"facet.field" => %w(category_ids_im blog_id_i))
  end

  it 'should set facet sort by count' do
    session.search Post do
      facet :category_ids, :sort => :count
    end
    connection.should have_last_search_with(:"f.category_ids_im.facet.sort" => 'true')
  end

  it 'should set facet sort by index' do
    session.search Post do
      facet :category_ids, :sort => :index
    end
    connection.should have_last_search_with(:"f.category_ids_im.facet.sort" => 'false')
  end

  it 'should throw ArgumentError if bogus facet sort provided' do
    lambda do
      session.search Post do
        facet :category_ids, :sort => :sideways
      end
    end.should raise_error(ArgumentError)
  end

  it 'should set the facet limit' do
    session.search Post do
      facet :category_ids, :limit => 10
    end
    connection.should have_last_search_with(:"f.category_ids_im.facet.limit" => 10)
  end

  it 'should set the facet minimum count' do
    session.search Post do
      facet :category_ids, :minimum_count => 5
    end
    connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 5)
  end

  it 'should set the facet minimum count to zero if zeros are allowed' do
    session.search Post do
      facet :category_ids, :zeros => true
    end
    connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 0)
  end

  it 'should set the facet minimum count to one by default' do
    session.search Post do
      facet :category_ids
    end
    connection.should have_last_search_with(:"f.category_ids_im.facet.mincount" => 1)
  end

  describe 'with date faceting' do
    before :each do
      @time_range = (Time.parse('2009-06-01 00:00:00 -0400')..
                     Time.parse('2009-07-01 00:00:00 -0400'))
    end

    it 'should not send date facet parameters if time range is not specified' do
      session.search Post do |query|
        query.facet :published_at
      end
      connection.should_not have_last_search_with(:"facet.date")
    end

    it 'should set the facet to a date facet' do
      session.search Post do |query|
        query.facet :published_at, :time_range => @time_range
      end
      connection.should have_last_search_with(:"facet.date" => ['published_at_d'])
    end

    it 'should set the facet start and end' do
      session.search Post do |query|
        query.facet :published_at, :time_range => @time_range
      end
      connection.should have_last_search_with(
        :"f.published_at_d.facet.date.start" => '2009-06-01T04:00:00Z',
        :"f.published_at_d.facet.date.end" => '2009-07-01T04:00:00Z'
      )
    end

    it 'should default the time interval to 1 day' do
      session.search Post do |query|
        query.facet :published_at, :time_range => @time_range
      end
      connection.should have_last_search_with(:"f.published_at_d.facet.date.gap" => "+86400SECONDS")
    end

    it 'should use custom time interval' do
      session.search Post do |query|
        query.facet :published_at, :time_range => @time_range, :time_interval => 3600
      end
      connection.should have_last_search_with(:"f.published_at_d.facet.date.gap" => "+3600SECONDS")
    end

    it 'should allow computation of one other time' do
      session.search Post do |query|
        query.facet :published_at, :time_range => @time_range, :time_other => :before
      end
      connection.should have_last_search_with(:"f.published_at_d.facet.date.other" => %w(before))
    end

    it 'should allow computation of two other times' do
      session.search Post do |query|
        query.facet :published_at, :time_range => @time_range, :time_other => [:before, :after]
      end
      connection.should have_last_search_with(:"f.published_at_d.facet.date.other" => %w(before after))
    end

    it 'should not allow computation of bogus other time' do
      lambda do
        session.search Post do |query|
          query.facet :published_at, :time_range => @time_range, :time_other => :bogus
        end
      end.should raise_error(ArgumentError)
    end

    it 'should not allow date faceting on a non-date field' do
      lambda do
        session.search Post do |query|
          query.facet :blog_id, :time_range => @time_range
        end
      end.should raise_error(ArgumentError)
    end
  end

  describe 'with query faceting' do
    it 'should turn faceting on' do
      session.search Post do
        facet :foo do
          row :bar do
            with(:average_rating).between(4.0..5.0)
          end
        end
      end
      connection.should have_last_search_with(:facet => 'true')
    end

    it 'should facet by query' do
      session.search Post do
        facet :foo do
          row :bar do
            with(:average_rating).between(4.0..5.0)
          end
        end
      end
      connection.should have_last_search_with(:"facet.query" => 'average_rating_f:[4\.0 TO 5\.0]')
    end

    it 'should request multiple query facets' do
      session.search Post do
        facet :foo do
          row :bar do
            with(:average_rating).between(3.0..4.0)
          end
          row :baz do
            with(:average_rating).between(4.0..5.0)
          end
        end
      end
      connection.should have_last_search_with(
        :"facet.query" => [
          'average_rating_f:[3\.0 TO 4\.0]',
          'average_rating_f:[4\.0 TO 5\.0]'
        ]
      )
    end

    it 'should request query facet with multiple conditions' do
      session.search Post do
        facet :foo do
          row :bar do
            with(:category_ids, 1)
            with(:blog_id, 2)
          end
        end
      end
      connection.should have_last_search_with(
        :"facet.query" => '(category_ids_im:1 AND blog_id_i:2)'
      )
    end

    it 'should request query facet with disjunction' do
      session.search Post do
        facet :foo do
          row :bar do
            any_of do
              with(:category_ids, 1)
              with(:blog_id, 2)
            end
          end
        end
      end
      connection.should have_last_search_with(
        :"facet.query" => '(category_ids_im:1 OR blog_id_i:2)'
      )
    end

    it 'should request query facet with internal dynamic field' do
      session.search Post do
        facet :test do
          row 'foo' do
            dynamic :custom_string do
              with :test, 'foo'
            end
          end
        end
      end
      connection.should have_last_search_with(
        :"facet.query" => 'custom_string\:test_s:foo'
      )
    end

    it 'should request query facet with external dynamic field' do
      session.search Post do
        dynamic :custom_string do
          facet :test do
            row 'foo' do
              with :test, 'foo'
            end
          end
        end
      end
      connection.should have_last_search_with(
        :"facet.query" => 'custom_string\:test_s:foo'
      )
    end

    it 'should not allow 0 arguments to facet method with block' do
      lambda do
        session.search Post do
          facet do
          end
        end
      end.should raise_error(ArgumentError)
    end

    it 'should not allow more than 1 argument to facet method with block' do
      lambda do
        session.search Post do
          facet :foo, :bar do
          end
        end
      end.should raise_error(ArgumentError)
    end
  end

  it 'builds query facets when passed :only argument to field facet declaration' do
    session.search Post do
      facet :category_ids, :only => [1, 3]
    end
    connection.should have_last_search_with(
      :"facet.query" => ['category_ids_im:1', 'category_ids_im:3']
    )
  end

  it 'converts limited query facet values to the correct type' do
    session.search Post do
      facet :published_at, :only => [Time.utc(2009, 8, 28, 15, 33), Time.utc(2008,8, 28, 15, 33)]
    end
    connection.should have_last_search_with(
      :"facet.query" => [
        'published_at_d:2009\-08\-28T15\:33\:00Z',
        'published_at_d:2008\-08\-28T15\:33\:00Z'
    ]
    )
  end

  it 'should allow faceting by dynamic string field' do
    session.search Post do
      dynamic :custom_string do
        facet :test
      end
    end
    connection.should have_last_search_with(:"facet.field" => %w(custom_string:test_s))
  end

  it 'should properly escape namespaced type names' do
    session.search(Namespaced::Comment)
    connection.should have_last_search_with(:q => 'type:Namespaced\:\:Comment')
  end

  it 'should build search for multiple types' do
    session.search(Post, Namespaced::Comment)
    connection.should have_last_search_with(:q => 'type:(Post OR Namespaced\:\:Comment)')
  end

  it 'should allow search on fields common to all types with DSL' do
    time = Time.parse('1983-07-08 05:00:00 -0400')
    session.search Post, Namespaced::Comment do
      with :published_at, time
    end
    connection.should have_last_search_with(:fq => ['published_at_d:1983\-07\-08T09\:00\:00Z'])
  end

  it 'should allow search on fields common to all types with conditions' do
    time = Time.parse('1983-07-08 05:00:00 -0400')
    session.search Post, Namespaced::Comment, :conditions => { :published_at => time }
    connection.should have_last_search_with(:fq => ['published_at_d:1983\-07\-08T09\:00\:00Z'])
  end

  it 'should allow search on dynamic fields common to all types' do
    session.search Post, Namespaced::Comment do
      dynamic :custom_string do
        with(:test, 'test')
      end
    end
    connection.should have_last_search_with(:fq => ['custom_string\\:test_s:test'])
  end

  it 'should combine all text fields' do
    session.search Post, Namespaced::Comment do
      keywords 'keywords'
    end
    connection.searches.last[:qf].split(' ').sort.should == 
      %w(author_name_text backwards_title_text body_text title_text)
  end

  it 'should allow specification of a text field that only exists in one type' do
    session.search Post, Namespaced::Comment do
      keywords 'keywords', :fields => :author_name
    end
    connection.searches.last[:qf].should == 'author_name_text'
  end

  it 'should raise Sunspot::UnrecognizedFieldError if search scoped to field not common to all types' do
    lambda do
      session.search Post, Namespaced::Comment do
        with :blog_id, 1
      end
    end.should raise_error(Sunspot::UnrecognizedFieldError)
  end

  it 'should raise Sunspot::UnrecognizedFieldError if search scoped to field configured differently between types' do
    lambda do
      session.search Post, Namespaced::Comment do
        with :average_rating, 2.2 # this is a float in Post but an integer in Comment
      end
    end.should raise_error(Sunspot::UnrecognizedFieldError)
  end

  it 'should raise Sunspot::UnrecognizedFieldError if a text field that does not exist for any type is specified' do
    lambda do
      session.search Post, Namespaced::Comment do
        keywords 'fulltext', :fields => :bogus
      end
    end.should raise_error(Sunspot::UnrecognizedFieldError)
  end

  it 'should ignore condition if field is not common to all types' do
    session.search Post, Namespaced::Comment, :conditions => { :blog_id => 1 }
    connection.should_not have_last_search_with(:fq)
  end

  it 'should allow building search using block argument rather than instance_eval' do
    @blog_id = 1
    session.search Post do |query|
      query.with(:blog_id, @blog_id)
    end
    connection.should have_last_search_with(:fq => ['blog_id_i:1'])
  end

  it 'should raise Sunspot::UnrecognizedFieldError for nonexistant fields in block scope' do
    lambda do
      session.search Post do
        with :bogus, 'Field'
      end
    end.should raise_error(Sunspot::UnrecognizedFieldError)
  end

  it 'should raise Sunspot::UnrecognizedFieldError for nonexistant fields in keywords' do
    lambda do
      session.search Post do
        keywords 'text', :fields => :bogus
      end
    end.should raise_error(Sunspot::UnrecognizedFieldError)
  end

  it 'should raise NoMethodError if bogus operator referenced' do
    lambda do
      session.search Post do
        with(:category_ids).resembling :bogus_condition
      end
    end.should raise_error(NoMethodError)
  end

  it 'should raise ArgumentError if no :page argument given to paginate' do
    lambda do
      session.search Post do
        paginate
      end
    end.should raise_error(ArgumentError)
  end

  it 'should raise ArgumentError if bogus argument given to paginate' do
    lambda do
      session.search Post do
        paginate :page => 4, :ugly => :puppy
      end
    end.should raise_error(ArgumentError)
  end

  it 'should raise ArgumentError if more than two arguments passed to scope method' do
    lambda do
      session.search Post do
        with(:category_ids, 4, 5)
      end
    end.should raise_error(ArgumentError)
  end

  private

  def config
    @config ||= Sunspot::Configuration.build
  end

  def connection
    @connection ||= Mock::Connection.new
  end

  def session
    @session ||= Sunspot::Session.new(config, connection)
  end
end