require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
#require 'rubygems'
#require 'parslet'
#require 'spec'
#require 'spec/autorun'

#load '../../lib/parsing_nesting/grammar.rb'
#load '../../lib/parsing_nesting/tree.rb'

module SolrQuerySpecHelper
  def parse(s, arg=nil)
    if arg
      ParsingNesting::Tree.parse(s, arg)
    else
      ParsingNesting::Tree.parse(s)
    end
  end
  
  # yields localparam string, and the actual internal query
  def local_param_match(query)
    query.should =~ /^ *_query_:\"\{([^}]+)\}(.*)" *$/
    query =~ /^ *_query_:\"\{([^}]+)\}(.*)" *$/
    (param_str = $1).should_not be_nil
    (query = $2).should_not be_nil
    
    yield [param_str, query] if block_given?
  end
  
  def bare_local_param_match(query)
    query.should =~ / *\{([^}]+)\}(.*)/
    query =~ /\{([^}]+)\}(.*)/
    (param_str = $1).should_not be_nil
    (query = $2).should_not be_nil
    yield [param_str, query] if block_given?
  end
  
  # Convenience for matching a lucene query combining nested queries,
  # and getting out the nested queries as matches. 
  # pass in a string representing a regexp that uses $QUERY as placeholder where
  # a nested _query_: will be. 
  #
  # * Any parens in your passed in regexp will
  # be paren literals, don't escape em yourself -- you can't do your
  # own captures, because if the regexp passes, it'll yield to a block
  # with a list, in order, of nested queries.
  #  
  #
  # * Can include $ALL to represent literal "*:*"
  #
  # Yes, the regexp matching isn't as robust as it could be, hard
  # to deal with like escaped end-quotes and stuff in a regexp, but
  # should mostly work.   
  def query_template_matcher(top_query, regexp_str)
    nested_re = '(_query_:".+")'
    regexp_str = regexp_str.gsub("(", '\(').gsub(')', '\)').gsub("$QUERY", nested_re).gsub("$ALL", "\\*\\:\\*")
    regexp = Regexp.new('^ *' + regexp_str + ' *$')

    top_query.should match( regexp )
    
    yield *regexp.match(top_query).captures if block_given?    
  end
end

