/*
 * 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.gwt.emultest.java8.util.stream;

import static java.util.stream.Collectors.averagingDouble;
import static java.util.stream.Collectors.averagingInt;
import static java.util.stream.Collectors.averagingLong;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.maxBy;
import static java.util.stream.Collectors.minBy;
import static java.util.stream.Collectors.partitioningBy;
import static java.util.stream.Collectors.summarizingDouble;
import static java.util.stream.Collectors.summarizingInt;
import static java.util.stream.Collectors.summarizingLong;
import static java.util.stream.Collectors.summingDouble;
import static java.util.stream.Collectors.summingInt;
import static java.util.stream.Collectors.summingLong;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

import com.google.gwt.emultest.java.util.EmulTestBase;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.DoubleSummaryStatistics;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IntSummaryStatistics;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.LongSummaryStatistics;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

/**
 * Tests {@link java.util.stream.Collectors}.
 * <p />
 * Methods that are presently only tested indirectly:
 * <ul>
 *   <li>reducing: counting, minBy/maxBy use this</li>
 *   <li>toCollection tested by toList (toSet now uses its own impl)</li>
 * </ul>
 */
public class CollectorsTest extends EmulTestBase {

  public void testAveragingDouble() {
    Collector<Double, ?, Double> c = averagingDouble(Double::doubleValue);
    applyItems((4.0 + 8.0) / 2.0, c, 4.0, 8.0);

    assertZeroItemsCollectedAs(0D, c);
    assertSingleItemCollectedAs(5D, c, 5D);
  }

  public void testAveragingInt() {
    Collector<Integer, ?, Double> c = averagingInt(Integer::intValue);
    applyItems((4.0 + 8.0) / 2.0, c, 4, 8);

    assertZeroItemsCollectedAs(0D, c);
    assertSingleItemCollectedAs(5D, c, 5);
  }

  public void testAveragingLong() {
    Collector<Long, ?, Double> c = averagingLong(Long::longValue);
    applyItems((4.0 + 8.0) / 2.0, c, 4L, 8L);

    assertZeroItemsCollectedAs(0D, c);
    assertSingleItemCollectedAs(5D, c, 5L);
  }

  public void testCollectingAndThen() {
    Collector<Object, ?, List<Object>> listIdentityCollector =
        collectingAndThen(toList(), Function.identity());
    // same test as toList():
    // same items (allow dups)
    applyItems(
        Arrays.asList("a", "a"),
        listIdentityCollector,
        "a", "a"
    );

    // ordered
    applyItems(
        Arrays.asList("a", "b"),
        listIdentityCollector,
        "a", "b"
    );
    assertZeroItemsCollectedAs(Collections.emptyList(), listIdentityCollector);
    assertSingleItemCollectedAs(Collections.singletonList("a"), listIdentityCollector, "a");

    Collector<Object, ?, Integer> uglyCount = collectingAndThen(toList(), List::size);
    // (nearly) same test as counting():
    applyItems(2, uglyCount, "1", new Object());

    assertZeroItemsCollectedAs(0, uglyCount);
    assertSingleItemCollectedAs(1, uglyCount, new Object());
  }

  public void testCounting() {
    Collector<Object, ?, Long> c = counting();
    applyItems(2L, c, "1", new Object());

    assertZeroItemsCollectedAs(0L, c);
    assertSingleItemCollectedAs(1L, c, new Object());
  }

