/*
 * 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.spelling;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.lucene.tests.util.LuceneTestCase.SuppressTempFileChecks;
import org.apache.lucene.tests.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.CursorMarkParams;
import org.apache.solr.common.params.GroupParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.params.SpellingParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
import org.apache.solr.core.SolrCore;
import org.apache.solr.handler.component.SearchComponent;
import org.apache.solr.handler.component.SpellCheckComponent;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestHandler;
import org.apache.solr.response.SolrQueryResponse;
import org.junit.BeforeClass;
import org.junit.Test;

@SuppressTempFileChecks(
    bugUrl = "https://issues.apache.org/jira/browse/SOLR-1877 Spellcheck IndexReader leak bug?")
public class SpellCheckCollatorTest extends SolrTestCaseJ4 {

  // if adding documents to this test, adjust me.
  private static final int NUM_DOCS_WITH_TERM_EVERYOTHER = 8;
  private static final int NUM_DOCS = 17;

  @BeforeClass
  public static void beforeClass() throws Exception {
    initCore("solrconfig-spellcheckcomponent.xml", "schema.xml");
    assertU(
        adoc(
            "id", "0",
            "lowerfilt", "faith hope and love to",
            "teststop", "metanoia"));
    assertU(
        adoc(
            "id", "1",
            "lowerfilt", "faith hope and loaves",
            "teststop", "everyother"));
    assertU(
        adoc(
            "id", "2",
            "lowerfilt", "fat hops and loaves"));
    assertU(
        adoc(
            "id", "3",
            "lowerfilt", "faith of homer",
            "teststop", "metanoia",
            "teststop", "everyother"));
    assertU(
        adoc(
            "id", "4",
            "lowerfilt", "fat of homer"));
    assertU(
        adoc(
            "id", "5",
            "lowerfilt1", "peace",
            "teststop", "everyother"));
    assertU(
        adoc(
            "id", "6",
            "lowerfilt", "hyphenated word"));
    assertU(
        adoc(
            "id", "7",
            "teststop", "Jane filled out a form at Charles De Gaulle",
            "teststop", "everyother"));
    assertU(
        adoc(
            "id", "8",
            "teststop", "Dick flew from Heathrow"));
    assertU(
        adoc(
            "id", "9",
            "teststop", "Jane is stuck in customs because Spot chewed up the form",
            "teststop", "everyother"));
    assertU(
        adoc(
            "id", "10",
            "teststop", "Once in Paris Dick built a fire on the hearth"));
    assertU(
        adoc(
            "id", "11",
            "teststop", "Dick waited for Jane as he watched the sparks flow upward",
            "teststop", "everyother"));
    assertU(
        adoc(
            "id", "12",
            "teststop", "This June parisian rendez-vous is ruined because of a customs snafu"));
    assertU(
        adoc(
            "id", "13",
            "teststop", "partisan political machine",
            "teststop", "metanoia",
            "teststop", "everyother"));
    assertU(
        adoc(
            "id", "14",
            "teststop", "metanoia"));
    assertU(
        adoc(
            "id", "15",
            "teststop", "metanoia",
            "teststop", "everyother"));
    assertU(
        adoc(
            "id", "16",
            "teststop", "metanoia"));
    assertU(commit());
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void testCollationWithRangeQuery() {
    SolrCore core = h.getCore();
    SearchComponent speller = core.getSearchComponent("spellcheck");
    assertNotNull("speller is null and it shouldn't be", speller);

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add(SpellCheckComponent.COMPONENT_NAME, "true");
    params.add(SpellingParams.SPELLCHECK_BUILD, "true");
    params.add(SpellingParams.SPELLCHECK_COUNT, "10");
    params.add(SpellingParams.SPELLCHECK_COLLATE, "true");
    params.add(SpellingParams.SPELLCHECK_ALTERNATIVE_TERM_COUNT, "10");
    params.add(CommonParams.Q, "id:[1 TO 10] AND lowerfilt:lovw");
    {
      SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
      SolrQueryResponse rsp = new SolrQueryResponse();
      rsp.addResponseHeader(new SimpleOrderedMap());
      SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
      handler.handleRequest(req, rsp);
      req.close();
      NamedList values = rsp.getValues();
      NamedList spellCheck = (NamedList) values.get("spellcheck");
      NamedList collationHolder = (NamedList) spellCheck.get("collations");
      List<String> collations = collationHolder.getAll("collation");
      assertEquals(1, collations.size());
      String collation = collations.iterator().next();
      System.out.println(collation);
      assertEquals(
          "Incorrect collation: " + collation, "id:[1 TO 10] AND lowerfilt:love", collation);
    }
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void testCollationWithHypens() {
    SolrCore core = h.getCore();
    SearchComponent speller = core.getSearchComponent("spellcheck");
    assertNotNull("speller is null and it shouldn't be", speller);

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add(SpellCheckComponent.COMPONENT_NAME, "true");
    params.add(SpellingParams.SPELLCHECK_BUILD, "true");
    params.add(SpellingParams.SPELLCHECK_COUNT, "10");
    params.add(SpellingParams.SPELLCHECK_COLLATE, "true");

    params.add(CommonParams.Q, "lowerfilt:(hypenated-wotd)");
    {
      SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
      SolrQueryResponse rsp = new SolrQueryResponse();
      rsp.addResponseHeader(new SimpleOrderedMap());
      SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
      handler.handleRequest(req, rsp);
      req.close();
      NamedList values = rsp.getValues();
      NamedList spellCheck = (NamedList) values.get("spellcheck");
      NamedList collationHolder = (NamedList) spellCheck.get("collations");
      List<String> collations = collationHolder.getAll("collation");
      assertEquals(1, collations.size());
      String collation = collations.iterator().next();
      assertEquals("Incorrect collation: " + collation, "lowerfilt:(hyphenated-word)", collation);
    }

    params.remove(CommonParams.Q);
    params.add("defType", "dismax");
    params.add("qf", "/lowerfilt");
    params.add(CommonParams.Q, "hypenated-wotd");
    {
      SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
      SolrQueryResponse rsp = new SolrQueryResponse();
      rsp.add("responseHeader", new SimpleOrderedMap());
      SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
      handler.handleRequest(req, rsp);
      req.close();
      NamedList values = rsp.getValues();
      NamedList spellCheck = (NamedList) values.get("spellcheck");
      NamedList collationHolder = (NamedList) spellCheck.get("collations");
      List<String> collations = collationHolder.getAll("collation");
      assertEquals(1, collations.size());
      String collation = collations.iterator().next();
      assertEquals("Incorrect collation: " + collation, "hyphenated-word", collation);
    }
  }

  public void testCollateWithOverride() {
    assertQ(
        req(
            SpellCheckComponent.COMPONENT_NAME,
            "true",
            SpellCheckComponent.SPELLCHECK_DICT,
            "direct",
            SpellingParams.SPELLCHECK_COUNT,
            "10",
            SpellingParams.SPELLCHECK_COLLATE,
            "true",
            SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES,
            "10",
            SpellingParams.SPELLCHECK_MAX_COLLATIONS,
            "10",
            "qt",
            "/spellCheckCompRH",
            "defType",
            "edismax",
            "qf",
            "teststop",
            "mm",
            "1",
            CommonParams.Q,
            "partisian politcal mashine"),
        "//lst[@name='spellcheck']/lst[@name='collations']/str[@name='collation']='parisian political machine'");
    assertQ(
        req(
            SpellCheckComponent.COMPONENT_NAME,
            "true",
            SpellCheckComponent.SPELLCHECK_DICT,
            "direct",
            SpellingParams.SPELLCHECK_COUNT,
            "10",
            SpellingParams.SPELLCHECK_COLLATE,
            "true",
            SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES,
            "10",
            SpellingParams.SPELLCHECK_MAX_COLLATIONS,
            "10",
            "qt",
            "/spellCheckCompRH",
            "defType",
            "edismax",
            "qf",
            "teststop",
            "mm",
            "1",
            SpellingParams.SPELLCHECK_COLLATE_PARAM_OVERRIDE + "mm",
            "100%",
            CommonParams.Q,
            "partisian politcal mashine"),
        "//lst[@name='spellcheck']/lst[@name='collations']/str[@name='collation']='partisan political machine'");
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void testCollateWithFilter() {
    SolrCore core = h.getCore();
    SearchComponent speller = core.getSearchComponent("spellcheck");
    assertNotNull("speller is null and it shouldn't be", speller);

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add(SpellCheckComponent.COMPONENT_NAME, "true");
    params.add(SpellingParams.SPELLCHECK_BUILD, "true");
    params.add(SpellingParams.SPELLCHECK_COUNT, "10");
    params.add(SpellingParams.SPELLCHECK_COLLATE, "true");
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES, "10");
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATIONS, "10");
    params.add(CommonParams.Q, "lowerfilt:(+fauth +home +loane)");
    params.add(CommonParams.FQ, "NOT(id:1)");

    // Because a FilterQuery is applied which removes doc id#1 from possible hits, we would
    // not want the collations to return us "lowerfilt:(+faith +hope +loaves)" as this only matches
    // doc id#1.
    SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
    SolrQueryResponse rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    NamedList values = rsp.getValues();
    NamedList spellCheck = (NamedList) values.get("spellcheck");
    NamedList collationHolder = (NamedList) spellCheck.get("collations");
    List<String> collations = collationHolder.getAll("collation");
    assertTrue(collations.size() > 0);
    for (String collation : collations) {
      assertNotEquals("lowerfilt:(+faith +hope +loaves)", collation);
    }
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void testCollateWithMultipleRequestHandlers() {
    SolrCore core = h.getCore();
    SearchComponent speller = core.getSearchComponent("spellcheck");
    assertNotNull("speller is null and it shouldn't be", speller);

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add(SpellCheckComponent.COMPONENT_NAME, "true");
    params.add(SpellingParams.SPELLCHECK_DICT, "multipleFields");
    params.add(SpellingParams.SPELLCHECK_BUILD, "true");
    params.add(SpellingParams.SPELLCHECK_COUNT, "10");
    params.add(SpellingParams.SPELLCHECK_COLLATE, "true");
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES, "1");
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATIONS, "1");
    params.add(CommonParams.Q, "peac");

    // SpellCheckCompRH has no "qf" defined.  It will not find "peace" from "peac" despite it being
    // in the dictionary because requrying against this Request Handler results in 0 hits.
    SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
    SolrQueryResponse rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    NamedList values = rsp.getValues();
    NamedList spellCheck = (NamedList) values.get("spellcheck");
    NamedList collationHolder = (NamedList) spellCheck.get("collations");
    String singleCollation = (String) collationHolder.get("collation");
    assertNull(singleCollation);

    // SpellCheckCompRH1 has "lowerfilt1" defined in the "qf" param.  It will find "peace" from
    // "peac" because requrying field "lowerfilt1" returns the hit.
    params.remove(SpellingParams.SPELLCHECK_BUILD);
    handler = core.getRequestHandler("/spellCheckCompRH1");
    rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    values = rsp.getValues();
    spellCheck = (NamedList) values.get("spellcheck");
    collationHolder = (NamedList) spellCheck.get("collations");
    singleCollation = (String) collationHolder.get("collation");
    assertEquals(singleCollation, "peace");
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void testExtendedCollate() {
    SolrCore core = h.getCore();
    SearchComponent speller = core.getSearchComponent("spellcheck");
    assertNotNull("speller is null and it shouldn't be", speller);

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add(CommonParams.QT, "spellCheckCompRH");
    params.add(CommonParams.Q, "lowerfilt:(+fauth +home +loane)");
    params.add(SpellingParams.SPELLCHECK_EXTENDED_RESULTS, "true");
    params.add(SpellCheckComponent.COMPONENT_NAME, "true");
    params.add(SpellingParams.SPELLCHECK_BUILD, "true");
    params.add(SpellingParams.SPELLCHECK_COUNT, "10");
    params.add(SpellingParams.SPELLCHECK_COLLATE, "true");

    // Testing backwards-compatible behavior.
    // Returns 1 collation as a single string.
    // All words are "correct" per the dictionary, but this collation would
    // return no results if tried.
    SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
    SolrQueryResponse rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    NamedList values = rsp.getValues();
    NamedList spellCheck = (NamedList) values.get("spellcheck");
    NamedList collationHolder = (NamedList) spellCheck.get("collations");
    String singleCollation = (String) collationHolder.get("collation");
    assertEquals("lowerfilt:(+faith +homer +loaves)", singleCollation);

    // Testing backwards-compatible response format but will only return a
    // collation that would return results.
    params.remove(SpellingParams.SPELLCHECK_BUILD);
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES, "5");
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATIONS, "1");
    handler = core.getRequestHandler("/spellCheckCompRH");
    rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    values = rsp.getValues();
    spellCheck = (NamedList) values.get("spellcheck");
    collationHolder = (NamedList) spellCheck.get("collations");
    singleCollation = (String) collationHolder.get("collation");
    assertEquals("lowerfilt:(+faith +hope +loaves)", singleCollation);

    // Testing returning multiple collations if more than one valid
    // combination exists.
    params.remove(SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES);
    params.remove(SpellingParams.SPELLCHECK_MAX_COLLATIONS);
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES, "10");
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATIONS, "2");
    handler = core.getRequestHandler("/spellCheckCompRH");
    rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    values = rsp.getValues();
    spellCheck = (NamedList) values.get("spellcheck");
    collationHolder = (NamedList) spellCheck.get("collations");
    List<String> collations = collationHolder.getAll("collation");
    assertEquals(2, collations.size());
    for (String multipleCollation : collations) {
      assertTrue(
          multipleCollation.equals("lowerfilt:(+faith +hope +love)")
              || multipleCollation.equals("lowerfilt:(+faith +hope +loaves)"));
    }

    // Testing return multiple collations with expanded collation response
    // format.
    params.add(SpellingParams.SPELLCHECK_COLLATE_EXTENDED_RESULTS, "true");
    handler = core.getRequestHandler("/spellCheckCompRH");
    rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    values = rsp.getValues();
    spellCheck = (NamedList) values.get("spellcheck");
    collationHolder = (NamedList) spellCheck.get("collations");
    List<NamedList> expandedCollationList = collationHolder.getAll("collation");
    Set<String> usedcollations = new HashSet<>();
    assertEquals(2, expandedCollationList.size());
    for (NamedList expandedCollation : expandedCollationList) {
      String multipleCollation = (String) expandedCollation.get("collationQuery");
      assertTrue(
          multipleCollation.equals("lowerfilt:(+faith +hope +love)")
              || multipleCollation.equals("lowerfilt:(+faith +hope +loaves)"));
      assertFalse(usedcollations.contains(multipleCollation));
      usedcollations.add(multipleCollation);

      assertEquals(1L, expandedCollation.get("hits"));

      NamedList misspellingsAndCorrections =
          (NamedList) expandedCollation.get("misspellingsAndCorrections");
      assertEquals(3, misspellingsAndCorrections.size());

      String correctionForFauth = (String) misspellingsAndCorrections.get("fauth");
      String correctionForHome = (String) misspellingsAndCorrections.get("home");
      String correctionForLoane = (String) misspellingsAndCorrections.get("loane");
      assertEquals("faith", correctionForFauth);
      assertEquals("hope", correctionForHome);
      assertTrue(correctionForLoane.equals("love") || correctionForLoane.equals("loaves"));
    }
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void testCollateWithGrouping() {
    SolrCore core = h.getCore();
    SearchComponent speller = core.getSearchComponent("spellcheck");
    assertNotNull("speller is null and it shouldn't be", speller);

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add(SpellCheckComponent.COMPONENT_NAME, "true");
    params.add(SpellingParams.SPELLCHECK_BUILD, "true");
    params.add(SpellingParams.SPELLCHECK_COUNT, "10");
    params.add(SpellingParams.SPELLCHECK_COLLATE, "true");
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES, "5");
    params.add(SpellingParams.SPELLCHECK_MAX_COLLATIONS, "1");
    params.add(CommonParams.Q, "lowerfilt:(+fauth)");
    params.add(GroupParams.GROUP, "true");
    params.add(GroupParams.GROUP_FIELD, "id");

    // Because a FilterQuery is applied which removes doc id#1 from possible hits, we would
    // not want the collations to return us "lowerfilt:(+faith +hope +loaves)" as this only matches
    // doc id#1.
    SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
    SolrQueryResponse rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    NamedList values = rsp.getValues();
    NamedList spellCheck = (NamedList) values.get("spellcheck");
    NamedList collationHolder = (NamedList) spellCheck.get("collations");
    List<String> collations = collationHolder.getAll("collation");
    assertEquals(1, collations.size());
  }

  @Test
  public void testContextSensitiveCollate() {
    //                     DirectSolrSpellChecker   IndexBasedSpellChecker
    String[] dictionary = {"direct", "default_teststop"};
    for (int i = 0; i <= 1; i++) {
      assertQ(
          req(
              "q",
              "teststop:(flew AND form AND heathrow)",
              "qt",
              "/spellCheckCompRH",
              "indent",
              "true",
              SpellCheckComponent.COMPONENT_NAME,
              "true",
              SpellCheckComponent.SPELLCHECK_BUILD,
              "true",
              SpellCheckComponent.SPELLCHECK_DICT,
              dictionary[i],
              SpellCheckComponent.SPELLCHECK_EXTENDED_RESULTS,
              "true",
              SpellCheckComponent.SPELLCHECK_COUNT,
              "10",
              SpellCheckComponent.SPELLCHECK_ALTERNATIVE_TERM_COUNT,
              "5",
              SpellCheckComponent.SPELLCHECK_MAX_RESULTS_FOR_SUGGEST,
              "0",
              SpellCheckComponent.SPELLCHECK_COLLATE,
              "true",
              SpellCheckComponent.SPELLCHECK_MAX_COLLATION_TRIES,
              "10",
              SpellCheckComponent.SPELLCHECK_MAX_COLLATIONS,
              "1",
              SpellCheckComponent.SPELLCHECK_COLLATE_EXTENDED_RESULTS,
              "true"),
          "//lst[@name='spellcheck']/lst[@name='suggestions']/lst[@name='flew']/arr[@name='suggestion']/lst/str[@name='word']='flow'",
          "//lst[@name='spellcheck']/lst[@name='suggestions']/lst[@name='form']/arr[@name='suggestion']/lst/str[@name='word']='from'",
          /* DirectSolrSpellChecker won't suggest if the edit distance > 2, so we can't test for this one...
                  "//lst[@name='spellcheck']/lst[@name='suggestions']/lst[@name='heathrow']/arr[@name='suggestion']/lst/str[@name='word']='hearth'",
          */
          "//lst[@name='spellcheck']/bool[@name='correctlySpelled']='false'",
          "//lst[@name='spellcheck']/lst[@name='collations']/lst[@name='collation']/str[@name='collationQuery']='teststop:(flew AND from AND heathrow)'",
          "//lst[@name='spellcheck']/lst[@name='collations']/lst[@name='collation']/long[@name='hits']=1",
          "//lst[@name='spellcheck']/lst[@name='collations']/lst[@name='collation']/lst[@name='misspellingsAndCorrections']/str[@name='form']='from'");

      assertQ(
          req(
              "q",
              "teststop:(june AND customs)",
              "qt",
              "/spellCheckCompRH",
              "indent",
              "true",
              SpellCheckComponent.COMPONENT_NAME,
              "true",
              SpellCheckComponent.SPELLCHECK_DICT,
              dictionary[i],
              SpellCheckComponent.SPELLCHECK_EXTENDED_RESULTS,
              "true",
              SpellCheckComponent.SPELLCHECK_COUNT,
              "10",
              SpellCheckComponent.SPELLCHECK_ALTERNATIVE_TERM_COUNT,
              "5",
              SpellCheckComponent.SPELLCHECK_MAX_RESULTS_FOR_SUGGEST,
              "1",
              SpellCheckComponent.SPELLCHECK_COLLATE,
              "true",
              SpellCheckComponent.SPELLCHECK_MAX_COLLATION_TRIES,
              "10",
              SpellCheckComponent.SPELLCHECK_MAX_COLLATIONS,
              "1",
              SpellCheckComponent.SPELLCHECK_COLLATE_EXTENDED_RESULTS,
              "true"),
          "//result[@numFound=1]",
          "//lst[@name='spellcheck']/lst[@name='suggestions']/lst[@name='june']/arr[@name='suggestion']/lst/str[@name='word']='jane'",
          "//lst[@name='spellcheck']/bool[@name='correctlySpelled']='false'",
          "//lst[@name='spellcheck']/lst[@name='collations']/lst[@name='collation']/str[@name='collationQuery']='teststop:(jane AND customs)'",
          "//lst[@name='spellcheck']/lst[@name='collations']/lst[@name='collation']/long[@name='hits']=1",
          "//lst[@name='spellcheck']/lst[@name='collations']/lst[@name='collation']/lst[@name='misspellingsAndCorrections']/str[@name='june']='jane'");
      // SOLR-5090, alternativeTermCount==0 was being evaluated, would sometimes throw NPE
      assertQ(
          req(
              "q",
              "teststop:(june customs)",
              "mm",
              "2",
              "qt",
              "/spellCheckCompRH",
              "indent",
              "true",
              SpellCheckComponent.COMPONENT_NAME,
              "true",
              SpellCheckComponent.SPELLCHECK_DICT,
              dictionary[i],
              SpellCheckComponent.SPELLCHECK_COUNT,
              "10",
              SpellCheckComponent.SPELLCHECK_ALTERNATIVE_TERM_COUNT,
              "0",
              SpellCheckComponent.SPELLCHECK_COLLATE,
              "true"));
    }
  }

  @Test
  public void testEstimatedHitCounts() {
    final String xpathPrefix =
        "//lst[@name='spellcheck']/lst[@name='collations']/lst[@name='collation']/";
    final SolrParams reusedParams =
        params(
            SpellCheckComponent.COMPONENT_NAME,
            "true",
            SpellCheckComponent.SPELLCHECK_DICT,
            "direct",
            SpellingParams.SPELLCHECK_COUNT,
            "1",
            SpellingParams.SPELLCHECK_COLLATE,
            "true",
            SpellingParams.SPELLCHECK_MAX_COLLATION_TRIES,
            "1",
            SpellingParams.SPELLCHECK_MAX_COLLATIONS,
            "1",
            SpellingParams.SPELLCHECK_COLLATE_EXTENDED_RESULTS,
            "true",
            "qt",
            "/spellCheckCompRH");

    // default case, no SPELLCHECK_COLLATE_MAX_COLLECT_DOCS should be exact num hits
    assertQ(
        req(reusedParams, CommonParams.Q, "teststop:metnoia"),
        xpathPrefix + "str[@name='collationQuery']='teststop:metanoia'",
        xpathPrefix + "long[@name='hits']=6");

    // specifying 0 means "exact" same as default, but specifying a value greater
    // than the total number of docs in the index should also result in it
    // "estimating" and getting exact number as well.
    for (String val : new String[] {"0", "30", "100", "10000"}) {
      assertQ(
          req(
              reusedParams,
              CommonParams.Q,
              "teststop:metnoia",
              SpellingParams.SPELLCHECK_COLLATE_MAX_COLLECT_DOCS,
              val),
          xpathPrefix + "str[@name='collationQuery']='teststop:metanoia'",
          xpathPrefix + "long[@name='hits']=6");
    }

    // values between 0 and the num docs in the index should not error, and should
    // produce an estimate no more than the total number of docs
    final int iters = atLeast(10);
    for (int iter = 0; iter < iters; iter++) {
      final int val = TestUtil.nextInt(random(), 1, 17);
      assertQ(
          req(
              reusedParams,
              CommonParams.Q,
              "teststop:metnoia",
              SpellingParams.SPELLCHECK_COLLATE_MAX_COLLECT_DOCS,
              "" + val),
          xpathPrefix + "str[@name='collationQuery']='teststop:metanoia'",
          xpathPrefix + "long[@name='hits' and . <= 17 and 0 < .]");
    }

    // "everYother" appears in every other doc in the index, so "everother"
    // should produce a "decent" approximation of the number of hits (8)
    // for any 5 <= SPELLCHECK_COLLATE_MAX_COLLECT_DOCS
    //
    // (we have to be kind of flexible with our definition of "decent"
    // since we're dealing with a fairly small index here)
    for (int val = 5; val <= 20; val++) {
      String hitsXPath = xpathPrefix + "long[@name='hits']"; // we will append to this...

      if (val <= NUM_DOCS_WITH_TERM_EVERYOTHER) {
        // strongest assertions we can make given an arbitrary MergePolicy on such a small index
        // is based on the idea that the docs may all come *first* or all come *last*
        // and then do the math on what estimate should come from that if we collected *exactly*
        // 'val'...
        //
        // if they are all "first" we will overestimate and assume everything is a match...
        int max = NUM_DOCS;
        // if they are all "last" we will under-estimate based on how non-matches we had to skip...
        int min = (/* min collected */ val) / (/* max docs possibly scanned */ NUM_DOCS);
        hitsXPath += "[" + min + " <= . and . <= " + max + "]";
      } else {
        // we've asked for a number greater than what can possibly be found in our tiny index, which
        // should force it to scan all docs so our hits should be exact
        hitsXPath += "[.=" + NUM_DOCS_WITH_TERM_EVERYOTHER + "]";
      }
      assertQ(
          req(
              reusedParams,
              CommonParams.Q,
              "teststop:everother",
              SpellingParams.SPELLCHECK_COLLATE_MAX_COLLECT_DOCS,
              "" + val),
          xpathPrefix + "str[@name='collationQuery']='teststop:everyother'",
          hitsXPath);
    }
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void testZeroTries() {
    SolrCore core = h.getCore();
    SearchComponent speller = core.getSearchComponent("spellcheck");
    assertNotNull("speller is null and it shouldn't be", speller);

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add(SpellCheckComponent.COMPONENT_NAME, "true");
    params.add(SpellCheckComponent.SPELLCHECK_BUILD, "true");
    params.add(SpellCheckComponent.SPELLCHECK_COUNT, "10");
    params.add(SpellCheckComponent.SPELLCHECK_COLLATE, "true");
    params.add(SpellCheckComponent.SPELLCHECK_MAX_COLLATION_TRIES, "0");
    params.add(SpellCheckComponent.SPELLCHECK_MAX_COLLATIONS, "2");
    params.add(CommonParams.Q, "lowerfilt:(+fauth)");
    SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
    SolrQueryResponse rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    NamedList values = rsp.getValues();
    NamedList spellCheck = (NamedList) values.get("spellcheck");
    NamedList collationList = (NamedList) spellCheck.get("collations");
    List<?> collations = (List<?>) collationList.getAll("collation");
    assertEquals(2, collations.size());
  }

  @Test
  @SuppressWarnings({"unchecked", "rawtypes"})
  public void testWithCursorMark() {
    SolrCore core = h.getCore();
    SearchComponent speller = core.getSearchComponent("spellcheck");
    assertNotNull("speller is null and it shouldn't be", speller);

    ModifiableSolrParams params = new ModifiableSolrParams();
    params.add(SpellCheckComponent.COMPONENT_NAME, "true");
    params.add(SpellCheckComponent.SPELLCHECK_BUILD, "true");
    params.add(SpellCheckComponent.SPELLCHECK_COUNT, "10");
    params.add(SpellCheckComponent.SPELLCHECK_COLLATE, "true");
    params.add(SpellCheckComponent.SPELLCHECK_MAX_COLLATION_TRIES, "2");
    params.add(SpellCheckComponent.SPELLCHECK_MAX_COLLATIONS, "1");
    params.add(CommonParams.Q, "lowerfilt:(+fauth)");
    params.add(CommonParams.SORT, "id asc");
    params.add(CursorMarkParams.CURSOR_MARK_PARAM, CursorMarkParams.CURSOR_MARK_START);
    SolrRequestHandler handler = core.getRequestHandler("/spellCheckCompRH");
    SolrQueryResponse rsp = new SolrQueryResponse();
    rsp.addResponseHeader(new SimpleOrderedMap());
    SolrQueryRequest req = new LocalSolrQueryRequest(core, params);
    handler.handleRequest(req, rsp);
    req.close();
    NamedList values = rsp.getValues();
    NamedList spellCheck = (NamedList) values.get("spellcheck");
    NamedList collationList = (NamedList) spellCheck.get("collations");
    List<?> collations = (List<?>) collationList.getAll("collation");
    assertEquals(1, collations.size());
  }
}
