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

import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.lucene.util.LuceneTestCase.SuppressCodecs;
import org.apache.lucene.util.TestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.schema.DoubleValueFieldType;
import org.apache.solr.schema.FloatValueFieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.IntValueFieldType;
import org.apache.solr.schema.LongValueFieldType;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.TrieField;
import org.junit.Before;
import org.junit.BeforeClass;

/**
 * Tests the behavior of <code>field(foo,min|max)</code> on numerious types of multivalued 'foo' fields,
 * as well as the beahvior of sorting on <code>foo asc|desc</code> to implicitly choose the min|max.
 */
@SuppressCodecs({"SimpleText"}) // see TestSortedSetSelector
public class TestMinMaxOnMultiValuedField extends SolrTestCaseJ4 {

  /** Initializes core and does some sanity checking of schema */
  @BeforeClass
  public static void beforeClass() throws Exception {

    initCore("solrconfig-functionquery.xml","schema11.xml");
    checkFields(new String[] {"i", "l", "f", "d"}, new String [] {"_p", "_ni_p"});
    checkFields(new String[] {"ti", "tl", "tf", "td"}, new String [] {"", "_dv", "_ni_dv"});
    checkFields(new String[] {"str", // no expectation on missing first/last
                              "str_missf_", "str_missl_",
                              "int_missf_", "int_missl_",
                              "long_missf_", "long_missl_",
                              "float_missf_", "float_missl_",
                              "double_missf_", "double_missl_",
                              "date_missf_", "date_missl_",
                              "enum_missf_", "enum_missl_",
                              "bool_missf_", "bool_missl_"  }, new String [] {"_dv"});
    checkFields(new String[] {"stxt_", // no expectation on missing first/last
                              "stxt_missf_", "stxt_missl_" }, new String [] { "_dv"});
    checkFields(new String [] { "stxt_" }, // no expectation on missing first/last
                new String [] { "_nodv", "_dv" });
    checkFields(new String [] { "stxt_missf_", "stxt_missl_" }, new String [] { "_dv"});
      
  }
  
  private static void checkFields(String[] types, String[] suffixes) {
    // sanity check the expected properties of our fields (ie: who broke the schema?)
    IndexSchema schema = h.getCore().getLatestSchema();
    for (String type : types) {
      for (String suffix : suffixes) {
        String f = "val_" + type + "s" + suffix;
        SchemaField sf = schema.getField(f);
        assertTrue(f + " is not multivalued", sf.multiValued());
        assertEquals(f + " doesn't have expected docValues status",
                     ((f.contains("dv") || f.endsWith("_p") || Boolean.getBoolean(NUMERIC_DOCVALUES_SYSPROP))
                      && !f.contains("nodv")),
                     sf.hasDocValues());
        assertEquals(f + " doesn't have expected index status",
                     ! f.contains("ni"), sf.indexed());

        if (f.contains("miss")) {
          // if name contains "miss" assert that the missing first/last props match
          // but don't make any asserts about fields w/o that in name
          // (schema11.xml's strings has some preexisting silliness that don't affect us)
          assertEquals(f + " sortMissingFirst is wrong",
                       f.contains("missf"), sf.sortMissingFirst());
          assertEquals(f + " sortMissingLast is wrong",
                       f.contains("missl"), sf.sortMissingLast());
        }
      }
    }
  }
  
  /** Deletes all docs (which may be left over from a previous test */
  @Before
  public void before() throws Exception {
    clearIndex();
    assertU(commit());
  }
  
  public void testBasics() throws Exception {
    testBasics("val_tis_dv", "val_tls_dv", "val_tfs_dv", "val_tds_dv");
    testBasics("val_tis_ni_dv", "val_tls_ni_dv", "val_tfs_ni_dv", "val_tds_ni_dv");
    testBasics("val_is_p", "val_ls_p", "val_fs_p", "val_ds_p");
    testBasics("val_is_ni_p", "val_ls_ni_p", "val_fs_ni_p", "val_ds_ni_p");
  }

