/*
 * Copyright 2016 Google Inc.
 *
 * Licensed 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 com.google.j2cl.jre.java8.util;

import static java.lang.Double.MAX_VALUE;
import static java.lang.Double.MIN_VALUE;
import static java.lang.Double.NEGATIVE_INFINITY;
import static java.lang.Double.NaN;
import static java.lang.Double.POSITIVE_INFINITY;

import com.google.j2cl.jre.java.util.EmulTestBase;
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.List;

/**
 * Tests {@link DoubleSummaryStatistics}.
 */
public class DoubleSummaryStatisticsTest extends EmulTestBase {

  private DoubleSummaryStatistics stats;

  @Override
  protected void setUp() throws Exception {
    stats = new DoubleSummaryStatistics();
  }

  public void testAverageAndSumWithCompensation() throws Exception {
    assertEquals(0d, stats.getAverage());
    assertEquals(0d, stats.getSum());

    double initial = 1.0d;
    long count = 100000;

    // 'precision' is the hardcoded result of Math.ulp(initial) in JVM,
    // since GWT does not emulate Math.ulp().
    // This value represents the distance from 'initial' (1.0d) to the
    // previous/next double. If we add half or less of that distance/precision
    // to 'initial' then the result will be truncated to 'initial' again due to
    // floating point arithmetic rounding.
    // With Kahan summation such rounding errors are detected and compensated
    // so the summation result should (nearly) equal the expected sum.
    double precision = 2.220446049250313E-16;
    double value = precision / 2;
    double expectedSum = initial + (count * value);
    long expectedCount = count + 1;
    double expectedAverage = expectedSum / expectedCount;

    stats.accept(initial);
    for (int i = 0; i < count; ++i) {
      stats.accept(value);
    }

    // TODO (jnehlmeier): While delta = 0 works we probably want to allow some error?
    // Or maybe use ulp differences instead of a delta?
    assertEquals(expectedAverage, stats.getAverage(), 0);
    assertEquals(expectedSum, stats.getSum(), 0);
  }

  public void testCombine() throws Exception {
    stats.accept(1.0d);
    stats.accept(2.0d);

    DoubleSummaryStatistics otherStats = new DoubleSummaryStatistics();
    otherStats.accept(3.0d);
    otherStats.accept(4.0d);

    stats.combine(otherStats);

    assertEquals(2.5d, stats.getAverage());
    assertEquals(1d, stats.getMin());
    assertEquals(4d, stats.getMax());
    assertEquals(10d, stats.getSum());
    assertEquals(4, stats.getCount());
  }

  public void testCountMaxMin() {
    assertEquals(0, stats.getCount());
    assertEquals(NEGATIVE_INFINITY, stats.getMax());
    assertEquals(POSITIVE_INFINITY, stats.getMin());

    double[][] testData = {
        //          aDouble,               max,               min
        {               1.0,               1.0,               1.0 },
        {              -1.0,               1.0,              -1.0 },
        {               2.5,               2.5,              -1.0 },
        {              -2.5,               2.5,              -2.5 },
        {         MAX_VALUE,         MAX_VALUE,              -2.5 },
        {         MIN_VALUE,         MAX_VALUE,              -2.5 },
        { POSITIVE_INFINITY, POSITIVE_INFINITY,              -2.5 },
        { NEGATIVE_INFINITY, POSITIVE_INFINITY, NEGATIVE_INFINITY },
    };

    for (int i = 0; i < testData.length; ++i) {
      long expectedCount = i + 1;
      double aDouble = testData[i][0];
      double expectedMax = testData[i][1];
      double expectedMin = testData[i][2];

      stats.accept(aDouble);

      assertEquals(expectedCount, stats.getCount());
      assertEquals(expectedMax, stats.getMax());
      assertEquals(expectedMin, stats.getMin());
    }
  }

  public void testInfinity() {
    stats.accept(NEGATIVE_INFINITY);
    stats.accept(NEGATIVE_INFINITY);
    assertEquals(NEGATIVE_INFINITY, stats.getAverage());
    assertEquals(NEGATIVE_INFINITY, stats.getMax());
    assertEquals(NEGATIVE_INFINITY, stats.getMin());
    assertEquals(NEGATIVE_INFINITY, stats.getSum());

    stats.accept(POSITIVE_INFINITY);
    assertTrue(Double.isNaN(stats.getAverage()));
    assertEquals(POSITIVE_INFINITY, stats.getMax());
    assertEquals(NEGATIVE_INFINITY, stats.getMin());
    assertTrue(Double.isNaN(stats.getSum()));

    stats = new DoubleSummaryStatistics();
    stats.accept(POSITIVE_INFINITY);
    stats.accept(POSITIVE_INFINITY);
    assertEquals(POSITIVE_INFINITY, stats.getAverage());
    assertEquals(POSITIVE_INFINITY, stats.getMax());
    assertEquals(POSITIVE_INFINITY, stats.getMin());
    assertEquals(POSITIVE_INFINITY, stats.getSum());

    stats.accept(NEGATIVE_INFINITY);
    assertTrue(Double.isNaN(stats.getAverage()));
    assertEquals(POSITIVE_INFINITY, stats.getMax());
    assertEquals(NEGATIVE_INFINITY, stats.getMin());
    assertTrue(Double.isNaN(stats.getSum()));
  }

  public void testNaN() {
    List<Double> testData = Arrays.asList(
        NaN, -1.5d, 2.5d, MAX_VALUE, MIN_VALUE, NaN,
        NEGATIVE_INFINITY, POSITIVE_INFINITY);

    stats.accept(5.0d);
    for (Double aDouble : testData) {
      stats.accept(aDouble);
      assertTrue(Double.isNaN(stats.getAverage()));
      assertTrue(Double.isNaN(stats.getMax()));
      assertTrue(Double.isNaN(stats.getMin()));
      assertTrue(Double.isNaN(stats.getSum()));
    }
  }
}
