package io.smallrye.mutiny.operators;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.IntStream;

import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.testng.annotations.Test;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.subscription.BackPressureFailure;
import io.smallrye.mutiny.subscription.BackPressureStrategy;
import io.smallrye.mutiny.subscription.MultiEmitter;
import io.smallrye.mutiny.test.MultiAssertSubscriber;

public class MultiCreateFromEmitterTest {

    @Test
    public void testWithDefaultBackPressure() {
        MultiAssertSubscriber<Integer> ts = MultiAssertSubscriber.create();
        Multi<Integer> multi = Multi.createFrom().emitter(emitter -> {
            emitter.emit(1);
            emitter.emit(2);
            emitter.emit(3);
            emitter.complete();
        });
        multi.subscribe(ts);
        ts.assertSubscribed()
                .request(Long.MAX_VALUE)
                .assertCompletedSuccessfully()
                .assertReceived(1, 2, 3);
    }

    @Test
    public void testRequestsAtSubscription() {
        MultiAssertSubscriber<Integer> ts = MultiAssertSubscriber.create(Long.MAX_VALUE);
        Multi<Integer> multi = Multi.createFrom().emitter(emitter -> {
            emitter.emit(1);
            emitter.emit(2);
            emitter.emit(3);
            emitter.complete();
        });
        multi.subscribe(ts);
        ts.assertSubscribed()
                .assertCompletedSuccessfully()
                .assertReceived(1, 2, 3);
    }

    @Test
    public void testWhenConsumerThrowsAnException() {
        AtomicBoolean onTerminationCalled = new AtomicBoolean();
        MultiAssertSubscriber<Integer> ts = MultiAssertSubscriber.create();
        Multi<Integer> multi = Multi.createFrom().emitter(emitter -> {
            emitter.onTermination(() -> onTerminationCalled.set(true));
            emitter.emit(1);
            emitter.emit(2);
            throw new IllegalStateException("boom");
        });
        multi.subscribe(ts);
        ts.assertSubscribed()
                .request(Long.MAX_VALUE)
                .assertHasFailedWith(IllegalStateException.class, "boom")
                .assertReceived(1, 2);
        assertThat(onTerminationCalled).isTrue();
    }

    @Test
    public void testWithRequests() {
        AtomicInteger terminated = new AtomicInteger();
        AtomicReference<MultiEmitter<? super Integer>> reference = new AtomicReference<>();
        Multi.createFrom().<Integer> emitter(emitter -> {
            reference.set(emitter);
            emitter.onTermination(terminated::incrementAndGet);
            emitter.emit(1);
            emitter.emit(2);
            emitter.emit(3);
            emitter.complete();
        })
                .subscribe().withSubscriber(MultiAssertSubscriber.create())
                .assertSubscribed()
                .run(() -> {
                    assertThat(reference.get()).isNotNull();
                    assertThat(reference.get().requested()).isEqualTo(0);
                })
                .request(2)
                .run(() -> {
                    // Already emitted
                    assertThat(reference.get().requested()).isEqualTo(0);
                })
                .assertNotTerminated()
                .assertReceived(1, 2)
                .request(2)
                .assertReceived(1, 2, 3)
                .assertCompletedSuccessfully();

        assertThat(terminated).hasValue(1);
    }

    @Test
    public void testWithMoreRequestsThanValue() {
        AtomicInteger terminated = new AtomicInteger();
        AtomicReference<MultiEmitter<? super Integer>> reference = new AtomicReference<>();
        Multi.createFrom().<Integer> emitter(emitter -> {
            reference.set(emitter);
            emitter.onTermination(terminated::incrementAndGet);
            emitter.emit(1);
            emitter.emit(2);
            emitter.emit(3);
            emitter.complete();
        })
                .subscribe().withSubscriber(MultiAssertSubscriber.create())
                .assertSubscribed()
                .run(() -> {
                    assertThat(reference.get()).isNotNull();
                    assertThat(reference.get().requested()).isEqualTo(0);
                })
                .request(2)
                .run(() -> {
                    // Already emitted
                    assertThat(reference.get().requested()).isEqualTo(0);
                })
                .assertNotTerminated()
                .assertReceived(1, 2)
                .request(10)
                .assertReceived(1, 2, 3)
                .run(() -> {
                    assertThat(reference.get().requested()).isEqualTo(10);
                })
                .assertCompletedSuccessfully();

        assertThat(terminated).hasValue(1);
    }