  private void testBasics(String intField, String longField, String floatField, String doubleField) throws Exception {
    assertTrue("Unexpected int field", h.getCore().getLatestSchema().getField(intField).getType() instanceof IntValueFieldType);
    assertTrue("Unexpected long field", h.getCore().getLatestSchema().getField(longField).getType() instanceof LongValueFieldType);
    assertTrue("Unexpected float field", h.getCore().getLatestSchema().getField(floatField).getType() instanceof FloatValueFieldType);
    assertTrue("Unexpected double field", h.getCore().getLatestSchema().getField(doubleField).getType() instanceof DoubleValueFieldType);

    clearIndex();
    assertU(adoc(sdoc("id", "1"
                      // int
                      ,intField, "42"
                      ,intField, "9"
                      ,intField, "-54"
                      // long
                      ,longField, "420"
                      ,longField, "90"
                      ,longField, "-540"
                      // float
                      ,floatField, "-42.5"
                      ,floatField, "-4.5"
                      ,floatField, "-13.5"
                      // double
                      ,doubleField, "-420.5"
                      ,doubleField, "-40.5"
                      ,doubleField, "-130.5"
                      )));
    assertU(commit());

    assertQ(req("q","id:1"
                // int
                ,"fl","exists_min_i:exists(field(" + intField + ",min))"
                ,"fl","exists_max_i:exists(field(" + intField + ",max))"
                ,"fl","min_i:field(" + intField + ",min)"
                ,"fl","max_i:field(" + intField + ",max)"
                // long
                ,"fl","exists_min_l:exists(field(" + longField + ",min))"
                ,"fl","exists_max_l:exists(field(" + longField + ",max))"
                ,"fl","min_l:field(" + longField + ",min)"
                ,"fl","max_l:field(" + longField + ",max)"
                // float
                ,"fl","exists_min_f:exists(field(" + floatField + ",min))"
                ,"fl","exists_max_f:exists(field(" + floatField + ",max))"
                ,"fl","min_f:field(" + floatField + ",min)"
                ,"fl","max_f:field(" + floatField + ",max)"
                // double
                ,"fl","exists_min_d:exists(field(" + doubleField + ",min))"
                ,"fl","exists_max_d:exists(field(" + doubleField + ",max))"
                ,"fl","min_d:field(" + doubleField + ",min)"
                ,"fl","max_d:field(" + doubleField + ",max)"
                
                )
            ,"//*[@numFound='1']"
            // int
            ,"//bool[@name='exists_min_i']='true'"
            ,"//bool[@name='exists_max_i']='true'"
            ,"//int[@name='min_i']='-54'"
            ,"//int[@name='max_i']='42'"
            // long
            ,"//bool[@name='exists_min_l']='true'"
            ,"//bool[@name='exists_max_l']='true'"
            ,"//long[@name='min_l']='-540'"
            ,"//long[@name='max_l']='420'"
            // float
            ,"//bool[@name='exists_min_f']='true'"
            ,"//bool[@name='exists_max_f']='true'"
            ,"//float[@name='min_f']='-42.5'"
            ,"//float[@name='max_f']='-4.5'"
            // double
            ,"//bool[@name='exists_min_d']='true'"
            ,"//bool[@name='exists_max_d']='true'"
            ,"//double[@name='min_d']='-420.5'"
            ,"//double[@name='max_d']='-40.5'"
            );
  }

  public void testBasicStrings() {
    checkBasicStrings("val_strs_dv");
  }
  public void testBasicSortableText() {
    checkBasicStrings("val_stxt_s_dv");
    checkBasicStrings("val_stxt_missf_s_dv");
    checkBasicStrings("val_stxt_missl_s_dv");
  }
  private void checkBasicStrings(final String field) {
    assertU(adoc(sdoc("id", "1",
                      field, "dog",
                      field, "xyz",
                      field, "cat")));
    assertU(adoc(sdoc("id", "2"))); // 2 has no values in tested field
    assertU(commit());

    // id=1: has values
    assertQ(req("q","id:1"
                ,"fl","exists_min_str:exists(field("+field+",min))"
                ,"fl","exists_max_str:exists(field("+field+",max))"
                ,"fl","min_str:field("+field+",min)"
                ,"fl","max_str:field("+field+",max)"
                
                )
            ,"//*[@numFound='1']"
            ,"//bool[@name='exists_min_str']='true'"
            ,"//bool[@name='exists_max_str']='true'"
            ,"//str[@name='min_str']='cat'"
            ,"//str[@name='max_str']='xyz'"
            );
    // id=2: no values
    assertQ(req("q","id:2"
                ,"fl","exists_min_str:exists(field("+field+",min))"
                ,"fl","exists_max_str:exists(field("+field+",max))"
                ,"fl","min_str:field("+field+",min)"
                ,"fl","max_str:field("+field+",max)"
                
                )
            ,"//*[@numFound='1']"
            ,"//bool[@name='exists_min_str']='false'"
            ,"//bool[@name='exists_max_str']='false'"
            ,"count(//*[@name='min_str'])=0"
            ,"count(//*[@name='max_str'])=0"
            );
  }

  public void testExpectedSortOrderingStrings() {
    testExpectedSortOrdering("val_strs_dv", false,
                             null, "a", "cat", "dog", "wako", "xyz", "zzzzz");
  }
  public void testExpectedSortOrderingSortableText() {
    testExpectedSortOrdering("val_stxt_s_dv", false,
                             null, "a", "cat", "dog", "wako", "xyz", "zzzzz");
  }

