@Test
 public void testOnlyOne() {
   List<Integer> ints = IntStreamEx.rangeClosed(1, 100).boxed().toList();
   checkShortCircuitCollector("One", Optional.empty(), 2, ints::stream, MoreCollectors.onlyOne());
   checkShortCircuitCollector(
       "FilterSeveral",
       Optional.empty(),
       2,
       () -> ints.stream().filter(x -> x % 20 == 0),
       MoreCollectors.onlyOne());
   checkShortCircuitCollector(
       "FilterSeveral2",
       Optional.empty(),
       40,
       ints::stream,
       MoreCollectors.filtering(x -> x % 20 == 0, MoreCollectors.onlyOne()));
   checkShortCircuitCollector(
       "FilterOne",
       Optional.of(60),
       1,
       () -> ints.stream().filter(x -> x % 60 == 0),
       MoreCollectors.onlyOne());
   checkShortCircuitCollector(
       "FilterNone",
       Optional.empty(),
       0,
       () -> ints.stream().filter(x -> x % 110 == 0),
       MoreCollectors.onlyOne());
 }
  @Test
  public void testHeadTail() {
    List<Integer> ints = IntStreamEx.range(1000).boxed().toList();
    checkShortCircuitCollector("tail(0)", Arrays.asList(), 0, ints::stream, MoreCollectors.tail(0));
    checkCollector("tail(1)", Arrays.asList(999), ints::stream, MoreCollectors.tail(1));
    checkCollector("tail(2)", Arrays.asList(998, 999), ints::stream, MoreCollectors.tail(2));
    checkCollector("tail(500)", ints.subList(500, 1000), ints::stream, MoreCollectors.tail(500));
    checkCollector("tail(999)", ints.subList(1, 1000), ints::stream, MoreCollectors.tail(999));
    checkCollector("tail(1000)", ints, ints::stream, MoreCollectors.tail(1000));
    checkCollector("tail(MAX)", ints, ints::stream, MoreCollectors.tail(Integer.MAX_VALUE));

    checkShortCircuitCollector("head(0)", Arrays.asList(), 0, ints::stream, MoreCollectors.head(0));
    checkShortCircuitCollector(
        "head(1)", Arrays.asList(0), 1, ints::stream, MoreCollectors.head(1));
    checkShortCircuitCollector(
        "head(2)", Arrays.asList(0, 1), 2, ints::stream, MoreCollectors.head(2));
    checkShortCircuitCollector(
        "head(500)", ints.subList(0, 500), 500, ints::stream, MoreCollectors.head(500));
    checkShortCircuitCollector(
        "head(999)", ints.subList(0, 999), 999, ints::stream, MoreCollectors.head(999));
    checkShortCircuitCollector("head(1000)", ints, 1000, ints::stream, MoreCollectors.head(1000));
    checkShortCircuitCollector(
        "head(MAX)", ints, 1000, ints::stream, MoreCollectors.head(Integer.MAX_VALUE));

    checkShortCircuitCollector(
        "head(10000)",
        IntStreamEx.rangeClosed(1, 10000).boxed().toList(),
        10000,
        () -> Stream.iterate(1, x -> x + 1),
        MoreCollectors.head(10000),
        true);
  }
  @Test
  public void testFlatMapping() {
    {
      Map<Integer, List<Integer>> expected =
          IntStreamEx.rangeClosed(1, 100)
              .boxed()
              .toMap(x -> IntStreamEx.rangeClosed(1, x).boxed().toList());
      Collector<Integer, ?, Map<Integer, List<Integer>>> groupingBy =
          Collectors.groupingBy(
              Function.identity(),
              MoreCollectors.flatMapping(
                  x -> IntStream.rangeClosed(1, x).boxed(), Collectors.toList()));
      checkCollector(
          "flatMappingSimple", expected, () -> IntStreamEx.rangeClosed(1, 100).boxed(), groupingBy);
    }

    Function<Entry<String, List<String>>, Stream<String>> valuesStream =
        e -> e.getValue() == null ? null : e.getValue().stream();
    List<Entry<String, List<String>>> list =
        EntryStream.of(
                "a", Arrays.asList("bb", "cc", "dd"), "b", Arrays.asList("ee", "ff"), "c", null)
            .append("c", Arrays.asList("gg"), "b", null, "a", Arrays.asList("hh"))
            .toList();
    {
      Map<String, List<String>> expected =
          EntryStream.of(list.stream())
              .flatMapValues(l -> l == null ? null : l.stream())
              .grouping();
      checkCollector(
          "flatMappingCombine",
          expected,
          list::stream,
          Collectors.groupingBy(
              Entry::getKey, MoreCollectors.flatMapping(valuesStream, Collectors.toList())));
      AtomicInteger openClose = new AtomicInteger();
      Collector<Entry<String, List<String>>, ?, Map<String, List<String>>> groupingBy =
          Collectors.groupingBy(
              Entry::getKey,
              MoreCollectors.flatMapping(
                  valuesStream.andThen(
                      s -> {
                        if (s == null) return null;
                        openClose.incrementAndGet();
                        return s.onClose(openClose::decrementAndGet);
                      }),
                  Collectors.toList()));
      checkCollector(
          "flatMappingCombineClosed",
          expected,
          list::stream,
          MoreCollectors.collectingAndThen(
              groupingBy,
              res -> {
                assertEquals(0, openClose.get());
                return res;
              }));
      boolean catched = false;
      try {
        Collector<Entry<String, List<String>>, ?, Map<String, List<String>>> groupingByException =
            Collectors.groupingBy(
                Entry::getKey,
                MoreCollectors.flatMapping(
                    valuesStream.andThen(
                        s -> {
                          if (s == null) return null;
                          openClose.incrementAndGet();
                          return s.onClose(openClose::decrementAndGet)
                              .peek(
                                  e -> {
                                    if (e.equals("gg")) throw new IllegalArgumentException(e);
                                  });
                        }),
                    Collectors.toList()));
        list.stream()
            .collect(
                MoreCollectors.collectingAndThen(
                    groupingByException,
                    res -> {
                      assertEquals(0, openClose.get());
                      return res;
                    }));
      } catch (IllegalArgumentException e1) {
        assertEquals("gg", e1.getMessage());
        catched = true;
      }
      assertTrue(catched);
    }
    {
      Map<String, List<String>> expected =
          EntryStream.of(
                  "a", Arrays.asList("bb"), "b", Arrays.asList("ee"), "c", Arrays.asList("gg"))
              .toMap();
      Collector<Entry<String, List<String>>, ?, List<String>> headOne =
          MoreCollectors.flatMapping(valuesStream, MoreCollectors.head(1));
      checkCollector(
          "flatMappingSubShort",
          expected,
          list::stream,
          Collectors.groupingBy(Entry::getKey, headOne));
      checkShortCircuitCollector(
          "flatMappingShort",
          expected,
          4,
          list::stream,
          MoreCollectors.groupingBy(Entry::getKey, StreamEx.of("a", "b", "c").toSet(), headOne));
      AtomicInteger cnt = new AtomicInteger();
      Collector<Entry<String, List<String>>, ?, List<String>> headPeek =
          MoreCollectors.flatMapping(
              valuesStream.andThen(s -> s == null ? null : s.peek(x -> cnt.incrementAndGet())),
              MoreCollectors.head(1));
      assertEquals(
          expected, StreamEx.of(list).collect(Collectors.groupingBy(Entry::getKey, headPeek)));
      assertEquals(3, cnt.get());
      cnt.set(0);
      assertEquals(
          expected,
          StreamEx.of(list)
              .collect(
                  MoreCollectors.groupingBy(
                      Entry::getKey, StreamEx.of("a", "b", "c").toSet(), headPeek)));
      assertEquals(3, cnt.get());
    }
    {
      Map<String, List<String>> expected =
          EntryStream.of(
                  "a",
                  Arrays.asList("bb", "cc"),
                  "b",
                  Arrays.asList("ee", "ff"),
                  "c",
                  Arrays.asList("gg"))
              .toMap();
      Collector<Entry<String, List<String>>, ?, List<String>> headTwo =
          MoreCollectors.flatMapping(valuesStream, MoreCollectors.head(2));
      checkCollector(
          "flatMappingSubShort",
          expected,
          list::stream,
          Collectors.groupingBy(Entry::getKey, headTwo));
      AtomicInteger openClose = new AtomicInteger();
      boolean catched = false;
      try {
        Collector<Entry<String, List<String>>, ?, Map<String, List<String>>> groupingByException =
            Collectors.groupingBy(
                Entry::getKey,
                MoreCollectors.flatMapping(
                    valuesStream.andThen(
                        s -> {
                          if (s == null) return null;
                          openClose.incrementAndGet();
                          return s.onClose(openClose::decrementAndGet)
                              .peek(
                                  e -> {
                                    if (e.equals("gg")) throw new IllegalArgumentException(e);
                                  });
                        }),
                    MoreCollectors.head(2)));
        list.stream()
            .collect(
                MoreCollectors.collectingAndThen(
                    groupingByException,
                    res -> {
                      assertEquals(0, openClose.get());
                      return res;
                    }));
      } catch (IllegalArgumentException e1) {
        assertEquals("gg", e1.getMessage());
        catched = true;
      }
      assertTrue(catched);
    }
  }