  public void testGroupingBy() {
    Collector<String, ?, Map<String, List<String>>> c1 = groupingBy(Function.identity());

    Map<String, List<String>> mapOfLists = new HashMap<>();
    mapOfLists.put("a", Arrays.asList("a", "a"));
    applyItems(mapOfLists, c1, "a", "a");
    mapOfLists.clear();
    mapOfLists.put("a", Collections.singletonList("a"));
    mapOfLists.put("b", Collections.singletonList("b"));
    applyItems(mapOfLists, c1, "a", "b");

    assertZeroItemsCollectedAs(Collections.emptyMap(), c1);
    assertSingleItemCollectedAs(
        Collections.singletonMap("a", Collections.singletonList("a")), c1, "a");

    Collector<String, ?, LinkedHashMap<String, Set<String>>> c2 =
        groupingBy(Function.identity(), LinkedHashMap::new, toSet());

    LinkedHashMap<String, Set<String>> linkedMapOfSets = new LinkedHashMap<>();
    linkedMapOfSets.put("a", Collections.singleton("a"));
    applyItems(linkedMapOfSets, c2, "a", "a");
    linkedMapOfSets.clear();
    linkedMapOfSets.put("a", Collections.singleton("a"));
    linkedMapOfSets.put("b", Collections.singleton("b"));
    applyItems(linkedMapOfSets, c2, "a", "b");

    // check to make sure we actually get the linked results, and that they are ordered how we want
    // them
    LinkedHashMap<String, Set<String>> out = applyItemsWithoutSplitting(c2, "a", "b");
    assertEquals(Arrays.asList("a", "b"), new ArrayList<>(out.keySet()));
    out = applyItemsWithoutSplitting(c2, "b", "a");
    assertEquals(Arrays.asList("b", "a"), new ArrayList<>(out.keySet()));

    assertZeroItemsCollectedAs(new LinkedHashMap<>(), c2);
    linkedMapOfSets.clear();
    linkedMapOfSets.put("a", Collections.singleton("a"));
    assertSingleItemCollectedAs(linkedMapOfSets, c2, "a");
  }

  public void testJoining() {
    Collector<CharSequence, ?, String> c = joining();
    applyItems("ab", c, "a", "b");
    applyItems("a,", c, "a", ",");
    applyItems("", c, "", "");
    assertZeroItemsCollectedAs("", c);
    assertSingleItemCollectedAs("a", c, "a");
    assertSingleItemCollectedAs("", c, "");

    c = joining(",");
    applyItems("a,b", c, "a", "b");
    applyItems("a,,", c, "a", ",");
    applyItems(",", c, "", "");
    assertZeroItemsCollectedAs("", c);
    assertSingleItemCollectedAs("a", c, "a");
    assertSingleItemCollectedAs("", c, "");

    c = joining("-", "{", "}");
    applyItems("{a-b}", c, "a", "b");
    assertZeroItemsCollectedAs("{}", c);
    assertSingleItemCollectedAs("{a}", c, "a");
    assertSingleItemCollectedAs("{}", c, "");
  }

  public void testMapping() {
    Collector<String, ?, List<String>> identityMapping = mapping(Function.identity(), toList());
    // same test as toList():
    // same items (allow dups)
    applyItems(
        Arrays.asList("a", "a"),
        identityMapping,
        "a", "a"
    );

    // ordered
    applyItems(
        Arrays.asList("a", "b"),
        identityMapping,
        "a", "b"
    );
    assertZeroItemsCollectedAs(Collections.emptyList(), identityMapping);
    assertSingleItemCollectedAs(Collections.singletonList("a"), identityMapping, "a");

    Collector<Integer, ?, List<String>> numberMapping = mapping(s -> "#" + s, toList());
    // poke the same tests as list, make sure the mapper is run
    applyItems(
        Arrays.asList("#1", "#2"),
        numberMapping,
        1, 2
    );

    // ordered
    applyItems(
        Arrays.asList("#1", "#2"),
        numberMapping,
        1, 2
    );
    assertZeroItemsCollectedAs(Collections.emptyList(), numberMapping);
    assertSingleItemCollectedAs(Collections.singletonList("#10"), numberMapping, 10);
  }

  public void testMaxBy() {
    Collector<String, ?, Optional<String>> c = maxBy(Comparator.naturalOrder());
    applyItems(Optional.of("z"), c, "a", "z");
    applyItems(Optional.of("z"), c, "z", "a");

    assertZeroItemsCollectedAs(Optional.empty(), c);
    assertSingleItemCollectedAs(Optional.of("foo"), c, "foo");
  }

  public void testMinBy() {
    Collector<String, ?, Optional<String>> c = minBy(Comparator.naturalOrder());
    applyItems(Optional.of("a"), c, "a", "z");
    applyItems(Optional.of("a"), c, "z", "a");

    assertZeroItemsCollectedAs(Optional.empty(), c);
    assertSingleItemCollectedAs(Optional.of("foo"), c, "foo");
  }

