/*
 * Copyright (c) 2016-present, RxJava 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 io.reactivex.rxjava3.internal.operators.flowable;

import static org.junit.Assert.*;

import java.util.*;
import java.util.concurrent.ExecutionException;

import org.junit.Test;
import org.reactivestreams.*;

import io.reactivex.rxjava3.core.*;
import io.reactivex.rxjava3.functions.*;
import io.reactivex.rxjava3.internal.subscriptions.BooleanSubscription;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subscribers.*;
import io.reactivex.rxjava3.testsupport.*;

public class FlowableMaterializeTest extends RxJavaTest {

    @Test
    public void materialize1() {
        // null will cause onError to be triggered before "three" can be
        // returned
        final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", null,
                "three");

        TestNotificationSubscriber observer = new TestNotificationSubscriber();
        Flowable<Notification<String>> m = Flowable.unsafeCreate(o1).materialize();
        m.subscribe(observer);

        try {
            o1.t.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        assertFalse(observer.onError);
        assertTrue(observer.onComplete);
        assertEquals(3, observer.notifications.size());

        assertTrue(observer.notifications.get(0).isOnNext());
        assertEquals("one", observer.notifications.get(0).getValue());

        assertTrue(observer.notifications.get(1).isOnNext());
        assertEquals("two", observer.notifications.get(1).getValue());

        assertTrue(observer.notifications.get(2).isOnError());
        assertEquals(NullPointerException.class, observer.notifications.get(2).getError().getClass());
    }

    @Test
    public void materialize2() {
        final TestAsyncErrorObservable o1 = new TestAsyncErrorObservable("one", "two", "three");

        TestNotificationSubscriber subscriber = new TestNotificationSubscriber();
        Flowable<Notification<String>> m = Flowable.unsafeCreate(o1).materialize();
        m.subscribe(subscriber);

        try {
            o1.t.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        assertFalse(subscriber.onError);
        assertTrue(subscriber.onComplete);
        assertEquals(4, subscriber.notifications.size());
        assertTrue(subscriber.notifications.get(0).isOnNext());
        assertEquals("one", subscriber.notifications.get(0).getValue());

        assertTrue(subscriber.notifications.get(1).isOnNext());
        assertEquals("two", subscriber.notifications.get(1).getValue());

        assertTrue(subscriber.notifications.get(2).isOnNext());
        assertEquals("three", subscriber.notifications.get(2).getValue());

        assertTrue(subscriber.notifications.get(3).isOnComplete());
    }

    @Test
    public void multipleSubscribes() throws InterruptedException, ExecutionException {
        final TestAsyncErrorObservable o = new TestAsyncErrorObservable("one", "two", null, "three");

        Flowable<Notification<String>> m = Flowable.unsafeCreate(o).materialize();

        assertEquals(3, m.toList().toFuture().get().size());
        assertEquals(3, m.toList().toFuture().get().size());
    }

    @Test
    public void backpressureOnEmptyStream() {
        TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L);
        Flowable.<Integer> empty().materialize().subscribe(ts);
        ts.assertNoValues();
        ts.request(1);
        ts.assertValueCount(1);
        assertTrue(ts.values().get(0).isOnComplete());
        ts.assertComplete();
    }

    @Test
    public void backpressureNoError() {
        TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L);
        Flowable.just(1, 2, 3).materialize().subscribe(ts);
        ts.assertNoValues();
        ts.request(1);
        ts.assertValueCount(1);
        ts.request(2);
        ts.assertValueCount(3);
        ts.request(1);
        ts.assertValueCount(4);
        ts.assertComplete();
    }

    @Test
    public void backpressureNoErrorAsync() throws InterruptedException {
        TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L);
        Flowable.just(1, 2, 3)
            .materialize()
            .subscribeOn(Schedulers.computation())
            .subscribe(ts);
        Thread.sleep(100);
        ts.assertNoValues();
        ts.request(1);
        Thread.sleep(100);
        ts.assertValueCount(1);
        ts.request(2);
        Thread.sleep(100);
        ts.assertValueCount(3);
        ts.request(1);
        Thread.sleep(100);
        ts.assertValueCount(4);
        ts.assertComplete();
    }

    @Test
    public void backpressureWithError() {
        TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L);
        Flowable.<Integer> error(new IllegalArgumentException()).materialize().subscribe(ts);
        ts.assertNoValues();
        ts.request(1);
        ts.assertValueCount(1);
        ts.assertComplete();
    }

    @Test
    public void backpressureWithEmissionThenError() {
        TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L);
        IllegalArgumentException ex = new IllegalArgumentException();
        Flowable.fromIterable(Arrays.asList(1)).concatWith(Flowable.<Integer> error(ex)).materialize()
                .subscribe(ts);
        ts.assertNoValues();
        ts.request(1);
        ts.assertValueCount(1);
        assertTrue(ts.values().get(0).isOnNext());
        ts.request(1);
        ts.assertValueCount(2);
        assertTrue(ts.values().get(1).isOnError());
        assertEquals(ex, ts.values().get(1).getError());
        ts.assertComplete();
    }

    @Test
    public void withCompletionCausingError() {
        TestSubscriberEx<Notification<Integer>> ts = new TestSubscriberEx<>();
        final RuntimeException ex = new RuntimeException("boo");
        Flowable.<Integer>empty().materialize().doOnNext(new Consumer<Object>() {
            @Override
            public void accept(Object t) {
                throw ex;
            }
        }).subscribe(ts);
        ts.assertError(ex);
        ts.assertNoValues();
        ts.assertTerminated();
    }

    @Test
    public void unsubscribeJustBeforeCompletionNotificationShouldPreventThatNotificationArriving() {
        TestSubscriber<Notification<Integer>> ts = new TestSubscriber<>(0L);

        Flowable.<Integer>empty().materialize()
                .subscribe(ts);
        ts.assertNoValues();
        ts.cancel();
        ts.request(1);
        ts.assertNoValues();
    }

    private static class TestNotificationSubscriber extends DefaultSubscriber<Notification<String>> {

        boolean onComplete;
        boolean onError;
        List<Notification<String>> notifications = new Vector<>();

        @Override
        public void onComplete() {
            this.onComplete = true;
        }

        @Override
        public void onError(Throwable e) {
            this.onError = true;
        }

        @Override
        public void onNext(Notification<String> value) {
            this.notifications.add(value);
        }

    }

    private static class TestAsyncErrorObservable implements Publisher<String> {

        String[] valuesToReturn;

        TestAsyncErrorObservable(String... values) {
            valuesToReturn = values;
        }

        volatile Thread t;

        @Override
        public void subscribe(final Subscriber<? super String> subscriber) {
            subscriber.onSubscribe(new BooleanSubscription());
            t = new Thread(new Runnable() {

                @Override
                public void run() {
                    for (String s : valuesToReturn) {
                        if (s == null) {
                            System.out.println("throwing exception");
                            try {
                                Thread.sleep(100);
                            } catch (Throwable e) {

                            }
                            subscriber.onError(new NullPointerException());
                            return;
                        } else {
                            subscriber.onNext(s);
                        }
                    }
                    System.out.println("subscription complete");
                    subscriber.onComplete();
                }

            });
            t.start();
        }
    }

    @Test
    public void backpressure() {
        TestSubscriber<Notification<Integer>> ts = Flowable.range(1, 5).materialize().test(0);

        ts.assertEmpty();

        ts.request(5);

        ts.assertValueCount(5)
        .assertNoErrors()
        .assertNotComplete();

        ts.request(1);

        ts.assertValueCount(6)
        .assertNoErrors()
        .assertComplete();
    }

    @Test
    public void dispose() {
        TestHelper.checkDisposed(Flowable.just(1).materialize());
    }

    @Test
    public void doubleOnSubscribe() {
        TestHelper.checkDoubleOnSubscribeFlowable(new Function<Flowable<Object>, Flowable<Notification<Object>>>() {
            @Override
            public Flowable<Notification<Object>> apply(Flowable<Object> f) throws Exception {
                return f.materialize();
            }
        });
    }

    @Test
    public void badSource() {
        TestHelper.checkBadSourceFlowable(new Function<Flowable<Object>, Object>() {
            @Override
            public Object apply(Flowable<Object> f) throws Exception {
                return f.materialize();
            }
        }, false, null, null, Notification.createOnComplete());
    }

    @Test
    public void badRequest() {
        TestHelper.assertBadRequestReported(Flowable.just(1).materialize());
    }
}
