/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DisjunctionMaxQuery;
import org.apache.lucene.search.PhraseQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.search.QParser;
import org.apache.solr.util.SolrPluginUtils.DisjunctionMaxQueryParser;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * Tests that the functions in SolrPluginUtils work as advertised.
 */
public class SolrPluginUtilsTest extends SolrTestCaseJ4 {

  @BeforeClass
  public static void beforeClass() throws Exception {
    initCore("solrconfig.xml","schema.xml");
  }

  @Test
  public void testPartialEscape() {

    assertEquals("",pe(""));
    assertEquals("foo",pe("foo"));
    assertEquals("foo\\:bar",pe("foo:bar"));
    assertEquals("+foo\\:bar",pe("+foo:bar"));
    assertEquals("foo \\! bar",pe("foo ! bar"));
    assertEquals("foo\\?",pe("foo?"));
    assertEquals("foo \"bar\"",pe("foo \"bar\""));
    assertEquals("foo\\! \"bar\"",pe("foo! \"bar\""));
        
  }

  @Test
  public void testStripUnbalancedQuotes() {
        
    assertEquals("",strip(""));
    assertEquals("foo",strip("foo"));
    assertEquals("foo \"bar\"",strip("foo \"bar\""));
    assertEquals("42",strip("42\""));
    assertEquals("\"how now brown cow?\"",strip("\"how now brown cow?\""));
    assertEquals("\"you go\" \"now!\"",strip("\"you go\" \"now!\""));
        
  }

  @Test
  public void testStripIllegalOperators() {

    assertEquals("",stripOp(""));
    assertEquals("foo",stripOp("foo"));
    assertEquals("foo -bar",stripOp("foo -bar"));
    assertEquals("foo +bar",stripOp("foo +bar"));
    assertEquals("foo + bar",stripOp("foo + bar"));
    assertEquals("foo+ bar",stripOp("foo+ bar"));
    assertEquals("foo+ bar",stripOp("foo+ bar"));
    assertEquals("foo+",stripOp("foo+"));
    assertEquals("foo bar",stripOp("foo bar -"));
    assertEquals("foo bar ",stripOp("foo bar - + ++"));
    assertEquals("foo bar",stripOp("foo --bar"));
    assertEquals("foo bar ",stripOp("foo -------------------------------------------------------------------------------------------------------------------------bar --"));
    assertEquals("foo bar ",stripOp("foo --bar -----------------------------------------------------------------------------------------------------------------------"));

  }

  @Test
  public void testParseFieldBoosts() throws Exception {

    Map<String,Float> e1 = new HashMap<>();
    e1.put("fieldOne",2.3f);
    e1.put("fieldTwo",null);
    e1.put("fieldThree",-0.4f);

    assertEquals("basic e1", e1, SolrPluginUtils.parseFieldBoosts
                 ("fieldOne^2.3 fieldTwo fieldThree^-0.4"));
    assertEquals("spacey e1", e1, SolrPluginUtils.parseFieldBoosts
                 ("  fieldOne^2.3   fieldTwo fieldThree^-0.4   "));
    assertEquals("really spacey e1", e1, SolrPluginUtils.parseFieldBoosts
                 (" \t fieldOne^2.3 \n  fieldTwo fieldThree^-0.4   "));
    assertEquals("really spacey e1", e1, SolrPluginUtils.parseFieldBoosts
                 (new String[]{" \t fieldOne^2.3 \n",
                               "  fieldTwo fieldThree^-0.4   ",
                               " "}));

    Map<String,Float> e2 = new HashMap<>();
    assertEquals("empty e2", e2, SolrPluginUtils.parseFieldBoosts
                 (""));
    assertEquals("spacey e2", e2, SolrPluginUtils.parseFieldBoosts
                 ("   \t   "));
  }

