/*
 * Copyright (C) 2016 Airbnb, Inc.
 *
 * 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 com.airbnb.rxgroups;

import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.Callable;

import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.annotations.NonNull;
import io.reactivex.functions.Function;
import io.reactivex.observers.TestObserver;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;

public class ObservableGroupTest {
  private final ObservableManager observableManager = new ObservableManager();
  private final TestAutoResubscribingObserver fooObserver =
      new TestAutoResubscribingObserver("foo");
  private final TestAutoResubscribingObserver barObserver =
      new TestAutoResubscribingObserver("bar");

  @Before public void setUp() throws IOException {
    RxJavaPlugins.setInitIoSchedulerHandler(new Function<Callable<Scheduler>, Scheduler>() {
      @Override
      public Scheduler apply(@NonNull Callable<Scheduler> schedulerCallable) throws Exception {
        return Schedulers.trampoline();
      }
    });
  }

  @Test public void shouldNotHaveObservable() {
    ObservableGroup group = observableManager.newGroup();
    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
  }

  @Test public void shouldAddRequestByObserverTag() {
    ObservableGroup group = observableManager.newGroup();
    ObservableGroup group2 = observableManager.newGroup();
    Observable<String> sourceObservable = Observable.never();
    sourceObservable.compose(group.transform(fooObserver)).subscribe(fooObserver);

    assertThat(group.hasObservables(fooObserver)).isEqualTo(true);
    assertThat(group2.hasObservables(fooObserver)).isEqualTo(false);
    assertThat(group.hasObservables(barObserver)).isEqualTo(false);
  }

  @Test public void shouldNotBeCompleted() {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<Object> subscriber = new TestObserver<>();
    Observable<String> sourceObservable = Observable.never();

    sourceObservable.compose(group.transform(fooObserver)).subscribe(fooObserver);
    subscriber.assertNotComplete();
  }

  @Test public void shouldBeSubscribed() {
    ObservableGroup group = observableManager.newGroup();
    Observable<String> sourceObservable = Observable.never();
    sourceObservable.compose(group.transform(fooObserver)).subscribe(fooObserver);

    assertThat(group.subscription(fooObserver).isCancelled()).isEqualTo(false);
  }

  @Test public void shouldDeliverSuccessfulEvent() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> sourceObservable = PublishSubject.create();
    TestObserver<String> subscriber = new TestObserver<>();

    sourceObservable.compose(group.transform(subscriber)).subscribe(subscriber);
    subscriber.assertNotComplete();

    sourceObservable.onNext("Foo Bar");
    sourceObservable.onComplete();

    subscriber.assertComplete();
    subscriber.assertValue("Foo Bar");
  }

  @Test public void shouldDeliverError() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<String> testObserver = new TestObserver<>();
    Observable<String> sourceObservable = Observable.error(new RuntimeException("boom"));
    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);

    testObserver.assertError(RuntimeException.class);
  }

  @Test public void shouldSeparateObservablesByGroupId() {
    ObservableGroup group = observableManager.newGroup();
    ObservableGroup group2 = observableManager.newGroup();
    Observable<String> observable1 = Observable.never();
    Observable<String> observable2 = Observable.never();

    observable1.compose(group.transform(barObserver)).subscribe(barObserver);
    assertThat(group.hasObservables(barObserver)).isEqualTo(true);
    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
    assertThat(group2.hasObservables(barObserver)).isEqualTo(false);
    assertThat(group2.hasObservables(fooObserver)).isEqualTo(false);

    observable2.compose(group2.transform(fooObserver)).subscribe(fooObserver);
    assertThat(group.hasObservables(barObserver)).isEqualTo(true);
    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
    assertThat(group2.hasObservables(barObserver)).isEqualTo(false);
    assertThat(group2.hasObservables(fooObserver)).isEqualTo(true);
  }

  @Test public void shouldClearObservablesByGroupId() {
    ObservableGroup group = observableManager.newGroup();
    ObservableGroup group2 = observableManager.newGroup();
    Observable<String> observable1 = Observable.never();
    Observable<String> observable2 = Observable.never();

    observable1.compose(group.transform(fooObserver)).subscribe(fooObserver);
    observable2.compose(group2.transform(fooObserver)).subscribe(fooObserver);

    observableManager.destroy(group);

    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
    assertThat(group2.hasObservables(fooObserver)).isEqualTo(true);
    assertThat(group.subscription(fooObserver)).isNull();
    assertThat(group2.subscription(fooObserver).isCancelled()).isEqualTo(false);

    observableManager.destroy(group2);
    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
    assertThat(group2.hasObservables(fooObserver)).isEqualTo(false);
    assertThat(group.subscription(fooObserver)).isNull();
    assertThat(group2.subscription(fooObserver)).isNull();
  }

  @Test public void shouldClearObservablesWhenLocked() {
    ObservableGroup group = observableManager.newGroup();
    Observable<String> observable1 = Observable.never();
    Observable<String> observable2 = Observable.never();
    TestObserver<String> subscriber1 = new TestObserver<>();
    TestObserver<String> subscriber2 = new TestObserver<>();

    observable1.compose(group.transform(subscriber1)).subscribe(subscriber1);
    observable2.compose(group.transform(subscriber2)).subscribe(subscriber2);

    group.dispose();
    observableManager.destroy(group);

    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
    assertThat(group.hasObservables(barObserver)).isEqualTo(false);
  }

  @Test public void shouldClearQueuedResults() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> sourceObservable = PublishSubject.create();
    TestObserver<String> subscriber1 = new TestObserver<>();

    sourceObservable.compose(group.transform(subscriber1)).subscribe(subscriber1);
    group.dispose();
    sourceObservable.onNext("Hello");
    sourceObservable.onComplete();
    observableManager.destroy(group);

    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
  }

  @Test public void shouldRemoveObservablesAfterTermination() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> sourceObservable = PublishSubject.create();
    TestObserver<String> subscriber = new TestObserver<>();
    sourceObservable.compose(group.transform(subscriber)).subscribe(subscriber);

    sourceObservable.onNext("Roberto Gomez Bolanos is king");
    sourceObservable.onComplete();

    subscriber.assertComplete();
    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
  }

  @Test public void shouldRemoveResponseAfterErrorDelivery() throws InterruptedException {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<String> testObserver = new TestObserver<>();
    PublishSubject<String> sourceObservable = PublishSubject.create();

    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);

    sourceObservable.onError(new RuntimeException("BOOM!"));

    testObserver.assertError(Exception.class);

    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
  }

  @Test public void shouldNotDeliverResultWhileUnsubscribed() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<String> testObserver = new TestObserver<>();
    PublishSubject<String> sourceObservable = PublishSubject.create();

    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);
    group.dispose();

    sourceObservable.onNext("Roberto Gomez Bolanos");
    sourceObservable.onComplete();

    testObserver.assertNotComplete();
    assertThat(group.hasObservables(testObserver)).isEqualTo(true);
  }

  @Test public void shouldDeliverQueuedEventsWhenResubscribed() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    TestAutoResubscribingObserver resubscribingObserver = new TestAutoResubscribingObserver("foo");
    PublishSubject<String> sourceObservable = PublishSubject.create();
    sourceObservable.compose(group.transform(resubscribingObserver))
        .subscribe(resubscribingObserver);
    group.dispose();

    sourceObservable.onNext("Hello World");
    sourceObservable.onComplete();

    resubscribingObserver.assertionTarget.assertNotComplete();
    resubscribingObserver.assertionTarget.assertNoValues();

    // TestObserver cannot be reused after being disposed in RxJava2
    resubscribingObserver = new TestAutoResubscribingObserver("foo");
    group.observable(resubscribingObserver).subscribe(resubscribingObserver);

    resubscribingObserver.assertionTarget.assertComplete();
    resubscribingObserver.assertionTarget.assertValue("Hello World");
    assertThat(group.hasObservables(resubscribingObserver)).isEqualTo(false);
  }

  @Test public void shouldDeliverQueuedErrorWhenResubscribed() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    TestAutoResubscribingObserver resubscribingObserver = new TestAutoResubscribingObserver("foo");
    PublishSubject<String> sourceObservable = PublishSubject.create();

    sourceObservable.compose(group.transform(resubscribingObserver))
        .subscribe(resubscribingObserver);
    group.dispose();

    sourceObservable.onError(new Exception("Exploded"));

    resubscribingObserver.assertionTarget.assertNotComplete();
    resubscribingObserver.assertionTarget.assertNoValues();

    resubscribingObserver = new TestAutoResubscribingObserver("foo");
    group.observable(resubscribingObserver).subscribe(resubscribingObserver);

    resubscribingObserver.assertionTarget.assertError(Exception.class);
    assertThat(group.hasObservables(resubscribingObserver)).isEqualTo(false);
  }

  @Test public void shouldNotDeliverEventsWhenResubscribedIfLocked() {
    ObservableGroup group = observableManager.newGroup();
    TestAutoResubscribingObserver testObserver = new TestAutoResubscribingObserver("foo");
    PublishSubject<String> sourceObservable = PublishSubject.create();
    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);
    group.dispose();

    sourceObservable.onNext("Hello World");
    sourceObservable.onComplete();

    group.lock();
    testObserver = new TestAutoResubscribingObserver("foo");
    group.observable(testObserver).subscribe(testObserver);

    testObserver.assertionTarget.assertNotComplete();
    testObserver.assertionTarget.assertNoValues();

    group.unlock();
    testObserver.assertionTarget.assertComplete();
    testObserver.assertionTarget.assertNoErrors();
    testObserver.assertionTarget.assertValue("Hello World");
    assertThat(group.hasObservables(testObserver)).isEqualTo(false);
  }

  @Test public void shouldUnsubscribeByContext() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    ObservableGroup group2 = observableManager.newGroup();
    PublishSubject<String> sourceObservable = PublishSubject.create();
    TestObserver<String> testObserver = new TestObserver<>();

    sourceObservable.compose(group2.transform(testObserver)).subscribe(testObserver);
    group.dispose();

    sourceObservable.onNext("Gremio Foot-ball Porto Alegrense");
    sourceObservable.onComplete();

    testObserver.assertComplete();
    testObserver.assertNoErrors();
    testObserver.assertValue("Gremio Foot-ball Porto Alegrense");

    assertThat(group2.hasObservables(fooObserver)).isEqualTo(false);
  }

  @Test public void shouldNotDeliverEventsAfterCancelled() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> sourceObservable = PublishSubject.create();
    TestObserver<String> testObserver = new TestObserver<>();

    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);
    observableManager.destroy(group);

    sourceObservable.onNext("Gremio Foot-ball Porto Alegrense");
    sourceObservable.onComplete();

    testObserver.assertNotComplete();
    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
  }

  @Test public void shouldNotRemoveSubscribersForOtherIds() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    ObservableGroup group2 = observableManager.newGroup();
    PublishSubject<String> subject1 = PublishSubject.create();
    TestAutoResubscribingObserver testSubscriber1 = new TestAutoResubscribingObserver("foo");
    PublishSubject<String> subject2 = PublishSubject.create();
    TestAutoResubscribingObserver testSubscriber2 = new TestAutoResubscribingObserver("bar");

    subject1.compose(group.transform(testSubscriber1)).subscribe(testSubscriber1);
    subject2.compose(group2.transform(testSubscriber2)).subscribe(testSubscriber2);
    group.dispose();

    subject1.onNext("Florinda Mesa");
    subject1.onComplete();
    subject2.onNext("Carlos Villagran");
    subject2.onComplete();

    testSubscriber1.assertionTarget.assertNotComplete();
    testSubscriber2.assertionTarget.assertNoErrors();
    testSubscriber2.assertionTarget.assertValue("Carlos Villagran");
  }

  @Test public void shouldOverrideExistingSubscriber() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> sourceObservable = PublishSubject.create();
    TestAutoResubscribingObserver testSubscriber1 = new TestAutoResubscribingObserver("foo");
    TestAutoResubscribingObserver testSubscriber2 = new TestAutoResubscribingObserver("foo");

    sourceObservable.compose(group.transform(testSubscriber1)).subscribe(testSubscriber1);
    sourceObservable.compose(group.transform(testSubscriber2)).subscribe(testSubscriber2);

    sourceObservable.onNext("Ruben Aguirre");
    sourceObservable.onComplete();

    testSubscriber1.assertionTarget.assertNotComplete();
    testSubscriber1.assertionTarget.assertNoValues();
    testSubscriber2.assertionTarget.assertComplete();
    testSubscriber2.assertionTarget.assertValue("Ruben Aguirre");
  }

  @Test public void shouldQueueMultipleRequests() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> subject1 = PublishSubject.create();
    TestObserver<String> testSubscriber1 = new TestObserver<>();
    PublishSubject<String> subject2 = PublishSubject.create();
    TestObserver<String> testSubscriber2 = new TestObserver<>();

    subject1.compose(group.transform(testSubscriber1)).subscribe(testSubscriber1);
    subject2.compose(group.transform(testSubscriber2)).subscribe(testSubscriber2);
    group.dispose();

    subject1.onNext("Chespirito");
    subject1.onComplete();
    subject2.onNext("Edgar Vivar");
    subject2.onComplete();

    testSubscriber1.assertNotComplete();
    testSubscriber2.assertNotComplete();
    assertThat(group.hasObservables(testSubscriber1)).isEqualTo(true);
    assertThat(group.hasObservables(testSubscriber2)).isEqualTo(true);
  }

  @Test public void shouldNotDeliverResultWhileLocked() throws Exception {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<String> testObserver = new TestObserver<>();
    PublishSubject<String> sourceObservable = PublishSubject.create();

    group.lock();
    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);

    sourceObservable.onNext("Chespirito");
    sourceObservable.onComplete();

    testObserver.assertNotComplete();
    testObserver.assertNoValues();
    assertThat(group.hasObservables(testObserver)).isEqualTo(true);
  }

  @Test public void shouldAutoResubscribeAfterUnlock() throws InterruptedException {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<String> testObserver = new TestObserver<>();
    PublishSubject<String> sourceObservable = PublishSubject.create();

    group.lock();
    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);

    sourceObservable.onNext("Chespirito");
    sourceObservable.onComplete();

    group.unlock();

    testObserver.assertComplete();
    testObserver.assertNoErrors();
    testObserver.assertValue("Chespirito");
    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
  }

  @Test public void shouldAutoResubscribeAfterLockAndUnlock() {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<String> testObserver = new TestObserver<>();
    PublishSubject<String> sourceObservable = PublishSubject.create();

    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);
    group.lock();

    sourceObservable.onNext("Chespirito");
    sourceObservable.onComplete();

    group.unlock();

    testObserver.assertTerminated();
    testObserver.assertNoErrors();
    testObserver.assertValue("Chespirito");
    assertThat(group.hasObservables(fooObserver)).isEqualTo(false);
  }

  @Test public void testUnsubscribeWhenLocked() {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<String> testObserver = new TestObserver<>();
    PublishSubject<String> sourceObservable = PublishSubject.create();

    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);
    group.lock();
    group.dispose();

    sourceObservable.onNext("Chespirito");
    sourceObservable.onComplete();

    group.unlock();

    testObserver.assertNotComplete();
    testObserver.assertNoValues();
    assertThat(group.hasObservables(testObserver)).isEqualTo(true);
  }

  @Test public void testAddThrowsAfterDestroyed() {
    ObservableGroup group = observableManager.newGroup();
    Observable<String> source = PublishSubject.create();
    TestObserver<String> observer = new TestObserver<>();
    group.destroy();
    source.compose(group.transform(observer)).subscribe(observer);
    observer.assertError(IllegalStateException.class);
  }

  @Test public void testResubscribeThrowsAfterDestroyed() {
    ObservableGroup group = observableManager.newGroup();
    Observable<String> source = PublishSubject.create();
    TestObserver<String> observer = new TestObserver<>();
    try {
      source.compose(group.transform(observer)).subscribe(observer);
      group.dispose();
      group.destroy();
      group.observable(fooObserver).subscribe(new TestObserver<>());
      fail();
    } catch (IllegalStateException ignored) {
    }
  }

  @Test public void shouldReplaceObservablesOfSameTagAndSameGroupId() {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> observable1 = PublishSubject.create();
    PublishSubject<String> observable2 = PublishSubject.create();
    TestAutoResubscribingObserver observer1 = new TestAutoResubscribingObserver("foo");
    TestAutoResubscribingObserver observer2 = new TestAutoResubscribingObserver("foo");
    observable1.compose(group.transform(observer1)).subscribe(observer1);
    observable2.compose(group.transform(observer2)).subscribe(observer2);

    assertThat(group.subscription(fooObserver).isCancelled()).isFalse();
    assertThat(group.hasObservables(fooObserver)).isTrue();

    observable1.onNext("Hello World 1");
    observable1.onComplete();

    observable2.onNext("Hello World 2");
    observable2.onComplete();

    observer2.assertionTarget.awaitTerminalEvent();
    observer2.assertionTarget.assertComplete();
    observer2.assertionTarget.assertValue("Hello World 2");

    observer1.assertionTarget.assertNoValues();
  }

  /**
   * The same observable tag can be used so long as it is associated with a different observer tag.
   */
  @Test public void shouldNotReplaceObservableOfSameTagAndSameGroupIdAndDifferentObservers() {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> observable1 = PublishSubject.create();
    TestObserver<String> observer1 = new TestObserver<>();
    TestObserver<String> observer2 = new TestObserver<>();
    String sharedObservableTag = "sharedTag";
    observable1.compose(group.transform(observer1, sharedObservableTag)).subscribe(observer1);
    observable1.compose(group.transform(observer2, sharedObservableTag)).subscribe(observer2);

    assertThat(group.subscription(observer1, sharedObservableTag).isCancelled()).isFalse();
    assertThat(group.hasObservables(observer1)).isTrue();

    assertThat(group.subscription(observer2, sharedObservableTag).isCancelled()).isFalse();
    assertThat(group.hasObservables(observer2)).isTrue();

    observable1.onNext("Hello World 1");
    observable1.onComplete();

    observer2.assertComplete();
    observer2.assertValue("Hello World 1");

    observer1.assertComplete();
    observer1.assertValue("Hello World 1");
  }

  @Test public void testCancelAndReAddSubscription() {
    ObservableGroup group = observableManager.newGroup();
    PublishSubject<String> sourceObservable = PublishSubject.create();
    sourceObservable.compose(group.transform(fooObserver)).subscribe(fooObserver);
    group.cancelAllObservablesForObserver(fooObserver);
    assertThat(group.subscription(fooObserver)).isNull();

    sourceObservable.compose(group.transform(fooObserver)).subscribe(fooObserver);

    assertThat(group.subscription(fooObserver).isCancelled()).isFalse();
  }

  @Test public void testDisposingObserver() {
    ObservableGroup group = observableManager.newGroup();
    TestObserver<String> testObserver = new TestObserver<>();
    PublishSubject<String> sourceObservable = PublishSubject.create();

    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);

    testObserver.dispose();

    sourceObservable.onNext("Chespirito");
    testObserver.assertNoValues();

    assertThat(group.hasObservables(testObserver)).isEqualTo(true);
  }

  @Test public void testDisposingObserverResubscribe() {
    ObservableGroup group = observableManager.newGroup();
    TestAutoResubscribingObserver testObserver = new TestAutoResubscribingObserver("foo");
    PublishSubject<String> sourceObservable = PublishSubject.create();

    sourceObservable.compose(group.transform(testObserver)).subscribe(testObserver);

    testObserver.dispose();

    sourceObservable.onNext("Chespirito");
    testObserver.assertionTarget.assertNoValues();

    testObserver = new TestAutoResubscribingObserver("foo");

    group.resubscribe(testObserver);
    testObserver.assertionTarget.assertValue("Chespirito");
  }

}