package com.baeldung.java8;

import com.baeldung.stream.Product;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.*;

import static org.junit.Assert.*;

public class Java8StreamApiUnitTest {

    private long counter;

    private static Logger log = LoggerFactory.getLogger(Java8StreamApiUnitTest.class);

    private List<Product> productList;

    @Before
    public void init() {
        productList = Arrays.asList(new Product(23, "potatoes"), new Product(14, "orange"), new Product(13, "lemon"), new Product(23, "bread"), new Product(13, "sugar"));
    }

    @Test
    public void checkPipeline_whenStreamOneElementShorter_thenCorrect() {

        List<String> list = Arrays.asList("abc1", "abc2", "abc3");
        long size = list.stream().skip(1).map(element -> element.substring(0, 3)).count();
        assertEquals(list.size() - 1, size);
    }

    @Test
    public void checkOrder_whenChangeQuantityOfMethodCalls_thenCorrect() {

        List<String> list = Arrays.asList("abc1", "abc2", "abc3");

        counter = 0;
        long sizeFirst = list.stream().skip(2).map(element -> {
            wasCalled();
            return element.substring(0, 3);
        }).count();
        assertEquals(1, counter);

        counter = 0;
        long sizeSecond = list.stream().map(element -> {
            wasCalled();
            return element.substring(0, 3);
        }).skip(2).count();
        assertEquals(3, counter);
    }

    @Test
    public void createEmptyStream_whenEmpty_thenCorrect() {

        Stream<String> streamEmpty = Stream.empty();
        assertEquals(0, streamEmpty.count());

        List<String> names = Collections.emptyList();
        Stream<String> streamOf = Product.streamOf(names);
        assertTrue(streamOf.count() == 0);
    }

    @Test
    public void createStream_whenCreated_thenCorrect() {

        Collection<String> collection = Arrays.asList("a", "b", "c");
        Stream<String> streamOfCollection = collection.stream();
        assertEquals(3, streamOfCollection.count());

        Stream<String> streamOfArray = Stream.of("a", "b", "c");
        assertEquals(3, streamOfArray.count());

        String[] arr = new String[] { "a", "b", "c" };
        Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);
        assertEquals(2, streamOfArrayPart.count());

        IntStream intStream = IntStream.range(1, 3);
        LongStream longStream = LongStream.rangeClosed(1, 3);
        Random random = new Random();
        DoubleStream doubleStream = random.doubles(3);
        assertEquals(2, intStream.count());
        assertEquals(3, longStream.count());
        assertEquals(3, doubleStream.count());

        IntStream streamOfChars = "abc".chars();
        IntStream str = "".chars();
        assertEquals(3, streamOfChars.count());

        Stream<String> streamOfString = Pattern.compile(", ").splitAsStream("a, b, c");
        assertEquals("a", streamOfString.findFirst().get());

        Path path = getPath();
        Stream<String> streamOfStrings = null;
        try {
            streamOfStrings = Files.lines(path, Charset.forName("UTF-8"));
        } catch (IOException e) {
            log.error("Error creating streams from paths {}", path, e.getMessage(), e);
        }
        assertEquals("a", streamOfStrings.findFirst().get());

        Stream<String> streamBuilder = Stream.<String> builder().add("a").add("b").add("c").build();
        assertEquals(3, streamBuilder.count());

