/*
 * 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 one.util.streamex.TestHelpers.*;
import org.jspecify.annotations.NonNull;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runners.MethodSorters;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collector;
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 StreamExTest {
    @Rule
    public final TemporaryFolder tmp = new TemporaryFolder();

    @Test
    public void testCreate() {
        assertEquals(asList(), StreamEx.empty().toList());
        // a repeating test is intended: check that empty() is not a singleton and can be called twice
        assertEquals(asList(), StreamEx.empty().toList());
        assertEquals(asList("a"), StreamEx.of("a").toList());
        assertEquals(asList("a"), StreamEx.of(Optional.of("a")).toList());
        assertEquals(asList(), StreamEx.of(Optional.empty()).toList());
        assertEquals(asList(), StreamEx.ofNullable(null).toList());
        assertEquals(asList("a"), StreamEx.ofNullable("a").toList());
        assertEquals(asList((String) null), StreamEx.of((String) null).toList());
        assertEquals(asList("a", "b"), StreamEx.of("a", "b").toList());
        assertEquals(asList("a", "b"), StreamEx.of(asList("a", "b")).toList());
        assertEquals(asList("a", "b"), StreamEx.of(Stream.of("a", "b")).toList());
        assertEquals(asList("a", "b"), StreamEx.split("a,b", ",").toList());
        assertEquals(asList("a", "c", "d"), StreamEx.split("abcBd", Pattern.compile("b", Pattern.CASE_INSENSITIVE))
                .toList());
        assertEquals(asList("a", "b"), StreamEx.ofLines(new StringReader("a\nb")).toList());
        assertEquals(asList("a", "b"), StreamEx.ofLines(new BufferedReader(new StringReader("a\nb"))).toList());
        assertEquals(asList("a", "b"), StreamEx.ofLines(getReader()).toList());
        assertEquals(asList("a", "a", "a", "a"), StreamEx.generate(() -> "a").limit(4).toList());
        assertEquals(asList("a", "a", "a", "a"), StreamEx.constant("a", 4).toList());
        assertEquals(asList("c", "d", "e"), StreamEx.of("abcdef".split(""), 2, 5).toList());

        assertEquals(asList("a1", "b2", "c3"), StreamEx.zip(asList("a", "b", "c"), asList(1, 2, 3), (s, i) -> s + i)
                .toList());
        assertEquals(asList("a1", "b2", "c3"), StreamEx.zip(new String[]{"a", "b", "c"}, new Integer[]{1, 2, 3}, (
                s, i) -> s + i).toList());

        assertEquals(asList("a", "b"), StreamEx.of(asList("a", "b").spliterator()).toList());
        assertEquals(asList("a", "b"), StreamEx.of(asList("a", "b").iterator()).toList());
        assertEquals(asList(), StreamEx.of(Collections.emptyIterator()).toList());
        assertEquals(asList(), StreamEx.of(Collections.emptyIterator()).parallel().toList());
        assertEquals(asList("a", "b"), StreamEx.of(new Vector<>(asList("a", "b")).elements()).toList());

        assertEquals(asList("a", "b", "c", "d"), StreamEx.ofReversed(asList("d", "c", "b", "a")).toList());
        assertEquals(asList("a", "b", "c", "d"), StreamEx.ofReversed(new String[]{"d", "c", "b", "a"}).toList());
    }

    @Test
    public void testIterate() {
        assertEquals(asList("a", "aa", "aaa", "aaaa"), StreamEx.iterate("a", x -> x + "a").limit(4).toList());
        assertEquals(asList("a", "aa", "aaa", "aaaa"), StreamEx.iterate("a", x -> x.length() <= 4, x -> x + "a")
                .toList());
        assertFalse(StreamEx.iterate("a", x -> x.length() <= 10, x -> x + "a").has("b"));
        assertEquals(0, StreamEx.iterate("", x -> !x.isEmpty(), x -> x.substring(1)).count());
        checkSpliterator("iterate", () -> StreamEx.iterate(1, x -> x < 100, x -> x * 2).spliterator());
    }

    @Test
    public void testCreateFromFile() throws IOException {
        File f = tmp.newFile();
        List<String> input = asList("Some", "Test", "Lines");
        Files.write(f.toPath(), input);
        assertEquals(input, StreamEx.ofLines(f.toPath()).toList());
        Files.write(f.toPath(), input, StandardCharsets.UTF_16);
        assertEquals(input, StreamEx.ofLines(f.toPath(), StandardCharsets.UTF_16).toList());
    }

    private static Reader getReader() {
        return new BufferedReader(new StringReader("a\nb"));
    }

    @Test
    public void testReader() {
        String input = IntStreamEx.range(5000).joining("\n");
        List<String> expectedList = IntStreamEx.range(1, 5000).mapToObj(String::valueOf).toList();
        Set<String> expectedSet = IntStreamEx.range(1, 5000).mapToObj(String::valueOf).toSet();
        List<String> actualList = StreamEx.ofLines(new StringReader(input)).skip(1).parallel().toList();
        assertEquals(expectedList, actualList);
        actualList = StreamEx.ofLines(new StringReader(input)).pairMap((a, b) -> b).parallel().toList();
        assertEquals(expectedList, actualList);

        Set<String> actualSet = StreamEx.ofLines(new StringReader(input)).pairMap((a, b) -> b).parallel().toSet();
        assertEquals(expectedSet, actualSet);
        actualSet = StreamEx.ofLines(new StringReader(input)).skip(1).parallel().toCollection(HashSet::new);
        assertEquals(expectedSet, actualSet);
    }

    @Test
    public void testZip() {
        assertThrows(IllegalArgumentException.class, () -> StreamEx.zip(asList("A"), asList("b", "c"), String::concat));
    }

    @Test
    public void testBasics() {
        assertFalse(StreamEx.of("a").isParallel());
        assertTrue(StreamEx.of("a").parallel().isParallel());
        assertFalse(StreamEx.of("a").parallel().sequential().isParallel());
        AtomicInteger i = new AtomicInteger();
        try (Stream<String> s = StreamEx.of("a").onClose(i::incrementAndGet)) {
            assertEquals(1, s.count());
        }
        assertEquals(1, i.get());
        assertEquals(asList(1, 2), StreamEx.of("a", "bb").map(String::length).toList());
        assertFalse(StreamEx.empty().findAny().isPresent());
        assertEquals("a", StreamEx.of("a").findAny().get());
        assertFalse(StreamEx.empty().findFirst().isPresent());
        assertEquals("a", StreamEx.of("a", "b").findFirst().get());
        assertEquals(asList("b", "c"), StreamEx.of("a", "b", "c").skip(1).toList());

        AtomicBoolean b = new AtomicBoolean(false);
        try (Stream<String> stream = StreamEx.of("a").onClose(() -> b.set(true))) {
            assertFalse(b.get());
            assertEquals(1, stream.count());
            assertFalse(b.get());
        }
        assertTrue(b.get());

        assertTrue(StreamEx.of("a", "b").anyMatch("a"::equals));
        assertFalse(StreamEx.of("a", "b").anyMatch("c"::equals));
        assertFalse(StreamEx.of("a", "b").allMatch("a"::equals));
        assertFalse(StreamEx.of("a", "b").allMatch("c"::equals));
        assertFalse(StreamEx.of("a", "b").noneMatch("a"::equals));
        assertTrue(StreamEx.of("a", "b").noneMatch("c"::equals));
        assertTrue(StreamEx.of().noneMatch("a"::equals));
        assertTrue(StreamEx.of().allMatch("a"::equals));
        assertFalse(StreamEx.of().anyMatch("a"::equals));

        assertEquals("abbccc", StreamEx.of("a", "bb", "ccc").collect(StringBuilder::new, StringBuilder::append,
            StringBuilder::append).toString());
        assertArrayEquals(new String[] { "a", "b", "c" }, StreamEx.of("a", "b", "c").toArray(String[]::new));
        assertArrayEquals(new Object[] { "a", "b", "c" }, StreamEx.of("a", "b", "c").toArray());
        assertEquals(3, StreamEx.of("a", "b", "c").spliterator().getExactSizeIfKnown());

        assertTrue(StreamEx.of("a", "b", "c").spliterator().hasCharacteristics(Spliterator.ORDERED));
        assertFalse(StreamEx.of("a", "b", "c").unordered().spliterator().hasCharacteristics(Spliterator.ORDERED));
    }

    @Test
    public void testCovariance() {
        StreamEx<Number> stream = StreamEx.of(1, 2, 3);
        List<Number> list = stream.toList();
        assertEquals(asList(1, 2, 3), list);

        StreamEx<Object> objStream = StreamEx.of(list.spliterator());
        List<Object> objList = objStream.toList();
        assertEquals(asList(1, 2, 3), objList);
    }

    @Test
    public void testToMutableList() {
        List<Integer> list = StreamEx.of(1, 2, 3).toMutableList();
        // Test that the returned list is mutable
        List<Integer> list2 = StreamEx.of(4, 5, 6).parallel().toMutableList();
        list2.add(7);
        list.addAll(list2);
        assertEquals(asList(1, 2, 3, 4, 5, 6, 7), list);
    }

    @Test
    public void testToMutableSet() {
        Set<Integer> set = StreamEx.of(1, 2, 3).toMutableSet();
        set.add(4);
        assertEquals(new HashSet<>(Arrays.asList(1, 2, 3, 4)), set);
    }

    @Test
    public void testToArray() {
        Number[] numbers = StreamEx.of(1, 2, 3).toArray(Number.class);
        assertArrayEquals(new Number[] { 1, 2, 3 }, numbers);
        assertEquals(Number.class, numbers.getClass().getComponentType());
        Integer[] emptyArray = {};
        assertSame(emptyArray, StreamEx.of(1, 2, 3).filter(x -> x > 3).toArray(emptyArray));
        assertArrayEquals(new Integer[] { 1, 2, 3 }, StreamEx.of(1, 2, 3).remove(x -> x > 3).toArray(emptyArray));
        assertThrows(IllegalArgumentException.class, () -> StreamEx.of().toArray(new Integer[1]));
    }

    @Test
    public void testForEach() {
        List<Integer> list = new ArrayList<>();
        StreamEx.of(1, 2, 3).forEach(list::add);
        assertEquals(asList(1, 2, 3), list);
        StreamEx.of(1, 2, 3).forEachOrdered(list::add);
        assertEquals(asList(1, 2, 3, 1, 2, 3), list);
        StreamEx.of(1, 2, 3).parallel().forEachOrdered(list::add);
        assertEquals(asList(1, 2, 3, 1, 2, 3, 1, 2, 3), list);
    }

    @Test
    public void testFlatMap() {
        assertArrayEquals(new int[] { 0, 0, 1, 0, 0, 1, 0, 0 }, StreamEx.of("111", "222", "333").flatMapToInt(s -> s
                .chars().map(ch -> ch - '0')).pairMap((a, b) -> b - a).toArray());
        assertArrayEquals(new long[] { 0, 0, 1, 0, 0, 1, 0, 0 }, StreamEx.of("111", "222", "333").flatMapToLong(s -> s
                .chars().mapToLong(ch -> ch - '0')).pairMap((a, b) -> b - a).toArray());
        assertArrayEquals(new double[] { 0, 0, 1, 0, 0, 1, 0, 0 }, StreamEx.of("111", "222", "333").flatMapToDouble(
            s -> s.chars().mapToDouble(ch -> ch - '0')).pairMap((a, b) -> b - a).toArray(), 0.0);
    }

    @Test
    public void testAndThen() {
        HashSet<String> set = StreamEx.of("a", "bb", "ccc").toListAndThen(HashSet::new);
        assertEquals(3, set.size());
        assertTrue(set.contains("bb"));

        ArrayList<String> list = StreamEx.of("a", "bb", "ccc").toSetAndThen(ArrayList::new);
        assertEquals(3, list.size());
        assertTrue(list.contains("bb"));

        ArrayList<String> linkedHashSet = StreamEx.of("a", "bb", "ccc", "a", "d", "bb", "e")
                .toCollectionAndThen(LinkedHashSet::new, ArrayList::new);
        assertEquals(asList("a", "bb", "ccc", "d", "e"), linkedHashSet);
    }

    @Test
    public void testToMap() {
        Map<String, Integer> expected = new HashMap<>();
        expected.put("a", 1);
        expected.put("bb", 2);
        expected.put("ccc", 3);

        Map<Integer, String> expected2 = new HashMap<>();
        expected2.put(1, "a");
        expected2.put(2, "bb");
        expected2.put(3, "ccc");

        streamEx(() -> Stream.of("a", "bb", "ccc"), supplier -> {
            Map<String, Integer> map = supplier.get().toMap(String::length);
            assertEquals(supplier.get().isParallel(), map instanceof ConcurrentMap);
            assertEquals(expected, map);

            Map<Integer, String> map2 = supplier.get().toMap(String::length, Function.identity());
            assertEquals(supplier.get().isParallel(), map2 instanceof ConcurrentMap);
            assertEquals(expected2, map2);
        });

        Map<Integer, String> expected3 = new HashMap<>();
        expected3.put(1, "a");
        expected3.put(2, "bbdd");
        expected3.put(3, "ccc");
        streamEx(() -> Stream.of("a", "bb", "ccc", "dd"), supplier -> {
            Map<Integer, String> seqMap3 = supplier.get().toMap(String::length, Function.identity(), String::concat);
            assertEquals(expected3, seqMap3);

            checkIllegalStateException(() -> supplier.get().toMap(String::length, Function.identity()), "2", "dd",
                "bb");
        });
    }

    @Test
    public void testValuesToMap() {
        Map<Integer, String> expected = new HashMap<>();
        expected.put(1, "a");
        expected.put(2, "bb");
        expected.put(3, "ccc");
        streamEx(() -> Stream.of("a", "bb", "ccc"), supplier -> {
            Map<Integer, String> map = supplier.get().valuesToMap(String::length);
            assertEquals(supplier.get().isParallel(), map instanceof ConcurrentMap);
            assertEquals(expected, map);

            SortedMap<Integer, String> smap = supplier.get().valuesToSortedMap(String::length);
            assertEquals(supplier.get().isParallel(), smap instanceof ConcurrentMap);
            assertEquals(expected, smap);

            NavigableMap<Integer, String> nmap = supplier.get().valuesToNavigableMap(String::length);
            assertEquals(supplier.get().isParallel(), nmap instanceof ConcurrentMap);
            assertEquals(expected, nmap);
        });
    }

    @Test
    public void testToSortedMap() {
        Map<String, Integer> expected = new HashMap<>();
        expected.put("a", 1);
        expected.put("bb", 2);
        expected.put("ccc", 3);

        Map<Integer, String> expected2 = new HashMap<>();
        expected2.put(1, "a");
        expected2.put(2, "bb");
        expected2.put(3, "ccc");

        streamEx(() -> Stream.of("a", "bb", "ccc"), supplier -> {
            SortedMap<String, Integer> map = supplier.get().toSortedMap(String::length);
            assertEquals(supplier.get().isParallel(), map instanceof ConcurrentMap);
            assertEquals(expected, map);

            SortedMap<Integer, String> map2 = supplier.get().toSortedMap(String::length, Function.identity());
            assertEquals(supplier.get().isParallel(), map2 instanceof ConcurrentMap);
            assertEquals(expected2, map2);
        });

        Map<Integer, String> expected3 = new HashMap<>();
        expected3.put(1, "a");
        expected3.put(2, "bbdd");
        expected3.put(3, "ccc");
        streamEx(() -> Stream.of("a", "bb", "ccc", "dd"), supplier -> {
            SortedMap<Integer, String> seqMap3 = supplier.get().toSortedMap(String::length, Function.identity(),
                String::concat);
            assertEquals(supplier.toString(), expected3, seqMap3);

            checkIllegalStateException(() -> supplier.get().toSortedMap(String::length, Function.identity()), "2", "dd",
                "bb");
        });
    }

    @Test
    public void testToNavigableMap() {
        Map<String, Integer> expected = new HashMap<>();
        expected.put("a", 1);
        expected.put("bb", 2);
        expected.put("ccc", 3);

        Map<Integer, String> expected2 = new HashMap<>();
        expected2.put(1, "a");
        expected2.put(2, "bb");
        expected2.put(3, "ccc");

        streamEx(() -> Stream.of("a", "bb", "ccc"), supplier -> {
            NavigableMap<String, Integer> map = supplier.get().toNavigableMap(String::length);
            assertEquals(supplier.get().isParallel(), map instanceof ConcurrentMap);
            assertEquals(expected, map);

            NavigableMap<Integer, String> map2 = supplier.get().toNavigableMap(String::length, Function.identity());
            assertEquals(supplier.get().isParallel(), map2 instanceof ConcurrentMap);
            assertEquals(expected2, map2);
        });

        Map<Integer, String> expected3 = new HashMap<>();
        expected3.put(1, "a");
        expected3.put(2, "bbdd");
        expected3.put(3, "ccc");
        streamEx(() -> Stream.of("a", "bb", "ccc", "dd"), supplier -> {
            NavigableMap<Integer, String> seqMap3 = supplier.get().toNavigableMap(String::length, Function.identity(),
                String::concat);
            assertEquals(supplier.toString(), expected3, seqMap3);

            checkIllegalStateException(() -> supplier.get().toNavigableMap(String::length, Function.identity()), "2",
                "dd", "bb");
        });
    }

    @Test
    public void testGroupingBy() {
        Map<Integer, List<String>> expected = new HashMap<>();
        expected.put(1, asList("a"));
        expected.put(2, asList("bb", "dd"));
        expected.put(3, asList("ccc"));

        Map<Integer, Set<String>> expectedMapSet = new HashMap<>();
        expectedMapSet.put(1, new HashSet<>(asList("a")));
        expectedMapSet.put(2, new HashSet<>(asList("bb", "dd")));
        expectedMapSet.put(3, new HashSet<>(asList("ccc")));

        streamEx(() -> StreamEx.of("a", "bb", "dd", "ccc"), supplier -> {
            assertEquals(expected, supplier.get().groupingBy(String::length));
            Map<Integer, List<String>> map = supplier.get().groupingTo(String::length, LinkedList::new);
            assertEquals(expected, map);
            assertTrue(map.get(1) instanceof LinkedList);
            assertEquals(expectedMapSet, supplier.get().groupingBy(String::length, Collectors.toSet()));
            assertEquals(expectedMapSet, supplier.get().groupingBy(String::length, HashMap::new, Collectors.toSet()));
            ConcurrentHashMap<Integer, Set<String>> chm = supplier.get().groupingBy(String::length,
                ConcurrentHashMap::new, Collectors.toSet());
            assertEquals(expectedMapSet, chm);
            chm = supplier.get().groupingTo(String::length, ConcurrentHashMap::new, TreeSet::new);
            assertTrue(chm.get(1) instanceof TreeSet);
        });
    }

    @Test
    public void testPartitioning() {
        Map<Boolean, List<String>> map = StreamEx.of("a", "bb", "c", "dd").partitioningBy(s -> s.length() > 1);
        assertEquals(asList("bb", "dd"), map.get(true));
        assertEquals(asList("a", "c"), map.get(false));
        Map<Boolean, Long> counts = StreamEx.of("a", "bb", "c", "dd", "eee").partitioningBy(s -> s.length() > 1,
            Collectors.counting());
        assertEquals(3L, (long) counts.get(true));
        assertEquals(2L, (long) counts.get(false));
        Map<Boolean, List<String>> mapLinked = StreamEx.of("a", "bb", "c", "dd").partitioningTo(s -> s.length() > 1,
            LinkedList::new);
        assertEquals(asList("bb", "dd"), mapLinked.get(true));
        assertEquals(asList("a", "c"), mapLinked.get(false));
        assertTrue(mapLinked.get(true) instanceof LinkedList);
    }

    @Test
    public void testIterable() {
        List<String> result = new ArrayList<>();
        for (String s : StreamEx.of("a", "b", "cc").filter(s -> s.length() < 2)) {
            result.add(s);
        }
        assertEquals(asList("a", "b"), result);
    }

    @Test
    public void testCreateFromMap() {
        Map<String, Integer> data = new LinkedHashMap<>();
        data.put("aaa", 10);
        data.put("bb", 25);
        data.put("c", 37);
        assertEquals(asList("aaa", "bb", "c"), StreamEx.ofKeys(data).toList());
        assertEquals(asList("aaa"), StreamEx.ofKeys(data, x -> x % 2 == 0).toList());
        assertEquals(asList(10, 25, 37), StreamEx.ofValues(data).toList());
        assertEquals(asList(10, 25), StreamEx.ofValues(data, s -> s.length() > 1).toList());
    }

    @Test
    public void testSelect() {
        assertEquals(asList("a", "b"), StreamEx.of(1, "a", 2, "b", 3, "cc").select(String.class).filter(s -> s
                .length() == 1).toList());
        StringBuilder sb = new StringBuilder();
        StringBuffer sbb = new StringBuffer();
        StreamEx.<CharSequence>of("test", sb, sbb).select(Appendable.class).forEach(a -> {
            try {
                a.append("b");
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
        assertEquals("b", sb.toString());
        assertEquals("b", sbb.toString());
    }

    @Test
    public void testFlatCollection() {
        Map<Integer, List<String>> data = new LinkedHashMap<>();
        data.put(1, asList("a", "b"));
        data.put(2, asList("c", "d"));
        data.put(3, null);
        assertEquals(asList("a", "b", "c", "d"), StreamEx.of(data.entrySet()).flatCollection(Entry::getValue).toList());
    }

    @Test
    public void testFlatArray() {
        Map<Integer, String[]> data = new LinkedHashMap<>();
        data.put(1, new String[] {"a", "b"});
        data.put(2, new String[] {"c", "d"});
        data.put(3, null);
        assertEquals(asList("a", "b", "c", "d"), StreamEx.of(data.entrySet()).flatArray(Entry::getValue).toList());
    }

    @Test
    public void testMapPartial() {
        Function<Integer, Optional<String>> literalOf = num -> num == 1
                ? Optional.of("one")
                : Optional.empty();

        List<Integer> original = asList(1, 2, 3, 4);
        List<String> expected = asList("one");
        streamEx(original::stream, s -> assertEquals(expected, s.get().mapPartial(literalOf).toList()));
    }

    @Test
    public void testAppend() {
        assertEquals(asList("a", "b", "c", "d", "e"), StreamEx.of("a", "b", "c", "dd").remove(s -> s.length() > 1)
                .append("d", "e").toList());
        assertEquals(asList("a", "b", "c", "d", "e"), StreamEx.of("a", "b", "c").append(Stream.of("d", "e"))
                .toList());
        assertEquals(asList("a", "b", "c", "d", "e"), StreamEx.of("a", "b", "c").append(asList("d", "e")).toList());

        List<Integer> list = asList(1, 2, 3, 4);
        assertEquals(asList(1.0, 2, 3L, 1, 2, 3, 4), StreamEx.of(1.0, 2, 3L).append(list).toList());

        StreamEx<Integer> s = StreamEx.of(1, 2, 3);
        assertSame(s, s.append());
        assertSame(s, s.append(Collections.emptyList()));
        assertSame(s, s.append(new ArrayList<>()));
        assertSame(s, s.append(Stream.empty()));
        assertNotSame(s, s.append(new ConcurrentLinkedQueue<>()));
    }

    @Test
    public void testPrepend() {
        Supplier<Stream<String>> sized = () -> StreamEx.of("a", "b", "c", "dd");
        Supplier<Stream<String>> notSized = () -> StreamEx.of(StreamEx.of("a", "b", "c", "dd").iterator());
        for (Supplier<Stream<String>> supplier : asList(sized, notSized)) {
            streamEx(supplier, s -> {
                assertEquals(asList("d", "e", "a", "b", "c"), s.get().remove(str -> str.length() > 1).prepend("d", "e")
                        .toList());
                assertEquals(asList("d", "e", "a", "b", "c", "dd"), s.get().prepend(Stream.of("d", "e"))
                        .toList());
                assertEquals(asList("d", "e", "a", "b", "c", "dd"), s.get().prepend(asList("d", "e")).toList());
            });
        }
        assertArrayEquals(new Object[] { 1, 2, 3, 1, 1 }, StreamEx.constant(1, Long.MAX_VALUE - 1).prepend(1, 2, 3)
                .limit(5).toArray());

        assertEquals(asList(4, 3, 2, 1), StreamEx.of(1).prepend(2).prepend(StreamEx.of(3).prepend(4)).toList());

        StreamEx<Integer> s = StreamEx.of(1, 2, 3);
        assertSame(s, s.prepend());
        assertSame(s, s.prepend(Collections.emptyList()));
        assertSame(s, s.prepend(new ArrayList<>()));
        assertSame(s, s.prepend(Stream.empty()));
        assertNotSame(s, s.prepend(new ConcurrentLinkedQueue<>()));

        assertTrue(StreamEx.of("a", "b").prepend(Stream.of("c").parallel()).isParallel());
        assertTrue(StreamEx.of("a", "b").parallel().prepend(Stream.of("c").parallel()).isParallel());
        assertTrue(StreamEx.of("a", "b").parallel().prepend(Stream.of("c")).isParallel());
        assertFalse(StreamEx.of("a", "b").prepend(Stream.of("c")).isParallel());
    }

    @Test
    public void testPrependTSO() {
        List<Integer> expected = IntStreamEx.rangeClosed(19999, 0, -1).boxed().toList();
        assertEquals(expected, IntStreamEx.range(20000).mapToObj(StreamEx::of).reduce(StreamEx::prepend).get()
                .toList());
        assertEquals(expected, IntStreamEx.range(20000).parallel().mapToObj(StreamEx::of).reduce(StreamEx::prepend)
                .get().toList());
    }

    @Test
    public void testNonNull() {
        List<String> data = asList("a", null, "b");
        assertEquals(asList("a", null, "b"), StreamEx.of(data).toList());
        assertEquals(asList("a", "b"), StreamEx.of(data).nonNull().toList());
    }

    @Test
    public void testSorting() {
        assertEquals(asList("a", "b", "c", "d"), StreamEx.of("b", "c", "a", "d").sorted().toList());
        assertEquals(asList("d", "c", "b", "a"), StreamEx.of("b", "c", "a", "d").reverseSorted().toList());

        List<String> data = asList("a", "bbb", "cc");
        assertEquals(asList("a", "cc", "bbb"), StreamEx.of(data).sorted(Comparator.comparingInt(String::length))
                .toList());
        assertEquals(asList("bbb", "cc", "a"), StreamEx.of(data).reverseSorted(Comparator.comparingInt(String::length))
                .toList());
        assertEquals(asList("a", "cc", "bbb"), StreamEx.of(data).sortedByInt(String::length).toList());
        assertEquals(asList("a", "cc", "bbb"), StreamEx.of(data).sortedByLong(String::length).toList());
        assertEquals(asList("a", "cc", "bbb"), StreamEx.of(data).sortedByDouble(String::length).toList());
        assertEquals(asList("a", "cc", "bbb"), StreamEx.of(data).sortedBy(String::length).toList());
    }

    @Test
    public void testMinMax() {
        withRandom(random -> {
            List<String> data = IntStreamEx.of(random, 1000, 1, 100).mapToObj(len -> IntStreamEx.constant(random
                    .nextInt('z' - 'a' + 1) + 'a', len).charsToString()).toList();
            String minStr = Collections.min(data, Comparator.comparingInt(String::length)), maxStr = Collections.max(
                data, Comparator.comparingInt(String::length));
            streamEx(data::stream, supplier -> {
                assertEquals(supplier.toString(), maxStr, supplier.get().max(Comparator.comparingInt(String::length))
                        .get());
                assertEquals(supplier.toString(), maxStr, supplier.get().maxByInt(String::length).get());
                assertEquals(supplier.toString(), maxStr, supplier.get().maxByLong(String::length).get());
                assertEquals(supplier.toString(), maxStr, supplier.get().maxByDouble(String::length).get());
                assertEquals(supplier.toString(), maxStr, supplier.get().maxBy(String::length).get());

                assertEquals(supplier.toString(), maxStr, supplier.get().min(Comparator.comparingInt(String::length)
                        .reversed()).get());
                assertEquals(supplier.toString(), minStr, supplier.get().minByInt(String::length).get());
                assertEquals(supplier.toString(), minStr, supplier.get().minByLong(String::length).get());
                assertEquals(supplier.toString(), minStr, supplier.get().minByDouble(String::length).get());
                assertEquals(supplier.toString(), minStr, supplier.get().minBy(String::length).get());
            });
            assertFalse(StreamEx.<String>empty().minByInt(String::length).isPresent());
        });
    }

    @Test
    public void testFind() {
        assertEquals("bb", StreamEx.of("a", "bb", "c").findFirst(s -> s.length() == 2).get());
        assertFalse(StreamEx.of("a", "bb", "c").findFirst(s -> s.length() == 3).isPresent());
    }

    @Test
    public void testHas() {
        assertTrue(StreamEx.of("a", "bb", "c").has("bb"));
        assertFalse(StreamEx.of("a", "bb", "c").has("cc"));
        assertFalse(StreamEx.of("a", "bb", "c").has(null));
        assertTrue(StreamEx.of("a", "bb", null, "c").has(null));
    }

    @Test
    public void testWithout() {
        assertEquals(asList("a", "bb", null), StreamEx.of("a", "bb", null, "c").without("c").toList());
        String s = null;
        assertEquals(asList("a", "bb", "c"), StreamEx.of("a", "bb", null, "c", null).without(s).toList());
        assertTrue(StreamEx.of("bb", "bb", "bb").without("bb").toList().isEmpty());
        assertEquals(asList("bb", "bb", "bb"), StreamEx.of("bb", "bb", "bb").without(s).toList());

        StreamEx<String> stream = StreamEx.of("a", "b", "c");
        assertSame(stream, stream.without());
        assertEquals(asList("a", "b", "c"), StreamEx.of("a", "b", null, "c").without(new String[] { null }).toList());
        assertEquals(asList(), StreamEx.of("a", "b", null, "c").without("c", null, "b", "a").toList());
    }

    @Test
    public void testJoining() {
        assertEquals("abc", StreamEx.of("a", "b", "c").joining());
        assertEquals("a,b,c", StreamEx.of("a", "b", "c").joining(","));
        assertEquals("[1;2;3]", StreamEx.of(1, 2, 3).joining(";", "[", "]"));

        withRandom(r -> {
            List<Integer> input1 = IntStreamEx.of(r, 1000, 0, 1000).boxed().toList();
            List<String> input2 = IntStreamEx.of(r, 1000, 0, 1000).mapToObj(String::valueOf).toList();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < input1.size(); i++) {
                if (sb.length() > 0)
                    sb.append(',');
                sb.append(input1.get(i)).append(':').append(input2.get(i));
            }
            String expected = sb.toString();
            assertEquals(expected, StreamEx.zip(input1, input2, (i, s) -> i + ":" + s).joining(","));
            assertEquals(expected, StreamEx.zip(input1, input2, (i, s) -> i + ":" + s).parallel().joining(","));
        });
    }

    @Test
    public void testFoldLeft() {
        List<String> input = asList("a", "bb", "ccc");
        streamEx(input::stream, supplier -> {
            assertEquals("ccc;bb;a;", supplier.get().foldLeft("", (u, v) -> v + ";" + u));
            assertTrue(supplier.get().foldLeft(false, (Boolean acc, String s) -> acc || s.equals("bb")));
            assertFalse(supplier.get().foldLeft(false, (Boolean acc, String s) -> acc || s.equals("d")));
            assertEquals(6, (int) supplier.get().foldLeft(0, (acc, v) -> acc + v.length()));
            assertEquals("{ccc={bb={a={}}}}", supplier.get().foldLeft(Collections.emptyMap(), (Map<String, Object> acc,
                    String v) -> Collections.singletonMap(v, acc)).toString());
        });
    }

    @Test
    public void testFoldLeftOptional() {
        // non-associative
        BinaryOperator<Integer> accumulator = (x, y) -> (x + y) * (x + y);
        streamEx(() -> StreamEx.constant(3, 4), supplier -> assertEquals(2322576, (int) supplier.get().foldLeft(
            accumulator).orElse(-1)));
        streamEx(() -> StreamEx.of(1, 2, 3), supplier -> assertEquals(144, (int) supplier.get().foldLeft(accumulator)
                .orElse(-1)));
        emptyStreamEx(Integer.class, supplier -> assertFalse(supplier.get().foldLeft(accumulator).isPresent()));
    }

    @Test
    public void testFoldRight() {
        assertEquals(";c;b;a", StreamEx.of("a", "b", "c").parallel().foldRight("", (u, v) -> v + ";" + u));
