/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License;
 * you may not use this file except in compliance with the Elastic License.
 */

package org.elasticsearch.xpack.analytics.ttest;

import org.apache.lucene.document.IntPoint;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedNumericDocValuesField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.store.Directory;
import org.elasticsearch.common.CheckedConsumer;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.fielddata.ScriptDocValues;
import org.elasticsearch.index.mapper.KeywordFieldMapper;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.NumberFieldMapper;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.script.MockScriptEngine;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.ScriptModule;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationExecutionException;
import org.elasticsearch.search.aggregations.AggregatorTestCase;
import org.elasticsearch.search.aggregations.InternalAggregation;
import org.elasticsearch.search.aggregations.bucket.global.GlobalAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.global.InternalGlobal;
import org.elasticsearch.search.aggregations.bucket.histogram.HistogramAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram;
import org.elasticsearch.search.aggregations.support.AggregationInspectionHelper;
import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
import org.elasticsearch.search.aggregations.support.MultiValuesSourceFieldConfig;
import org.elasticsearch.search.aggregations.support.ValuesSourceType;
import org.elasticsearch.search.lookup.LeafDocLookup;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

import static java.util.Arrays.asList;
import static java.util.Collections.singleton;

public class TTestAggregatorTests extends AggregatorTestCase {

    /**
     * Script to return the {@code _value} provided by aggs framework.
     */
    public static final String ADD_HALF_SCRIPT = "add_one";

    public static final String TERM_FILTERING = "term_filtering";