  @Test  
  public void testDisjunctionMaxQueryParser() throws Exception {
        
    Query out;
    String t;

    SolrQueryRequest req = req("df", "text");
    QParser qparser = QParser.getParser("hi", "dismax", req);

    DisjunctionMaxQueryParser qp =
      new SolrPluginUtils.DisjunctionMaxQueryParser(qparser, req.getParams().get("df"));

    qp.addAlias("hoss", 0.01f, SolrPluginUtils.parseFieldBoosts
                ("title^2.0 title_stemmed name^1.2 subject^0.5"));
    qp.addAlias("test", 0.01f, SolrPluginUtils.parseFieldBoosts("text^2.0"));
    qp.addAlias("unused", 1.0f, SolrPluginUtils.parseFieldBoosts
                ("subject^0.5 sind^1.5"));
                     

    /* first some sanity tests that don't use aliasing at all */

    t = "XXXXXXXX";
    out = qp.parse(t);
    assertNotNull(t+" sanity test gave back null", out);
    assertTrue(t+" sanity test isn't TermQuery: " + out.getClass(),
               out instanceof TermQuery);
    assertEquals(t+" sanity test is wrong field",
                 qp.getDefaultField(),
                 ((TermQuery)out).getTerm().field());

    t = "subject:XXXXXXXX";
    out = qp.parse(t);
    assertNotNull(t+" sanity test gave back null", out);
    assertTrue(t+" sanity test isn't TermQuery: " + out.getClass(),
               out instanceof TermQuery);
    assertEquals(t+" sanity test is wrong field", "subject",
                 ((TermQuery)out).getTerm().field());

    /* field has untokenzied type, so this should be a term anyway */
    t = "sind:\"simple phrase\"";
    out = qp.parse(t);
    assertNotNull(t+" sanity test gave back null", out);
    assertTrue(t+" sanity test isn't TermQuery: " + out.getClass(),
               out instanceof TermQuery);
    assertEquals(t+" sanity test is wrong field", "sind",
                 ((TermQuery)out).getTerm().field());

    t = "subject:\"simple phrase\"";
    out = qp.parse(t);
    assertNotNull(t+" sanity test gave back null", out);
    assertTrue(t+" sanity test isn't PhraseQuery: " + out.getClass(),
               out instanceof PhraseQuery);
    assertEquals(t+" sanity test is wrong field", "subject",
                 ((PhraseQuery)out).getTerms()[0].field());

        
    /* now some tests that use aliasing */

    /* basic usage of single "term" */
    t = "hoss:XXXXXXXX";
    out = qp.parse(t);
    assertNotNull(t+" was null", out);
    assertTrue(t+" wasn't a DMQ:" + out.getClass(),
               out instanceof DisjunctionMaxQuery);
    assertEquals(t+" wrong number of clauses", 4,
                 countItems(((DisjunctionMaxQuery)out).iterator()));
        

    /* odd case, but should still work, DMQ of one clause */
    t = "test:YYYYY";
    out = qp.parse(t);
    assertNotNull(t+" was null", out);
    assertTrue(t+" wasn't a DMQ:" + out.getClass(),
               out instanceof DisjunctionMaxQuery);
    assertEquals(t+" wrong number of clauses", 1,
                 countItems(((DisjunctionMaxQuery)out).iterator()));
        
    /* basic usage of multiple "terms" */
    t = "hoss:XXXXXXXX test:YYYYY";
    out = qp.parse(t);
    assertNotNull(t+" was null", out);
    assertTrue(t+" wasn't a boolean:" + out.getClass(),
               out instanceof BooleanQuery);
    {
      BooleanQuery bq = (BooleanQuery)out;
      List<BooleanClause> clauses = new ArrayList<>(bq.clauses());
      assertEquals(t+" wrong number of clauses", 2,
                   clauses.size());
      Query sub = clauses.get(0).getQuery();
      assertTrue(t+" first wasn't a DMQ:" + sub.getClass(),
                 sub instanceof DisjunctionMaxQuery);
      assertEquals(t+" first had wrong number of clauses", 4,
                   countItems(((DisjunctionMaxQuery)sub).iterator()));
      sub = clauses.get(1).getQuery();
      assertTrue(t+" second wasn't a DMQ:" + sub.getClass(),
                 sub instanceof DisjunctionMaxQuery);
      assertEquals(t+" second had wrong number of clauses", 1,
                   countItems(((DisjunctionMaxQuery)sub).iterator()));
    }
            
    /* a phrase, and a term that is a stop word for some fields */
    t = "hoss:\"XXXXXX YYYYY\" hoss:the";
    out = qp.parse(t);
    assertNotNull(t+" was null", out);
    assertTrue(t+" wasn't a boolean:" + out.getClass(),
               out instanceof BooleanQuery);
    {
      BooleanQuery bq = (BooleanQuery)out;
      List<BooleanClause> clauses = new ArrayList<>(bq.clauses());
      assertEquals(t+" wrong number of clauses", 2,
                   clauses.size());
      Query sub = clauses.get(0).getQuery();
      assertTrue(t+" first wasn't a DMQ:" + sub.getClass(),
                 sub instanceof DisjunctionMaxQuery);
      assertEquals(t+" first had wrong number of clauses", 4,
                   countItems(((DisjunctionMaxQuery)sub).iterator()));
      sub = clauses.get(1).getQuery();
      assertTrue(t+" second wasn't a DMQ:" + sub.getClass(),
                 sub instanceof DisjunctionMaxQuery);
      assertEquals(t+" second had wrong number of clauses (stop words)", 2,
                   countItems(((DisjunctionMaxQuery)sub).iterator()));
    }
        

        
  }

