package org.jooby.rx;

import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.expectLastCall;
import static org.easymock.EasyMock.isA;
import static org.junit.Assert.assertEquals;

import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.function.Function;

import org.jooby.Deferred;
import org.jooby.Env;
import org.jooby.Route;
import org.jooby.Router;
import org.jooby.exec.Exec;
import org.jooby.test.MockUnit;
import org.jooby.test.MockUnit.Block;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import com.google.inject.Binder;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;

import javaslang.control.Try.CheckedRunnable;
import rx.plugins.RxJavaPlugins;
import rx.plugins.RxJavaSchedulersHook;
import rx.schedulers.Schedulers;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Rx.class, Exec.class, Executors.class, ForkJoinPool.class, Thread.class,
    System.class, RxJavaPlugins.class, Schedulers.class })
public class RxTest {

  private Block onStop = unit -> {
    Env env = unit.get(Env.class);
    expect(env.onStop(unit.capture(CheckedRunnable.class))).andReturn(env);
  };

  @Test
  public void configure() throws Exception {
    Config conf = ConfigFactory.empty()
        .withValue("rx.foo", ConfigValueFactory.fromAnyRef("bar"));
    new MockUnit(Env.class, Binder.class, ExecutorService.class, Router.class)
        .expect(unit -> {
          unit.mockStatic(Schedulers.class);
          Schedulers.shutdown();
        })
        .expect(unit -> {
          unit.mockStatic(System.class);
          expect(System.setProperty("rx.foo", "bar")).andReturn(null);
        })
        .expect(unit -> {
          RxJavaPlugins plugins = unit.mock(RxJavaPlugins.class);
          plugins.registerSchedulersHook(isA(RxJavaSchedulersHook.class));

          unit.mockStatic(RxJavaPlugins.class);
          expect(RxJavaPlugins.getInstance()).andReturn(plugins);
        })
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.map(unit.capture(Route.Mapper.class))).andReturn(routes);

