package com.github.davidmoten.rx2.flowable;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Test;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import com.github.davidmoten.guavamini.Lists;
import com.github.davidmoten.junit.Asserts;
import com.github.davidmoten.rx2.Consumers;
import com.github.davidmoten.rx2.Functions;
import com.github.davidmoten.rx2.Statistics;
import com.github.davidmoten.rx2.exceptions.ThrowingException;
import com.github.davidmoten.rx2.util.Pair;

import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.schedulers.TestScheduler;
import io.reactivex.subscribers.TestSubscriber;

public class TransformersTest {

    @Test
    public void testIsUtilityClass() {
        Asserts.assertIsUtilityClass(Transformers.class);
    }

    @Test
    public void testStatisticsOnEmptyStream() {
        Flowable<Integer> nums = Flowable.empty();
        Statistics s = nums.compose(Transformers.<Integer>collectStats()).blockingLast();
        assertEquals(0, s.count());
        assertEquals(0, s.sum(), 0.0001);
        assertTrue(Double.isNaN(s.mean()));
        assertTrue(Double.isNaN(s.sd()));
    }

    @Test
    public void testStatisticsOnSingleElement() {
        Flowable<Integer> nums = Flowable.just(1);
        Statistics s = nums.compose(Transformers.<Integer>collectStats()).blockingLast();
        assertEquals(1, s.count());
        assertEquals(1, s.sum(), 0.0001);
        assertEquals(1.0, s.mean(), 0.00001);
        assertEquals(0, s.sd(), 0.00001);
    }

    @Test
    public void testStatisticsOnMultipleElements() {
        Flowable<Integer> nums = Flowable.just(1, 4, 10, 20);
        Statistics s = nums.compose(Transformers.<Integer>collectStats()).blockingLast();
        assertEquals(4, s.count());
        assertEquals(35.0, s.sum(), 0.0001);
        assertEquals(8.75, s.mean(), 0.00001);
        assertEquals(7.258615570478987, s.sd(), 0.00001);
        assertEquals(1 + 16 + 100 + 400, s.sumSquares(), 0.0001);
    }

    @Test
    public void testStatisticsPairOnEmptyStream() {
        Flowable<Integer> nums = Flowable.empty();
        boolean isEmpty = nums.compose(Transformers.collectStats(Functions.<Integer>identity())).isEmpty()
                .blockingGet();
        assertTrue(isEmpty);
    }

    @Test
    public void testStatisticsPairOnSingleElement() {
        Flowable<Integer> nums = Flowable.just(1);
        Pair<Integer, Statistics> s = nums.compose(Transformers.collectStats(Functions.<Integer>identity()))
                .blockingLast();
        assertEquals(1, (int) s.a());
        assertEquals(1, s.b().count());
        assertEquals(1, s.b().sum(), 0.0001);
        assertEquals(1.0, s.b().mean(), 0.00001);
        assertEquals(0, s.b().sd(), 0.00001);
    }

    @Test
    public void testStatisticsPairOnMultipleElements() {
        Flowable<Integer> nums = Flowable.just(1, 4, 10, 20);
        Pair<Integer, Statistics> s = nums.compose(Transformers.collectStats(Functions.<Integer>identity()))
                .blockingLast();
        assertEquals(4, s.b().count());
        assertEquals(35.0, s.b().sum(), 0.0001);
        assertEquals(8.75, s.b().mean(), 0.00001);
        assertEquals(7.258615570478987, s.b().sd(), 0.00001);
    }

    @Test
    public void testToString() {
        Statistics s = Statistics.create();
        s = s.add(1).add(2);
        assertEquals("Statistics [count=2, sum=3.0, sumSquares=5.0, mean=1.5, sd=0.5]", s.toString());
    }

    @Test
    public void testInsert() {
        Flowable.just(1, 2) //
                .compose(Transformers.insert(Maybe.just(3))) //
                .test() //
                .assertValues(1, 3, 2, 3) //
                .assertComplete();
    }

    @Test
    public void testInsertWithDelays() {
        TestScheduler s = new TestScheduler();
        TestSubscriber<Integer> ts = //
                Flowable.just(1).delay(1, TimeUnit.SECONDS, s) //
                        .concatWith(Flowable.just(2).delay(3, TimeUnit.SECONDS, s)) //
                        .compose(Transformers.insert(Maybe.just(3).delay(2, TimeUnit.SECONDS, s))) //
                        .test();
        ts.assertNoValues();
        s.advanceTimeBy(1, TimeUnit.SECONDS);
        ts.assertValues(1);
        s.advanceTimeBy(2, TimeUnit.SECONDS);
        ts.assertValues(1, 3);
        s.advanceTimeBy(1, TimeUnit.SECONDS);
        ts.assertValues(1, 3, 2);
        ts.assertComplete();
    }

    @Test
    public void testInsertBackpressure() {
        Flowable.just(1, 2) //
                .compose(Transformers.insert(Maybe.just(3))) //
                .test(0) //
                .assertNoValues() //
                .requestMore(1) //
                .assertValues(1) //
                .requestMore(3) //
                .assertValues(1, 3, 2, 3) //
                .requestMore(1) //
                .assertValueCount(4) //
                .assertComplete();
    }

    @Test
    public void testInsertSourceError() {
        Flowable.<Integer>error(new IOException("boo")) //
                .compose(Transformers.insert(Maybe.just(3))) //
                .test() //
                .assertNoValues() //
                .assertErrorMessage("boo");
    }

    @Test
    public void testInsertError() {
        Flowable.just(1, 2) //
                .compose(Transformers.insert(Maybe.error(new IOException("boo")))) //
                .test() //
                .assertValue(1) //
                .assertErrorMessage("boo");
    }