  private static int countItems(Iterator i) {
    int count = 0;
    while (i.hasNext()) {
      count++;
      i.next();
    }
    return count;
  }

  @Test                                    
  public void testMinShouldMatchCalculator() {

    /* zero is zero is zero */
    assertEquals(0, calcMSM(5, "0"));
    assertEquals(0, calcMSM(5, "0%"));
    assertEquals(0, calcMSM(5, " -5 "));
    assertEquals(0, calcMSM(5, "\n -100% \n"));

    /* basic integers */
    assertEquals(3, calcMSM(5, " \n3\n "));
    assertEquals(2, calcMSM(5, "-3"));
    assertEquals(3, calcMSM(3, "3"));
    assertEquals(0, calcMSM(3, "-3"));
    assertEquals(3, calcMSM(3, "5"));
    assertEquals(0, calcMSM(3, "-5"));

    /* positive percentages with rounding */
    assertEquals(0, calcMSM(3, " \n25% \n"));
    assertEquals(1, calcMSM(4, "25%"));
    assertEquals(1, calcMSM(5, " 25% "));
    assertEquals(2, calcMSM(10, "25%"));
        
    /* negative percentages with rounding */
    assertEquals(3, calcMSM(3, " \n-25%\n "));
    assertEquals(3, calcMSM(4, "-25%"));
    assertEquals(4, calcMSM(5, "-25%"));
    assertEquals(8, calcMSM(10, "-25%"));

    /* conditional */
    assertEquals(1, calcMSM(1, "3<0"));
    assertEquals(2, calcMSM(2, "3<0"));
    assertEquals(3, calcMSM(3, "3<0"));
    assertEquals(0, calcMSM(4, "3<0"));
    assertEquals(0, calcMSM(5, "3<0"));
    assertEquals(1, calcMSM(1, "3<25%"));
    assertEquals(2, calcMSM(2, " 3\n<\n25% "));
    assertEquals(3, calcMSM(3, "3<25%"));
    assertEquals(1, calcMSM(4, "\n 3 < \n25%\n "));
    assertEquals(1, calcMSM(5, "3<25%"));

    /* multiple conditionals */
    assertEquals(1, calcMSM(1, "\n3 < -25% 10 < -3 \n"));
    assertEquals(2, calcMSM(2, " 3 < -25% 10 < -3\n"));
    assertEquals(3, calcMSM(3, " 3 < -25% \n 10 < -3 \n"));
    assertEquals(3, calcMSM(4, " 3 < -25% 10 < -3 "));
    assertEquals(4, calcMSM(5, " 3 < -25% 10 < -3"));
    assertEquals(5, calcMSM(6, "3<-25% 10<-3"));
    assertEquals(6, calcMSM(7, " 3 < -25% 10 < -3 "));
    assertEquals(6, calcMSM(8, " 3 < -25% 10 \n < -3\n"));
    assertEquals(7, calcMSM(9, " 3 < -25% 10 < -3 \n"));
    assertEquals(8, calcMSM(10, " 3 < -25% 10 < -3"));
    assertEquals(8, calcMSM(11, "3<-25% 10<-3"));
    assertEquals(9, calcMSM(12, "3<-25% 10<-3"));
    assertEquals(97, calcMSM(100, "3<-25% 10<-3"));

    BooleanQuery.Builder q = new BooleanQuery.Builder();
    q.add(new TermQuery(new Term("a","b")), Occur.SHOULD);
    q.add(new TermQuery(new Term("a","c")), Occur.SHOULD);
    q.add(new TermQuery(new Term("a","d")), Occur.SHOULD);
    q.add(new TermQuery(new Term("a","d")), Occur.SHOULD);

    SolrPluginUtils.setMinShouldMatch(q, "0");
    assertEquals(0, q.build().getMinimumNumberShouldMatch());
        
    SolrPluginUtils.setMinShouldMatch(q, "1");
    assertEquals(1, q.build().getMinimumNumberShouldMatch());
        
    SolrPluginUtils.setMinShouldMatch(q, "50%");
    assertEquals(2, q.build().getMinimumNumberShouldMatch());

    SolrPluginUtils.setMinShouldMatch(q, "99");
    assertEquals(4, q.build().getMinimumNumberShouldMatch());

    q.add(new TermQuery(new Term("a","e")), Occur.MUST);
    q.add(new TermQuery(new Term("a","f")), Occur.MUST);

    SolrPluginUtils.setMinShouldMatch(q, "50%");
    assertEquals(2, q.build().getMinimumNumberShouldMatch());
        
  }

