/*
 * Copyright (c) 2011-2018 Pivotal Software Inc, All Rights Reserved.
 *
 * 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
 *
 *       https://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 reactor.core.publisher;

import java.lang.ref.WeakReference;
import java.time.Duration;
import java.util.Date;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;

import reactor.core.Scannable;
import reactor.test.StepVerifier;
import reactor.test.publisher.TestPublisher;
import reactor.util.function.Tuple2;

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

public class MonoProcessorTest {

	@Test
	public void noRetentionOnTermination() throws InterruptedException {
		Date date = new Date();
		CompletableFuture<Date> future = new CompletableFuture<>();

		WeakReference<Date> refDate = new WeakReference<>(date);
		WeakReference<CompletableFuture<Date>> refFuture = new WeakReference<>(future);

		Mono<Date> source = Mono.fromFuture(future);
		Mono<String> data = source.map(Date::toString).as(MonoProcessor::new);

		future.complete(date);
		assertThat(data.block()).isEqualTo(date.toString());

		date = null;
		future = null;
		source = null;
		System.gc();

		int cycles;
		for (cycles = 10; cycles > 0 ; cycles--) {
			if (refDate.get() == null && refFuture.get() == null) break;
			Thread.sleep(100);
		}

		assertThat(refFuture.get()).isNull();
		assertThat(refDate.get()).isNull();
		assertThat(cycles).isNotZero()
		                  .isPositive();
	}

	@Test
	public void noRetentionOnTerminationError() throws InterruptedException {
		CompletableFuture<Date> future = new CompletableFuture<>();

		WeakReference<CompletableFuture<Date>> refFuture = new WeakReference<>(future);

		Mono<Date> source = Mono.fromFuture(future);
		Mono<String> data = source.map(Date::toString).as(MonoProcessor::new);

		future.completeExceptionally(new IllegalStateException());

		assertThatExceptionOfType(IllegalStateException.class)
				.isThrownBy(data::block);

		future = null;
		source = null;
		System.gc();

		int cycles;
		for (cycles = 10; cycles > 0 ; cycles--) {
			if (refFuture.get() == null) break;
			Thread.sleep(100);
		}

		assertThat(refFuture.get()).isNull();
		assertThat(cycles).isNotZero()
		                  .isPositive();
	}

	@Test
	public void noRetentionOnTerminationCancel() throws InterruptedException {
		CompletableFuture<Date> future = new CompletableFuture<>();

		WeakReference<CompletableFuture<Date>> refFuture = new WeakReference<>(future);

		Mono<Date> source = Mono.fromFuture(future);
		Mono<String> data = source.map(Date::toString).as(MonoProcessor::new);

		future = null;
		source = null;

		data.subscribe().dispose();

		System.gc();

		int cycles;
		for (cycles = 10; cycles > 0 ; cycles--) {
			if (refFuture.get() == null) break;
			Thread.sleep(100);
		}

		assertThat(refFuture.get()).isNull();
		assertThat(cycles).isNotZero()
		                  .isPositive();
	}

	@Test(expected = IllegalStateException.class)
	public void MonoProcessorResultNotAvailable() {
		MonoProcessor<String> mp = MonoProcessor.create();
		mp.block(Duration.ofMillis(1));
	}

	@SuppressWarnings("deprecation")
	@Test
	public void MonoProcessorRejectedDoOnSuccessOrError() {
		MonoProcessor<String> mp = MonoProcessor.create();
		AtomicReference<Throwable> ref = new AtomicReference<>();

		mp.doOnSuccessOrError((s, f) -> ref.set(f)).subscribe();
		mp.onError(new Exception("test"));

		assertThat(ref.get()).hasMessage("test");
		assertThat(mp.isSuccess()).isFalse();
		assertThat(mp.isError()).isTrue();
	}

	@Test
	public void MonoProcessorRejectedDoOnTerminate() {
		MonoProcessor<String> mp = MonoProcessor.create();
		AtomicInteger invoked = new AtomicInteger();

		mp.doOnTerminate(invoked::incrementAndGet).subscribe();
		mp.onError(new Exception("test"));

		assertThat(invoked.get()).isEqualTo(1);
		assertThat(mp.isSuccess()).isFalse();
		assertThat(mp.isError()).isTrue();
	}

	@Test
	public void MonoProcessorRejectedSubscribeCallback() {
		MonoProcessor<String> mp = MonoProcessor.create();
		AtomicReference<Throwable> ref = new AtomicReference<>();

		mp.subscribe(v -> {}, ref::set);
		mp.onError(new Exception("test"));

		assertThat(ref.get()).hasMessage("test");
		assertThat(mp.isSuccess()).isFalse();
		assertThat(mp.isError()).isTrue();
	}

	@SuppressWarnings("deprecation")
	@Test
	public void MonoProcessorSuccessDoOnSuccessOrError() {
		MonoProcessor<String> mp = MonoProcessor.create();
		AtomicReference<String> ref = new AtomicReference<>();

		mp.doOnSuccessOrError((s, f) -> ref.set(s)).subscribe();
		mp.onNext("test");

		assertThat(ref.get()).isEqualToIgnoringCase("test");
		assertThat(mp.isSuccess()).isTrue();
		assertThat(mp.isError()).isFalse();
	}

	@Test
	public void MonoProcessorSuccessDoOnTerminate() {
		MonoProcessor<String> mp = MonoProcessor.create();
		AtomicInteger invoked = new AtomicInteger();

		mp.doOnTerminate(invoked::incrementAndGet).subscribe();
		mp.onNext("test");

		assertThat(invoked.get()).isEqualTo(1);
		assertThat(mp.isSuccess()).isTrue();
		assertThat(mp.isError()).isFalse();
	}

	@Test
	public void MonoProcessorSuccessSubscribeCallback() {
		MonoProcessor<String> mp = MonoProcessor.create();
		AtomicReference<String> ref = new AtomicReference<>();

		mp.subscribe(ref::set);
		mp.onNext("test");

		assertThat(ref.get()).isEqualToIgnoringCase("test");
		assertThat(mp.isSuccess()).isTrue();
		assertThat(mp.isError()).isFalse();
	}

	@Test
	public void MonoProcessorRejectedDoOnError() {
		MonoProcessor<String> mp = MonoProcessor.create();
		AtomicReference<Throwable> ref = new AtomicReference<>();

		mp.doOnError(ref::set).subscribe();
		mp.onError(new Exception("test"));

		assertThat(ref.get()).hasMessage("test");
		assertThat(mp.isSuccess()).isFalse();
		assertThat(mp.isError()).isTrue();
	}

	@Test(expected = NullPointerException.class)
	public void MonoProcessorRejectedSubscribeCallbackNull() {
		MonoProcessor<String> mp = MonoProcessor.create();

		mp.subscribe((Subscriber<String>)null);
	}

	@Test
	public void MonoProcessorSuccessDoOnSuccess() {
		MonoProcessor<String> mp = MonoProcessor.create();
		AtomicReference<String> ref = new AtomicReference<>();

		mp.doOnSuccess(ref::set).subscribe();
		mp.onNext("test");

		assertThat(ref.get()).isEqualToIgnoringCase("test");
		assertThat(mp.isSuccess()).isTrue();
		assertThat(mp.isError()).isFalse();
	}

	@Test
	public void MonoProcessorSuccessChainTogether() {
		MonoProcessor<String> mp = MonoProcessor.create();
		MonoProcessor<String> mp2 = MonoProcessor.create();
		mp.subscribe(mp2);

		mp.onNext("test");

		assertThat(mp2.peek()).isEqualToIgnoringCase("test");
		assertThat(mp.isSuccess()).isTrue();
		assertThat(mp.isError()).isFalse();
	}

	@Test
	public void MonoProcessorRejectedChainTogether() {
		MonoProcessor<String> mp = MonoProcessor.create();
		MonoProcessor<String> mp2 = MonoProcessor.create();
		mp.subscribe(mp2);

		mp.onError(new Exception("test"));

		assertThat(mp2.getError()).hasMessage("test");
		assertThat(mp.isSuccess()).isFalse();
		assertThat(mp.isError()).isTrue();
	}

	@Test
	public void MonoProcessorDoubleFulfill() {
		MonoProcessor<String> mp = MonoProcessor.create();

		StepVerifier.create(mp)
		            .then(() -> {
			            mp.onNext("test1");
			            mp.onNext("test2");
		            })
		            .expectNext("test1")
		            .expectComplete()
		            .verifyThenAssertThat()
		            .hasDroppedExactly("test2");
	}

	@Test
	public void MonoProcessorNullFulfill() {
		MonoProcessor<String> mp = MonoProcessor.create();

		mp.onNext(null);

		assertThat(mp.isTerminated()).isTrue();
		assertThat(mp.isSuccess()).isTrue();
		assertThat(mp.peek()).isNull();
	}

	@Test
	public void MonoProcessorMapFulfill() {
		MonoProcessor<Integer> mp = MonoProcessor.create();

		mp.onNext(1);

		MonoProcessor<Integer> mp2 = mp.map(s -> s * 2)
		                               .toProcessor();
		mp2.subscribe();

		assertThat(mp2.isTerminated()).isTrue();
		assertThat(mp2.isSuccess()).isTrue();
		assertThat(mp2.peek()).isEqualTo(2);
	}

	@Test
	public void MonoProcessorThenFulfill() {
		MonoProcessor<Integer> mp = MonoProcessor.create();

		mp.onNext(1);

		MonoProcessor<Integer> mp2 = mp.flatMap(s -> Mono.just(s * 2))
		                               .toProcessor();
		mp2.subscribe();

		assertThat(mp2.isTerminated()).isTrue();
		assertThat(mp2.isSuccess()).isTrue();
		assertThat(mp2.peek()).isEqualTo(2);
	}

	@Test
	public void MonoProcessorMapError() {
		MonoProcessor<Integer> mp = MonoProcessor.create();

		mp.onNext(1);

		MonoProcessor<Integer> mp2 = MonoProcessor.create();

		StepVerifier.create(mp.<Integer>map(s -> {
			throw new RuntimeException("test");
		}).subscribeWith(mp2), 0)
		            .thenRequest(1)
		            .then(() -> {
			            assertThat(mp2.isTerminated()).isTrue();
			            assertThat(mp2.isSuccess()).isFalse();
			            assertThat(mp2.getError()).hasMessage("test");
		            })
		            .verifyErrorMessage("test");
	}

	@Test(expected = Exception.class)
	public void MonoProcessorDoubleError() {
		MonoProcessor<String> mp = MonoProcessor.create();

		mp.onError(new Exception("test"));
		mp.onError(new Exception("test"));
	}

	@Test(expected = Exception.class)
	public void MonoProcessorDoubleSignal() {
		MonoProcessor<String> mp = MonoProcessor.create();

		mp.onNext("test");
		mp.onError(new Exception("test"));
	}

	@Test
	public void zipMonoProcessor() {
		MonoProcessor<Integer> mp = MonoProcessor.create();
		MonoProcessor<Integer> mp2 = MonoProcessor.create();
		MonoProcessor<Tuple2<Integer, Integer>> mp3 = MonoProcessor.create();

		StepVerifier.create(Mono.zip(mp, mp2)
		                        .subscribeWith(mp3))
		            .then(() -> assertThat(mp3.isPending()).isTrue())
		            .then(() -> mp.onNext(1))
		            .then(() -> assertThat(mp3.isPending()).isTrue())
		            .then(() -> mp2.onNext(2))
		            .then(() -> {
			            assertThat(mp3.isTerminated()).isTrue();
			            assertThat(mp3.isSuccess()).isTrue();
			            assertThat(mp3.isPending()).isFalse();
			            assertThat(mp3.peek()
			                          .getT1()).isEqualTo(1);
			            assertThat(mp3.peek()
			                          .getT2()).isEqualTo(2);
		            })
		            .expectNextMatches(t -> t.getT1() == 1 && t.getT2() == 2)
		            .verifyComplete();
	}

	@Test
	public void zipMonoProcessor2() {
		MonoProcessor<Integer> mp = MonoProcessor.create();
		MonoProcessor<Integer> mp3 = MonoProcessor.create();

		StepVerifier.create(Mono.zip(d -> (Integer)d[0], mp)
		                        .subscribeWith(mp3))
		            .then(() -> assertThat(mp3.isPending()).isTrue())
		            .then(() -> mp.onNext(1))
		            .then(() -> {
			            assertThat(mp3.isTerminated()).isTrue();
			            assertThat(mp3.isSuccess()).isTrue();
			            assertThat(mp3.isPending()).isFalse();
			            assertThat(mp3.peek()).isEqualTo(1);
		            })
		            .expectNext(1)
		            .verifyComplete();
	}

	@Test
	public void zipMonoProcessorRejected() {
		MonoProcessor<Integer> mp = MonoProcessor.create();
		MonoProcessor<Integer> mp2 = MonoProcessor.create();
		MonoProcessor<Tuple2<Integer, Integer>> mp3 = MonoProcessor.create();

		StepVerifier.create(Mono.zip(mp, mp2)
		                        .subscribeWith(mp3))
		            .then(() -> assertThat(mp3.isPending()).isTrue())
		            .then(() -> mp.onError(new Exception("test")))
		            .then(() -> {
			            assertThat(mp3.isTerminated()).isTrue();
			            assertThat(mp3.isSuccess()).isFalse();
			            assertThat(mp3.isPending()).isFalse();
			            assertThat(mp3.getError()).hasMessage("test");
		            })
		            .verifyErrorMessage("test");
	}


	@Test
	public void filterMonoProcessor() {
		MonoProcessor<Integer> mp = MonoProcessor.create();
		MonoProcessor<Integer> mp2 = MonoProcessor.create();
		StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2))
		            .then(() -> mp.onNext(2))
		            .then(() -> assertThat(mp2.isError()).isFalse())
		            .then(() -> assertThat(mp2.isSuccess()).isTrue())
		            .then(() -> assertThat(mp2.peek()).isEqualTo(2))
		            .then(() -> assertThat(mp2.isTerminated()).isTrue())
		            .expectNext(2)
		            .verifyComplete();
	}


	@Test
	public void filterMonoProcessorNot() {
		MonoProcessor<Integer> mp = MonoProcessor.create();
		MonoProcessor<Integer> mp2 = MonoProcessor.create();
		StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2))
		            .then(() -> mp.onNext(1))
		            .then(() -> assertThat(mp2.isError()).isFalse())
		            .then(() -> assertThat(mp2.isSuccess()).isTrue())
		            .then(() -> assertThat(mp2.peek()).isNull())
		            .then(() -> assertThat(mp2.isTerminated()).isTrue())
		            .verifyComplete();
	}

	@Test
	public void filterMonoProcessorError() {
		MonoProcessor<Integer> mp = MonoProcessor.create();
		MonoProcessor<Integer> mp2 = MonoProcessor.create();
		StepVerifier.create(mp.filter(s -> {throw new RuntimeException("test"); })
					.subscribeWith
						(mp2))
		            .then(() -> mp.onNext(2))
		            .then(() -> assertThat(mp2.isError()).isTrue())
		            .then(() -> assertThat(mp2.isSuccess()).isFalse())
		            .then(() -> assertThat(mp2.getError()).hasMessage("test"))
		            .then(() -> assertThat(mp2.isTerminated()).isTrue())
		            .verifyErrorMessage("test");
	}

	@Test
	public void doOnSuccessMonoProcessorError() {
		MonoProcessor<Integer> mp = MonoProcessor.create();
		MonoProcessor<Integer> mp2 = MonoProcessor.create();
		AtomicReference<Throwable> ref = new AtomicReference<>();

		StepVerifier.create(mp.doOnSuccess(s -> {throw new RuntimeException("test"); })
		                      .doOnError(ref::set)
					.subscribeWith
						(mp2))
		            .then(() -> mp.onNext(2))
		            .then(() -> assertThat(mp2.isError()).isTrue())
		            .then(() -> assertThat(ref.get()).hasMessage("test"))
		            .then(() -> assertThat(mp2.isSuccess()).isFalse())
		            .then(() -> assertThat(mp2.getError()).hasMessage("test"))
		            .then(() -> assertThat(mp2.isTerminated()).isTrue())
		            .verifyErrorMessage("test");
	}

	@Test
	public void fluxCancelledByMonoProcessor() {
		AtomicLong cancelCounter = new AtomicLong();
		Flux.range(1, 10)
		    .doOnCancel(cancelCounter::incrementAndGet)
		    .publishNext()
		    .subscribe();

		assertThat(cancelCounter.get()).isEqualTo(1);
	}

	@Test
	public void monoNotCancelledByMonoProcessor() {
		AtomicLong cancelCounter = new AtomicLong();
		MonoProcessor<String> monoProcessor = Mono.just("foo")
		                                          .doOnCancel(cancelCounter::incrementAndGet)
		                                          .toProcessor();
		monoProcessor.subscribe();

		assertThat(cancelCounter.get()).isEqualTo(0);
	}

	@Test
	public void scanProcessor() {
		MonoProcessor<String> test = MonoProcessor.create();
		Subscription subscription = Operators.emptySubscription();
		test.onSubscribe(subscription);

		assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE);
		assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse();
		assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse();

		test.onComplete();
		assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue();
		assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse();
	}

	@Test
	public void scanProcessorCancelled() {
		MonoProcessor<String> test = MonoProcessor.create();
		Subscription subscription = Operators.emptySubscription();
		test.onSubscribe(subscription);

		assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE);
		assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse();
		assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse();

		test.cancel();
		assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse();
		assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue();
	}

	@Test
	public void scanProcessorSubscription() {
		MonoProcessor<String> test = MonoProcessor.create();
		Subscription subscription = Operators.emptySubscription();
		test.onSubscribe(subscription);

		assertThat(test.scan(Scannable.Attr.ACTUAL)).isNull();
		assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(subscription);
	}

	@Test
	public void scanProcessorError() {
		MonoProcessor<String> test = MonoProcessor.create();
		Subscription subscription = Operators.emptySubscription();
		test.onSubscribe(subscription);

		test.onError(new IllegalStateException("boom"));

		assertThat(test.scan(Scannable.Attr.ERROR)).hasMessage("boom");
		assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue();
	}

	@Test
	public void monoToProcessorReusesInstance() {
		MonoProcessor<String> monoProcessor = Mono.just("foo")
		                                          .toProcessor();

		assertThat(monoProcessor)
				.isSameAs(monoProcessor.toProcessor())
				.isSameAs(monoProcessor.subscribe());
	}

	@Test
	public void monoToProcessorConnects() {
		TestPublisher<String> tp = TestPublisher.create();
		MonoProcessor<String> connectedProcessor = tp.mono().toProcessor();

		assertThat(connectedProcessor.subscription).isNotNull();
	}

	@Test
	public void monoToProcessorChain() {
		StepVerifier.withVirtualTime(() -> Mono.just("foo")
		                                       .toProcessor()
		                                       .delayElement(Duration.ofMillis(500)))
		            .expectSubscription()
		            .expectNoEvent(Duration.ofMillis(500))
		            .expectNext("foo")
		            .verifyComplete();
	}

	@Test
	public void monoToProcessorChainColdToHot() {
		AtomicInteger subscriptionCount = new AtomicInteger();
		Mono<String> coldToHot = Mono.just("foo")
		                             .doOnSubscribe(sub -> subscriptionCount.incrementAndGet())
		                             .toProcessor() //this actually subscribes
		                             .filter(s -> s.length() < 4);

		assertThat(subscriptionCount.get()).isEqualTo(1);

		coldToHot.block();
		coldToHot.block();
		coldToHot.block();

		assertThat(subscriptionCount.get()).isEqualTo(1);
	}

	@Test
	public void monoProcessorBlockIsUnbounded() {
		long start = System.nanoTime();

		String result = Mono.just("foo")
		                    .delayElement(Duration.ofMillis(500))
		                    .toProcessor()
		                    .block();

		assertThat(result).isEqualTo("foo");
		assertThat(Duration.ofNanos(System.nanoTime() - start))
				.isGreaterThanOrEqualTo(Duration.ofMillis(500));
	}

	@Test
	public void monoProcessorBlockNegativeIsImmediateTimeout() {
		long start = System.nanoTime();

		assertThatExceptionOfType(IllegalStateException.class)
				.isThrownBy(() -> Mono.just("foo")
				                      .delayElement(Duration.ofMillis(500))
				                      .toProcessor()
				                      .block(Duration.ofSeconds(-1)))
				.withMessage("Timeout on Mono blocking read");

		assertThat(Duration.ofNanos(System.nanoTime() - start))
				.isLessThan(Duration.ofMillis(500));
	}

	@Test
	public void monoProcessorBlockZeroIsImmediateTimeout() {
		long start = System.nanoTime();

		assertThatExceptionOfType(IllegalStateException.class)
				.isThrownBy(() -> Mono.just("foo")
				                      .delayElement(Duration.ofMillis(500))
				                      .toProcessor()
				                      .block(Duration.ZERO))
		        .withMessage("Timeout on Mono blocking read");

		assertThat(Duration.ofNanos(System.nanoTime() - start))
				.isLessThan(Duration.ofMillis(500));
	}

	@Test
	public void disposeBeforeValueSendsCancellationException() {
		MonoProcessor<String> processor = MonoProcessor.create();
		AtomicReference<Throwable> e1 = new AtomicReference<>();
		AtomicReference<Throwable> e2 = new AtomicReference<>();
		AtomicReference<Throwable> e3 = new AtomicReference<>();
		AtomicReference<Throwable> late = new AtomicReference<>();

		processor.subscribe(v -> Assertions.fail("expected first subscriber to error"), e1::set);
		processor.subscribe(v -> Assertions.fail("expected second subscriber to error"), e2::set);
		processor.subscribe(v -> Assertions.fail("expected third subscriber to error"), e3::set);

		processor.dispose();

		assertThat(e1.get()).isInstanceOf(CancellationException.class);
		assertThat(e2.get()).isInstanceOf(CancellationException.class);
		assertThat(e3.get()).isInstanceOf(CancellationException.class);

		processor.subscribe(v -> Assertions.fail("expected late subscriber to error"), late::set);
		assertThat(late.get()).isInstanceOf(CancellationException.class);
	}
}