  public void testPartitioningBy() {
    Collector<Boolean, ?, Map<Boolean, List<Boolean>>> c1 = partitioningBy(Boolean::valueOf);

    Map<Boolean, List<Boolean>> mapOfLists = new HashMap<>();
    mapOfLists.put(true, Collections.singletonList(true));
    mapOfLists.put(false, Collections.singletonList(false));
    applyItems(mapOfLists, c1, true, false);

    mapOfLists.clear();
    mapOfLists.put(false, Collections.emptyList());
    mapOfLists.put(true, Arrays.asList(true, true));
    applyItems(mapOfLists, c1, true, true);

    mapOfLists.clear();
    mapOfLists.put(false, Arrays.asList(false, false));
    mapOfLists.put(true, Collections.emptyList());
    applyItems(mapOfLists, c1, false, false);

    mapOfLists.clear();
    mapOfLists.put(false, Collections.emptyList());
    mapOfLists.put(true, Collections.emptyList());
    assertZeroItemsCollectedAs(mapOfLists, c1);

    mapOfLists.clear();
    mapOfLists.put(false, Collections.emptyList());
    mapOfLists.put(true, Collections.singletonList(true));
    assertSingleItemCollectedAs(mapOfLists, c1, true);

    mapOfLists.clear();
    mapOfLists.put(false, Collections.singletonList(false));
    mapOfLists.put(true, Collections.emptyList());
    assertSingleItemCollectedAs(mapOfLists, c1, false);

    Collector<Boolean, ?, Map<Boolean, Set<Boolean>>> c2 =
        partitioningBy(Boolean::valueOf, toSet());

    Map<Boolean, Set<Boolean>> mapOfSets = new HashMap<>();
    mapOfSets.put(true, Collections.singleton(true));
    mapOfSets.put(false, Collections.singleton(false));
    applyItems(mapOfSets, c2, true, false);

    mapOfSets.clear();
    mapOfSets.put(false, Collections.emptySet());
    mapOfSets.put(true, Collections.singleton(true));
    applyItems(mapOfSets, c2, true, true);

    mapOfSets.clear();
    mapOfSets.put(false, Collections.singleton(false));
    mapOfSets.put(true, Collections.emptySet());
    applyItems(mapOfSets, c2, false, false);
  }

  public void testSummarizingDouble() {
    Collector<Double, ?, DoubleSummaryStatistics> c = summarizingDouble(Double::doubleValue);
    DoubleSummaryStatistics stats = new DoubleSummaryStatistics();
    stats.accept(5.1);
    stats.accept(7);
    BiPredicate<DoubleSummaryStatistics, DoubleSummaryStatistics> equals = (s1, s2) ->
        s1.getSum() == s2.getSum()
            && s1.getAverage() == s2.getAverage()
            && s1.getCount() == s2.getCount()
            && s1.getMin() == s2.getMin()
            && s1.getMax() == s2.getMax();
    applyItems(stats, c, 5.1, 7.0, equals);
    applyItems(stats, c, 7.0, 5.1, equals);//probably unnecessary to run these backward

    assertZeroItemsCollectedAs(new DoubleSummaryStatistics(), c, equals);
    stats = new DoubleSummaryStatistics();
    stats.accept(7.3);
    assertSingleItemCollectedAs(stats, c, 7.3, equals);
  }

  public void testSummarizingInt() {
    Collector<Integer, ?, IntSummaryStatistics> c = summarizingInt(Integer::intValue);
    IntSummaryStatistics stats = new IntSummaryStatistics();
    stats.accept(2);
    stats.accept(10);
    BiPredicate<IntSummaryStatistics, IntSummaryStatistics> equals = (s1, s2) ->
        s1.getSum() == s2.getSum()
            && s1.getAverage() == s2.getAverage()
            && s1.getCount() == s2.getCount()
            && s1.getMin() == s2.getMin()
            && s1.getMax() == s2.getMax();
    applyItems(stats, c, 2, 10, equals);
    applyItems(stats, c, 10, 2, equals);

    assertZeroItemsCollectedAs(new IntSummaryStatistics(), c, equals);
    stats = new IntSummaryStatistics();
    stats.accept(7);
    assertSingleItemCollectedAs(stats, c, 7, equals);
  }

  public void testSummarizingLong() {
    Collector<Long, ?, LongSummaryStatistics> c = summarizingLong(Long::longValue);
    LongSummaryStatistics stats = new LongSummaryStatistics();
    stats.accept(2);
    stats.accept(10);
    BiPredicate<LongSummaryStatistics, LongSummaryStatistics> equals = (s1, s2) ->
        s1.getSum() == s2.getSum()
            && s1.getAverage() == s2.getAverage()
            && s1.getCount() == s2.getCount()
            && s1.getMin() == s2.getMin()
            && s1.getMax() == s2.getMax();
    applyItems(stats, c, 2L, 10L, equals);
    applyItems(stats, c, 10L, 2L, equals);

    assertZeroItemsCollectedAs(new LongSummaryStatistics(), c, equals);
    stats = new LongSummaryStatistics();
    stats.accept(7L);
    assertSingleItemCollectedAs(stats, c, 7L, equals);
  }