          Env env = unit.get(Env.class);
          expect(env.router()).andReturn(routes);
          expect(env.onStop(unit.capture(CheckedRunnable.class))).andReturn(env);
        })
        .expect(onStop)
        .run(unit -> {
          new Rx().configure(unit.get(Env.class), conf, unit.get(Binder.class));
        }, unit -> {
          unit.captured(CheckedRunnable.class).get(1).run();
        });
  }

  @SuppressWarnings({"rawtypes", "unchecked" })
  @Test
  public void withObservableAdapter() throws Exception {
    Config conf = ConfigFactory.empty()
        .withValue("rx.foo", ConfigValueFactory.fromAnyRef("bar"));
    rx.Observable<String> value = rx.Observable.just("1");
    new MockUnit(Env.class, Binder.class, ExecutorService.class, Router.class, Function.class)
        .expect(unit -> {
          unit.mockStatic(Schedulers.class);
          Schedulers.shutdown();
        })
        .expect(unit -> {
          unit.mockStatic(System.class);
          expect(System.setProperty("rx.foo", "bar")).andReturn(null);
        })
        .expect(unit -> {
          RxJavaPlugins plugins = unit.mock(RxJavaPlugins.class);
          plugins.registerSchedulersHook(isA(RxJavaSchedulersHook.class));

          unit.mockStatic(RxJavaPlugins.class);
          expect(RxJavaPlugins.getInstance()).andReturn(plugins);
        })
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.map(unit.capture(Route.Mapper.class))).andReturn(routes);

          Env env = unit.get(Env.class);
          expect(env.router()).andReturn(routes);
          expect(env.onStop(unit.capture(CheckedRunnable.class))).andReturn(env);
        })
        .expect(onStop)
        .expect(unit -> {
          Function adapter = unit.get(Function.class);
          expect(adapter.apply(value)).andReturn(value);
        })
        .run(unit -> {
          new Rx()
              .withObservable(unit.get(Function.class))
              .configure(unit.get(Env.class), conf, unit.get(Binder.class));
        }, unit -> {
          unit.captured(CheckedRunnable.class).get(1).run();

          Deferred deferred = (Deferred) unit.captured(Route.Mapper.class).get(0).map(value);
          deferred.handler(null, (r, x) -> {
          });
        });
  }

  @SuppressWarnings({"rawtypes", "unchecked" })
  @Test
  public void withSingleAdapter() throws Exception {
    Config conf = ConfigFactory.empty()
        .withValue("rx.foo", ConfigValueFactory.fromAnyRef("bar"));
    rx.Single<String> value = rx.Single.just("1");
    new MockUnit(Env.class, Binder.class, ExecutorService.class, Router.class, Function.class)
        .expect(unit -> {
          unit.mockStatic(Schedulers.class);
          Schedulers.shutdown();
        })
        .expect(unit -> {
          unit.mockStatic(System.class);
          expect(System.setProperty("rx.foo", "bar")).andReturn(null);
        })
        .expect(unit -> {
          RxJavaPlugins plugins = unit.mock(RxJavaPlugins.class);
          plugins.registerSchedulersHook(isA(RxJavaSchedulersHook.class));

          unit.mockStatic(RxJavaPlugins.class);
          expect(RxJavaPlugins.getInstance()).andReturn(plugins);
        })
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.map(unit.capture(Route.Mapper.class))).andReturn(routes);

          Env env = unit.get(Env.class);
          expect(env.router()).andReturn(routes);
          expect(env.onStop(unit.capture(CheckedRunnable.class))).andReturn(env);
        })
        .expect(onStop)
        .expect(unit -> {
          Function adapter = unit.get(Function.class);
          expect(adapter.apply(value)).andReturn(value);
        })
        .run(unit -> {
          new Rx()
              .withSingle(unit.get(Function.class))
              .configure(unit.get(Env.class), conf, unit.get(Binder.class));
        }, unit -> {
          unit.captured(CheckedRunnable.class).get(1).run();

          Deferred deferred = (Deferred) unit.captured(Route.Mapper.class).get(0).map(value);
          deferred.handler(null, (r, x) -> {
          });
        });
  }

  @SuppressWarnings({"rawtypes", "unchecked" })
  @Test
  public void withCompletableAdapter() throws Exception {
    Config conf = ConfigFactory.empty()
        .withValue("rx.foo", ConfigValueFactory.fromAnyRef("bar"));
    rx.Completable value = rx.Completable.complete();
    new MockUnit(Env.class, Binder.class, ExecutorService.class, Router.class, Function.class)
        .expect(unit -> {
          unit.mockStatic(Schedulers.class);
          Schedulers.shutdown();
        })
        .expect(unit -> {
          unit.mockStatic(System.class);
          expect(System.setProperty("rx.foo", "bar")).andReturn(null);
        })
        .expect(unit -> {
          RxJavaPlugins plugins = unit.mock(RxJavaPlugins.class);
          plugins.registerSchedulersHook(isA(RxJavaSchedulersHook.class));

          unit.mockStatic(RxJavaPlugins.class);
          expect(RxJavaPlugins.getInstance()).andReturn(plugins);
        })
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.map(unit.capture(Route.Mapper.class))).andReturn(routes);

          Env env = unit.get(Env.class);
          expect(env.router()).andReturn(routes);
          expect(env.onStop(unit.capture(CheckedRunnable.class))).andReturn(env);
        })
        .expect(onStop)
        .expect(unit -> {
          Function adapter = unit.get(Function.class);
          expect(adapter.apply(value)).andReturn(value);
        })
        .run(unit -> {
          new Rx()
              .withCompletable(unit.get(Function.class))
              .configure(unit.get(Env.class), conf, unit.get(Binder.class));
        }, unit -> {
          unit.captured(CheckedRunnable.class).get(1).run();

          Deferred deferred = (Deferred) unit.captured(Route.Mapper.class).get(0).map(value);
          deferred.handler(null, (r, x) -> {
          });
        });
  }

  @Test
  public void shutdownError() throws Exception {
    Config conf = ConfigFactory.empty()
        .withValue("rx.foo", ConfigValueFactory.fromAnyRef("bar"));
    new MockUnit(Env.class, Binder.class, ExecutorService.class, Router.class)
        .expect(unit -> {
          unit.mockStatic(Schedulers.class);
          Schedulers.shutdown();
          expectLastCall().andThrow(new IllegalStateException("intentional err"));
        })
        .expect(unit -> {
          unit.mockStatic(System.class);
          expect(System.setProperty("rx.foo", "bar")).andReturn(null);
        })
        .expect(unit -> {
          RxJavaPlugins plugins = unit.mock(RxJavaPlugins.class);
          plugins.registerSchedulersHook(isA(RxJavaSchedulersHook.class));

          unit.mockStatic(RxJavaPlugins.class);
          expect(RxJavaPlugins.getInstance()).andReturn(plugins);
        })
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.map(unit.capture(Route.Mapper.class))).andReturn(routes);

          Env env = unit.get(Env.class);
          expect(env.onStop(unit.capture(CheckedRunnable.class))).andReturn(env);
          expect(env.router()).andReturn(routes);
        })
        .expect(onStop)
        .run(unit -> {
          new Rx().configure(unit.get(Env.class), conf, unit.get(Binder.class));
        }, unit -> {
          unit.captured(CheckedRunnable.class).get(1).run();
        });
  }

  @Test
  public void shouldNotBreakOnExistingHook() throws Exception {
    Config conf = ConfigFactory.empty()
        .withValue("rx.foo", ConfigValueFactory.fromAnyRef("bar"));
    new MockUnit(Env.class, Binder.class, ExecutorService.class, Router.class)
        .expect(unit -> {
          unit.mockStatic(Schedulers.class);
          Schedulers.shutdown();
        })
        .expect(unit -> {
          unit.mockStatic(System.class);
          expect(System.setProperty("rx.foo", "bar")).andReturn(null);
        })
        .expect(unit -> {
          RxJavaPlugins plugins = unit.mock(RxJavaPlugins.class);
          plugins.registerSchedulersHook(isA(RxJavaSchedulersHook.class));
          expectLastCall().andThrow(new IllegalStateException("Hook present"));
          expect(plugins.getSchedulersHook())
              .andReturn(new ExecSchedulerHook(Collections.emptyMap()));

          unit.mockStatic(RxJavaPlugins.class);
          expect(RxJavaPlugins.getInstance()).andReturn(plugins);
        })
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.map(unit.capture(Route.Mapper.class))).andReturn(routes);

          Env env = unit.get(Env.class);
          expect(env.onStop(unit.capture(CheckedRunnable.class))).andReturn(env);
          expect(env.router()).andReturn(routes);

        })
        .expect(onStop)
        .run(unit -> {
          new Rx().configure(unit.get(Env.class), conf, unit.get(Binder.class));
        }, unit -> {
          unit.captured(CheckedRunnable.class).get(1).run();
        });
  }

  @Test(expected = IllegalStateException.class)
  public void shouldBreakOnDiffHook() throws Exception {
    Config conf = ConfigFactory.empty()
        .withValue("rx.foo", ConfigValueFactory.fromAnyRef("bar"));
    new MockUnit(Env.class, Binder.class, ExecutorService.class, Router.class)
        .expect(unit -> {
          unit.mockStatic(Schedulers.class);
          Schedulers.shutdown();
        })
        .expect(unit -> {
          unit.mockStatic(System.class);
          expect(System.setProperty("rx.foo", "bar")).andReturn(null);
        })
        .expect(unit -> {
          RxJavaPlugins plugins = unit.mock(RxJavaPlugins.class);
          plugins.registerSchedulersHook(isA(RxJavaSchedulersHook.class));
          expectLastCall().andThrow(new IllegalStateException("Hook present"));
          expect(plugins.getSchedulersHook()).andReturn(unit.mock(RxJavaSchedulersHook.class));

          unit.mockStatic(RxJavaPlugins.class);
          expect(RxJavaPlugins.getInstance()).andReturn(plugins);
        })
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.map(unit.capture(Route.Mapper.class))).andReturn(routes);

          Env env = unit.get(Env.class);
          expect(env.onStop(unit.capture(CheckedRunnable.class))).andReturn(env);
          expect(env.router()).andReturn(routes);
        })
        .expect(onStop)
        .run(unit -> {
          new Rx().configure(unit.get(Env.class), conf, unit.get(Binder.class));
        }, unit -> {
          unit.captured(CheckedRunnable.class).get(1).run();
        });
  }

  @Test
  public void config() throws Exception {
    assertEquals(ConfigFactory.parseResources(Rx.class, "rx.conf"), new Rx().config());
  }
}