    @Test
    public void testInsertCancel() {
        TestSubscriber<Integer> ts = Flowable.just(1, 2) //
                .compose(Transformers.insert(Maybe.just(3))) //
                .test(0) //
                .assertNoValues() //
                .requestMore(1) //
                .assertValues(1);
        ts.cancel();
        ts.requestMore(100) //
                .assertValues(1) //
                .assertNotTerminated();
    }
    
    @Test
    public void testInsertMapperError() {
        Flowable.just(1, 2) //
                .compose(Transformers.insert(Functions.<Integer, Maybe<Integer>>throwing())) //
                .test() //
                .assertValue(1) //
                .assertError(ThrowingException.class);
    }
    
    @Test
    public void testInsertNothing() {
        Flowable.just(1, 2, 3) //
                .compose(Transformers.insert(Functions.constant(Maybe.<Integer>empty()))) //
                .test() //
                .assertValues(1, 2, 3) //
                .assertComplete();
    }
    
    @Test
    public void testInsertNoOnNextOrCompleteEventsProcessedAfterMapperError() {
        assertEquals(1, countMapperCalls(true));
    }
    
    @Test
    public void testInsertNoOnNextOrErrorEventsProcessedAfterMapperError() {
        assertEquals(1, countMapperCalls(false));
    }

    private int countMapperCalls(final boolean complete) {
        final AtomicInteger count = new AtomicInteger();
        final Function<Integer, Maybe<Integer>> mapper = new Function<Integer, Maybe<Integer>>() {
            @Override
            public Maybe<Integer> apply(Integer t) throws Exception {
                count.incrementAndGet();
                throw new ThrowingException();
            }
        };
        // construct a Flowable that ignores cancellation (and always expects a
        // Long.MAX_VALUE request)
        new Flowable<Integer>() {

            @Override
            protected void subscribeActual(final Subscriber<? super Integer> s) {
                s.onSubscribe(new Subscription() {

                    @Override
                    public void request(long n) {
                        s.onNext(1);
                        s.onNext(2);
                        if (complete) {
                            s.onComplete();
                        } else {
                            s.onError(new IOException("boo"));
                        }
                    }

                    @Override
                    public void cancel() {
                        // ignore
                    }
                });
            }
        } //
                .compose(Transformers.insert(mapper)) //
                .test() //
                .assertValue(1) //
                .assertError(ThrowingException.class);
        return count.get();
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testBufferMaxCountAndTimeout() {
        int maxSize = 3;
        Flowable.just(1, 2, 3) //
                .compose(Transformers.<Integer>buffer(maxSize, 0, TimeUnit.SECONDS, Schedulers.trampoline())).test() //
                .assertValues(Lists.newArrayList(1), Lists.newArrayList(2), Lists.newArrayList(3)) //
                .assertComplete();
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testBufferMaxCountAndTimeoutAsyncCountWins() {
        int maxSize = 3;
        Flowable.just(1, 2, 3, 4) //
                .concatWith(Flowable.just(5).delay(500, TimeUnit.MILLISECONDS)) //
                .compose(Transformers.<Integer>buffer(maxSize, 100, TimeUnit.MILLISECONDS)) //
                .test() //
                .awaitDone(10, TimeUnit.SECONDS) //
                .assertValues(Lists.newArrayList(1, 2, 3), Lists.newArrayList(4), Lists.newArrayList(5)) //
                .assertComplete();
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testBufferMaxCountAndTimeoutAsyncTimeoutWins() {
        int maxSize = 3;
        Flowable.just(1, 2) //
        .timeout(1, TimeUnit.SECONDS) //
                .concatWith(Flowable.just(3).delay(500, TimeUnit.MILLISECONDS)) //
                .compose(Transformers.<Integer>buffer(maxSize, 100, TimeUnit.MILLISECONDS)) //
                .test() //
                .awaitDone(10, TimeUnit.SECONDS) //
                .assertValues(Lists.newArrayList(1, 2), Lists.newArrayList(3)) //
                .assertComplete();
    }
    
    @Test
    public void testBufferTimeoutSourceError() {
        Flowable.<Integer>error(new ThrowingException()) //
           .compose(Transformers.<Integer>buffer(3, 100, TimeUnit.MILLISECONDS)) //
           .test() //
           .awaitDone(10, TimeUnit.SECONDS) //
           .assertNoValues() //
           .assertError(ThrowingException.class);
    }
    
    @Test
    public void testInsertTimeoutValueError() {
        Flowable.just(1).concatWith(Flowable.<Integer>never()) //
                .compose(Transformers.<Integer>insert( //
                        Functions.constant(7L), //
                        TimeUnit.MILLISECONDS, //
                        Functions.<Integer, Integer>throwing())) //
                .test() //
                .awaitDone(10, TimeUnit.SECONDS) //
                .assertValues(1) //
                .assertError(ThrowingException.class);
    }
    
    @Test
    public void testInsertTimeoutCancel() {
        Flowable.just(1, 2) //
                .compose(Transformers.<Integer>insert( //
                        Functions.constant(7L), //
                        TimeUnit.SECONDS, //
                        Functions.<Integer, Integer>throwing())) //
                .take(1) //
                .test() //
                .awaitDone(10, TimeUnit.SECONDS) //
                .assertValue(1);
    }
    
    public static void main(String[] args) {
        Flowable.interval(1, TimeUnit.SECONDS) //
            .compose(Transformers.insert(Maybe.just(-1L).delay(500, TimeUnit.MILLISECONDS))) //
            .doOnNext(Consumers.println()) //
            .count() //
            .blockingGet();
    }
    
}