  @Test
  public void testMinShouldMatchAutoRelax() {
    /* The basics should not be affected by autoRelax */
    BooleanQuery.Builder q = new BooleanQuery.Builder();
    q.add(new TermQuery(new Term("a","b")), Occur.SHOULD);
    q.add(new TermQuery(new Term("a","c")), Occur.SHOULD);
    q.add(new TermQuery(new Term("a","d")), Occur.SHOULD);
    q.add(new TermQuery(new Term("a","d")), Occur.SHOULD);

    SolrPluginUtils.setMinShouldMatch(q, "0", true);
    assertEquals(0, q.build().getMinimumNumberShouldMatch());

    SolrPluginUtils.setMinShouldMatch(q, "1", true);
    assertEquals(1, q.build().getMinimumNumberShouldMatch());

    SolrPluginUtils.setMinShouldMatch(q, "50%", true);
    assertEquals(2, q.build().getMinimumNumberShouldMatch());

    SolrPluginUtils.setMinShouldMatch(q, "99", true);
    assertEquals(4, q.build().getMinimumNumberShouldMatch());

    q.add(new TermQuery(new Term("a","e")), Occur.MUST);
    q.add(new TermQuery(new Term("a","f")), Occur.MUST);

    SolrPluginUtils.setMinShouldMatch(q, "50%", true);
    assertEquals(2, q.build().getMinimumNumberShouldMatch());

    /* Simulate stopwords through uneven disjuncts */
    q = new BooleanQuery.Builder();
    q.add(new DisjunctionMaxQuery(Collections.singleton(new TermQuery(new Term("a","foo"))), 0.0f), Occur.SHOULD);
    DisjunctionMaxQuery dmq = new DisjunctionMaxQuery(
        Arrays.asList(
            new TermQuery(new Term("a","foo")),
            new TermQuery(new Term("b","foo"))),
        0f);
    q.add(dmq, Occur.SHOULD);
    dmq = new DisjunctionMaxQuery(
        Arrays.asList(
            new TermQuery(new Term("a","bar")),
            new TermQuery(new Term("b","bar"))),
        0f);
    q.add(dmq, Occur.SHOULD);

    // Without relax
    SolrPluginUtils.setMinShouldMatch(q, "100%", false);
    assertEquals(3, q.build().getMinimumNumberShouldMatch());

    // With relax
    SolrPluginUtils.setMinShouldMatch(q, "100%", true);
    assertEquals(2, q.build().getMinimumNumberShouldMatch());

    // Still same result with a MUST clause extra
    q.add(new TermQuery(new Term("a","must")), Occur.MUST);
    SolrPluginUtils.setMinShouldMatch(q, "100%", true);
    assertEquals(2, q.build().getMinimumNumberShouldMatch());

    // Combination of dismax and non-dismax SHOULD clauses
    q.add(new TermQuery(new Term("b","should")), Occur.SHOULD);
    SolrPluginUtils.setMinShouldMatch(q, "100%", true);
    assertEquals(3, q.build().getMinimumNumberShouldMatch());
  }

  private static class InvokeSettersTestClass {
    private float aFloat = random().nextFloat();
    public float getAFloat() {
      return aFloat;
    }
    public void setAFloat(float aFloat) {
      this.aFloat = aFloat;
    }
    public void setAFloat(String aFloat) {
      this.aFloat = Float.parseFloat(aFloat);
    }
  }

  @Test
  public void testInvokeSetters() {
    final Float theFloat = random().nextFloat();
    implTestInvokeSetters(theFloat, theFloat);
    implTestInvokeSetters(theFloat, theFloat.toString());
  }

  public void implTestInvokeSetters(final Float theFloat, final Object theFloatObject) {
    final InvokeSettersTestClass bean = new InvokeSettersTestClass();
    final Map<String,Object> initArgs = new HashMap<>();
    initArgs.put("aFloat", theFloatObject);
    SolrPluginUtils.invokeSetters(bean, initArgs.entrySet());
    assertEquals(bean.getAFloat(), theFloat.floatValue(), 0.0);
  }

  /** macro */
  public String pe(CharSequence s) {
    return SolrPluginUtils.partialEscape(s).toString();
  }
    
  /** macro */
  public String strip(CharSequence s) {
    return SolrPluginUtils.stripUnbalancedQuotes(s).toString();
  }
   
  /** macro */
  public String stripOp(CharSequence s) {
    return SolrPluginUtils.stripIllegalOperators(s).toString();
  }
   
  /** macro */
  public int calcMSM(int clauses, String spec) {
    return SolrPluginUtils.calculateMinShouldMatch(clauses, spec);
  }
}

