/*
 * 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.*;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.*;
import java.util.stream.Collector.Characteristics;

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 MoreCollectorsTest {
    static class MyNumber implements Comparable<MyNumber> {
        final int value;
        
        MyNumber(int value) {
            this.value = value;
        }

        @Override
        public int hashCode() {
            return value;
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof MyNumber && ((MyNumber) obj).value == value;
        }

        @Override
        public int compareTo(MyNumber o) {
            return Integer.compare(value, o.value);
        }
    }

    @Test(expected = UnsupportedOperationException.class)
    public void testInstantiate() throws Throwable {
        Constructor<MoreCollectors> constructor = MoreCollectors.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        try {
            constructor.newInstance();
        } catch (InvocationTargetException e) {
            throw e.getCause();
        }
    }

    @Test
    public void testToArray() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.toArray(null));
        List<String> input = asList("a", "bb", "c", "", "cc", "eee", "bb", "ddd");
        streamEx(input::stream, supplier -> {
            Map<Integer, String[]> result = supplier.get().groupingBy(String::length, HashMap::new,
                MoreCollectors.toArray(String[]::new));
            assertArrayEquals(new String[] { "" }, result.get(0));
            assertArrayEquals(new String[] { "a", "c" }, result.get(1));
            assertArrayEquals(new String[] { "bb", "cc", "bb" }, result.get(2));
            assertArrayEquals(new String[] { "eee", "ddd" }, result.get(3));
        });
    }

    @Test
    public void testEmpty() {
        List<Integer> list = Stream.of(1, 2, 3).collect(MoreCollectors.head(0));
        assertTrue(list.isEmpty());
    }

    @Test
    public void testDistinctCount() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.distinctCount(null));
        List<String> input = asList("a", "bb", "c", "cc", "eee", "bb", "bc", "ddd");
        streamEx(input::stream, supplier -> {
            Map<String, Integer> result = supplier.get().groupingBy(s -> s.substring(0, 1), HashMap::new,
                MoreCollectors.distinctCount(String::length));
            assertEquals(1, (int) result.get("a"));
            assertEquals(1, (int) result.get("b"));
            assertEquals(2, (int) result.get("c"));
            assertEquals(1, (int) result.get("d"));
            assertEquals(1, (int) result.get("e"));
        });
    }

    @Test
    public void testDistinctBy() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.distinctBy(null));
        List<String> input = asList("a", "bb", "c", "cc", "eee", "bb", "bc", "ddd", "ca", "ce", "cf", "ded", "dump");
        streamEx(input::stream, supplier -> {
            Map<String, List<String>> result = supplier.get().groupingBy(s -> s.substring(0, 1), HashMap::new,
                MoreCollectors.distinctBy(String::length));
            assertEquals(asList("a"), result.get("a"));
            assertEquals(asList("bb"), result.get("b"));
            assertEquals(asList("c", "cc"), result.get("c"));
            assertEquals(asList("ddd", "dump"), result.get("d"));
            assertEquals(asList("eee"), result.get("e"));
        });
    }

    @Test
    public void testMaxAll() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.maxAll((Comparator<String>) null));
        assertThrows(NullPointerException.class, () -> MoreCollectors.maxAll((Collector<String, ?, ?>) null));
        assertThrows(NullPointerException.class, () -> MoreCollectors.maxAll(null, null));
        assertThrows(NullPointerException.class, () -> MoreCollectors.minAll((Comparator<String>) null));
        assertThrows(NullPointerException.class, () -> MoreCollectors.minAll((Collector<String, ?, ?>) null));
        assertThrows(NullPointerException.class, () -> MoreCollectors.minAll(null, null));
        List<String> input = asList("a", "bb", "c", "", "cc", "eee", "bb", "ddd");
        checkCollector("maxAll", asList("eee", "ddd"), input::stream, MoreCollectors.maxAll(Comparator
                .comparingInt(String::length)));
        Collector<String, ?, String> maxAllJoin = MoreCollectors.maxAll(Comparator.comparingInt(String::length),
            Collectors.joining(","));
        checkCollector("maxAllJoin", "eee,ddd", input::stream, maxAllJoin);
        checkCollector("minAll", 1L, input::stream, MoreCollectors.minAll(Comparator.comparingInt(String::length),
            Collectors.counting()));
        checkCollector("minAllEmpty", asList(""), input::stream, MoreCollectors.minAll(Comparator
                .comparingInt(String::length)));
        checkCollectorEmpty("maxAll", Collections.emptyList(), MoreCollectors.maxAll(Comparator
                .comparingInt(String::length)));
        checkCollectorEmpty("maxAllJoin", "", maxAllJoin);

        withRandom(r -> {
            List<Integer> ints = IntStreamEx.of(r, 10000, 1, 1000).boxed().toList();
            List<Integer> expectedMax = getMaxAll(ints, Comparator.naturalOrder());
            List<Integer> expectedMin = getMaxAll(ints, Comparator.reverseOrder());
            Collector<Integer, ?, SimpleEntry<Integer, Long>> downstream = MoreCollectors.pairing(MoreCollectors.first(),
                Collectors.counting(), (opt, cnt) -> new AbstractMap.SimpleEntry<>(opt.get(), cnt));

            checkCollector("maxAll", expectedMax, ints::stream, MoreCollectors.maxAll(Integer::compare));
            checkCollector("minAll", expectedMin, ints::stream, MoreCollectors.minAll());
            checkCollector("entry", new SimpleEntry<>(expectedMax.get(0), (long) expectedMax.size()), ints::stream,
                MoreCollectors.maxAll(downstream));
            checkCollector("entry", new SimpleEntry<>(expectedMin.get(0), (long) expectedMin.size()), ints::stream,
                MoreCollectors.minAll(downstream));
        });
        
        MyNumber a = new MyNumber(1), b = new MyNumber(1), c = new MyNumber(1000), d = new MyNumber(1000);
        List<MyNumber> nums = IntStreamEx.range(10, 100).mapToObj(MyNumber::new).append(a, c).prepend(b, d).toList();
        streamEx(nums::stream, supplier -> {
            List<MyNumber> list = supplier.get().collect(MoreCollectors.maxAll());
            assertEquals(2, list.size());
            assertSame(d, list.get(0));
            assertSame(c, list.get(1));

            list = supplier.get().collect(MoreCollectors.minAll());
            assertEquals(2, list.size());
            assertSame(b, list.get(0));
            assertSame(a, list.get(1));
        });
    }

    private static List<Integer> getMaxAll(List<Integer> ints, Comparator<Integer> c) {
        List<Integer> expectedMax = null;
        for (Integer i : ints) {
            if (expectedMax == null || c.compare(i, expectedMax.get(0)) > 0) {
                expectedMax = new ArrayList<>();
                expectedMax.add(i);
            } else if (i.equals(expectedMax.get(0))) {
                expectedMax.add(i);
            }
        }
        return expectedMax;
    }

    @Test
    public void testFirstLast() {
        Supplier<Stream<Integer>> s = () -> IntStreamEx.range(1000).boxed();
        checkShortCircuitCollector("first", Optional.of(0), 1, s, MoreCollectors.first());
        checkShortCircuitCollector("firstLong", Optional.of(0), 1, () -> Stream.of(1).flatMap(
            x -> IntStream.range(0, 1000000000).boxed()), MoreCollectors.first(), true);
        checkShortCircuitCollector("first", Optional.of(1), 1, () -> Stream.iterate(1, x -> x + 1), MoreCollectors
                .first(), true);
        assertEquals(1, (int) StreamEx.iterate(1, x -> x + 1).parallel().collect(MoreCollectors.first()).get());

        checkCollector("last", Optional.of(999), s, MoreCollectors.last());
        checkCollectorEmpty("first", Optional.empty(), MoreCollectors.first());
        checkCollectorEmpty("last", Optional.empty(), MoreCollectors.last());
        assertThrows(NullPointerException.class, () -> StreamEx.of(null, 1).collect(MoreCollectors.first()));
        assertThrows(NullPointerException.class, () -> StreamEx.of(1, null).collect(MoreCollectors.last()));
        assertEquals(Optional.of(1), StreamEx.of(null, 1).collect(MoreCollectors.last()));
        assertEquals(Optional.of(1), StreamEx.of(1, null).collect(MoreCollectors.first()));
    }

    @Test
    public void testHeadParallel() {
        List<Integer> expected = IntStreamEx.range(0, 2000, 2).boxed().toList();
        List<Integer> expectedShort = asList(0, 1);
        for (int i = 0; i < 1000; i++) {
            assertEquals("#" + i, expectedShort, IntStreamEx.range(1000).boxed().parallel().collect(
                MoreCollectors.head(2)));
            assertEquals("#" + i, expected, IntStreamEx.range(10000).boxed().parallel().filter(x -> x % 2 == 0)
                    .collect(MoreCollectors.head(1000)));
        }
        assertEquals(expectedShort, StreamEx.iterate(0, x -> x + 1).parallel().collect(MoreCollectors.head(2)));
    }

    @Test
    public void testHeadTail() {
        List<Integer> ints = IntStreamEx.range(1000).boxed().toList();
        checkShortCircuitCollector("tail(0)", asList(), 0, ints::stream, MoreCollectors.tail(0));
        checkShortCircuitCollector("tail(0)", asList(), 0, ints::stream, MoreCollectors.tail(-1));
        checkCollector("tail(1)", asList(999), ints::stream, MoreCollectors.tail(1));
        checkCollector("tail(2)", 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)", asList(), 0, ints::stream, MoreCollectors.head(0));
        checkShortCircuitCollector("head(1)", asList(0), 1, ints::stream, MoreCollectors.head(1));
        checkShortCircuitCollector("head(2)", 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);

        for (int size : new int[] { 1, 10, 20, 40, 60, 80, 90, 98, 99, 100 }) {
            checkShortCircuitCollector("head-unordered-" + size, Collections.nCopies(size, "test"), size,
                () -> StreamEx.constant("test", 100), MoreCollectors.head(size));
        }
    }

    @Test
    public void testGreatest() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.greatest(null, 0));
        assertThrows(NullPointerException.class, () -> MoreCollectors.least(null, 0));
        withRandom(r -> {
            List<Integer> ints = IntStreamEx.of(r, 1000, 1, 1000).boxed().toList();
            List<Integer> sorted = StreamEx.of(ints).sorted().toList();
            List<Integer> revSorted = StreamEx.of(ints).reverseSorted().toList();
            Comparator<Integer> byString = Comparator.comparing(String::valueOf);
            checkShortCircuitCollector("least(0)", Collections.emptyList(), 0, ints::stream, MoreCollectors.least(0));
            checkCollector("least(5)", sorted.subList(0, 5), ints::stream, MoreCollectors.least(5));
            checkCollector("least(20)", sorted.subList(0, 20), ints::stream, MoreCollectors.least(20));
            checkCollector("least(MAX)", sorted, ints::stream, MoreCollectors.least(Integer.MAX_VALUE));
            checkCollector("least(byString, 20)", StreamEx.of(ints).sorted(byString).limit(20).toList(), ints::stream,
                MoreCollectors.least(byString, 20));

            checkShortCircuitCollector("greatest(0)", Collections.emptyList(), 0, ints::stream, MoreCollectors.greatest(0));
            checkCollector("greatest(5)", revSorted.subList(0, 5), ints::stream, MoreCollectors.greatest(5));
            checkCollector("greatest(20)", revSorted.subList(0, 20), ints::stream, MoreCollectors.greatest(20));
            checkCollector("greatest(MAX)", revSorted, ints::stream, MoreCollectors.greatest(Integer.MAX_VALUE));
            checkCollector("greatest(byString, 20)", StreamEx.of(ints).reverseSorted(byString).limit(20).toList(),
                ints::stream, MoreCollectors.greatest(byString, 20));
            checkCollector("greatest(byString, 30)", StreamEx.of(ints).reverseSorted(byString).limit(30).toList(),
                ints::stream, MoreCollectors.greatest(byString, 30));
        });

        Supplier<Stream<Integer>> s = () -> IntStreamEx.range(100).boxed();
        checkCollector("1", IntStreamEx.range(1).boxed().toList(), s, MoreCollectors.least(1));
        checkCollector("2", IntStreamEx.range(2).boxed().toList(), s, MoreCollectors.least(2));
        checkCollector("10", IntStreamEx.range(10).boxed().toList(), s, MoreCollectors.least(10));
        checkCollector("100", IntStreamEx.range(100).boxed().toList(), s, MoreCollectors.least(100));
        checkCollector("200", IntStreamEx.range(100).boxed().toList(), s, MoreCollectors.least(200));
        checkCollector("least(1)", Collections.singletonList(801), 
            () -> IntStreamEx.range(0, 1000).filter(x -> x > 800).boxed(), MoreCollectors.least(1));
        checkCollector("least(1)", Collections.emptyList(), 
            () -> IntStreamEx.range(0, 1000).filter(x -> x > 1200).boxed(), MoreCollectors.least(1));
    }

    @Test
    public void testCountingInt() {
        checkCollector("counting", 1000, () -> IntStreamEx.range(1000).boxed(), MoreCollectors.countingInt());
        checkCollectorEmpty("counting", 0, MoreCollectors.countingInt());
    }

    @Test
    public void testMinIndex() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.minIndex(null));
        withRandom(r -> {
            List<Integer> ints = IntStreamEx.of(r, 1000, 5, 47).boxed().toList();
            long expectedMin = IntStreamEx.ofIndices(ints).minBy(ints::get).getAsInt();
            long expectedMax = IntStreamEx.ofIndices(ints).maxBy(ints::get).getAsInt();
            long expectedMinString = IntStreamEx.ofIndices(ints).minBy(i -> String.valueOf(ints.get(i))).getAsInt();
            long expectedMaxString = IntStreamEx.ofIndices(ints).maxBy(i -> String.valueOf(ints.get(i))).getAsInt();
            Comparator<Integer> cmp = Comparator.comparing(String::valueOf);
            checkCollector("minIndex", OptionalLong.of(expectedMin), ints::stream, MoreCollectors.minIndex());
            checkCollector("maxIndex", OptionalLong.of(expectedMax), ints::stream, MoreCollectors.maxIndex());
            checkCollector("minIndex", OptionalLong.of(expectedMinString), ints::stream, MoreCollectors.minIndex(cmp));
            checkCollector("maxIndex", OptionalLong.of(expectedMaxString), ints::stream, MoreCollectors.maxIndex(cmp));
            Supplier<Stream<String>> supplier = () -> ints.stream().map(Object::toString);
            checkCollector("minIndex", OptionalLong.of(expectedMinString), supplier, MoreCollectors.minIndex());
            checkCollector("maxIndex", OptionalLong.of(expectedMaxString), supplier, MoreCollectors.maxIndex());
            checkCollectorEmpty("minIndex", OptionalLong.empty(), MoreCollectors.<String>minIndex());
            checkCollectorEmpty("maxIndex", OptionalLong.empty(), MoreCollectors.<String>maxIndex());
        });
        checkCollector("minIndex", OptionalLong.of(999), () -> IntStreamEx.range(1000, 0, -1).boxed(), 
            MoreCollectors.minIndex());
    }

    @Test
    public void testGroupingByEnum() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingByEnum((Class<TimeUnit>) null,
            Function.identity(), Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingByEnum(TimeUnit.class,
            null, Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingByEnum(TimeUnit.class,
            Function.identity(), null));
        EnumMap<TimeUnit, Long> expected = new EnumMap<>(TimeUnit.class);
        EnumSet.allOf(TimeUnit.class).forEach(tu -> expected.put(tu, 0L));
        expected.put(TimeUnit.SECONDS, 1L);
        expected.put(TimeUnit.DAYS, 2L);
        expected.put(TimeUnit.NANOSECONDS, 1L);
        checkCollector("groupingByEnum", expected, () -> Stream.of(TimeUnit.SECONDS, TimeUnit.DAYS, TimeUnit.DAYS,
            TimeUnit.NANOSECONDS), MoreCollectors.groupingByEnum(TimeUnit.class, Function.identity(), Collectors
                .counting()));
    }

    @Test(expected = IllegalStateException.class)
    public void testGroupingByWithDomainException() {
        List<Integer> list = asList(1, 2, 20, 3, 31, 4);
        Collector<Integer, ?, Map<Integer, List<Integer>>> c = MoreCollectors.groupingBy(i -> i % 10, StreamEx.of(0, 1,
            2, 3).toSet(), Collectors.toList());
        Map<Integer, List<Integer>> map = list.stream().collect(c);
        System.out.println(map);
    }

    @Test
    public void testGroupingByWithDomain() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingBy(null,
            Collections.emptySet(), Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingBy(Function.identity(),
            null, Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingBy(Function.identity(),
            Collections.emptySet(), null));
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingBy(null,
            Collections.emptySet(), HashMap::new, Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingBy(Function.identity(),
            null, HashMap::new, Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingBy(Function.identity(),
            Collections.emptySet(), null, Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.groupingBy(Function.identity(),
            Collections.emptySet(), HashMap::new, null));
        List<String> data = asList("a", "foo", "test", "ququq", "bar", "blahblah");
        Collector<String, ?, String> collector = MoreCollectors.collectingAndThen(MoreCollectors.groupingBy(
            String::length, IntStreamEx.range(10).boxed().toSet(), TreeMap::new, MoreCollectors.first()),
            Object::toString);
        checkShortCircuitCollector("groupingWithDomain",
            "{0=Optional.empty, 1=Optional[a], 2=Optional.empty, 3=Optional[foo], 4=Optional[test], 5=Optional[ququq], "
                + "6=Optional.empty, 7=Optional.empty, 8=Optional[blahblah], 9=Optional.empty}", data.size(),
            data::stream, collector);

        Map<String, String> name2sex = new LinkedHashMap<>();
        name2sex.put("Mary", "Girl");
        name2sex.put("John", "Boy");
        name2sex.put("James", "Boy");
        name2sex.put("Lucie", "Girl");
        name2sex.put("Fred", "Boy");
        name2sex.put("Thomas", "Boy");
        name2sex.put("Jane", "Girl");
        name2sex.put("Ruth", "Girl");
        name2sex.put("Melanie", "Girl");
        Collector<Entry<String, String>, ?, Map<String, List<String>>> groupingBy = MoreCollectors.groupingBy(
            Entry::getValue, StreamEx.of("Girl", "Boy").toSet(), MoreCollectors.mapping(Entry::getKey, MoreCollectors
                    .head(2)));
        AtomicInteger counter = new AtomicInteger();
        Map<String, List<String>> map = EntryStream.of(name2sex).peek(c -> counter.incrementAndGet()).collect(
            groupingBy);
        assertEquals(asList("Mary", "Lucie"), map.get("Girl"));
        assertEquals(asList("John", "James"), map.get("Boy"));
        assertEquals(4, counter.get());

        Collector<Entry<String, String>, ?, Map<String, String>> groupingByJoin = MoreCollectors.groupingBy(
            Entry::getValue, StreamEx.of("Girl", "Boy").toSet(), MoreCollectors.mapping(Entry::getKey, Joining.with(
                ", ").maxChars(16).cutAfterDelimiter()));
        counter.set(0);
        Map<String, String> mapJoin = EntryStream.of(name2sex).peek(c -> counter.incrementAndGet()).collect(
            groupingByJoin);
        assertEquals("Mary, Lucie, ...", mapJoin.get("Girl"));
        assertEquals("John, James, ...", mapJoin.get("Boy"));
        assertEquals(7, counter.get());
    }

    @Test
    public void testToBooleanArray() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.toBooleanArray(null));
        withRandom(r -> {
            List<Integer> input = IntStreamEx.of(r, 1000, 1, 100).boxed().toList();
            boolean[] expected = new boolean[input.size()];
            for (int i = 0; i < expected.length; i++)
                expected[i] = input.get(i) > 50;
            streamEx(input::stream, supplier -> assertArrayEquals(expected, supplier.get().collect(
                MoreCollectors.toBooleanArray(x -> x > 50))));
        });
    }

    @Test
    public void testPartitioningBy() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.partitioningBy(null, Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.partitioningBy(x -> x.hashCode() > 0, null));

        Collector<Integer, ?, Map<Boolean, Optional<Integer>>> by20 = MoreCollectors.partitioningBy(x -> x % 20 == 0, MoreCollectors.first());
        Collector<Integer, ?, Map<Boolean, Optional<Integer>>> by200 = MoreCollectors.partitioningBy(x -> x % 200 == 0, MoreCollectors.first());
        Supplier<Stream<Integer>> supplier = () -> IntStreamEx.range(1, 100).boxed();
        {
            Map<Boolean, Optional<Integer>> expected = new HashMap<>();
            expected.put(Boolean.TRUE, Optional.of(20));
            expected.put(Boolean.FALSE, Optional.of(1));
            checkShortCircuitCollector("by20", expected, 20, supplier, by20);
        }
        {
            Map<Boolean, Optional<Integer>> expected = new HashMap<>();
            expected.put(Boolean.TRUE, Optional.empty());
            expected.put(Boolean.FALSE, Optional.of(1));
            checkShortCircuitCollector("by200", expected, 99, supplier, by200);
        }
    }

    @Test
    public void testMapping() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.mapping(null, Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.mapping(Function.identity(), null));

        {
            List<String> input = asList("Abc", "Bac", "Aac", "Abv", "Bbc", "Bgd", "Atc", "Bpv");
            Map<Character, List<String>> expected = EntryStream.of('A', asList("Abc", "Aac"), 'B', asList("Bac", "Bbc"))
                                                               .toMap();
            AtomicInteger cnt = new AtomicInteger();
            Collector<String, ?, Map<Character, List<String>>> groupMap =
                    Collectors.groupingBy(s -> s.charAt(0),
                                          MoreCollectors.mapping(x -> {
                                              cnt.incrementAndGet();
                                              return x;
                                          }, MoreCollectors.head(2)));
            checkCollector("groupMap", expected, input::stream, groupMap);
            cnt.set(0);
            assertEquals(expected, input.stream().collect(groupMap));
            assertEquals(4, cnt.get());
        }

        checkCollector("mapping-toList", asList("a", "b", "c"), asList("a1", "b2", "c3")::stream,
                       MoreCollectors.mapping(str -> str.substring(0, 1)));

        {
            List<String> input = asList("Capital", "lower", "Foo", "bar");
            Map<Boolean, Optional<Integer>> expected = new HashMap<>();
            expected.put(Boolean.TRUE, Optional.of(7));
            expected.put(Boolean.FALSE, Optional.of(5));

            Collector<String, ?, Map<Boolean, Optional<Integer>>> collectorFirst = MoreCollectors.partitioningBy(
                    str -> Character.isUpperCase(str.charAt(0)),
                    MoreCollectors.mapping(String::length, MoreCollectors.first()));
            checkShortCircuitCollector("mapping", expected, 2, input::stream, collectorFirst);
        }
        {
            List<String> input = asList("Capital", "lower", "Foo", "bar");
            Map<Boolean, Optional<Integer>> expected = new HashMap<>();
            expected.put(Boolean.TRUE, Optional.of(3));
            expected.put(Boolean.FALSE, Optional.of(3));
            Collector<String, ?, Map<Boolean, Optional<Integer>>> collectorLast = MoreCollectors.partitioningBy(
                    str -> Character.isUpperCase(str.charAt(0)),
                    MoreCollectors.mapping(String::length, MoreCollectors.last()));
            checkCollector("last", expected, input::stream, collectorLast);
        }
    }

    @Test
    public void testAndInt() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.andingInt(null));
        List<Integer> ints = asList(0b1100, 0b0110, 0b101110, 0b11110011);
        Collector<Integer, ?, OptionalInt> collector = MoreCollectors.andingInt(Integer::intValue);
        checkShortCircuitCollector("andInt", OptionalInt.of(0), 4, ints::stream, collector);
        checkCollectorEmpty("andIntEmpty", OptionalInt.empty(), collector);
        assertEquals(OptionalInt.of(0), IntStreamEx.iterate(16384, i -> i + 1).parallel().boxed().collect(collector));
        assertEquals(OptionalInt.of(16384), IntStreamEx.iterate(16384, i -> i + 1).parallel().limit(16383).boxed()
                .collect(collector));
        Collector<Integer, ?, Integer> unwrapped = MoreCollectors.collectingAndThen(MoreCollectors
                .andingInt(Integer::intValue), OptionalInt::getAsInt);
        assertTrue(unwrapped.characteristics().contains(Characteristics.UNORDERED));
        checkShortCircuitCollector("andIntUnwrapped", 0, 4, ints::stream, unwrapped);
        checkShortCircuitCollector("andIntUnwrapped", 0, 2, asList(0x1, 0x10, 0x100)::stream, unwrapped);
    }

    @Test
    public void testAndLong() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.andingLong(null));
        List<Long> longs = asList(0xFFFFFFFFFFFFFFFFL, 0xFFFFFFFF00000000L, 0xFFFFFFFF0000L);
        checkShortCircuitCollector("andLong", OptionalLong.of(0xFFFF00000000L), 3, longs::stream, MoreCollectors
                .andingLong(Long::longValue));
        longs = asList(1L, 2L, 3L, 4L);
        checkShortCircuitCollector("andLong", OptionalLong.of(0), 2, longs::stream, MoreCollectors
                .andingLong(Long::longValue));
        checkCollectorEmpty("andLongEmpty", OptionalLong.empty(), MoreCollectors.andingLong(Long::longValue));
    }

    @Test
    public void testAndLongFlatMap() {
        checkShortCircuitCollector("andLongFlat", OptionalLong.of(0), 2, () -> LongStreamEx.of(0).flatMap(
            x -> LongStream.range(1, 100000000)).boxed(), MoreCollectors.andingLong(Long::longValue), true);
    }

    @Test
    public void testFiltering() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.filtering(null, Collectors.toList()));
        assertThrows(NullPointerException.class, () -> MoreCollectors.filtering(x -> true, null));
        Collector<Integer, ?, Optional<Integer>> firstEven = MoreCollectors.filtering(x -> x % 2 == 0, MoreCollectors
                .first());
        Collector<Integer, ?, Optional<Integer>> firstOdd = MoreCollectors.filtering(x -> x % 2 != 0, MoreCollectors
                .first());
        Collector<Integer, ?, Integer> sumOddEven = MoreCollectors.pairing(firstEven, firstOdd, (e, o) -> e.get()
            + o.get());
        List<Integer> ints = asList(1, 3, 5, 7, 9, 10, 8, 6, 4, 2, 3, 7, 11);
        checkShortCircuitCollector("sumOddEven", 11, 6, ints::stream, sumOddEven);
        Collector<Integer, ?, Long> countEven = MoreCollectors.filtering(x -> x % 2 == 0, Collectors.counting());
        checkCollector("filtering", 5L, ints::stream, countEven);
        
        checkCollector("filtering-toList", asList(1, 5, 3, 7), asList(1, 2, 4, 5, 4, 3, 0, 7, 8, 10)::stream,
            MoreCollectors.filtering(x -> x % 2 == 1));
    }

    @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());
        checkShortCircuitCollector("FilterSeveral", Optional.empty(), 40, ints::stream, MoreCollectors.onlyOne(x -> x % 20 == 0));
        checkShortCircuitCollector("FilterOne", Optional.of(60), 100, ints::stream, MoreCollectors.onlyOne(x -> x % 60 == 0));
        checkShortCircuitCollector("FilterNone", Optional.empty(), 100, ints::stream, MoreCollectors.onlyOne(x -> x % 110 == 0));
        assertThrows(NullPointerException.class, () -> StreamEx.of((String) null).collect(MoreCollectors.onlyOne()));
    }

    @Test
    public void testToEnumSet() {
        assertThrows(NullPointerException.class, () -> MoreCollectors.toEnumSet(null));
        TimeUnit[] vals = TimeUnit.values();
        List<TimeUnit> enumValues = IntStreamEx.range(100).map(x -> x % vals.length).elements(vals).toList();
        checkShortCircuitCollector("toEnumSet", EnumSet.allOf(TimeUnit.class), vals.length, enumValues::stream,
            MoreCollectors.toEnumSet(TimeUnit.class));
        enumValues = IntStreamEx.range(100).map(x -> x % (vals.length - 1)).elements(vals).toList();
        EnumSet<TimeUnit> expected = EnumSet.allOf(TimeUnit.class);
        expected.remove(vals[vals.length - 1]);
        checkShortCircuitCollector("toEnumSet", expected, 100, enumValues::stream, MoreCollectors
                .toEnumSet(TimeUnit.class));