  public void testExpectedSortMissingOrderings() {

    // NOTE: we never test the "true" min/max value for a type, because
    // (in this simple test) we aren't using a secondary sort, so there is no way to disambiguate
    // docs that have those values from docs that have those *effective* sort values

    testSortMissingMinMax("val_str",  "a", "aaaaaa", "xxxxx", "zzzzzzzzzzzzzzzzzzz");
    testSortMissingMinMax("val_stxt", "a", "aaaaaa", "xxxxx", "zzzzzzzzzzzzzzzzzzz");
    
    testSortMissingMinMax("val_int",
                          Integer.MIN_VALUE+1L, -9999, 0, 99999, Integer.MAX_VALUE-1L);
    testSortMissingMinMax("val_long",
                          Long.MIN_VALUE+1L, -99999999L, 0, 9999999999L, Long.MAX_VALUE-1L);
    testSortMissingMinMax("val_float",
                          Math.nextAfter(Float.NEGATIVE_INFINITY, 0F), -99.99F,
                          0F, 99.99F, Math.nextAfter(Float.POSITIVE_INFINITY, 0F));
    testSortMissingMinMax("val_double",
                          Math.nextAfter(Double.NEGATIVE_INFINITY, 0D), -99.99D, 
                          0D, 99.99D, Math.nextAfter(Double.POSITIVE_INFINITY, 0F));
    testSortMissingMinMax("val_date",
                          "-1000000-01-01T00:00:00Z", "NOW-1YEAR", "NOW", "NOW+1YEAR", "+1000000-01-01T00:00:00Z");
    testSortMissingMinMax("val_bool", false, true);
    testSortMissingMinMax("val_enum", "Not Available", "Low", "High", "Critical");

  }
  

  @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6709")
  public void testIntFieldCache() {
    testSimpleInt("val_tis");
    testExpectedSortOrderingInt("val_tis", true);
  }
  
  public void testPointInt() {
    testSimpleInt("val_is_p");
    testSimpleInt("val_is_ni_p");
    
    testExpectedSortOrderingInt("val_is_p", false);
    testExpectedSortOrderingInt("val_is_ni_p", false);
  }
  
  public void testIntDocValues() {
    testSimpleInt("val_tis_dv");
    testSimpleInt("val_tis_ni_dv");
    
    testExpectedSortOrderingInt("val_tis_dv", true);
    testExpectedSortOrderingInt("val_tis_ni_dv", true);
  }

  @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6709")
  public void testLongFieldCache() {
    testSimpleLong("val_tls");
    testExpectedSortOrderingLong("val_tls", true);
  }
  
  public void testLongDocValues() {
    testSimpleLong("val_tls_dv");
    testSimpleLong("val_tls_ni_dv");
    
    testExpectedSortOrderingLong("val_tls_dv", true);
    testExpectedSortOrderingLong("val_tls_ni_dv", true);
  }
  
  public void testPointLong() {
    testSimpleLong("val_ls_p");
    testSimpleLong("val_ls_ni_p");
    
    testExpectedSortOrderingLong("val_ls_p", false);
    testExpectedSortOrderingLong("val_ls_ni_p", false);
  }


  @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6709")
  public void testFloatFieldCache() {
    testSimpleFloat("val_tfs");
    testExpectedSortOrderingFloat("val_tfs", true);
  }
  
  public void testFloatDocValues() {
    testSimpleFloat("val_tfs_dv");
    testSimpleFloat("val_tfs_ni_dv");
    
    testExpectedSortOrderingFloat("val_tfs_dv", true);
    testExpectedSortOrderingFloat("val_tfs_ni_dv", true);
  }
  
  public void testPointFloat() {
    testSimpleFloat("val_fs_p");
    testSimpleFloat("val_fs_ni_p");
    
    testExpectedSortOrderingFloat("val_fs_p", false);
    testExpectedSortOrderingFloat("val_fs_ni_p", false);
  }
  
  @AwaitsFix(bugUrl = "https://issues.apache.org/jira/browse/LUCENE-6709")
  public void testDoubleFieldCache() {
    testSimpleDouble("val_tds");
    
    testExpectedSortOrderingDouble("val_tds", true);
  }
  
  public void testDoubleDocValues() {
    testSimpleDouble("val_tds_dv");
    testSimpleDouble("val_tds_ni_dv");
    
    testExpectedSortOrderingDouble("val_tds_dv", true);
    testExpectedSortOrderingDouble("val_tds_ni_dv", true);
  }

  public void testPointDouble() {
    testSimpleDouble("val_ds_p");
    testSimpleDouble("val_ds_ni_p");
    
    testExpectedSortOrderingDouble("val_ds_p", false);
    testExpectedSortOrderingDouble("val_ds_ni_p", false);
  }

