/*
 * 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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.lucene.tests.util.LuceneTestCase.SuppressCodecs;
import org.apache.lucene.tests.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 numerous types of multivalued 'foo'
 * fields, as well as the behavior 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 have 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() {
    clearIndex();
    assertU(commit());
  }

  public void testBasics() {
    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) {
    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() {

    @SuppressWarnings({"rawtypes"})
    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
   */
  @SuppressWarnings({"unchecked"})
  protected void testSimpleValues(
      final String fieldname,
      final Class<?> clazz,
      @SuppressWarnings({"rawtypes"}) final Comparable... vals) {
    clearIndex();

    assertTrue(0 < vals.length);
    @SuppressWarnings({"rawtypes"})
    Comparable min = vals[0];
    @SuppressWarnings({"rawtypes"})
    Comparable max = vals[0];

    final String type = clazz.getName();
    final SolrInputDocument doc1 = sdoc("id", "1");
    for (@SuppressWarnings({"rawtypes"}) 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 than the
   *     "0")
   * @param positive a "positive" value for this field (ie: in a function context, is more than the
   *     "0")
   */
  protected void testSimpleSort(
      final String fieldname,
      @SuppressWarnings({"rawtypes"}) final Comparable negative,
      @SuppressWarnings({"rawtypes"}) 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:
   *
   * <ul>
   *   <li><code>fieldPrefix</code> + <code>"_missf_s_dv"</code> asc
   *   <li><code>fieldPrefix</code> + <code>"_missf_s_dv"</code> desc
   *   <li><code>fieldPrefix</code> + <code>"_missl_s_dv"</code> asc
   *   <li><code>fieldPrefix</code> + <code>"_missl_s_dv"</code> desc
   * </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><code>field(f,min) asc</code>
   *   <li><code>field(f,min) desc</code>
   *   <li><code>f desc</code> (implicitly max)
   *   <li><code>field(f,max) desc</code>
   *   <li><code>field(f,max) asc</code>
   * </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>
   *
   * @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<>(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 corresponding document in the result
   *       will have that value in the specified field.
   *   <li>For each null value in the original list, the corresponding document in teh result will
   *       have <em>NO</em> values in the specified field.
   *   <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>Every document in the result will have a randomly assigned 'id', unique relative to all
   *       other documents in the result.
   * </ul>
   */
  private static 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;
  }
}