  public void testSummingDouble() {
    Collector<Double, ?, Double> c = summingDouble(Double::doubleValue);
    applyItems(4.1 + 8.2, c, 4.1, 8.2);

    assertZeroItemsCollectedAs(0d, c);
    assertSingleItemCollectedAs(7.3, c, 7.3);
  }

  public void testSummingInt() {
    Collector<Integer, ?, Integer> c = summingInt(Integer::intValue);
    applyItems(4 + 8, c, 4, 8);

    assertZeroItemsCollectedAs(0, c);
    assertSingleItemCollectedAs(7, c, 7);
  }

  public void testSummingLong() {
    Collector<Long, ?, Long> c = summingLong(Long::longValue);
    applyItems(4L + 8L, c, 4L, 8L);

    assertZeroItemsCollectedAs(0L, c);
    assertSingleItemCollectedAs(5L, c, 5L);
  }

  public void testList() {
    Collector<String, ?, List<String>> c = toList();

    // same items (allow dups)
    applyItems(
        Arrays.asList("a", "a"),
        c,
        "a", "a"
    );

    // ordered
    applyItems(
        Arrays.asList("a", "b"),
        c,
        "a", "b"
    );
    assertZeroItemsCollectedAs(Collections.emptyList(), c);
    assertSingleItemCollectedAs(Collections.singletonList("a"), c, "a");
  }

  public void testMap() {
    Function<String, String> toUpper = s -> s.toUpperCase(Locale.ROOT);
    Collector<String, ?, Map<String, String>> c = toMap(Function.identity(), toUpper);

    // two distinct items
    Map<String, String> map = new HashMap<>();
    map.put("a", "A");
    map.put("b", "B");
    applyItems(map, c, "a", "b");

    // inline applyItems and test each to confirm failure for duplicates
    try {
      applyItemsWithoutSplitting(c, "a", "a");
      fail("expected IllegalStateException");
    } catch (IllegalStateException expected) {
      assertEquals("Duplicate key a", expected.getMessage());
    }
    try {
      applyItemsWithSplitting(c, "a", "a");
      fail("expected IllegalStateException");
    } catch (IllegalStateException expected) {
      assertEquals("Duplicate key a", expected.getMessage());
    }

    assertZeroItemsCollectedAs(Collections.emptyMap(), c);
    assertSingleItemCollectedAs(Collections.singletonMap("a", "A"), c, "a");

    List<String> seen = new ArrayList<>();
    c = toMap(Function.identity(), Function.identity(), (s, s2) -> {
      seen.add("first: " + s);
      seen.add("second: " + s2);
      return s + "," + s2;
    });
    map = new HashMap<>();
    map.put("a", "a,a");
    applyItems(map, c, "a", "a");
    assertEquals(Arrays.asList("first: a", "second: a", "first: a", "second: a"), seen);
  }

  public void testMapInvalid() {
    Collector<String, ?, Map<String, String>> c = toMap(Function.identity(), ignore -> null);

    // inline applyItems and test each to confirm failure for duplicates
    try {
      applyItemsWithoutSplitting(c, "a", "b");
      fail("expected NullPointerException");
    } catch (NullPointerException expected) {
      // expected
    }
    try {
      applyItemsWithSplitting(c, "a", "a");
      fail("expected NullPointerException");
    } catch (NullPointerException expected) {
      // expected
    }
  }

  public void testSet() {
    Collector<String, ?, Set<String>> c = toSet();

    // same items (no dups)
    applyItems(
        Collections.singleton("a"),
        c,
        "a", "a"
    );

    // different items
    applyItems(
        new HashSet<>(Arrays.asList("a", "b")),
        c,
        "a", "b"
    );

    assertZeroItemsCollectedAs(Collections.emptySet(), c);
    assertSingleItemCollectedAs(Collections.singleton("a"), c, "a");
  }