    @Test
    public void testCancellation() {
        AtomicInteger cancelled = new AtomicInteger();
        MultiAssertSubscriber<Object> subscriber = Multi.createFrom().emitter(emitter -> {
            emitter.onTermination(cancelled::incrementAndGet);
            emitter.emit(1);
            emitter.emit(2);
            emitter.emit(3);
            emitter.complete();
        })
                .subscribe().withSubscriber(MultiAssertSubscriber.create())
                .assertSubscribed()
                .request(2)
                .assertNotTerminated()
                .assertReceived(1, 2)
                .cancel()
                .request(1)
                .assertReceived(1, 2)
                .assertNotTerminated();

        assertThat(cancelled).hasValue(1);
        subscriber.cancel();
        assertThat(cancelled).hasValue(1);
    }

    @Test
    public void testOnTerminationOnCompletion() {
        AtomicInteger termination = new AtomicInteger();
        MultiAssertSubscriber<Integer> subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            emitter.onTermination(termination::incrementAndGet);
            emitter.emit(1);
            emitter.emit(2);
            emitter.emit(3);
            emitter.complete();
        })
                .subscribe().withSubscriber(MultiAssertSubscriber.create())
                .assertSubscribed()
                .request(2)
                .assertNotTerminated()
                .assertReceived(1, 2)
                .request(1)
                .assertReceived(1, 2, 3)
                .assertCompletedSuccessfully();

        assertThat(termination).hasValue(1);
        subscriber.cancel();
        assertThat(termination).hasValue(1);
    }

    @Test
    public void testOnTerminationOnFailure() {
        AtomicInteger termination = new AtomicInteger();
        MultiAssertSubscriber<Integer> subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            emitter.onTermination(termination::incrementAndGet);
            emitter.emit(1);
            emitter.emit(2);
            emitter.fail(new IOException("boom"));
            emitter.emit(3);
            emitter.complete();
        })
                .subscribe().withSubscriber(MultiAssertSubscriber.create())
                .assertSubscribed()
                .request(2)
                .assertReceived(1, 2)
                .assertHasFailedWith(IOException.class, "boom")
                .request(1)
                .assertReceived(1, 2);

        assertThat(termination).hasValue(1);
        subscriber.cancel();
        assertThat(termination).hasValue(1);
    }

    @Test
    public void testOnTerminationWhenEmpty() {
        AtomicInteger termination = new AtomicInteger();
        MultiAssertSubscriber<Integer> subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            emitter.onTermination(termination::incrementAndGet);
            emitter.complete();
        })
                .subscribe().withSubscriber(MultiAssertSubscriber.create())
                .assertSubscribed()
                .request(2)
                .assertHasNotReceivedAnyItem()
                .request(1)
                .assertHasNotReceivedAnyItem()
                .assertCompletedSuccessfully();

        assertThat(termination).hasValue(1);
        subscriber.cancel();
        assertThat(termination).hasValue(1);
    }

    @Test
    public void testCancellationWhenSubscriberThrowAnException() {
        AtomicBoolean errored = new AtomicBoolean();
        AtomicBoolean cancelled = new AtomicBoolean();
        AtomicInteger numberOfResults = new AtomicInteger();

        Multi<Integer> source = Multi.createFrom().emitter(emitter -> {
            emitter.onTermination(() -> cancelled.set(true));
            emitter.emit(1);
            emitter.emit(2);
            emitter.emit(3);
            emitter.complete();

        });

        //noinspection SubscriberImplementation
        source.subscribe(new Subscriber<Integer>() {
            @Override
            public void onSubscribe(Subscription s) {
                s.request(10);
            }

            @Override
            public void onNext(Integer integer) {
                numberOfResults.incrementAndGet();
                throw new RuntimeException("BOOM!");
            }

            @Override
            public void onError(Throwable t) {
                errored.set(true);
            }

            @Override
            public void onComplete() {
                // ignored.
            }
        });

        assertThat(errored).isFalse();
        assertThat(cancelled).isTrue();
        assertThat(numberOfResults.get()).isEqualTo(1);
    }

    @Test
    public void testIgnoreBackPressureBehavior() {
        Multi<Integer> multi = Multi.createFrom().emitter(e -> e.emit(1).emit(2).emit(3).complete(),
                BackPressureStrategy.IGNORE);

        multi.subscribe().withSubscriber(MultiAssertSubscriber.create(1))
                .assertSubscribed()
                .assertReceived(1, 2, 3)
                .request(2)
                .assertReceived(1, 2, 3)
                .assertCompletedSuccessfully();
    }

    @Test
    public void testWithoutBackPressure() {
        MultiAssertSubscriber<Integer> subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            IntStream.range(0, 1000).forEach(emitter::emit);
            emitter.complete();
        }, BackPressureStrategy.IGNORE).subscribe()
                .withSubscriber(MultiAssertSubscriber.create())
                .assertCompletedSuccessfully();
        assertThat(subscriber.items()).hasSize(1000);

        subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            IntStream.range(0, 1000).forEach(emitter::emit);
            emitter.complete();
        }, BackPressureStrategy.IGNORE).subscribe()
                .withSubscriber(MultiAssertSubscriber.create(5))
                // The request is ignored by the strategy.
                .assertCompletedSuccessfully();
        assertThat(subscriber.items()).hasSize(1000);
    }

    @Test
    public void testLatestBackPressureBehavior() {
        Multi<Integer> multi = Multi.createFrom().emitter(e -> e.emit(1).emit(2).emit(3).complete(),
                BackPressureStrategy.LATEST);

        multi.subscribe().withSubscriber(MultiAssertSubscriber.create(1))
                .assertSubscribed()
                .assertReceived(1)
                .request(2)
                .assertReceived(1, 3)
                .assertCompletedSuccessfully();
    }

    @Test
    public void testWithLatestBackPressure() {
        MultiAssertSubscriber<Integer> subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            IntStream.range(0, 1000).forEach(emitter::emit);
            emitter.complete();
        }, BackPressureStrategy.LATEST).subscribe()
                .withSubscriber(MultiAssertSubscriber.create(20))
                .request(Long.MAX_VALUE)
                .assertCompletedSuccessfully();
        // 21 because the 20 first are consumed, and then only the latest is kept.
        assertThat(subscriber.items()).hasSize(21);

        subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            IntStream.range(0, 1000).forEach(emitter::emit);
            emitter.complete();
        }, BackPressureStrategy.LATEST).subscribe()
                .withSubscriber(MultiAssertSubscriber.create())
                .request(20)
                .request(Long.MAX_VALUE)
                .assertCompletedSuccessfully();
        assertThat(subscriber.items()).hasSize(1).containsExactly(999);

        Multi.createFrom().<Integer> emitter(MultiEmitter::complete, BackPressureStrategy.LATEST)
                .subscribe().withSubscriber(MultiAssertSubscriber.create(20))
                .assertCompletedSuccessfully()
                .assertHasNotReceivedAnyItem();

        subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            IntStream.range(0, 1000).forEach(emitter::emit);
            emitter.fail(new IOException("boom"));
        }, BackPressureStrategy.LATEST).subscribe()
                .withSubscriber(MultiAssertSubscriber.create())
                .request(20)
                .request(Long.MAX_VALUE)
                .assertHasFailedWith(IOException.class, "boom");
        assertThat(subscriber.items()).hasSize(1).containsExactly(999);

    }

    @Test
    public void testDropBackPressureBehavior() {
        Multi<Integer> multi = Multi.createFrom().emitter(e -> e.emit(1).emit(2).emit(3).complete(), BackPressureStrategy.DROP);

        multi.subscribe().withSubscriber(MultiAssertSubscriber.create(1))
                .assertSubscribed()
                .assertReceived(1)
                .request(2)
                .assertReceived(1)
                .assertCompletedSuccessfully();

        multi.subscribe().withSubscriber(MultiAssertSubscriber.create(3))
                .assertSubscribed()
                .assertReceived(1, 2, 3)
                .request(2)
                .assertReceived(1, 2, 3)
                .assertCompletedSuccessfully();

        multi.subscribe().withSubscriber(MultiAssertSubscriber.create(0))
                .assertSubscribed()
                .assertHasNotReceivedAnyItem()
                .request(2)
                .assertHasNotReceivedAnyItem()
                .assertCompletedSuccessfully();
    }

    @Test
    public void testWithDropBackPressure() {
        MultiAssertSubscriber<Integer> subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            IntStream.range(0, 1000).forEach(emitter::emit);
            emitter.complete();
        }, BackPressureStrategy.DROP).subscribe()
                .withSubscriber(MultiAssertSubscriber.create(20))
                .request(Long.MAX_VALUE)
                .assertCompletedSuccessfully();
        // 20 because the 20 first are consumed, others are dropped
        assertThat(subscriber.items()).hasSize(20);

        subscriber = Multi.createFrom().<Integer> emitter(emitter -> {
            IntStream.range(0, 1000).forEach(emitter::emit);
            emitter.complete();
        }, BackPressureStrategy.DROP).subscribe()
                .withSubscriber(MultiAssertSubscriber.create())
                .request(20)
                .request(Long.MAX_VALUE)
                .assertCompletedSuccessfully();
        assertThat(subscriber.items()).isEmpty();

        Multi.createFrom().<Integer> emitter(MultiEmitter::complete, BackPressureStrategy.DROP)
                .subscribe().withSubscriber(MultiAssertSubscriber.create(20))
                .assertCompletedSuccessfully()
                .assertHasNotReceivedAnyItem();

        Multi.createFrom().<Integer> emitter(emitter -> {
            IntStream.range(0, 1000).forEach(emitter::emit);
            emitter.fail(new IOException("boom"));
        }, BackPressureStrategy.DROP).subscribe()
                .withSubscriber(MultiAssertSubscriber.create())
                .request(20)
                .request(Long.MAX_VALUE)
                .assertHasFailedWith(IOException.class, "boom")
                .assertHasNotReceivedAnyItem();
    }

    @Test
    public void testErrorBackPressureBehavior() {
        Multi<Integer> multi = Multi.createFrom().emitter(e -> e.emit(1).emit(2).emit(3).complete(),
                BackPressureStrategy.ERROR);

        multi.subscribe().withSubscriber(MultiAssertSubscriber.create(1))
                .assertSubscribed()
                .assertReceived(1)
                .assertHasFailedWith(BackPressureFailure.class, "requests");

        multi.subscribe().withSubscriber(MultiAssertSubscriber.create(3))
                .assertSubscribed()
                .assertReceived(1, 2, 3)
                .assertCompletedSuccessfully();

        Multi.createFrom().emitter(MultiEmitter::complete, BackPressureStrategy.ERROR)
                .subscribe().withSubscriber(MultiAssertSubscriber.create())
                .assertCompletedSuccessfully()
                .assertHasNotReceivedAnyItem();
    }

    @Test
    public void testThatWeCanHaveMultipleSubscribers() {
        AtomicInteger count = new AtomicInteger();

        List<MultiEmitter<? super Integer>> emitters = new ArrayList<>();
        Multi<Integer> multi = Multi.createFrom().emitter(e -> {
            int i = count.incrementAndGet();
            emitters.add(e);
            e.emit(i);
            e.emit(i);
        });

        MultiAssertSubscriber<Integer> subscriber1 = multi.subscribe()
                .withSubscriber(MultiAssertSubscriber.create(10))
                .assertReceived(1, 1)
                .assertNotTerminated();

        MultiAssertSubscriber<Integer> subscriber2 = multi.subscribe()
                .withSubscriber(MultiAssertSubscriber.create(10))
                .assertReceived(2, 2)
                .assertNotTerminated();

        emitters.forEach(MultiEmitter::complete);
        subscriber1.assertCompletedSuccessfully();
        subscriber2.assertCompletedSuccessfully();

    }

    @Test
    public void testThatWeCanHaveMultipleSubscribersWhenUsingBackPressure() {
        AtomicInteger count = new AtomicInteger();

        List<MultiEmitter<? super Integer>> emitters = new ArrayList<>();
        Multi<Integer> multi = Multi.createFrom().emitter(e -> {
            int i = count.incrementAndGet();
            emitters.add(e);
            e.emit(i);
            e.emit(i);
        }, BackPressureStrategy.DROP);

        MultiAssertSubscriber<Integer> subscriber1 = multi.subscribe()
                .withSubscriber(MultiAssertSubscriber.create(10))
                .assertReceived(1, 1)
                .assertNotTerminated();

        MultiAssertSubscriber<Integer> subscriber2 = multi.subscribe()
                .withSubscriber(MultiAssertSubscriber.create(10))
                .assertReceived(2, 2)
                .assertNotTerminated();

        emitters.forEach(MultiEmitter::complete);
        subscriber1.assertCompletedSuccessfully();
        subscriber2.assertCompletedSuccessfully();

    }
}