content: /*
   * Copyright 2015, 2024 StreamEx contributors
   *
   * 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 one.util.streamex.api;
  
  import one.util.streamex.EntryStream;
  import one.util.streamex.IntStreamEx;
  import one.util.streamex.MoreCollectors;
  import one.util.streamex.StreamEx;
  import one.util.streamex.TestHelpers.*;
  import org.junit.FixMethodOrder;
  import org.junit.Test;
  import org.junit.runners.MethodSorters;
  
  import java.util.*;
  import java.util.AbstractMap.SimpleEntry;
  import java.util.Map.Entry;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.concurrent.ConcurrentMap;
  import java.util.concurrent.ConcurrentSkipListMap;
  import java.util.function.Function;
  import java.util.function.Supplier;
  import java.util.regex.Pattern;
  import java.util.stream.Collectors;
  import java.util.stream.IntStream;
  import java.util.stream.Stream;
  
  import static java.util.Arrays.asList;
  import static one.util.streamex.TestHelpers.*;
  import static org.junit.Assert.*;
  
  /**
   * @author Tagir Valeev
   */
  @FixMethodOrder(MethodSorters.NAME_ASCENDING)
  public class EntryStreamTest {
      private static Map<String, Integer> createMap() {
          Map<String, Integer> data = new LinkedHashMap<>();
          data.put("a", 1);
          data.put("bb", 22);
          data.put("ccc", 33);
          return data;
      }
  
      @Test
      public void testCreate() {
          // double test is intended to ensure that EntryStream.empty() returns non-shared object 
          // and can be consumed several times.
          assertEquals(0, EntryStream.empty().count());
          assertEquals(0, EntryStream.empty().count());
  
          Map<String, Integer> data = createMap();
          assertEquals(data, EntryStream.of(data).toMap());
          assertEquals(data, EntryStream.of(data.entrySet().stream()).toMap());
  
          Map<String, Integer> expected = new HashMap<>();
          expected.put("aaa", 3);
          expected.put("bbb", 3);
          expected.put("c", 1);
          assertEquals(expected, StreamEx.of("aaa", "bbb", "c").mapToEntry(String::length).toMap());
          assertEquals(expected, StreamEx.of("aaa", "bbb", "c").mapToEntry(s -> s, String::length).toMap());
          assertEquals(Collections.singletonMap("foo", 1), EntryStream.of("foo", 1).toMap());
          assertEquals(createMap(), EntryStream.of("a", 1, "bb", 22, "ccc", 33).toMap());
  
          assertEquals(expected, StreamEx.of(Collections.singletonMap("aaa", 3), Collections.singletonMap("bbb", 3),
              Collections.singletonMap("c", 1), Collections.emptyMap()).flatMapToEntry(m -> m).toMap());
  
          assertEquals(Collections.singletonMap("aaa", 3), EntryStream.of(
              Collections.singletonMap("aaa", 3).entrySet().spliterator()).toMap());
          assertEquals(Collections.singletonMap("aaa", 3), EntryStream.of(
              Collections.singletonMap("aaa", 3).entrySet().iterator()).toMap());
      }
  
      @Test
      public void testCreateKeyValuePairs() {
          checkAsString("a->1", EntryStream.of("a", 1));
          checkAsString("a->1;b->2", EntryStream.of("a", 1, "b", 2));
          checkAsString("a->1;b->2;c->3", EntryStream.of("a", 1, "b", 2, "c", 3));
          checkAsString("a->1;b->2;c->3;d->4", EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4));
          checkAsString("a->1;b->2;c->3;d->4;e->5", EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5));
          checkAsString("a->1;b->2;c->3;d->4;e->5;f->6", EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f", 6));
          checkAsString("a->1;b->2;c->3;d->4;e->5;f->6;g->7", EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5, "f",
              6, "g", 7));
          checkAsString("a->1;b->2;c->3;d->4;e->5;f->6;g->7;h->8", EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5,
              "f", 6, "g", 7, "h", 8));
          checkAsString("a->1;b->2;c->3;d->4;e->5;f->6;g->7;h->8;i->9", EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4,
              "e", 5, "f", 6, "g", 7, "h", 8, "i", 9));
          checkAsString("a->1;b->2;c->3;d->4;e->5;f->6;g->7;h->8;i->9;j->10", EntryStream.of("a", 1, "b", 2, "c", 3, "d",
              4, "e", 5, "f", 6, "g", 7, "h", 8, "i", 9, "j", 10));
      }
      
      @Test
      public void testOfList() {
          List<Entry<Integer, String>> list1 = EntryStream.of(asList("a", "b")).toList();
          List<Entry<Integer, String>> list2 = EntryStream.of(asList("a", "c")).toList();
          List<Entry<Integer, String>> list3 = EntryStream.of(asList("a", "b")).toList();
          assertNotEquals(list1, list2);
          assertEquals(list1, list3);
      }
  
      @Test
      public void testGenerate() {
          entryStream(() -> EntryStream.generate(() -> "a", () -> 1).limit(10),
                  es -> assertEquals("a-1,a-1,a-1,a-1,a-1,a-1,a-1,a-1,a-1,a-1", es.get().join("-").joining(",")));
      }
  
      @Test
      public void testSequential() {
          EntryStream<String, Integer> stream = EntryStream.of(createMap());
          assertFalse(stream.isParallel());
          stream = stream.parallel();
          assertTrue(stream.isParallel());
          stream = stream.sequential();
          assertFalse(stream.isParallel());
      }
  
      @Test
      public void testZip() {
          Map<String, Integer> expected = new HashMap<>();
          expected.put("aaa", 3);
          expected.put("bbb", 3);
          expected.put("c", 1);
          assertEquals(expected, EntryStream.zip(asList("aaa", "bbb", "c"), asList(3, 3, 1)).toMap());
          assertEquals(expected, EntryStream.zip(new String[] { "aaa", "bbb", "c" }, new Integer[] { 3, 3, 1 }).toMap());
      }
  
      @Test
      public void testWithIndex() {
          Map<Integer, String> map = EntryStream.of(asList("a", "bbb", "cc")).toMap();
          assertEquals(3, map.size());
          assertEquals("a", map.get(0));
          assertEquals("bbb", map.get(1));
          assertEquals("cc", map.get(2));
          Map<Integer, String> map2 = EntryStream.of(new String[] { "a", "bbb", "cc" }).toMap();
          assertEquals(map, map2);
  
          Map<Integer, List<String>> grouping = EntryStream.of(new String[] { "a", "bbb", "cc", null, null }).append(
              EntryStream.of(new String[] { "bb", "bbb", "c", null, "e" })).distinct().grouping();
          assertEquals(asList("a", "bb"), grouping.get(0));
          assertEquals(asList("bbb"), grouping.get(1));
          assertEquals(asList("cc", "c"), grouping.get(2));
          assertEquals(Collections.singletonList(null), grouping.get(3));
          assertEquals(asList(null, "e"), grouping.get(4));
  
          assertEquals("0=a,1=bbb,2=cc", EntryStream.of(new String[] { "a", "bbb", "cc" }).map(Object::toString).joining(
              ","));
  
          Entry<Integer, String> entry = EntryStream.of(asList("a")).findFirst().get();
          // Test equals contract
          assertNotEquals(new Object(), entry);
          assertNotEquals(entry, new Object());
          assertEquals(new AbstractMap.SimpleImmutableEntry<>(0, "a"), entry);
          assertEquals(entry, new AbstractMap.SimpleImmutableEntry<>(0, "a"));
          assertThrows(UnsupportedOperationException.class, () ->
                  EntryStream.of(Collections.singletonList("1")).forEach(e -> e.setValue("2")));
      }
  
      @Test
      public void testMap() {
          assertEquals(asList("1a", "22bb", "33ccc"), EntryStream.of(createMap()).map(
              entry -> entry.getValue() + entry.getKey()).toList());
      }
  
      @Test
      public void testMapKeyValue() {
          assertEquals(asList("1a", "22bb", "33ccc"), EntryStream.of(createMap()).mapKeyValue((k, v) -> v + k).toList());
      }
  
      @Test
      public void testFilter() {
          assertEquals(Collections.singletonMap("a", 1), EntryStream.of(createMap()).filterKeys(s -> s.length() < 2)
                  .toMap());
          assertEquals(Collections.singletonMap("bb", 22), EntryStream.of(createMap()).filterValues(v -> v % 2 == 0)
                  .toMap());
          assertEquals(Collections.singletonMap("ccc", 33), EntryStream.of(createMap()).filterKeyValue(
              (str, num) -> !str.equals("a") && num != 22).toMap());
      }
  
      @Test
      public void testAnyMatch() {
          assertTrue(EntryStream.of(createMap()).anyMatch((k, v) -> "a".equals(k)));
          assertTrue(EntryStream.of(createMap()).anyMatch((k, v) -> v == 22));
          assertTrue(EntryStream.of(createMap()).anyMatch((k, v) -> "ccc".equals(k) && v == 33));
          assertFalse(EntryStream.of(createMap()).anyMatch((k, v) -> "b".equals(k) && v == 1));
      }
  
      @Test
      public void testAllMatch() {
          assertTrue(EntryStream.of(createMap()).allMatch((k, v) -> k.length() < 4));
          assertFalse(EntryStream.of(createMap()).allMatch((k, v) -> v < 30));
  
          Map<String, Integer> map = createMap();
          assertTrue(EntryStream.of(createMap()).allMatch((k, v) -> v.equals(map.get(k))));
          map.put("a", 2);
          assertFalse(EntryStream.of(createMap()).allMatch((k, v) -> v.equals(map.get(k))));
      }
  
      @Test
      public void testNoneMatch() {
          assertTrue(EntryStream.of(createMap()).noneMatch((k, v) -> k.contains("d")));
          assertFalse(EntryStream.of(createMap()).noneMatch((k, v) -> v < 10));
          assertTrue(EntryStream.of(createMap()).noneMatch((k, v) -> k.length() + 1 == v));
          assertFalse(EntryStream.of(createMap()).noneMatch((k, v) -> k.length() == v));
      }
  
      @Test
      public void testPeek() {
          List<String> keys = new ArrayList<>();
          assertEquals(createMap(), EntryStream.of(createMap()).peekKeys(keys::add).toMap());
          assertEquals(asList("a", "bb", "ccc"), keys);
  
          List<Integer> values = new ArrayList<>();
          assertEquals(createMap(), EntryStream.of(createMap()).peekValues(values::add).toMap());
          assertEquals(asList(1, 22, 33), values);
  
          Map<String, Integer> map = new LinkedHashMap<>();
          assertEquals(createMap(), EntryStream.of(createMap()).peekKeyValue(map::put).toMap());
          assertEquals(createMap(), map);
      }
  
      @Test
      public void testRemove() {
          Map<String, List<Integer>> data = new HashMap<>();
          data.put("aaa", Collections.emptyList());
          data.put("bbb", Collections.singletonList(1));
          assertEquals(asList("bbb"), EntryStream.of(data).removeValues(List::isEmpty).keys().toList());
          assertEquals(asList("aaa"), EntryStream.of(data).removeKeys(Pattern.compile("bbb").asPredicate()).keys()
                  .toList());
          assertEquals(EntryStream.of("a", 1, "bb", 22).toMap(), EntryStream.of(createMap()).removeKeyValue(
              (str, num) -> !str.equals("a") && num != 22).toMap());
      }
  
      @Test
      public void testLimit() {
          assertEquals(Collections.singletonMap("a", 1), EntryStream.of(createMap()).limit(1).toMap());
      }
  
      @Test
      public void testKeys() {
          assertEquals(new HashSet<>(asList("a", "bb", "ccc")), EntryStream.of(createMap()).keys().toSet());
      }
  
      @Test
      public void testValues() {
          assertEquals(new HashSet<>(asList(1, 22, 33)), EntryStream.of(createMap()).values().toSet());
      }
  
      @Test
      public void testMapKeys() {
          Map<Integer, Integer> expected = new HashMap<>();
          expected.put(1, 1);
          expected.put(2, 22);
          expected.put(3, 33);
          Map<Integer, Integer> result = EntryStream.of(createMap()).mapKeys(String::length).toMap();
          assertEquals(expected, result);
      }
  
      @Test
      public void testMapKeysPartial() {
          Map<Integer, Integer> original = new HashMap<>();
          original.put(1, 1);
          original.put(2, 2);
          original.put(3, 3);
          original.put(4, 4);
  
          Map<Integer, Integer> expected = new HashMap<>();
          expected.put(1, 2);
          expected.put(2, 4);
  
          Map<Integer, Integer> actual = EntryStream.of(original)
                  .mapKeysPartial(key -> {
                      if (key % 2 == 0) {
                          return Optional.of(key / 2);
                      }
                      return Optional.empty();
                  }).toMap();
  
          assertEquals(expected, actual);
      }
  
      @Test
      public void testMapValues() {
          Map<String, String> expected = new HashMap<>();
          expected.put("a", "1");
          expected.put("bb", "22");
          expected.put("ccc", "33");
          Map<String, String> result = EntryStream.of(createMap()).mapValues(String::valueOf).toMap();
          assertEquals(expected, result);
      }
  
      @Test
      public void testMapValuesPartial() {
          Map<Integer, Integer> original = new HashMap<>();
          original.put(1, 1);
          original.put(2, 2);
          original.put(3, 3);
          original.put(4, 4);
  
          Map<Integer, Integer> expected = new HashMap<>();
          expected.put(2, 1);
          expected.put(4, 2);
  
          Map<Integer, Integer> actual = EntryStream.of(original)
                  .mapValuesPartial(value -> {
                      if (value % 2 == 0) {
                          return Optional.of(value / 2);
                      }
                      return Optional.empty();
                  }).toMap();
  
          assertEquals(expected, actual);
      }
  
      @Test
      public void testMapToValue() {
          Map<String, Integer> expected = new HashMap<>();
          expected.put("a", 2);
          expected.put("bb", 24);
          expected.put("ccc", 36);
          Map<String, Integer> result = EntryStream.of(createMap()).mapToValue((str, num) -> str.length() + num).toMap();
          assertEquals(expected, result);
      }
  
      @Test
      public void testMapToKey() {
          Map<String, Integer> expected = new HashMap<>();
          expected.put("a:1", 1);
          expected.put("bb:22", 22);
          expected.put("ccc:33", 33);
          Map<String, Integer> result = EntryStream.of(createMap()).mapToKey((str, num) -> str + ":" + num).toMap();
          assertEquals(expected, result);
      }
  
      @Test
      public void testAppend() {
          assertEquals(asList(22, 33, 5, 22, 33), EntryStream.of(createMap()).append("dddd", 5).append(createMap())
                  .filterKeys(k -> k.length() > 1).values().toList());
          assertEquals(EntryStream.of(createMap()).toList(), EntryStream.empty().append("a", 1, "bb", 22, "ccc", 33)
                  .toList());
          checkAsString("bb->22;a->1;ccc->33", EntryStream.of("bb", 22).append("a", 1, "ccc", 33));
  
          EntryStream<String, Integer> stream = EntryStream.of("a", 1, "b", 2);
          assertSame(stream, stream.append(Collections.emptyMap()));
          assertNotSame(stream, stream.append(new ConcurrentHashMap<>()));
      }
  
      @Test
      public void testPrepend() {
          assertEquals(asList(5, 22, 33, 22, 33), EntryStream.of(createMap()).prepend(createMap()).prepend("dddd", 5)
                  .filterKeys(k -> k.length() > 1).values().toList());
          checkAsString("a->1;ccc->33;bb->22", EntryStream.of("bb", 22).prepend("a", 1, "ccc", 33));
          checkAsString("a->1;ccc->33;dddd->40;bb->22", EntryStream.of("bb", 22).prepend("a", 1, "ccc", 33, "dddd", 40));
  
          EntryStream<String, Integer> stream = EntryStream.of("a", 1, "b", 2);
          assertSame(stream, stream.prepend(Collections.emptyMap()));
          assertNotSame(stream, stream.prepend(new ConcurrentHashMap<>()));
      }
  
      @Test
      public void testToMap() {
          Map<String, Integer> base = IntStreamEx.range(100).mapToEntry(String::valueOf, Integer::valueOf).toMap();
          entryStream(() -> EntryStream.of(base), supplier -> {
              TreeMap<String, Integer> result = supplier.get().toCustomMap(TreeMap::new);
              assertEquals(base, result);
          });
  
          Map<Integer, String> expected = new HashMap<>();
          expected.put(3, "aaa");
          expected.put(2, "bbdd");
          Function<StreamExSupplier<String>, EntryStream<Integer, String>> fn = supplier -> supplier.get().mapToEntry(
              String::length, Function.identity());
          streamEx(() -> StreamEx.of("aaa", "bb", "dd"), supplier -> {
              HashMap<Integer, String> customMap = fn.apply(supplier).toCustomMap(String::concat, HashMap::new);
              assertEquals(expected, customMap);
              Map<Integer, String> map = fn.apply(supplier).toMap(String::concat);
              assertEquals(expected, map);
              SortedMap<Integer, String> sortedMap = fn.apply(supplier).toSortedMap(String::concat);
              assertEquals(expected, sortedMap);
              NavigableMap<Integer, String> navigableMap = fn.apply(supplier).toNavigableMap(String::concat);
              assertEquals(expected, navigableMap);
  
              checkIllegalStateException(() -> fn.apply(supplier).toMap(), "2", "dd", "bb");
              checkIllegalStateException(() -> fn.apply(supplier).toSortedMap(), "2", "dd", "bb");
              checkIllegalStateException(() -> fn.apply(supplier).toNavigableMap(), "2", "dd", "bb");
              checkIllegalStateException(() -> fn.apply(supplier).toCustomMap(HashMap::new), "2", "dd", "bb");
              checkIllegalStateException(() -> fn.apply(supplier).toCustomMap(ConcurrentHashMap::new), "2", "dd", "bb");
          });
  
          assertEquals(createMap(), EntryStream.of(createMap()).parallel().toMap());
          assertTrue(EntryStream.of(createMap()).parallel().toMap() instanceof ConcurrentMap);
          SortedMap<String, Integer> sortedMap2 = EntryStream.of(createMap()).toSortedMap();
          assertEquals(createMap(), sortedMap2);
          assertFalse(sortedMap2 instanceof ConcurrentMap);
          sortedMap2 = EntryStream.of(createMap()).parallel().toSortedMap();
          assertEquals(createMap(), sortedMap2);
          assertTrue(sortedMap2 instanceof ConcurrentMap);
      }
  
      @Test
      public void testToMapAndThen() {
          Map<String, Integer> map = EntryStream.of(createMap()).append("d", 4).toMapAndThen(EntryStream::of).append("e",
              5).toMap();
          Map<String, Integer> expected = EntryStream.of("a", 1, "bb", 22, "ccc", 33, "d", 4, "e", 5).toMap();
          assertEquals(expected, map);
      }
  
      @Test
      public void testFlatMap() {
          assertEquals(asList((int) 'a', (int) 'b', (int) 'b', (int) 'c', (int) 'c', (int) 'c'), EntryStream.of(
              createMap()).flatMap(entry -> entry.getKey().chars().boxed()).toList());
          assertEquals(asList("a", "b", "b", "c", "c", "c"), EntryStream.of(createMap()).flatCollection(
              entry -> asList(entry.getKey().split(""))).toList());
          assertEquals(asList("a", 1, "bb", 22, "ccc", 33), EntryStream.of(createMap()).flatMapKeyValue(
              Stream::of).toList());
      }
  
      @Test
      public void testFlatMapKeys() {
          Map<String, List<Integer>> data = new HashMap<>();
          data.put("aaa", asList(1, 2, 3));
          data.put("bb", asList(2, 3, 4));
          Map<Integer, List<String>> result = EntryStream.of(data).invert().flatMapKeys(List::stream).grouping();
          Map<Integer, List<String>> expected = new HashMap<>();
          expected.put(1, asList("aaa"));
          expected.put(2, asList("aaa", "bb"));
          expected.put(3, asList("aaa", "bb"));
          expected.put(4, asList("bb"));
          assertEquals(expected, result);
          assertEquals(0, EntryStream.<Stream<String>, String>of(null, "a").flatMapKeys(Function.identity()).count());
      }
  
      @Test
      public void testFlatMapToValue() {
          checkAsString("a->a;a->aa;a->aaa;b->b;b->bb", EntryStream.of("a", 3, "b", 2, "c", 0).flatMapToValue(
              (str, cnt) -> cnt == 0 ? null : IntStream.rangeClosed(1, cnt).mapToObj(
                  idx -> StreamEx.constant(str, idx).joining())));
      }
  
      @Test
      public void testFlatMapToKey() {
          checkAsString("a->3;aa->3;aaa->3;b->2;bb->2", EntryStream.of("a", 3, "c", 0, "b", 2).flatMapToKey(
              (str, cnt) -> cnt == 0 ? null : IntStream.rangeClosed(1, cnt).mapToObj(
                  idx -> StreamEx.constant(str, idx).joining())));
      }
  
      @Test
      public void testFlatMapValues() {
          Map<String, List<Integer>> data1 = new HashMap<>();
          data1.put("aaa", asList(1, 2, 3));
          data1.put("bb", asList(4, 5, 6));
          Map<String, List<Integer>> data2 = new HashMap<>();
          data2.put("aaa", asList(10));
          data2.put("bb", asList(20));
          data2.put("cc", null);
          Map<String, List<Integer>> result = StreamEx.of(data1, data2, null).flatMapToEntry(m -> m).flatMapValues(
              l -> l == null ? null : l.stream()).grouping();
          Map<String, List<Integer>> expected = new HashMap<>();
          expected.put("aaa", asList(1, 2, 3, 10));
          expected.put("bb", asList(4, 5, 6, 20));
          assertEquals(expected, result);
  
          // Find the key which contains the biggest value in the list
          assertEquals("bb", EntryStream.of(data1).flatMapValues(List::stream).maxByInt(Entry::getValue).map(
              Entry::getKey).orElse(null));
      }
  
      @Test
      public void testMapToKeyPartial() {
          Map<Integer, Integer> original = new HashMap<>();
          original.put(1, 1);
          original.put(2, 5);
          original.put(3, 3);
          original.put(4, 4);
  
          Map<Integer, Integer> expected = new HashMap<>();
          expected.put(1, 1);
          expected.put(9, 3);
          expected.put(16, 4);
  
          Map<Integer, Integer> actual = EntryStream.of(original)
                  .mapToKeyPartial((key, value) -> {
                      if (key.equals(value)) {
                          return Optional.of(key * value);
                      }
                      return Optional.empty();
                  }).toMap();
  
          assertEquals(expected, actual);
  
      }
  
      @Test
      public void testMapToValuePartial() {
          Map<Integer, Integer> original = new HashMap<>();
          original.put(1, 1);
          original.put(2, 5);
          original.put(3, 3);
          original.put(4, 4);
  
          Map<Integer, Integer> expected = new HashMap<>();
          expected.put(1, 1);
          expected.put(3, 9);
          expected.put(4, 16);
  
          Map<Integer, Integer> actual = EntryStream.of(original)
                  .mapToValuePartial((key, value) -> {
                      if (key.equals(value)) {
                          return Optional.of(key * value);
                      }
                      return Optional.empty();
                  }).toMap();
  
          assertEquals(expected, actual);
  
      }
  
      @Test
      public void testMapKeyValuePartial() {
          Map<Integer, Integer> original = new HashMap<>();
          original.put(1, 1);
          original.put(2, 5);
          original.put(3, 3);
          original.put(4, 4);
  
          List<Integer> expected = asList(1, 9, 16);
          List<Integer> actual = EntryStream.of(original)
                  .mapKeyValuePartial((key, value) -> {
                      if (key.equals(value)) {
                          return Optional.of(key * value);
                      }
                      return Optional.empty();
                  }).toList();
  
          assertEquals(expected, actual);
      }
  
      @Test
      public void testGrouping() {
          Map<String, Integer> data = new LinkedHashMap<>();
          data.put("ab", 1);
          data.put("ac", 2);
          data.put("ba", 3);
          data.put("bc", 4);
          Map<String, List<Integer>> expected = new LinkedHashMap<>();
          expected.put("a", asList(1, 2));
          expected.put("b", asList(3, 4));
          Supplier<EntryStream<String, Integer>> s = () -> EntryStream.of(data).mapKeys(k -> k.substring(0, 1));
          Map<String, List<Integer>> result = s.get().grouping();
          assertEquals(expected, result);
          TreeMap<String, List<Integer>> resultTree = s.get().grouping(TreeMap::new);
          assertEquals(expected, resultTree);
          result = s.get().parallel().grouping();
          assertEquals(expected, result);
          resultTree = s.get().parallel().grouping(TreeMap::new);
          assertEquals(expected, resultTree);
  
          streamEx(() -> IntStreamEx.range(1000).boxed(), supplier -> {
              assertEquals(EntryStream.of(0, 500, 1, 500).toMap(), supplier.get().mapToEntry(i -> i / 500, i -> i)
                      .grouping(MoreCollectors.countingInt()));
              ConcurrentSkipListMap<Integer, Integer> map = supplier.get().mapToEntry(i -> i / 500, i -> i).grouping(
                  ConcurrentSkipListMap::new, MoreCollectors.countingInt());
              assertEquals(EntryStream.of(0, 500, 1, 500).toMap(), map);
              Map<Integer, Integer> map2 = supplier.get().mapToEntry(i -> i / 500, i -> i).grouping(
                  TreeMap::new, MoreCollectors.countingInt());
              assertEquals(map, map2);
          });
      }
  
      @Test
      public void testGroupingTo() {
          Map<String, Integer> data = new LinkedHashMap<>();
          data.put("ab", 1);
          data.put("ac", 2);
          data.put("ba", 3);
          data.put("bc", 4);
          Map<String, List<Integer>> expected = new LinkedHashMap<>();
          expected.put("a", asList(1, 2));
          expected.put("b", asList(3, 4));
          Supplier<EntryStream<String, Integer>> s = () -> EntryStream.of(data).mapKeys(k -> k.substring(0, 1));
          Map<String, List<Integer>> result = s.get().groupingTo(LinkedList::new);
          assertEquals(expected, result);
          assertTrue(result.get("a") instanceof LinkedList);
          result = s.get().parallel().groupingTo(LinkedList::new);
          assertEquals(expected, result);
          assertTrue(result.get("a") instanceof LinkedList);
          SortedMap<String, List<Integer>> resultTree = s.get().groupingTo(TreeMap::new, LinkedList::new);
          assertTrue(result.get("a") instanceof LinkedList);
          assertEquals(expected, resultTree);
          resultTree = s.get().parallel().groupingTo(TreeMap::new, LinkedList::new);
          assertTrue(result.get("a") instanceof LinkedList);
          assertEquals(expected, resultTree);
          resultTree = s.get().parallel().groupingTo(ConcurrentSkipListMap::new, LinkedList::new);
          assertTrue(result.get("a") instanceof LinkedList);
          assertEquals(expected, resultTree);
      }
  
      @Test
      public void testSorting() {
          Map<String, Integer> data = createMap();
          LinkedHashMap<String, Integer> result = EntryStream.of(data).reverseSorted(Entry.comparingByValue())
                  .toCustomMap(LinkedHashMap::new);
          assertEquals("{ccc=33, bb=22, a=1}", result.toString());
          assertThrows(ClassCastException.class, () -> EntryStream.of(data).sorted().toList());
      }
  
      @Test
      public void testDistinct() {
          Map<String, List<Integer>> expected = new LinkedHashMap<>();
          expected.put("aaa", asList(3));
          expected.put("bbb", asList(3, 3));
          expected.put("cc", asList(2));
          assertEquals(expected, StreamEx.of("aaa", "bbb", "bbb", "cc").mapToEntry(String::length).grouping());
  
          Map<String, List<Integer>> expectedDistinct = new LinkedHashMap<>();
          expectedDistinct.put("aaa", asList(3));
          expectedDistinct.put("bbb", asList(3));
          expectedDistinct.put("cc", asList(2));
          assertEquals(expectedDistinct, StreamEx.of("aaa", "bbb", "bbb", "cc").mapToEntry(String::length).distinct()
                  .grouping());
      }
  
      @Test
      public void testNonNull() {
          Map<String, String> input = new LinkedHashMap<>();
          input.put("a", "b");
          input.put("b", null);
          input.put(null, "c");
          assertEquals(asList("b", null), EntryStream.of(input).nonNullKeys().values().toList());
          assertEquals(asList("a", null), EntryStream.of(input).nonNullValues().keys().toList());
          assertEquals(Collections.singletonMap("a", "b"), EntryStream.of(input).nonNullValues().nonNullKeys().toMap());
      }
  
      @Test
      public void testSelect() {
          Map<Object, Object> map = new LinkedHashMap<>();
          map.put("a", 1);
          map.put("b", "2");
          map.put(3, "c");
          assertEquals(Collections.singletonMap("a", 1), EntryStream.of(map).selectValues(Integer.class).toMap());
          assertEquals(Collections.singletonMap(3, "c"), EntryStream.of(map).selectKeys(Integer.class).toMap());
  
          // Weird way to create a map from the array. Don't do this in production
          // code!
          Object[] interleavingArray = { "a", 1, "bb", 22, "ccc", 33 };
          Map<String, Integer> result = EntryStream.of(
              StreamEx.of(interleavingArray).pairMap(SimpleEntry::new)).selectKeys(String.class)
                  .selectValues(Integer.class).toMap();
          assertEquals(createMap(), result);
      }
  
      @Test
      public void testInvert() {
          Map<Integer, String> result = EntryStream.of(createMap()).invert().toMap();
          Map<Integer, String> expected = new LinkedHashMap<>();
          expected.put(1, "a");
          expected.put(22, "bb");
          expected.put(33, "ccc");
          assertEquals(expected, result);
      }
  
      @Test(expected = IllegalStateException.class)
      public void testCollision() {
          StreamEx.of("aa", "aa").mapToEntry(String::length).toCustomMap(LinkedHashMap::new);
      }
  
      @Test
      public void testForKeyValue() {
          Map<String, Integer> output = new HashMap<>();
          EntryStream.of(createMap()).forKeyValue(output::put);
          assertEquals(output, createMap());
      }
  
      @Test
      public void testJoin() {
          assertEquals("a = 1; bb = 22; ccc = 33", EntryStream.of(createMap()).join(" = ").joining("; "));
          assertEquals("{[a = 1]; [bb = 22]; [ccc = 33]}", EntryStream.of(createMap()).join(" = ", "[", "]").joining(
              "; ", "{", "}"));
      }
  
      @Test
      public void testOfPairs() {
          withRandom(r -> {
              Point[] pts = StreamEx.generate(() -> new Point(r.nextDouble(), r.nextDouble())).limit(100).toArray(
                  Point[]::new);
              double expected = StreamEx.of(pts).cross(pts).mapKeyValue(Point::distance).mapToDouble(Double::doubleValue)
                      .max().getAsDouble();
              entryStream(() -> EntryStream.ofPairs(pts), supplier -> assertEquals(expected, supplier.get().mapKeyValue(
                  Point::distance).mapToDouble(Double::doubleValue).max().getAsDouble(), 0.0));
          });
      }
  
      @Test
      public void testDistinctKeysValues() {
          entryStream(() -> EntryStream.of(1, "a", 1, "b", 2, "b", 2, "c", 1, "c", 3, "c"), s -> {
              checkAsString("1->a;2->b;3->c", s.get().distinctKeys());
              checkAsString("1->a;1->b;2->c", s.get().distinctValues());
          });
      }
  
      @Test
      public void testOfTree() {
          entryStream(() -> EntryStream.ofTree("a", (Integer depth, String str) -> null), supplier -> checkAsString(
              "0->a", supplier.get()));
  
          List<Object> input = Arrays.asList("aa", null, asList(asList("bbbb", "cc", null, asList()), "ddd", Arrays
                  .asList("e"), asList("fff")), "ggg");
          @SuppressWarnings("unchecked")
          Supplier<Stream<Entry<Integer, Object>>> base = () -> EntryStream.ofTree(input, List.class, (depth, l) -> l
                  .stream());
          entryStream(base, supplier -> assertEquals("{1=[aa, ggg], 2=[ddd], 3=[bbbb, cc, e, fff]}", supplier.get()
                  .selectValues(String.class).grouping(TreeMap::new).toString()));
  
          Set<Integer> set = new HashSet<>();
          try (EntryStream<Integer, String> stream = EntryStream.ofTree("", (Integer depth, String str) -> depth >= 3 ? null : Stream.of("a", "b")
                  .map(str::concat).onClose(() -> set.add(depth)))) {
              assertEquals(15, stream.count());
          }
          assertEquals(StreamEx.of(0, 1, 2).toSet(), set);
          boolean catched = false;
          try (EntryStream<Integer, String> stream = EntryStream.ofTree("", (Integer depth, String str) -> depth > 1000 ? null : Stream.of("a", "b")
                  .map(str::concat).onClose(() -> {
                      throw new IllegalArgumentException(String.valueOf(depth));
                  }))) {
              stream.count();
          } catch (IllegalArgumentException iae) {
              catched = true;
              assertEquals("1000", iae.getMessage());
              assertArrayEquals(IntStreamEx.rangeClosed(1000, 0, -1).toArray(), 
                      StreamEx.<Throwable>ofTree(iae, ex -> StreamEx.of(ex.getSuppressed()))
                      .mapToInt(throwable -> Integer.parseInt(throwable.getMessage())).toArray());
          }
          assertTrue(catched);
  
          entryStream(() -> EntryStream.ofTree("", (Integer depth, String str) -> depth >= 3 ? null : Stream.of("a", "b")
                  .map(str::concat)), supplier -> {
              assertEquals(asList("", "a", "aa", "aaa", "aab", "ab", "aba", "abb", "b", "ba", "baa", "bab", "bb", "bba",
                  "bbb"), supplier.get().values().toList());
              assertTrue(supplier.get().values().has("bbb"));
              assertFalse(supplier.get().values().has("ccc"));
              assertEquals(asList("a", "b", "aa", "ab", "ba", "bb", "aaa", "aab", "aba", "abb", "baa", "bab", "bba",
                  "bbb"), supplier.get().sorted(Entry.comparingByKey()).values().without("").toList());
          });
      }
  
      @Test
      public void testOfTreeDeep() {
          List<Integer> numbers = EntryStream.ofTree(1, (d, n) -> n >= 10000 ? null : StreamEx.of(n + 1))
                  .values().toList();
          assertEquals(IntStreamEx.rangeClosed(1, 10000).boxed().toList(), numbers);
  
          assertEquals(700, EntryStream.ofTree(1, (d, n) -> n >= 300 ? null : StreamEx.constant(n + 1, n % 100 == 0 ? 2 : 1))
                  .peekKeyValue((d, n) -> assertEquals(d + 1, (int) n))
                  .count());
      }
  
      @Test
      public void testCollapseKeys() {
          entryStream(() -> EntryStream.of(1, "a", 1, "b", 2, "c", 3, "d", 1, "e", 1, "f", 1, "g"), s -> checkAsString(
              "1->[a, b];2->[c];3->[d];1->[e, f, g]", s.get().collapseKeys()));
          entryStream(() -> EntryStream.of(1, "a", 1, "b", 2, "c", 3, "d", 1, "e", 1, "f", 1, "g"), s -> checkAsString(
              "1->ab;2->c;3->d;1->efg", s.get().collapseKeys(String::concat)));
          entryStream(() -> EntryStream.of(1, "a", 1, "b", 2, "c", 3, "d", 1, "e", 1, "f", 1, "g"), s -> checkAsString(
              "1->a+b;2->c;3->d;1->e+f+g", s.get().collapseKeys(Collectors.joining("+"))));
  
          // batches by 3
          Stream<String> s = Stream.of("a", "b", "c", "d", "e", "f", "g", "h", "i", "j");
          assertEquals(asList(asList("a", "b", "c"), asList("d", "e", "f"), asList("g", "h", "i"), asList("j")),
              IntStreamEx.ints().flatMapToObj(i -> StreamEx.constant(i, 3)).zipWith(s).collapseKeys().values().toList());
      }
  
      @Test
      public void testChain() {
          assertEquals(EntryStream.of(1, "a", 2, "b", 3, "c").toList(), EntryStream.of(1, "a", 2, "b", 2, "b", 3, "c")
                  .chain(StreamEx::of).collapse(Objects::equals).toList());
      }
  
      @Test
      public void testImmutableMap() {
          repeat(4, n -> {
              Map<Integer, Integer> expected = new HashMap<>();
              for (int i = n; i < 4; i++)
                  expected.put(i, i);
              streamEx(() -> IntStreamEx.range(4).atLeast(n).boxed(), s -> {
                  Map<Integer, Integer> map = s.get().mapToEntry(Function.identity()).toImmutableMap();
                  assertEquals(expected, map);
                  try {
                      map.put(-1, -1);
                      fail("added");
                  } catch (UnsupportedOperationException e) {
                      // expected
                  }
              });
          });
      }
  
      @Test
      public void testInto() {
          for (AbstractMap<String, Integer> m : Arrays.<AbstractMap<String, Integer>>asList(new HashMap<>(), new TreeMap<>(), new ConcurrentHashMap<>())) {
              AbstractMap<String, Integer> res = EntryStream.of("a", 1, "b", 2, "c", 3).into(m);
              assertSame(m, res);
              assertEquals(EntryStream.of("a", 1, "b", 2, "c", 3).toMap(), m);
              Map<String, Integer> res2 = EntryStream.of("d", 4, "e", 5).parallel().into(m);
              assertSame(m, res2);
              assertEquals(EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4, "e", 5).toMap(), m);
              checkIllegalStateException(() -> EntryStream.of("x", 10, "c", 4).into(m), "c", "3", "4");
              checkIllegalStateException(() -> EntryStream.of("y", 20, "d", 5).parallel().into(m), "d", "4", "5");
          }
      }
  
      @Test
      public void testPrefixKeys() {
          Map<String, Integer> map = EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4).prefixValues(Integer::sum).toMap();
          assertEquals(EntryStream.of("a", 1, "b", 3, "c", 6, "d", 10).toMap(), map);
      }
  
      @Test
      public void testPrefixValues() {
          Map<String, Integer> map = EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4).prefixKeys(String::concat).toMap();
          assertEquals(EntryStream.of("a", 1, "ab", 2, "abc", 3, "abcd", 4).toMap(), map);
      }
  
      @Test
      public void testWithoutKeys() {
          assertEquals("Stream is not changed",
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4).toMap(),
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4)
                                  .withoutKeys().withoutKeys().withoutKeys().toMap());
  
          assertEquals(EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4).toMap(),
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4)
                                  .withoutKeys().withoutKeys("A").withoutKeys("B", "C","D").toMap());
  
          entryStream(() -> EntryStream.of(1, "a", 1, "b", 2, "c", 3, "d", 1, "e", 1, "f", 1, "g"),
                      s -> checkAsString("2->c;3->d", s.get().withoutKeys(1)));
  
          assertEquals(EntryStream.of("b", 2, "d", 4).toMap(),
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4)
                                  .withoutKeys("a").withoutKeys("c").toMap());
  
          assertEquals(EntryStream.of("a", 1, "B", 4).toMap(),
                       EntryStream.of("a", 1, "A", 2, "b", 3, "B", 4)
                                  .withoutKeys("A").withoutKeys("b").toMap());
  
          entryStream(() -> EntryStream.generate(() -> "a", () -> 1).limit(10),
                      s -> checkAsString("", s.get().withoutKeys("a")));
  
          entryStream(() -> EntryStream.of(1, "a", 1, "b", 2, "c", 3, "d", 4, "e", 4, "f", 1, "g"),
                      s -> checkAsString("2->c;3->d", s.get().withoutKeys(1, 4)));
          assertEquals(EntryStream.of("b", 2, "d", 4).toMap(),
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4)
                                  .withoutKeys("a", "c").toMap());
      }
  
      @Test
      public void testWithoutValues() {
          assertEquals("Stream is not changed",
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4).toMap(),
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4)
                                  .withoutValues().withoutValues().withoutValues().toMap());
  
          assertEquals(EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4).toMap(),
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4)
                                  .withoutValues().withoutValues(6).withoutValues(7, 8).toMap());
  
          entryStream(() -> EntryStream.of(1, "a", 1, "b", 2, "b", 3, "c", 4, "c", 5, "c", 6, "d"),
                      s -> checkAsString("1->a;1->b;2->b;6->d", s.get().withoutValues("c")));
  
          assertEquals(EntryStream.of("c", 3, "d", 4).toMap(),
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4)
                                  .withoutValues(1).withoutValues(2).toMap());
  
          entryStream(() -> EntryStream.generate(() -> "a", () -> 1).limit(10),
                      s -> checkAsString("", s.get().withoutValues(1)));
  
          entryStream(() -> EntryStream.of(1, "a", 1, "b", 2, "b", 3, "c", 4, "c", 5, "c", 6, "d"),
                      s -> checkAsString("1->a;6->d", s.get().withoutValues("b", "c")));
          entryStream(() -> EntryStream.of(1, "a", 1, "A", 2, "b", 3, "B", 4, "c", 5, "C"),
                      s -> checkAsString("1->A;2->b;4->c;5->C", s.get().withoutValues("a", "B")));
  
          assertEquals(EntryStream.of("c", 3, "d", 4).toMap(),
                       EntryStream.of("a", 1, "b", 2, "c", 3, "d", 4)
                                  .withoutValues(1, 2).toMap());
  
      }
  }
  