    @Override
    protected AggregationBuilder createAggBuilderForTypeTest(MappedFieldType fieldType, String fieldName) {
        return new TTestAggregationBuilder("foo")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName(fieldName)
                .setFilter(QueryBuilders.rangeQuery(fieldName).lt(10)).build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName(fieldName)
                .setFilter(QueryBuilders.rangeQuery(fieldName).gte(10)).build());
    }

    @Override
    protected List<ValuesSourceType> getSupportedValuesSourceTypes() {
        return List.of(CoreValuesSourceType.NUMERIC);
    }

    @Override
    protected ScriptService getMockScriptService() {
        Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>();

        scripts.put(ADD_HALF_SCRIPT, vars -> {
            LeafDocLookup leafDocLookup = (LeafDocLookup) vars.get("doc");
            String fieldname = (String) vars.get("fieldname");
            ScriptDocValues<?> scriptDocValues = leafDocLookup.get(fieldname);
            return  ((Number) scriptDocValues.get(0)).doubleValue() + 0.5;
        });

        scripts.put(TERM_FILTERING, vars -> {
            LeafDocLookup leafDocLookup = (LeafDocLookup) vars.get("doc");
            int term = (Integer) vars.get("term");
            ScriptDocValues<?> termDocValues = leafDocLookup.get("term");
            int currentTerm = ((Number) termDocValues.get(0)).intValue();
            if (currentTerm == term) {
                return ((Number) leafDocLookup.get("field").get(0)).doubleValue();
            }
            return null;
        });

        MockScriptEngine scriptEngine = new MockScriptEngine(MockScriptEngine.NAME,
            scripts,
            Collections.emptyMap());
        Map<String, ScriptEngine> engines = Collections.singletonMap(scriptEngine.getType(), scriptEngine);

        return new ScriptService(Settings.EMPTY, engines, ScriptModule.CORE_CONTEXTS);
    }

    public void testNoMatchingField() throws IOException {
        testCase(new MatchAllDocsQuery(), randomFrom(TTestType.values()), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("wrong_a", 102), new NumericDocValuesField("wrong_b", 89)));
            iw.addDocument(asList(new NumericDocValuesField("wrong_a", 99), new NumericDocValuesField("wrong_b", 93)));
        }, tTest -> assertEquals(Double.NaN, tTest.getValue(), 0));
    }

    public void testNotEnoughRecords() throws IOException {
        testCase(new MatchAllDocsQuery(), randomFrom(TTestType.values()), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 89)));
        }, tTest -> assertEquals(Double.NaN, tTest.getValue(), 0));
    }

    public void testSameValues() throws IOException {
        TTestType tTestType = randomFrom(TTestType.values());
        testCase(new MatchAllDocsQuery(), tTestType, iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 102)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 99)));
            iw.addDocument(asList(new NumericDocValuesField("a", 111), new NumericDocValuesField("b", 111)));
            iw.addDocument(asList(new NumericDocValuesField("a", 97), new NumericDocValuesField("b", 97)));
            iw.addDocument(asList(new NumericDocValuesField("a", 101), new NumericDocValuesField("b", 101)));
        }, tTest -> assertEquals(tTestType == TTestType.PAIRED ? Double.NaN : 1, tTest.getValue(), 0));
    }

    public void testMatchesSortedNumericDocValues() throws IOException {
        testCase(new MatchAllDocsQuery(), TTestType.PAIRED, iw -> {
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 102), new SortedNumericDocValuesField("b", 89)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 93)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 111), new SortedNumericDocValuesField("b", 72)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 97), new SortedNumericDocValuesField("b", 98)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 101), new SortedNumericDocValuesField("b", 102)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 98)));
        }, tTest -> assertEquals(0.09571844217 * 2, tTest.getValue(), 0.000001));
    }

    public void testMultiplePairedValues() {
        AggregationExecutionException ex = expectThrows(AggregationExecutionException.class, () ->
            testCase(new MatchAllDocsQuery(), TTestType.PAIRED, iw -> {
                iw.addDocument(asList(new SortedNumericDocValuesField("a", 102), new SortedNumericDocValuesField("a", 103),
                    new SortedNumericDocValuesField("b", 89)));
                iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 93)));
            }, tTest -> fail("Should have thrown exception"))
        );
        assertEquals(
            "Encountered more than one value for a single document. Use a script to combine multiple values per doc into a single value.",
            ex.getMessage());
    }

    public void testSameFieldAndNoFilters() {
        TTestType tTestType = randomFrom(TTestType.values());
        MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType.setName("field");
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("field").setMissing(100).build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("field").setMissing(100).build())
            .testType(tTestType);

        IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () ->
            testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
                iw.addDocument(singleton(new SortedNumericDocValuesField("field", 102)));
                iw.addDocument(singleton(new SortedNumericDocValuesField("field", 99)));
            }, tTest -> fail("Should have thrown exception"), fieldType)
        );
        assertEquals(
            "The same field [field] is used for both population but no filters are specified.",
            ex.getMessage());
    }

    public void testMultipleUnpairedValues() throws IOException {
        TTestType tTestType = randomFrom(TTestType.HETEROSCEDASTIC, TTestType.HOMOSCEDASTIC);
        testCase(new MatchAllDocsQuery(), tTestType, iw -> {
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 102), new SortedNumericDocValuesField("a", 103),
                new SortedNumericDocValuesField("b", 89)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 93)));
        }, tTest -> assertEquals(tTestType == TTestType.HETEROSCEDASTIC ? 0.0607303911 : 0.01718374671, tTest.getValue(), 0.000001));
    }

    public void testUnpairedValuesWithFilters() throws IOException {
        TTestType tTestType = randomFrom(TTestType.HETEROSCEDASTIC, TTestType.HOMOSCEDASTIC);
        testCase(new MatchAllDocsQuery(), tTestType, iw -> {
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 102), new SortedNumericDocValuesField("a", 103),
                new SortedNumericDocValuesField("b", 89)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 93)));
        }, tTest -> assertEquals(tTestType == TTestType.HETEROSCEDASTIC ? 0.0607303911 : 0.01718374671, tTest.getValue(), 0.000001));
    }

    public void testMissingValues() throws IOException {
        TTestType tTestType = randomFrom(TTestType.values());
        testCase(new MatchAllDocsQuery(), tTestType, iw -> {
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 102), new SortedNumericDocValuesField("b", 89)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a1", 99), new SortedNumericDocValuesField("b", 93)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 111), new SortedNumericDocValuesField("b1", 72)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 97), new SortedNumericDocValuesField("b", 98)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 101), new SortedNumericDocValuesField("b", 102)));
            iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 98)));
        }, tTest -> {
            switch (tTestType) {
                case PAIRED:
                    assertEquals(0.4385093524, tTest.getValue(), 0.000001);
                    break;
                case HOMOSCEDASTIC:
                    assertEquals(0.1066843841, tTest.getValue(), 0.000001);
                    break;
                case HETEROSCEDASTIC:
                    assertEquals(0.1068382282, tTest.getValue(), 0.000001);
                    break;
                default:
                    fail("unknown t-test type " + tTestType);
            }
        });
    }

    public void testUnmappedWithMissingField() throws IOException {
        TTestType tTestType = randomFrom(TTestType.values());
        boolean missA = randomBoolean();
        boolean missB = missA == false || randomBoolean(); // at least one of the fields should be missing
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName(missA ? "not_a" : "a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName(missB ? "not_b" : "b");
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").setMissing(100).build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").setMissing(100).build())
            .testType(tTestType);
        testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 89)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 93)));
        }, (Consumer<InternalTTest>) tTest -> {
            if (missA && missB) {
                assertEquals(Double.NaN, tTest.getValue(), 0);
            } else {
                if (missA) {
                    switch (tTestType) {
                        case PAIRED:
                            assertEquals(0.1392089745, tTest.getValue(), 0.000001);
                            break;
                        case HOMOSCEDASTIC:
                            assertEquals(0.04600190799, tTest.getValue(), 0.000001);
                            break;
                        case HETEROSCEDASTIC:
                            assertEquals(0.1392089745, tTest.getValue(), 0.000001);
                            break;
                        default:
                            fail("unknown t-test type " + tTestType);
                    }
                } else {
                    switch (tTestType) {
                        case PAIRED:
                            assertEquals(0.7951672353, tTest.getValue(), 0.000001);
                            break;
                        case HOMOSCEDASTIC:
                            assertEquals(0.7705842661, tTest.getValue(), 0.000001);
                            break;
                        case HETEROSCEDASTIC:
                            assertEquals(0.7951672353, tTest.getValue(), 0.000001);
                            break;
                        default:
                            fail("unknown t-test type " + tTestType);
                    }
                }
            }
        }, fieldType1, fieldType2);
    }

    public void testUnsupportedType() {
        TTestType tTestType = randomFrom(TTestType.values());
        boolean wrongA = randomBoolean();
        boolean wrongB = wrongA == false || randomBoolean(); // at least one of the fields should have unsupported type
        MappedFieldType fieldType1;
        if (wrongA) {
            fieldType1 = new KeywordFieldMapper.KeywordFieldType();
            fieldType1.setHasDocValues(true);
        } else {
            fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        }
        fieldType1.setName("a");
        MappedFieldType fieldType2;
        if (wrongB) {
            fieldType2 = new KeywordFieldMapper.KeywordFieldType();
            fieldType2.setHasDocValues(true);
        } else {
            fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        }
        fieldType2.setName("b");
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").build())
            .testType(tTestType);

        IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () ->
            testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
                iw.addDocument(asList(new SortedNumericDocValuesField("a", 102), new SortedNumericDocValuesField("a", 103),
                    new SortedNumericDocValuesField("b", 89)));
                iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 93)));
            }, tTest -> fail("Should have thrown exception"), fieldType1, fieldType2)
        );
        assertEquals(
            "Expected numeric type on field [" + (wrongA ? "a" : "b") + "], but got [keyword]",
            ex.getMessage());
    }

    public void testBadMissingField() {
        TTestType tTestType = randomFrom(TTestType.values());
        boolean missA = randomBoolean();
        boolean missB = missA == false || randomBoolean(); // at least one of the fields should be have bad missing
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MultiValuesSourceFieldConfig.Builder a = new MultiValuesSourceFieldConfig.Builder().setFieldName("a");
        if (missA) {
            a.setMissing("bad_number");
        }
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");
        MultiValuesSourceFieldConfig.Builder b = new MultiValuesSourceFieldConfig.Builder().setFieldName("b");
        if (missB) {
            b.setMissing("bad_number");
        }
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test").a(a.build()).b(b.build()).testType(tTestType);

        NumberFormatException ex = expectThrows(NumberFormatException.class, () ->
            testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
                iw.addDocument(asList(new SortedNumericDocValuesField("a", 102), new SortedNumericDocValuesField("b", 89)));
                iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 93)));
            }, tTest -> fail("Should have thrown exception"), fieldType1, fieldType2)
        );
        assertEquals("For input string: \"bad_number\"", ex.getMessage());
    }


    public void testUnmappedWithBadMissingField() {
        TTestType tTestType = randomFrom(TTestType.values());
        boolean missA = randomBoolean();
        boolean missB = missA == false || randomBoolean(); // at least one of the fields should be have bad missing
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MultiValuesSourceFieldConfig.Builder a = new MultiValuesSourceFieldConfig.Builder();
        if (missA) {
            a.setFieldName("not_a").setMissing("bad_number");
        } else {
            a.setFieldName("a");
        }
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);

        MultiValuesSourceFieldConfig.Builder b = new MultiValuesSourceFieldConfig.Builder();
        if (missB) {
            b.setFieldName("not_b").setMissing("bad_number");
        } else {
            b.setFieldName("b");
        }
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test").a(a.build()).b(b.build()).testType(tTestType);

        NumberFormatException ex = expectThrows(NumberFormatException.class, () ->
            testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
                iw.addDocument(asList(new SortedNumericDocValuesField("a", 102), new SortedNumericDocValuesField("b", 89)));
                iw.addDocument(asList(new SortedNumericDocValuesField("a", 99), new SortedNumericDocValuesField("b", 93)));
            }, tTest -> fail("Should have thrown exception"), fieldType1, fieldType2)
        );
        assertEquals("For input string: \"bad_number\"", ex.getMessage());
    }

    public void testEmptyBucket() throws IOException {
        TTestType tTestType = randomFrom(TTestType.values());
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");
        MappedFieldType fieldTypePart = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldTypePart.setName("part");
        HistogramAggregationBuilder histogram = new HistogramAggregationBuilder("histo").field("part").interval(10).minDocCount(0)
            .subAggregation(new TTestAggregationBuilder("t_test")
                .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").build())
                .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").build())
                .testType(tTestType));

        testCase(histogram, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 89),
                new NumericDocValuesField("part", 1)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 93),
                new NumericDocValuesField("part", 1)));
            iw.addDocument(asList(new NumericDocValuesField("a", 111), new NumericDocValuesField("b", 72),
                new NumericDocValuesField("part", 1)));
            iw.addDocument(asList(new NumericDocValuesField("a", 97), new NumericDocValuesField("b", 98),
                new NumericDocValuesField("part", 21)));
            iw.addDocument(asList(new NumericDocValuesField("a", 101), new NumericDocValuesField("b", 102),
                new NumericDocValuesField("part", 21)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 98),
                new NumericDocValuesField("part", 21)));
        }, (Consumer<InternalHistogram>) histo -> {
            assertEquals(3, histo.getBuckets().size());
            assertNotNull(histo.getBuckets().get(0).getAggregations().asMap().get("t_test"));
            InternalTTest tTest = (InternalTTest) histo.getBuckets().get(0).getAggregations().asMap().get("t_test");
            assertEquals(tTestType == TTestType.PAIRED ? 0.1939778614 :
                tTestType == TTestType.HOMOSCEDASTIC ? 0.05878871029 : 0.07529006595, tTest.getValue(), 0.000001);

            assertNotNull(histo.getBuckets().get(1).getAggregations().asMap().get("t_test"));
            tTest = (InternalTTest) histo.getBuckets().get(1).getAggregations().asMap().get("t_test");
            assertEquals(Double.NaN, tTest.getValue(), 0.000001);

            assertNotNull(histo.getBuckets().get(2).getAggregations().asMap().get("t_test"));
            tTest = (InternalTTest) histo.getBuckets().get(2).getAggregations().asMap().get("t_test");
            assertEquals(tTestType == TTestType.PAIRED ? 0.6666666667 :
                tTestType == TTestType.HOMOSCEDASTIC ? 0.8593081179 : 0.8594865044, tTest.getValue(), 0.000001);

        }, fieldType1, fieldType2, fieldTypePart);
    }

    @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/54365")
    public void testFormatter() throws IOException {
        TTestType tTestType = randomFrom(TTestType.values());
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").build())
            .testType(tTestType).format("0.00%");

        testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 89)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 93)));
            iw.addDocument(asList(new NumericDocValuesField("a", 111), new NumericDocValuesField("b", 72)));
        }, (Consumer<InternalTTest>) tTest -> {
            assertEquals(tTestType == TTestType.PAIRED ? 0.1939778614 :
                tTestType == TTestType.HOMOSCEDASTIC ? 0.05878871029 : 0.07529006595, tTest.getValue(), 0.000001);
            assertEquals(tTestType == TTestType.PAIRED ? "19.40%" :
                tTestType == TTestType.HOMOSCEDASTIC ? "5.88%" : "7.53%", tTest.getValueAsString());
        }, fieldType1, fieldType2);
    }

    public void testGetProperty() throws IOException {
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");
        GlobalAggregationBuilder globalBuilder = new GlobalAggregationBuilder("global")
            .subAggregation(new TTestAggregationBuilder("t_test")
                .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").build())
                .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").build())
                .testType(TTestType.PAIRED));

        testCase(globalBuilder, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 89)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 93)));
            iw.addDocument(asList(new NumericDocValuesField("a", 111), new NumericDocValuesField("b", 72)));
        }, (Consumer<InternalGlobal>) global -> {
            assertEquals(3, global.getDocCount());
            assertTrue(AggregationInspectionHelper.hasValue(global));
            assertNotNull(global.getAggregations().asMap().get("t_test"));
            InternalTTest tTest = (InternalTTest) global.getAggregations().asMap().get("t_test");
            assertEquals(tTest, global.getProperty("t_test"));
            assertEquals(0.1939778614, (Double) global.getProperty("t_test.value"), 0.000001);
        }, fieldType1, fieldType2);
    }

    public void testScript() throws IOException {
        boolean fieldInA = randomBoolean();
        TTestType tTestType = randomFrom(TTestType.values());

        MappedFieldType fieldType = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType.setName("field");

        MultiValuesSourceFieldConfig a = new MultiValuesSourceFieldConfig.Builder().setFieldName("field").build();
        MultiValuesSourceFieldConfig b = new MultiValuesSourceFieldConfig.Builder().setScript(
            new Script(ScriptType.INLINE, MockScriptEngine.NAME, ADD_HALF_SCRIPT, Collections.singletonMap("fieldname", "field"))).build();
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test").
            a(fieldInA ? a : b).b(fieldInA ? b : a).testType(tTestType);

        testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(singleton(new NumericDocValuesField("field", 1)));
            iw.addDocument(singleton(new NumericDocValuesField("field", 2)));
            iw.addDocument(singleton(new NumericDocValuesField("field", 3)));
        }, (Consumer<InternalTTest>) tTest -> {
            assertEquals(tTestType == TTestType.PAIRED ? 0 : 0.5733922538, tTest.getValue(), 0.000001);
        }, fieldType);
    }

    public void testPaired() throws IOException {
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").build())
            .testType(TTestType.PAIRED);
        int tails = randomIntBetween(1, 2);
        if (tails == 1 || randomBoolean()) {
            aggregationBuilder.tails(tails);
        }
        testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 89)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 93)));
            iw.addDocument(asList(new NumericDocValuesField("a", 111), new NumericDocValuesField("b", 72)));
            iw.addDocument(asList(new NumericDocValuesField("a", 97), new NumericDocValuesField("b", 98)));
            iw.addDocument(asList(new NumericDocValuesField("a", 101), new NumericDocValuesField("b", 102)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 98)));
        }, (Consumer<InternalTTest>) ttest -> {
            assertEquals(0.09571844217 * tails, ttest.getValue(), 0.00001);
        }, fieldType1, fieldType2);
    }

    public void testHomoscedastic() throws IOException {
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").build())
            .testType(TTestType.HOMOSCEDASTIC);
        int tails = randomIntBetween(1, 2);
        if (tails == 1 || randomBoolean()) {
            aggregationBuilder.tails(tails);
        }
        testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 89)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 93)));
            iw.addDocument(asList(new NumericDocValuesField("a", 111), new NumericDocValuesField("b", 72)));
            iw.addDocument(asList(new NumericDocValuesField("a", 97), new NumericDocValuesField("b", 98)));
            iw.addDocument(asList(new NumericDocValuesField("a", 101), new NumericDocValuesField("b", 102)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 98)));
        }, (Consumer<InternalTTest>) ttest -> {
            assertEquals(0.03928288693 * tails, ttest.getValue(), 0.00001);
        }, fieldType1, fieldType2);
    }

    public void testHeteroscedastic() throws IOException {
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").build());
        if (randomBoolean()) {
            aggregationBuilder.testType(TTestType.HETEROSCEDASTIC);
        }
        int tails = randomIntBetween(1, 2);
        if (tails == 1 || randomBoolean()) {
            aggregationBuilder.tails(tails);
        }
        testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new NumericDocValuesField("b", 89)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 93)));
            iw.addDocument(asList(new NumericDocValuesField("a", 111), new NumericDocValuesField("b", 72)));
            iw.addDocument(asList(new NumericDocValuesField("a", 97), new NumericDocValuesField("b", 98)));
            iw.addDocument(asList(new NumericDocValuesField("a", 101), new NumericDocValuesField("b", 102)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new NumericDocValuesField("b", 98)));
        }, (Consumer<InternalTTest>) ttest -> {
            assertEquals(0.04538666214 * tails, ttest.getValue(), 0.00001);
        }, fieldType1, fieldType2);
    }

    public void testFiltered() throws IOException {
        TTestType tTestType = randomFrom(TTestType.values());
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");
        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").setFilter(QueryBuilders.termQuery("b", 1)).build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").setFilter(QueryBuilders.termQuery("b", 2)).build())
            .testType(tTestType);
        int tails = randomIntBetween(1, 2);
        if (tails == 1 || randomBoolean()) {
            aggregationBuilder.tails(tails);
        }
        CheckedConsumer<RandomIndexWriter, IOException> buildIndex = iw -> {
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new IntPoint("b", 1)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new IntPoint("b", 1)));
            iw.addDocument(asList(new NumericDocValuesField("a", 111), new IntPoint("b", 1)));
            iw.addDocument(asList(new NumericDocValuesField("a", 97), new IntPoint("b", 1)));
            iw.addDocument(asList(new NumericDocValuesField("a", 101), new IntPoint("b", 1)));
            iw.addDocument(asList(new NumericDocValuesField("a", 99), new IntPoint("b", 1)));

            iw.addDocument(asList(new NumericDocValuesField("a", 89), new IntPoint("b", 2)));
            iw.addDocument(asList(new NumericDocValuesField("a", 93), new IntPoint("b", 2)));
            iw.addDocument(asList(new NumericDocValuesField("a", 72), new IntPoint("b", 2)));
            iw.addDocument(asList(new NumericDocValuesField("a", 98), new IntPoint("b", 2)));
            iw.addDocument(asList(new NumericDocValuesField("a", 102), new IntPoint("b", 2)));
            iw.addDocument(asList(new NumericDocValuesField("a", 98), new IntPoint("b", 2)));

            iw.addDocument(asList(new NumericDocValuesField("a", 189), new IntPoint("b", 3)));
            iw.addDocument(asList(new NumericDocValuesField("a", 193), new IntPoint("b", 3)));
            iw.addDocument(asList(new NumericDocValuesField("a", 172), new IntPoint("b", 3)));
            iw.addDocument(asList(new NumericDocValuesField("a", 198), new IntPoint("b", 3)));
            iw.addDocument(asList(new NumericDocValuesField("a", 1102), new IntPoint("b", 3)));
            iw.addDocument(asList(new NumericDocValuesField("a", 198), new IntPoint("b", 3)));
        };
        if (tTestType == TTestType.PAIRED) {
            IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () ->
                testCase(aggregationBuilder, new MatchAllDocsQuery(), buildIndex, tTest -> fail("Should have thrown exception"),
                    fieldType1, fieldType2)
            );
            assertEquals("Paired t-test doesn't support filters", ex.getMessage());
        } else {
            testCase(aggregationBuilder, new MatchAllDocsQuery(), buildIndex, (Consumer<InternalTTest>) ttest -> {
                if (tTestType == TTestType.HOMOSCEDASTIC) {
                    assertEquals(0.03928288693 * tails, ttest.getValue(), 0.00001);
                } else {
                    assertEquals(0.04538666214 * tails, ttest.getValue(), 0.00001);
                }
            }, fieldType1, fieldType2);
        }
    }

    public void testFilterByFilterOrScript() throws IOException {
        boolean fieldInA = randomBoolean();
        TTestType tTestType = randomFrom(TTestType.HOMOSCEDASTIC, TTestType.HETEROSCEDASTIC);

        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("field");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("term");

        boolean filterTermOne = randomBoolean();

        MultiValuesSourceFieldConfig.Builder a = new MultiValuesSourceFieldConfig.Builder().setFieldName("field").setFilter(
            QueryBuilders.termQuery("term", filterTermOne? 1 : 2)
        );
        MultiValuesSourceFieldConfig.Builder b = new MultiValuesSourceFieldConfig.Builder().setScript(
            new Script(ScriptType.INLINE, MockScriptEngine.NAME, TERM_FILTERING, Collections.singletonMap("term", filterTermOne? 2 : 1))
        );

        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test").
            a(fieldInA ? a.build() : b.build()).b(fieldInA ? b.build() : a.build()).testType(tTestType);

        testCase(aggregationBuilder, new MatchAllDocsQuery(), iw -> {
            iw.addDocument(asList(new NumericDocValuesField("field", 1), new IntPoint("term", 1), new NumericDocValuesField("term", 1)));
            iw.addDocument(asList(new NumericDocValuesField("field", 2), new IntPoint("term", 1), new NumericDocValuesField("term", 1)));
            iw.addDocument(asList(new NumericDocValuesField("field", 3), new IntPoint("term", 1), new NumericDocValuesField("term", 1)));

            iw.addDocument(asList(new NumericDocValuesField("field", 4), new IntPoint("term", 2), new NumericDocValuesField("term", 2)));
            iw.addDocument(asList(new NumericDocValuesField("field", 5), new IntPoint("term", 2), new NumericDocValuesField("term", 2)));
            iw.addDocument(asList(new NumericDocValuesField("field", 6), new IntPoint("term", 2), new NumericDocValuesField("term", 2)));
        }, (Consumer<InternalTTest>) tTest -> {
            assertEquals(0.02131164113, tTest.getValue(), 0.000001);
        }, fieldType1, fieldType2);
    }

    private void testCase(Query query, TTestType type,
                          CheckedConsumer<RandomIndexWriter, IOException> buildIndex,
                          Consumer<InternalTTest> verify) throws IOException {
        MappedFieldType fieldType1 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType1.setName("a");
        MappedFieldType fieldType2 = new NumberFieldMapper.NumberFieldType(NumberFieldMapper.NumberType.INTEGER);
        fieldType2.setName("b");

        TTestAggregationBuilder aggregationBuilder = new TTestAggregationBuilder("t_test")
            .a(new MultiValuesSourceFieldConfig.Builder().setFieldName("a").build())
            .b(new MultiValuesSourceFieldConfig.Builder().setFieldName("b").build());
        if (type != TTestType.HETEROSCEDASTIC || randomBoolean()) {
            aggregationBuilder.testType(type);
        }
        testCase(aggregationBuilder, query, buildIndex, verify, fieldType1, fieldType2);
    }

    private <T extends AggregationBuilder, V extends InternalAggregation> void testCase(
        T aggregationBuilder, Query query,
        CheckedConsumer<RandomIndexWriter, IOException> buildIndex,
        Consumer<V> verify, MappedFieldType... fieldType) throws IOException {
        try (Directory directory = newDirectory()) {
            RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory);
            buildIndex.accept(indexWriter);
            indexWriter.close();

            try (IndexReader indexReader = DirectoryReader.open(directory)) {
                IndexSearcher indexSearcher = newSearcher(indexReader, true, true);

                V agg = searchAndReduce(indexSearcher, query, aggregationBuilder, fieldType);
                verify.accept(agg);

            }
        }
    }
}