  /**
   * This method attempts to apply a collector to items as a stream might do, so that we can simply
   * verify the output. Taken from the Collector class's javadoc.
   */
  public static <T, A, R> void applyItems(
      R expected, Collector<T, A, R> collector, T t1, T t2, BiPredicate<R, R> equals) {
    assertTrue(
        "failed without splitting",
        equals.test(expected, applyItemsWithoutSplitting(collector, t1, t2)));
    assertTrue(
        "failed with splitting", equals.test(expected, applyItemsWithSplitting(collector, t1, t2)));
  }

  /**
   * This method attempts to apply a collector to items as a stream might do, so that we
   * can simply verify the output. Taken from the Collector class's javadoc.
   */
  public static <T, A, R> void applyItems(R expected, Collector<T, A, R> collector, T t1, T t2) {
    applyItems(expected, collector, t1, t2, Object::equals);
  }

  /**
   * Helper for applyItems.
   */
  private static <T, A, R> R applyItemsWithoutSplitting(Collector<T, A, R> collector, T t1, T t2) {
    Supplier<A> supplier = collector.supplier();
    BiConsumer<A, T> accumulator = collector.accumulator();
    // unused in this impl
    BinaryOperator<A> combiner = collector.combiner();
    Function<A, R> finisher = collector.finisher();

    A a1 = supplier.get();

    accumulator.accept(a1, t1);
    accumulator.accept(a1, t2);

    // result without splitting
    R r1 = finisher.apply(a1);
    return r1;
  }

  /**
   * Helper for applyItems.
   */
  private static <T, A, R> R applyItemsWithSplitting(Collector<T, A, R> collector, T t1, T t2) {
    Supplier<A> supplier = collector.supplier();
    BiConsumer<A, T> accumulator = collector.accumulator();
    // actually used in this impl
    BinaryOperator<A> combiner = collector.combiner();
    Function<A, R> finisher = collector.finisher();

    A a2 = supplier.get();
    accumulator.accept(a2, t1);
    A a3 = supplier.get();
    accumulator.accept(a3, t2);

    // result with splitting
    R r2 = finisher.apply(combiner.apply(a2, a3));
    return r2;
  }

  private static <T, A, R> void assertZeroItemsCollectedAs(
      R expected, Collector<T, A, R> collector) {
    assertZeroItemsCollectedAs(expected, collector, Object::equals);
  }

  private static <T, A, R> void assertZeroItemsCollectedAs(
      R expected, Collector<T, A, R> collector, BiPredicate<R, R> equals) {
    Supplier<A> supplier = collector.supplier();
    // unused in this impl
    BiConsumer<A, T> accumulator = collector.accumulator();
    // shouldn't really be used, just handy to poke the internals quick
    BinaryOperator<A> combiner = collector.combiner();
    Function<A, R> finisher = collector.finisher();

    R actual = finisher.apply(supplier.get());
    assertTrue(equals, expected, actual);
    // doesn't actually ever happen, just internal checks
    actual = finisher.apply(combiner.apply(supplier.get(), supplier.get()));
    assertTrue(equals, expected, actual);
  }

  private static <T, A, R> void assertSingleItemCollectedAs(
      R expected, Collector<T, A, R> collector, T item) {
    assertSingleItemCollectedAs(expected, collector, item, Object::equals);
  }

  private static <T, A, R> void assertSingleItemCollectedAs(
      R expected, Collector<T, A, R> collector, T item, BiPredicate<R, R> equals) {
    Supplier<A> supplier = collector.supplier();
    BiConsumer<A, T> accumulator = collector.accumulator();
    // shouldn't really be used, just handy to poke the internals quick
    BinaryOperator<A> combiner = collector.combiner();
    Function<A, R> finisher = collector.finisher();

    A a1 = supplier.get();

    accumulator.accept(a1, item);

    R actual = finisher.apply(a1);
    // normal test
    assertTrue(equals, expected, actual);
    // these shouldn't really be used, just handy to poke the internals quick
    actual = finisher.apply(combiner.apply(a1, supplier.get()));
    assertTrue(equals, expected, actual);
    actual = finisher.apply(combiner.apply(supplier.get(), a1));
    assertTrue(equals, expected, actual);
  }

  private static <T, U> void assertTrue(BiPredicate<T, U> predicate, T expected, U actual) {
    assertTrue("expected= " + expected + ", actual=" + actual, predicate.test(expected, actual));
  }

}