describe "NestingParser" do
    describe "Translating to Solr" do
    include SolrQuerySpecHelper
       
    describe "with basic simple query" do
      before do
        @query = parse("one two three").to_query(:qf => "field field2^5", :pf=>"$pf_title")
      end
      it "should include LocalParams" do
        local_param_match(@query) do |params, query|
          params.should include("pf=$pf_title")
          params.should include('qf=\'field field2^5\'')          
        end                
      end
      
      it "should include the query" do
        local_param_match(@query) do |params, query|
          query.should == "one two three"
        end
      end
    end

    describe "with custom qf" do
      it "should insist on dismax for nested query" do
        query = parse("one two three").to_query(:defType => "field", :qf=>"$qf")
        local_param_match(query) do |params, query|
          params.should match(/^\!dismax /)
          params.should_not match(/field/)
        end
      end

      it "should insist on edismax for nested query" do
        query = parse("one two three", 'edismax').to_query(:defType => "field", :qf=>"$qf")
        local_param_match(query) do |params, query|
          params.should match(/^\!edismax qf=\$qf/)
          params.should_not match(/field/)
        end
      end
    end
      
    
    describe "with simple mandatory/excluded terms" do
      before do
        @inner_query = 'red +army -"soviet union" germany'
        @full_query = parse(@inner_query).to_query(:qf => "foo", :pf=>"bar")
      end
      it "should include query" do
        local_param_match(@full_query) do |params, query|
          query.should == @inner_query.gsub('"', '\\\\"')
        end
      end
    end
    
    describe "with embeddable AND query" do
      before do
        @query = parse("one two AND three").to_query({:qf => "$qf"})
      end
      it "should flatten to one dismax query" do
        local_param_match(@query) do |params, query|
          query.should == "one +two +three"
        end
      end
      
      describe ", with mandatory/excluded" do
        before do
          @query = parse("one -two AND +three").to_query({:qf=>"$qf"})
        end
        it "should preserve +/- operators" do
          local_param_match(@query) do |params, query|
            query.should == "one -two +three"
          end
        end
      end
      
      describe ", deeply nested" do
        before do
          @query = parse("blue (green AND -violet) AND (+big AND (-small AND medium))").to_query({:qf=>"$qf"})
        end
        it "should flatten into dismax" do
          local_param_match(@query) do |params, query|
            query.should == "blue +green -violet +big -small +medium"
          end
        end
      end     
      
      it "for simple OR list, forcing mm=1" do
        query = parse("one OR two OR three").to_query(:qf => "$qf", :mm=>"50%")        
        local_param_match(query) do |params, query|
          params.should include("mm=1")
          query.should == "one two three"
        end                
      end
    end
   
    
    describe "that needs to create multiple nested queries" do

      
      it "for two lists, OR'd" do
        query = parse("(one two three) OR (red -green +blue)").to_query(:qf => "$qf")  
        
        query_template_matcher(query, "( *$QUERY +OR +$QUERY *)" ) do |first_half, second_half|
                
          local_param_match(first_half) do |params, query|
            params.should include("qf=$qf")
            query.should == "one two three"
          end
          
          local_param_match(second_half) do |params, query|
            params.should include("qf=$qf")
            query.should == "red -green +blue"
          end
        end
      end
      
      it "for AND list that can not be flattened" do
        params = {:qf=>"$qf", :pf=>"$pf", :mm=>"50%"}
        query = parse("a OR b AND x OR y").to_query( params )
        
        query_template_matcher(query, "( *$QUERY +AND +$QUERY *)" ) do |first, second|                
          first.should == parse("a OR b").to_query(params)
          second.should == parse("x OR y").to_query(params)
        end
      end
      
      it "for AND of two lists" do
        params = {:qf => "$qf", :pf=>"$pf", :mm=>"50%"}
        query = parse("(one +two three) AND (four five -six)").to_query( params )
                
        query_template_matcher(query, "( *$QUERY +AND +$QUERY *)" ) do |first, second|
          first.should == parse("one +two three").to_query(params)
          second.should == parse("four five -six").to_query(params)          
        end
                
      end
      
      it "for crazy complicated query" do
        query = parse("red AND dawn OR (-night -afternoon) AND NOT (moscow OR beach) ").to_query(:qf => "$qf", :pf =>"$pf", :mm=>"50%")
        
        query_template_matcher(query, "( *$QUERY +AND +( *$QUERY +OR +($ALL AND NOT $QUERY *) *) +AND NOT $QUERY *)") do |red_q, dawn_q, night_q, moscow_q|
        
          local_param_match(red_q) { |params, query| query.should == "red" }            
        
          local_param_match(dawn_q) { |params, query| query.should == "dawn"}
          
          local_param_match(night_q) do |params, query|
            params.should include("mm=1")
            query.should == "night afternoon"
          end
          
          local_param_match(moscow_q) do |params, query|
            params.should include("mm=1")
            query.should == "moscow beach"
          end
        
        end
                
      end
      
      
    end
    
    describe "for NOT operator" do
      it "simple" do
        query = parse("NOT frog").to_query
          
        query_template_matcher(query, "NOT $QUERY") do |q|
          q.should == parse("frog").to_query
        end                    
      end
      it "binds tightly" do
        query = parse("one NOT two three").to_query
       
        query_template_matcher(query, "$QUERY AND NOT $QUERY") do |q1, q2|
          
          local_param_match(q1) do |params, query|
            query.should == "one three"
          end
          
          local_param_match(q2) do |params, query|
            query.should == "two"
          end
        end
        
      end
      it "complicated operand" do
        query = parse("one OR two NOT (three OR four AND five)").to_query
        #"_query_:'{!dismax mm=1}one two' AND NOT ( _query_:'{!dismax mm=1}three four' AND _query_:'{!dismax }five' )"
        query_template_matcher(query, "$QUERY +AND NOT +( *$QUERY +AND +$QUERY *)") do |external_or, internal_or, internal_term|
          external_or.should == parse("one OR two").to_query
          internal_or.should == parse("three OR four").to_query 
          internal_term.should == parse("five").to_query 
        end        
      end
      
      it "uses workaround on NOT as operand to OR" do
        query = parse("two OR (NOT (three))").to_query
        query_template_matcher(query, "( *$QUERY +OR +($ALL +AND +NOT +$QUERY) *)")        
      end
      
    end
    
    describe "for pure negative" do
      it "should convert simple pure negative" do
        query = parse('-one -two -"a phrase"').to_query(:qf => "$qf", :mm => "100%")
        
        query_template_matcher(query, " *NOT $QUERY") do |query|
          local_param_match(query) do |params, query|
            params.should include("mm=1")
            query.should == 'one two \\"a phrase\\"'
          end
        end
        
      end
      
      it "should convert pure negative AND" do
        query = parse("-one AND -two AND -three").to_query(:qf => "$qf", :mm => "100%")
        
        query_template_matcher(query, "NOT $QUERY") do |query|
          local_param_match(query) do |params, query|
            params.should =~ /mm=1 |$/
            query.should == 'one two three'
          end
        end
      end
      
      it "should convert pure negative OR" do
        query = parse("-one OR -two OR -three").to_query        
        
        query_template_matcher(query, "NOT $QUERY") do |query|
          local_param_match(query) do |params, query|
            params.should include("mm=100%")
            query.should == "one two three"
          end
        end

      end
      
      it "should convert crazy pure negative combo" do        
        query = parse("(-one -two) OR -three OR (-five AND -six)").to_query
        
        query_template_matcher(query, "( *($ALL +AND +NOT +$QUERY) +OR +( *$ALL +AND +NOT +$QUERY *) +OR +( *$ALL +AND +NOT +$QUERY *) *)")                
      end
    end
    
    
    # When a single parse will be the whole query, we use
    # different more compact production
    describe "Single Query" do
      before do
        @solr_local_params = {"qf" => "$title_qf", "pf" => "$title_pf"}
      end
      describe "simple search" do
        it "should work with local params" do
          hash = parse("one +two -three").to_single_query_params(@solr_local_params)
          hash[:defType].should == "dismax"
          bare_local_param_match(hash[:q]) do |params, query|
            query.should == "one +two -three"
            params.should include("pf=$title_pf")
            params.should include("qf=$title_qf")
          end
        end
        
        it "should work without local params" do
          hash = parse("one +two -three").to_single_query_params({})
          hash[:defType].should == "dismax"
          hash[:q].should == "one +two -three"          
        end
      end
      describe "simple pure negative" do
        it "should be nested NOT" do
          hash = parse("-one -two").to_single_query_params({})
          hash[:defType].should == "dismax"
          query_template_matcher(hash[:q], "NOT $QUERY") do |query|
            local_param_match(query) do |params, query|
              query.should == "one two"
              params.should include("mm=1")
            end            
          end
        end
      end
      describe "complex query" do
        it "should parse" do
          hash = parse("one AND (two OR three)").to_single_query_params({})
          hash[:defType].should == "lucene"
          query_template_matcher(hash[:q], "( *$QUERY +AND +$QUERY *)") do |first, second|
            local_param_match(first) do |params, query|
              params.should include("dismax")
              query.should == "one"
            end
            local_param_match(second) do |params, query|
              params.should include("mm=1")
              params.should include("dismax")
              query.should == "two three"
            end
          end
        end
      end
      
      
    end  

    
  end
  
  
  
  
end