  public void testBadRequests() {

    // useful error msg when bogus selector is requested (ie: not min or max)
    assertQEx("no error asking for bogus selector",
              "hoss",
              req("q","*:*", "fl", "field(val_tds_dv,'hoss')"),
              SolrException.ErrorCode.BAD_REQUEST);

    assertQEx("no error asking for bogus selector",
        "hoss",
        req("q","*:*", "fl", "field(val_ds_p,'hoss')"),
        SolrException.ErrorCode.BAD_REQUEST);
    
    // useful error until/unless LUCENE-6709
    assertFalse(h.getCore().getLatestSchema().getField("val_is_ndv_p").hasDocValues());
    assertTrue(h.getCore().getLatestSchema().getField("val_is_ndv_p").multiValued());
    assertQEx("no error asking for max on a non docVals field",
              "val_is_ndv_p",
              req("q","*:*", "fl", "field(val_is_ndv_p,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);
    assertQEx("no error asking for max on a non docVals field",
              "max",
              req("q","*:*", "fl", "field(val_is_ndv_p,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);
    assertQEx("no error asking for max on a non docVals field",
              "docValues",
              req("q","*:*", "fl", "field(val_is_ndv_p,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);
    
    // useful error if min/max is unsupported for fieldtype
    assertQEx("no error mentioning field name when asking for max on type that doesn't support it",
              "cat_length",
              req("q","*:*", "fl", "field(cat_length,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);
    assertQEx("no error mentioning type when asking for max on type that doesn't support it",
              "text_length",
              req("q","*:*", "fl", "field(cat_length,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);
    // type supports, but field doesn't have docValues
    assertQEx("no error mentioning field name when asking for max on a non-dv str field",
              "cat",
              req("q","*:*", "fl", "field(cat,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);
    assertQEx("no error mentioning 'docValues' when asking for max on a non-dv str field",
              "docValues",
              req("q","*:*", "fl", "field(cat,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);
    assertQEx("no error mentioning field name when asking for max on a non-dv sortable text field",
              "val_stxt_s_nodv",
              req("q","*:*", "fl", "field(val_stxt_s_nodv,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);
    assertQEx("no error mentioning 'docValues' when asking for max on a non-dv sortable field",
              "docValues",
              req("q","*:*", "fl", "field(val_stxt_s_nodv,'max')"),
              SolrException.ErrorCode.BAD_REQUEST);

    
  }

  public void testRandom() throws Exception {

    Comparable[] vals = new Comparable[TestUtil.nextInt(random(), 1, 17)];

    // random ints
    for (int i = 0; i < vals.length; i++) {
      vals[i] = random().nextInt();
    }
    testSimpleValues("val_tis_dv", int.class, vals);
    testSimpleValues("val_is_p", int.class, vals);
    testSimpleValues("val_tis_ni_dv", int.class, vals);
    testSimpleValues("val_is_ni_p", int.class, vals);

    // random longs
    for (int i = 0; i < vals.length; i++) {
      vals[i] = random().nextLong();
    }
    testSimpleValues("val_tls_dv", long.class, vals);
    testSimpleValues("val_ls_p", long.class, vals);
    testSimpleValues("val_tls_ni_dv", long.class, vals);
    testSimpleValues("val_ls_ni_p", long.class, vals);
    
    // random floats
    for (int i = 0; i < vals.length; i++) {
      // Random.nextFloat is lame
      Float f = Float.NaN;
      while (f.isNaN()) {
        f = Float.intBitsToFloat(random().nextInt());
      }
      vals[i] = f;
    }
    testSimpleValues("val_tfs_dv", float.class, vals);
    testSimpleValues("val_fs_p", float.class, vals);
    testSimpleValues("val_tfs_ni_dv", float.class, vals);
    testSimpleValues("val_fs_ni_p", float.class, vals);
    
    // random doubles
    for (int i = 0; i < vals.length; i++) {
      // Random.nextDouble is lame
      Double d = Double.NaN;
      while (d.isNaN()) {
        d = Double.longBitsToDouble(random().nextLong());
      }
      vals[i] = d;
    }
    testSimpleValues("val_tds_dv", double.class, vals);
    testSimpleValues("val_ds_p", double.class, vals);
    testSimpleValues("val_tds_ni_dv", double.class, vals);
    testSimpleValues("val_ds_ni_p", double.class, vals);

  }

  
  /** @see #testSimpleValues */
  protected void testSimpleInt(final String fieldname) {
    // most basic case
    testSimpleValues(fieldname, int.class, 0);
    
    // order of values shouldn't matter
    testSimpleValues(fieldname, int.class, -42, 51, 3);
    testSimpleValues(fieldname, int.class, 51, 3, -42);

    // extreme's of the data type
    testSimpleValues(fieldname, int.class, Integer.MIN_VALUE, 42, -550);
    testSimpleValues(fieldname, int.class, Integer.MAX_VALUE, 0, Integer.MIN_VALUE);

    testSimpleSort(fieldname, -42, 666);
  }
  
  /** @see #testSimpleValues */
  protected void testSimpleLong(final String fieldname) {
    // most basic case
    testSimpleValues(fieldname, long.class, 0);
    
    // order of values shouldn't matter
    testSimpleValues(fieldname, long.class, -42L, 51L, 3L);
    testSimpleValues(fieldname, long.class, 51L, 3L, -42L);

    // extreme's of the data type
    testSimpleValues(fieldname, long.class, Long.MIN_VALUE, 42L, -550L);
    testSimpleValues(fieldname, long.class, Long.MAX_VALUE, 0L, Long.MIN_VALUE);
    
    testSimpleSort(fieldname, -42, 666);
  }
  
  /** @see #testSimpleValues */
  protected void testSimpleFloat(final String fieldname) {
    // most basic case
    testSimpleValues(fieldname, float.class, 0.0F);
    
    // order of values shouldn't matter
    testSimpleValues(fieldname, float.class, -42.5F, 51.3F, 3.1415F);
    testSimpleValues(fieldname, float.class, 51.3F, 3.1415F, -42.5F);

    // extreme's of the data type
    testSimpleValues(fieldname, float.class, Float.NEGATIVE_INFINITY, 42.5F, -550.4F);
    testSimpleValues(fieldname, float.class, Float.POSITIVE_INFINITY, 0.0F, Float.NEGATIVE_INFINITY);
    
    testSimpleSort(fieldname, -4.2, 6.66);
  }
  
  /** @see #testSimpleValues */
  protected void testSimpleDouble(final String fieldname) {
    // most basic case
    testSimpleValues(fieldname, double.class, 0.0D);
    
    // order of values shouldn't matter
    testSimpleValues(fieldname, double.class, -42.5D, 51.3D, 3.1415D);
    testSimpleValues(fieldname, double.class, 51.3D, 3.1415D, -42.5D);

    // extreme's of the data type
    testSimpleValues(fieldname, double.class, Double.NEGATIVE_INFINITY, 42.5D, -550.4D);
    testSimpleValues(fieldname, double.class, Double.POSITIVE_INFINITY, 0.0D, Double.NEGATIVE_INFINITY);
    
    testSimpleSort(fieldname, -4.2, 6.66);
  }
  
  /** Tests a single doc with a few explicit values, as well as testing exists with and w/o values */
  protected void testSimpleValues(final String fieldname, final Class<?> clazz, final Comparable... vals) {
    clearIndex();
    
    assert 0 < vals.length;
    
    Comparable min = vals[0];
    Comparable max = vals[0];
    
    final String type = clazz.getName();
    final SolrInputDocument doc1 = sdoc("id", "1");
    for (Comparable v : vals) {
      doc1.addField(fieldname, v);
      if (0 < min.compareTo(v)) {
        min = v;
      }
      if (0 > max.compareTo(v)) {
        max = v;
      }
    }
    assertU(adoc(doc1));
    assertU(adoc(sdoc("id", "2"))); // fieldname doesn't exist
    assertU(commit());

    // doc with values
    assertQ(fieldname,
            req("q","id:1",
                "fl","exists_val_min:exists(field("+fieldname+",min))",
                "fl","exists_val_max:exists(field("+fieldname+",max))",
                "fl","val_min:field("+fieldname+",min)",
                "fl","val_max:field("+fieldname+",max)")
            ,"//*[@numFound='1']"
            ,"//bool[@name='exists_val_min']='true'"
            ,"//bool[@name='exists_val_max']='true'"
            ,"//"+type+"[@name='val_min']='"+min+"'"
            ,"//"+type+"[@name='val_max']='"+max+"'"
            );

    // doc w/o values
    assertQ(fieldname,
            req("q","id:2",
                "fl","exists_val_min:exists(field("+fieldname+",min))",
                "fl","exists_val_max:exists(field("+fieldname+",max))",
                "fl","val_min:field("+fieldname+",min)",
                "fl","val_max:field("+fieldname+",max)")
            ,"//*[@numFound='1']"
            ,"//bool[@name='exists_val_min']='false'"
            ,"//bool[@name='exists_val_max']='false'"
            ,"count(//"+type+"[@name='val_min'])=0"
            ,"count(//"+type+"[@name='val_max'])=0"
            );

    // sanity check no sort error when there are missing values
    for (String dir : new String[] { "asc", "desc" }) {
      for (String mm : new String[] { "min", "max" }) {
        for (String func : new String[] { "field("+fieldname+","+mm+")",
                                          "def(field("+fieldname+","+mm+"),42)",
                                          "sum(32,field("+fieldname+","+mm+"))"  }) {
          assertQ(fieldname,
                  req("q","*:*", 
                      "fl", "id",
                      "sort", func + " " + dir)
                  ,"//*[@numFound='2']"
                  // no assumptions about order for now, see bug: SOLR-8005
                  ,"//str[@name='id']='1'"
                  ,"//str[@name='id']='2'"
                  );
        }
      }
    }
  }

  /** 
   * Tests sort order of min/max realtive to other docs w/o any values.
   * @param fieldname The field to test
   * @param negative a "negative" value for this field (ie: in a function context, is less then the "0")
   * @param positive a "positive" value for this field (ie: in a function context, is more then the "0")
   */
  protected void testSimpleSort(final String fieldname,
                                final Comparable negative, final Comparable positive) {
    clearIndex();

    int numDocsExpected = 1;
    for (int i = 1; i < 4; i++) { // pos docids
      if (random().nextBoolean()) {
        assertU(adoc(sdoc("id",i))); // fieldname doesn't exist
        numDocsExpected++;
      }
    }
    
    assertU(adoc(sdoc("id", "0",
                      fieldname, negative,
                      fieldname, positive)));
    
    for (int i = 1; i < 4; i++) { // neg docids
      if (random().nextBoolean()) {
        assertU(adoc(sdoc("id",-i))); // fieldname doesn't exist
        numDocsExpected++;
      }
    }
    assertU(commit());

    // need to wrap with "def" until SOLR-8005 is resolved
    assertDocWithValsIsFirst(numDocsExpected, "def(field("+fieldname+",min),0) asc");
    assertDocWithValsIsLast(numDocsExpected,  "def(field("+fieldname+",min),0) desc");
    
    assertDocWithValsIsFirst(numDocsExpected, "def(field("+fieldname+",max),0) desc");
    assertDocWithValsIsLast(numDocsExpected,  "def(field("+fieldname+",max),0) asc");

    // def wrapper shouldn't be needed since it's already part of another function
    assertDocWithValsIsFirst(numDocsExpected, "sum(32,field("+fieldname+",max)) desc");
    assertDocWithValsIsLast(numDocsExpected,  "sum(32,field("+fieldname+",max)) asc");
    
    assertDocWithValsIsFirst(numDocsExpected, "sum(32,field("+fieldname+",min)) asc");
    assertDocWithValsIsLast(numDocsExpected,  "sum(32,field("+fieldname+",min)) desc");
  }

  /** helper for testSimpleSort */
  private static void assertDocWithValsIsFirst(final int numDocs, final String sort) {
    assertQ(sort,
            req("q","*:*", "rows", ""+numDocs, "sort", sort)
            ,"//result[@numFound='"+numDocs+"']"
            ,"//result/doc[1]/str[@name='id']='0'"
            );
  }
  /** helper for testSimpleSort */
  private static void assertDocWithValsIsLast(final int numDocs, final String sort) {
    assertQ(sort,
            req("q","*:*", "rows", ""+numDocs, "sort", sort)
            ,"//result[@numFound='"+numDocs+"']"
            ,"//result/doc["+numDocs+"]/str[@name='id']='0'"
            );
  }

  /** @see #testExpectedSortOrdering */
  private void testExpectedSortOrderingInt(final String f, final boolean trieFieldHack) {
    // first a quick test where every doc has a value
    testExpectedSortOrdering(f, trieFieldHack,
                             Integer.MIN_VALUE, -9999, 0, 1000, Integer.MAX_VALUE);

    // now where one doc has no values
    testExpectedSortOrdering(f, trieFieldHack,
                             Integer.MIN_VALUE, -9999, -42, -15, -3,
                             null, 7, 53, 1000, 121212112, Integer.MAX_VALUE);
  }
  
  /** @see #testExpectedSortOrdering */
  private void testExpectedSortOrderingLong(final String f, final boolean trieFieldHack) {
    // first a quick test where every doc has a value
    testExpectedSortOrdering(f, trieFieldHack,
                             Long.MIN_VALUE, -4200L, 0, 121212112, Long.MAX_VALUE);

    // now where one doc has no values
    testExpectedSortOrdering(f, trieFieldHack,
                             Long.MIN_VALUE, ((long)Integer.MIN_VALUE)-1L, -4200L,
                             -150L, -3L, null, 70L, 530L, 121212112,
                             1L+(long)Integer.MAX_VALUE, Long.MAX_VALUE);
                                           
  }
  
  /** @see #testExpectedSortOrdering */
  private void testExpectedSortOrderingFloat(final String f, final boolean trieFieldHack) {
    // first a quick test where every doc has a value
    testExpectedSortOrdering(f, trieFieldHack,
                             Float.NEGATIVE_INFINITY, -15.0, 0F, 121212.112, Float.POSITIVE_INFINITY);

    // now where one doc has no values
    testExpectedSortOrdering(f, trieFieldHack,
                             Float.NEGATIVE_INFINITY, -9999.999, -42.3, -15.0, -0.3,
                             null, 0.7, 5.3, 1000, 121212.112, Float.POSITIVE_INFINITY);
                             
  }
  
  /** @see #testExpectedSortOrdering */
  private void testExpectedSortOrderingDouble(final String f, final boolean trieFieldHack) {
    // first a quick test where every doc has a value
    testExpectedSortOrdering(f, trieFieldHack,
                             Double.NEGATIVE_INFINITY, -9999.999D,
                             0D, 121212.112D, Double.POSITIVE_INFINITY);

    // now where one doc has no values
    testExpectedSortOrdering(f, trieFieldHack,
                             Double.NEGATIVE_INFINITY, -9999.999D, -42.3D, -15.0D, -0.3D,
                             null, 0.7D, 5.3D, 1000, 121212.112D, Double.POSITIVE_INFINITY);
  }

  /**
   * Given a <code>fieldPrefix</code> and a list of sorted values which may <em>not</em> contain null, this method tests that sortMissingLast and sortMissingFirst fields using those prefixes sort correctly when {@link #buildMultiValueSortedDocuments} is used to generate documents containing these values <em>and</em> an additional document with no values in the field.
   *
   * <p>
   * Permutations tested:
   * </p>
   * <ul>
   *  <li><code>fieldPrefix</code> + <code>"_missf_s_dv"</code> asc</li>
   *  <li><code>fieldPrefix</code> + <code>"_missf_s_dv"</code> desc</li>
   *  <li><code>fieldPrefix</code> + <code>"_missl_s_dv"</code> asc</li>
   *  <li><code>fieldPrefix</code> + <code>"_missl_s_dv"</code> desc</li>
   * </ul>
   *
   * @see #buildMultiValueSortedDocuments
   * @see #testExpectedSortOrdering(String,List)
   */
  private void testSortMissingMinMax(final String fieldPrefix,
                                     Object... sortedValues) {

    for (Object obj : sortedValues) { // sanity check
      assertNotNull("this helper method can't be used with 'null' values", obj);
    }
    
    for (String suffix : Arrays.asList("_missf_s_dv", "_missl_s_dv")) {

      final String f = fieldPrefix + suffix;
      final boolean first = f.contains("missf");
    
      final List<Object> asc_vals = new ArrayList<>(sortedValues.length + 1);
      Collections.addAll(asc_vals, sortedValues);
      final List<Object> desc_vals = new ArrayList<>(sortedValues.length + 1);
      Collections.addAll(desc_vals, sortedValues);
      Collections.reverse(desc_vals);
      
      asc_vals.add(first ? 0 : sortedValues.length, null);
      desc_vals.add(first ? 0 : sortedValues.length, null);
      
      testExpectedSortOrdering(f + " asc", buildMultiValueSortedDocuments(f, asc_vals));
      testExpectedSortOrdering(f + " desc", buildMultiValueSortedDocuments(f, desc_vals));
    }
  }

  /**
   * Given a (multivalued) field name and an (ascending) sorted list of values, this method uses {@link #buildMultiValueSortedDocuments} to generate and test multiple function &amp; sort permutations ...
   * <ul>
   *  <li><code>f asc</code> (implicitly min)</li>
   *  <li><code>field(f,min) asc</code></li>
   *  <li><code>field(f,min) desc</code></li>
   *  <li><code>f desc</code> (implicitly max)</li>
   *  <li><code>field(f,max) desc</code></li>
   *  <li><code>field(f,max) asc</code></li>
   * </ul>
   *
   * <p>
   * <b>NOTE:</b> if the sortedValues includes "null" then the field must <em>NOT</em> use <code>sortMissingFirst</code> or <code>sortMissingLast</code></b>
   * </p>
   *
   * @param f the field to test
   * @param trieFieldHack if this param and {@link #NUMERIC_POINTS_SYSPROP} are both true, then the <code>field(f,min|max)</code> functions will be wrapped in <code>def(...,0)</code> and the implicit <code>f asc|desc</code> syntax will not be tested -- see SOLR-8005 for the reason.
   * @param sortedValues the values to use when building the docs and validating the sort
   *
   * @see #buildMultiValueSortedDocuments
   * @see #testExpectedSortOrdering(String,List)
   * @see #clearIndex
   */
  private void testExpectedSortOrdering(final String f, boolean trieFieldHack,
                                        Object... sortedValues) {

    SchemaField sf = h.getCore().getLatestSchema().getField(f);
    assertFalse("this utility method does not work with fields that are sortMissingFirst|Last: " + f,
                sf.sortMissingFirst() || sf.sortMissingLast());
    
    // make a copy we can re-order later
    final List<Object> vals = new ArrayList<Object>(sortedValues.length);
    Collections.addAll(vals, sortedValues);
      
    String minFunc = "field("+f+",min)";
    String maxFunc = "field("+f+",max)";

    if (Boolean.getBoolean(NUMERIC_POINTS_SYSPROP)) {
      // we don't need to mess with this hack at all if we're using all point numerics
      trieFieldHack = false;
    }

    if (trieFieldHack // SOLR-8005
        // if this line of code stops compiling, then trie fields have been removed from solr
        // and the entire trieFieldHack param should be removed from this method (and callers)
        && null != TrieField.class) {
      
      // the SOLR-8005 hack is only needed if/when a doc has no value...
      trieFieldHack = false; // assume we're safe
      for (Object val : vals) {
        if (null == val) { // we're not safe
          trieFieldHack = true;
          break;
        }
      }
    }
    if (trieFieldHack) {
      // if we've made it this far, and we still need the hack, we have to wrap our
      // functions with a default...
      minFunc = "def(" + minFunc + ",0)";
      maxFunc = "def(" + maxFunc + ",0)";
      // and we can't test implicit min/max default behavior...
    }
    
    // // // // min
    
    final List<SolrInputDocument> min_asc = buildMultiValueSortedDocuments(f, vals);
    
    // explicit min + asc
    testExpectedSortOrdering(minFunc + " asc", min_asc);
    // implicit: asc -> min
    if (!trieFieldHack) testExpectedSortOrdering(f + " asc", min_asc);
    
    final List<SolrInputDocument> min_desc = new ArrayList<>(min_asc);
    Collections.reverse(min_desc);
    
    // explicit min + desc
    testExpectedSortOrdering(minFunc + " desc", min_desc);

    // // // // max
    Collections.reverse(vals);
    
    final List<SolrInputDocument> max_desc = buildMultiValueSortedDocuments(f, vals);

    // explicit: max + desc
    testExpectedSortOrdering(maxFunc +" desc", max_desc);
    // implicit: desc -> max
    if (!trieFieldHack) testExpectedSortOrdering(f + " desc", max_desc); 
    
    final List<SolrInputDocument> max_asc = new ArrayList<>(max_desc);
    Collections.reverse(max_asc);
    
    // explicit max + asc
    testExpectedSortOrdering(maxFunc + " asc", max_asc);
  }
  
  /**
   * Given a sort clause, and a list of documents in sorted order, this method will clear the index 
   * and then add the documents in a random order (to ensure the index insertion order is not a factor) 
   * and then validate that a <code>*:*</code> query returns the documents in the original order.
   *
   * @see #buildMultiValueSortedDocuments
   * @see #clearIndex
   */   
  private void testExpectedSortOrdering(final String sort,
                                        final List<SolrInputDocument> sortedDocs) {
    clearIndex();

    // shuffle a copy of the doc list (to ensure index order isn't linked to uniqueKey order)
    List<SolrInputDocument> randOrderedDocs = new ArrayList<>(sortedDocs);
    Collections.shuffle(randOrderedDocs, random());

    for (SolrInputDocument doc : randOrderedDocs) {
      assertU(adoc(doc));
    }
    assertU(commit());

    // now use the original sorted docs to build up the expected sort order as a list of xpath
    List<String> xpaths = new ArrayList<>(sortedDocs.size() + 1);
    xpaths.add("//result[@numFound='"+sortedDocs.size()+"']");
    int seq = 0;
    for (SolrInputDocument doc : sortedDocs) {
      xpaths.add("//result/doc["+(++seq)+"]/str[@name='id']='"+doc.getFieldValue("id")+"'");
    }
    assertQ(req("q", "*:*", "rows", "" + sortedDocs.size(), "sort", sort),
            xpaths.toArray(new String[xpaths.size()]));
  }

  /**
   * Given a (multivalued) field name and an (ascending) sorted list of values, this method will generate a List of Solr Documents of the same size such that:
   * <ul>
   *  <li>For each non-null value in the original list, the corrisponding document in the result will have that value in the specified field.</li>
   *  <li>For each null value in the original list, the corrisponding document in teh result will have <em>NO</em> values in the specified field.</li>
   *  <li>If a document has a value in the field, then some random number of values that come <em>after</em> that value in the original list may also be included in the specified field.</li>
   *  <li>Every document in the result will have a randomly asssigned 'id', unique realitive to all other documents in the result.</li>
   * </ul>
   */
  private static final List<SolrInputDocument> buildMultiValueSortedDocuments(final String f,
                                                                              final List<Object> vals) {
    // build a list of docIds that we can shuffle (so the id order doesn't match the value order)
    List<Integer> ids = new ArrayList<>(vals.size());
    for (int i = 0; i < vals.size(); i++) {
      ids.add(i+1);
    }
    Collections.shuffle(ids, random());
    
    final List<SolrInputDocument> docs = new ArrayList<>(vals.size());
    for (int i = 0; i < vals.size(); i++) {
      SolrInputDocument doc = new SolrInputDocument();
      doc.addField("id", ids.get(i));
      Object primaryValue = vals.get(i);
      if (null != primaryValue) {
        doc.addField(f, primaryValue);
        final int extraValCount = random().nextInt(vals.size() - i);
        for (int j = 0; j < extraValCount; j++) {
          Object extraVal = vals.get(TestUtil.nextInt(random(), i+1, vals.size() - 1));
          if (null != extraVal) {
            doc.addField(f, extraVal);
          }
        }
      }
      docs.add(doc);
    }
    return docs;
  }
}