        Stream<String> streamGenerated = Stream.generate(() -> "element").limit(10);
        assertEquals(10, streamGenerated.count());

        Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);
        assertTrue(40 <= streamIterated.findAny().get());
    }

    @Test
    public void runStreamPipeline_whenOrderIsRight_thenCorrect() {

        List<String> list = Arrays.asList("abc1", "abc2", "abc3");
        Optional<String> stream = list.stream().filter(element -> {
            log.info("filter() was called");
            return element.contains("2");
        }).map(element -> {
            log.info("map() was called");
            return element.toUpperCase();
        }).findFirst();
    }

    @Test
    public void reduce_whenExpected_thenCorrect() {

        OptionalInt reduced = IntStream.range(1, 4).reduce((a, b) -> a + b);
        assertEquals(6, reduced.getAsInt());

        int reducedTwoParams = IntStream.range(1, 4).reduce(10, (a, b) -> a + b);
        assertEquals(16, reducedTwoParams);

        int reducedThreeParams = Stream.of(1, 2, 3).reduce(10, (a, b) -> a + b, (a, b) -> {
            log.info("combiner was called");
            return a + b;
        });
        assertEquals(16, reducedThreeParams);

        int reducedThreeParamsParallel = Arrays.asList(1, 2, 3).parallelStream().reduce(10, (a, b) -> a + b, (a, b) -> {
            log.info("combiner was called");
            return a + b;
        });
        assertEquals(36, reducedThreeParamsParallel);
    }

    @Test
    public void collecting_whenAsExpected_thenCorrect() {

        List<String> collectorCollection = productList.stream().map(Product::getName).collect(Collectors.toList());

        assertTrue(collectorCollection instanceof List);
        assertEquals(5, collectorCollection.size());

        String listToString = productList.stream().map(Product::getName).collect(Collectors.joining(", ", "[", "]"));

        assertTrue(listToString.contains(",") && listToString.contains("[") && listToString.contains("]"));

        double averagePrice = productList.stream().collect(Collectors.averagingInt(Product::getPrice));
        assertTrue(17.2 == averagePrice);

        int summingPrice = productList.stream().collect(Collectors.summingInt(Product::getPrice));
        assertEquals(86, summingPrice);

        IntSummaryStatistics statistics = productList.stream().collect(Collectors.summarizingInt(Product::getPrice));
        assertEquals(23, statistics.getMax());

        Map<Integer, List<Product>> collectorMapOfLists = productList.stream().collect(Collectors.groupingBy(Product::getPrice));
        assertEquals(3, collectorMapOfLists.keySet().size());

        Map<Boolean, List<Product>> mapPartioned = productList.stream().collect(Collectors.partitioningBy(element -> element.getPrice() > 15));
        assertEquals(2, mapPartioned.keySet().size());

    }

    @Test(expected = UnsupportedOperationException.class)
    public void collect_whenThrows_thenCorrect() {
        Set<Product> unmodifiableSet = productList.stream().collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
        unmodifiableSet.add(new Product(4, "tea"));
    }

    @Test
    public void customCollector_whenResultContainsAllElementsFrSource_thenCorrect() {
        Collector<Product, ?, LinkedList<Product>> toLinkedList = Collector.of(LinkedList::new, LinkedList::add, (first, second) -> {
            first.addAll(second);
            return first;
        });

        LinkedList<Product> linkedListOfPersons = productList.stream().collect(toLinkedList);
        assertTrue(linkedListOfPersons.containsAll(productList));
    }

    @Test
    public void parallelStream_whenWorks_thenCorrect() {
        Stream<Product> streamOfCollection = productList.parallelStream();
        boolean isParallel = streamOfCollection.isParallel();
        boolean haveBigPrice = streamOfCollection.map(product -> product.getPrice() * 12).anyMatch(price -> price > 200);
        assertTrue(isParallel && haveBigPrice);
    }

    @Test
    public void parallel_whenIsParallel_thenCorrect() {
        IntStream intStreamParallel = IntStream.range(1, 150).parallel().map(element -> element * 34);
        boolean isParallel = intStreamParallel.isParallel();
        assertTrue(isParallel);
    }

    @Test
    public void parallel_whenIsSequential_thenCorrect() {
        IntStream intStreamParallel = IntStream.range(1, 150).parallel().map(element -> element * 34);
        IntStream intStreamSequential = intStreamParallel.sequential();
        boolean isParallel = intStreamParallel.isParallel();
        assertFalse(isParallel);
    }

    private Path getPath() {
        Path path = null;
        try {
            path = Files.createTempFile(null, ".txt");
        } catch (IOException e) {
            log.error(e.getMessage());
        }

        try (BufferedWriter writer = Files.newBufferedWriter(path)) {
            writer.write("a\nb\nc");
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        return path;
    }

    private void wasCalled() {
        counter++;
    }
}