package org.jooby.mongodb;

import com.google.common.collect.ImmutableMap;
import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.binder.LinkedBindingBuilder;
import com.google.inject.name.Names;
import com.mongodb.ConnectionString;
import com.mongodb.ReadConcern;
import com.mongodb.ReadPreference;
import com.mongodb.WriteConcern;
import com.mongodb.async.client.MongoClientSettings;
import com.mongodb.connection.ClusterSettings;
import com.mongodb.connection.ClusterType;
import com.mongodb.connection.ConnectionPoolSettings;
import com.mongodb.connection.ServerSettings;
import com.mongodb.connection.SocketSettings;
import com.mongodb.connection.SslSettings;
import com.mongodb.rx.client.AggregateObservable;
import com.mongodb.rx.client.DistinctObservable;
import com.mongodb.rx.client.FindObservable;
import com.mongodb.rx.client.ListCollectionsObservable;
import com.mongodb.rx.client.ListDatabasesObservable;
import com.mongodb.rx.client.MapReduceObservable;
import com.mongodb.rx.client.MongoClient;
import com.mongodb.rx.client.MongoClients;
import com.mongodb.rx.client.MongoCollection;
import com.mongodb.rx.client.MongoDatabase;
import com.mongodb.rx.client.MongoObservable;
import com.mongodb.rx.client.ObservableAdapter;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.bson.codecs.configuration.CodecRegistry;
import static org.easymock.EasyMock.expect;
import org.jooby.Env;
import org.jooby.Route;
import org.jooby.Router;
import org.jooby.funzy.Throwing;
import org.jooby.funzy.Try;
import org.jooby.rx.Rx;
import org.jooby.test.MockUnit;
import org.jooby.test.MockUnit.Block;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import rx.Observable;
import rx.Scheduler;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

@RunWith(PowerMockRunner.class)
@PrepareForTest({MongoRx.class, MongoClients.class, MongoClientSettings.class,
    ClusterSettings.class, ConnectionPoolSettings.class, SocketSettings.class, ServerSettings.class,
    SslSettings.class, Rx.class, Observable.class})
public class MongodbRxTest {

  private Block settings = unit -> {
    MongoClientSettings.Builder builder = unit.powerMock(MongoClientSettings.Builder.class);
    expect(builder.clusterSettings(unit.get(ClusterSettings.class))).andReturn(builder);
    expect(builder.connectionPoolSettings(unit.get(ConnectionPoolSettings.class)))
        .andReturn(builder);
    expect(builder.socketSettings(unit.get(SocketSettings.class))).andReturn(builder);
    expect(builder.heartbeatSocketSettings(unit.get(SocketSettings.class))).andReturn(builder);
    expect(builder.serverSettings(unit.get(ServerSettings.class))).andReturn(builder);
    expect(builder.sslSettings(unit.get(SslSettings.class))).andReturn(builder);

    unit.registerMock(MongoClientSettings.Builder.class, builder);

    MongoClientSettings settings = unit.mock(MongoClientSettings.class);
    expect(builder.build()).andReturn(settings);
    unit.registerMock(MongoClientSettings.class, settings);

    unit.mockStatic(MongoClientSettings.class);

    expect(MongoClientSettings.builder()).andReturn(builder);
  };

  private Block server = unit -> {
    ServerSettings settings = unit.mock(ServerSettings.class);
    unit.registerMock(ServerSettings.class, settings);

    ServerSettings.Builder builder = unit.mock(ServerSettings.Builder.class);
    expect(builder.build()).andReturn(settings);

    unit.mockStatic(ServerSettings.class);
    expect(ServerSettings.builder()).andReturn(builder);
  };

  private Block mongo = unit -> {
    unit.mockStatic(MongoClients.class);
    expect(MongoClients.create(unit.get(MongoClientSettings.class)))
        .andReturn(unit.get(MongoClient.class));
  };

  private Block env = unit -> {
    Router routes = unit.mock(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(Throwing.Runnable.class))).andReturn(env);
  };

  private Block socket = unit -> {
    unit.mockStatic(SocketSettings.class);
  };

  private Block database = unit -> {
    MongoClient client = unit.get(MongoClient.class);
    expect(client.getDatabase("pets")).andReturn(unit.get(MongoDatabase.class));
  };

  @SuppressWarnings("unchecked")
  private Block collection = unit -> {
    MongoDatabase db = unit.get(MongoDatabase.class);
    expect(db.getCollection("Pets")).andReturn(unit.get(MongoCollection.class));
  };

  @Test
  public void configure() throws Exception {
    String db = "mongodb://localhost";
    new MockUnit(Env.class, Binder.class, MongoClient.class)
        .expect(instances(0))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class)))
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(env)
        .run(unit -> {
          new MongoRx()
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        });
  }

  @Test
  public void configure1() throws Exception {
    String db = "mongodb://localhost";
    new MockUnit(Env.class, Binder.class, MongoClient.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(env)
        .run(unit -> {
          new MongoRx()
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        });
  }

  @Test
  public void conf() throws Exception {
    assertEquals("mongodb://localhost", new MongoRx().config().getString("db"));
  }

  @Test
  public void onStop() throws Exception {
    String db = "mongodb://localhost";
    new MockUnit(Env.class, Binder.class, MongoClient.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(env)
        .expect(unit -> {
          MongoClient client = unit.get(MongoClient.class);
          client.close();
        })
        .run(unit -> {
          new MongoRx()
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        }, unit -> {
          unit.captured(Throwing.Runnable.class).iterator().next().run();
        });
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Test
  public void withObservable() throws Exception {
    String db = "mongodb://localhost/pets";
    new MockUnit(Env.class, Binder.class, MongoClient.class, MongoDatabase.class, Scheduler.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(database)
        .expect(bind(Key.get(MongoDatabase.class, Names.named("pets"))))
        .expect(unit -> {
          MongoDatabase database = unit.get(MongoDatabase.class);
          expect(database.withObservableAdapter(unit.capture(ObservableAdapter.class)))
              .andReturn(database);

          Observable observable = unit.powerMock(Observable.class);
          unit.registerMock(Observable.class, observable);
          expect(observable.observeOn(unit.get(Scheduler.class))).andReturn(observable);
        })
        .expect(env)
        .run(unit -> {
          new MongoRx()
              .observableAdapter(o -> o.observeOn(unit.get(Scheduler.class)))
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        }, unit -> {
          unit.captured(ObservableAdapter.class).iterator().next()
              .adapt(unit.get(Observable.class));
        });
  }

  @Test
  public void withCodecRegistry() throws Exception {
    String db = "mongodb://localhost/pets";
    new MockUnit(Env.class, Binder.class, MongoClient.class, MongoDatabase.class,
        CodecRegistry.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(database)
        .expect(bind(Key.get(MongoDatabase.class, Names.named("pets"))))
        .expect(unit -> {
          unit.get(CodecRegistry.class);
          MongoDatabase database = unit.get(MongoDatabase.class);
          expect(database.withCodecRegistry(unit.get(CodecRegistry.class))).andReturn(database);
        })
        .expect(env)
        .run(unit -> {
          new MongoRx()
              .codecRegistry(unit.get(CodecRegistry.class))
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        });
  }

  @Test
  public void withCallback() throws Exception {
    String db = "mongodb://localhost";
    new MockUnit(Env.class, Binder.class, MongoClient.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(env)
        .expect(unit -> {
          MongoClientSettings.Builder settings = unit.get(MongoClientSettings.Builder.class);
          expect(settings.readConcern(ReadConcern.LOCAL)).andReturn(settings);
        })
        .run(unit -> {
          new MongoRx()
              .doWith(settings -> {
                settings.readConcern(ReadConcern.LOCAL);
              })
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        });
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Test
  public void mongoRxMapper() throws Exception {
    String db = "mongodb://localhost";
    new MockUnit(Env.class, Binder.class, MongoClient.class, FindObservable.class,
        ListCollectionsObservable.class, ListDatabasesObservable.class, AggregateObservable.class,
        DistinctObservable.class, MapReduceObservable.class, MongoObservable.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(env)
        .expect(unit -> {
          Observable observable = unit.powerMock(Observable.class);
          expect(observable.toList()).andReturn(unit.powerMock(Observable.class)).times(6);

          Observable mobservable = unit.powerMock(Observable.class);

          FindObservable o1 = unit.get(FindObservable.class);
          expect(o1.toObservable()).andReturn(observable);

          ListCollectionsObservable o2 = unit.get(ListCollectionsObservable.class);
          expect(o2.toObservable()).andReturn(observable);

          ListDatabasesObservable o3 = unit.get(ListDatabasesObservable.class);
          expect(o3.toObservable()).andReturn(observable);

          AggregateObservable o4 = unit.get(AggregateObservable.class);
          expect(o4.toObservable()).andReturn(observable);

          DistinctObservable o5 = unit.get(DistinctObservable.class);
          expect(o5.toObservable()).andReturn(observable);

          MapReduceObservable o6 = unit.get(MapReduceObservable.class);
          expect(o6.toObservable()).andReturn(observable);

          MongoObservable o7 = unit.get(MongoObservable.class);
          expect(o7.toObservable()).andReturn(mobservable);
        })
        .run(unit -> {
          new MongoRx()
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        }, unit -> {
          Route.Mapper mongorx = unit.captured(Route.Mapper.class).iterator().next();

          assertTrue(mongorx.map(unit.get(FindObservable.class)) instanceof Observable);
          assertTrue(
              mongorx.map(unit.get(ListCollectionsObservable.class)) instanceof Observable);
          assertTrue(
              mongorx.map(unit.get(ListDatabasesObservable.class)) instanceof Observable);
          assertTrue(
              mongorx.map(unit.get(AggregateObservable.class)) instanceof Observable);
          assertTrue(
              mongorx.map(unit.get(DistinctObservable.class)) instanceof Observable);
          assertTrue(
              mongorx.map(unit.get(MapReduceObservable.class)) instanceof Observable);
          assertTrue(
              mongorx.map(unit.get(MongoObservable.class)) instanceof Observable);

          assertEquals("x", mongorx.map("x"));
        });
  }

  @Test
  public void withDatabase() throws Exception {
    String db = "mongodb://localhost/pets";
    new MockUnit(Env.class, Binder.class, MongoClient.class, MongoDatabase.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(database)
        .expect(bind(Key.get(MongoDatabase.class, Names.named("pets"))))
        .expect(env)
        .run(unit -> {
          new MongoRx()
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        });
  }

  @Test
  public void withCollection() throws Exception {
    String db = "mongodb://localhost/pets.Pets";
    new MockUnit(Env.class, Binder.class, MongoClient.class, MongoDatabase.class,
        MongoCollection.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named("db"))))
        .expect(database)
        .expect(bind(Key.get(MongoDatabase.class, Names.named("pets"))))
        .expect(collection)
        .expect(bind(Key.get(MongoCollection.class, Names.named("Pets"))))
        .expect(env)
        .run(unit -> {
          new MongoRx()
              .configure(unit.get(Env.class), conf(null, "db", db), unit.get(Binder.class));
        });
  }

  @Test
  public void withDirectDb() throws Exception {
    String db = "mongodb://localhost/pets.Pets";
    new MockUnit(Env.class, Binder.class, MongoClient.class, MongoDatabase.class,
        MongoCollection.class)
        .expect(instances(1))
        .expect(cluster(db))
        .expect(pool(db))
        .expect(socket)
        .expect(socket(db))
        .expect(server)
        .expect(ssl(db))
        .expect(settings)
        .expect(mongo)
        .expect(bind(Key.get(MongoClient.class, Names.named(db))))
        .expect(database)
        .expect(bind(Key.get(MongoDatabase.class, Names.named("pets"))))
        .expect(collection)
        .expect(bind(Key.get(MongoCollection.class, Names.named("Pets"))))
        .expect(env)
        .run(unit -> {
          new MongoRx(db)
              .configure(unit.get(Env.class), conf(null), unit.get(Binder.class));
        });
  }

  private Block instances(final int n) {
    return unit -> {
      Field field = MongoRx.class.getDeclaredField("instances");
      field.setAccessible(true);
      AtomicInteger instances = (AtomicInteger) field.get(null);
      instances.set(n);
    };
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private Block bind(final Key key) {
    return unit -> {
      Binder binder = unit.get(Binder.class);
      LinkedBindingBuilder bb = unit.mock(LinkedBindingBuilder.class);
      bb.toInstance(unit.get(key.getTypeLiteral().getRawType()));
      expect(binder.bind(key)).andReturn(bb);
    };
  }

  private Block cluster(final String db) {
    return unit -> {
      ClusterSettings settings = unit.mock(ClusterSettings.class);
      unit.registerMock(ClusterSettings.class, settings);

      ClusterSettings.Builder builder = unit.mock(ClusterSettings.Builder.class);
      expect(builder.applyConnectionString(new ConnectionString(db))).andReturn(builder);
      expect(builder.build()).andReturn(settings);

      unit.mockStatic(ClusterSettings.class);
      expect(ClusterSettings.builder()).andReturn(builder);
    };
  }

  private Block pool(final String db) {
    return unit -> {
      ConnectionPoolSettings settings = unit.mock(ConnectionPoolSettings.class);
      unit.registerMock(ConnectionPoolSettings.class, settings);

      ConnectionPoolSettings.Builder builder = unit.mock(ConnectionPoolSettings.Builder.class);
      expect(builder.applyConnectionString(new ConnectionString(db))).andReturn(builder);
      expect(builder.build()).andReturn(settings);

      unit.mockStatic(ConnectionPoolSettings.class);
      expect(ConnectionPoolSettings.builder()).andReturn(builder);
    };
  }

  private Block socket(final String db) {
    return unit -> {
      SocketSettings settings = Try.apply(() -> unit.get(SocketSettings.class))
          .orElseGet(() -> unit.mock(SocketSettings.class));
      unit.registerMock(SocketSettings.class, settings);

      SocketSettings.Builder builder = Try.apply(() -> unit.get(SocketSettings.Builder.class))
          .orElseGet(() -> unit.mock(SocketSettings.Builder.class));
      expect(builder.applyConnectionString(new ConnectionString(db))).andReturn(builder).times(2);
      expect(builder.build()).andReturn(settings).times(2);

      expect(SocketSettings.builder()).andReturn(builder).times(2);
    };
  }

  private Block ssl(final String db) {
    return unit -> {
      SslSettings settings = unit.mock(SslSettings.class);
      unit.registerMock(SslSettings.class, settings);

      SslSettings.Builder builder = unit.mock(SslSettings.Builder.class);
      expect(builder.applyConnectionString(new ConnectionString(db))).andReturn(builder);
      expect(builder.build()).andReturn(settings);

      unit.mockStatic(SslSettings.class);
      expect(SslSettings.builder()).andReturn(builder);
    };
  }

  @Test
  public void cluster() {
    ClusterSettings cluster = MongoRx.cluster(new ConnectionString("mongodb://localhost"),
        conf("cluster",
            "maxWaitQueueSize", 5,
            "replicaSetName", "r",
            "requiredClusterType", "replica_set",
            "serverSelectionTimeout", "3s"));

    assertEquals(5, cluster.getMaxWaitQueueSize());
    assertEquals("r", cluster.getRequiredReplicaSetName());
    assertEquals(ClusterType.REPLICA_SET, cluster.getRequiredClusterType());
    assertEquals(3, cluster.getServerSelectionTimeout(TimeUnit.SECONDS));
  }

  @Test
  public void pool() {
    ConnectionPoolSettings pool = MongoRx.pool(new ConnectionString("mongodb://localhost"),
        conf("pool",
            "maintenanceFrequency", "1s",
            "maintenanceInitialDelay", "2s",
            "maxConnectionIdleTime", "3s",
            "maxConnectionLifeTime", 4500,
            "maxSize", 3,
            "maxWaitQueueSize", 9,
            "maxWaitTime", "1m",
            "minSize", 1));

    assertEquals(1, pool.getMaintenanceFrequency(TimeUnit.SECONDS));
    assertEquals(2, pool.getMaintenanceInitialDelay(TimeUnit.SECONDS));
    assertEquals(3, pool.getMaxConnectionIdleTime(TimeUnit.SECONDS));
    assertEquals(4500, pool.getMaxConnectionLifeTime(TimeUnit.MILLISECONDS));
    assertEquals(60, pool.getMaxWaitTime(TimeUnit.SECONDS));
    assertEquals(3, pool.getMaxSize());
    assertEquals(9, pool.getMaxWaitQueueSize());
    assertEquals(1, pool.getMinSize());
  }

  @Test
  public void server() {
    ServerSettings server = MongoRx.server(
        conf("server",
            "heartbeatFrequency", "1s",
            "minHeartbeatFrequency", "2s"));

    assertEquals(1, server.getHeartbeatFrequency(TimeUnit.SECONDS));
    assertEquals(2, server.getMinHeartbeatFrequency(TimeUnit.SECONDS));
  }

  @Test
  public void socket() {
    SocketSettings socket = MongoRx.socket("socket", new ConnectionString("mongodb://localhost"),
        conf("socket",
            "connectTimeout", "1s",
            "readTimeout", "2s",
            "receiveBufferSize", 15,
            "sendBufferSize", 20));

    assertEquals(1, socket.getConnectTimeout(TimeUnit.SECONDS));
    assertEquals(2, socket.getReadTimeout(TimeUnit.SECONDS));
    assertEquals(15, socket.getReceiveBufferSize());
    assertEquals(20, socket.getSendBufferSize());
  }

  @Test
  public void ssl() {
    SslSettings ssl = MongoRx.ssl(new ConnectionString("mongodb://localhost"),
        conf("ssl",
            "enabled", true,
            "invalidHostNameAllowed", true));

    assertEquals(true, ssl.isEnabled());
    assertEquals(true, ssl.isInvalidHostNameAllowed());
  }

  @Test
  public void settings() {
    MongoClientSettings settings = MongoRx.settings(new ConnectionString("mongodb://localhost"),
        conf(null,
            "readConcern", "local",
            "readPreference", "primary",
            "writeConcern", "w3"))
        .build();

    assertEquals(ReadConcern.LOCAL, settings.getReadConcern());
    assertEquals(ReadPreference.primary(), settings.getReadPreference());
    assertEquals(WriteConcern.W3, settings.getWriteConcern());
  }

  @Test(expected = IllegalArgumentException.class)
  public void wrongReadConcern() {
    MongoRx.settings(new ConnectionString("mongodb://localhost"),
        conf(null,
            "readConcern", "Xxx",
            "readPreference", "primary",
            "writeConcern", "w3"))
        .build();
  }

  @Test(expected = IllegalArgumentException.class)
  public void wrongWriteConcern() {
    MongoRx.settings(new ConnectionString("mongodb://localhost"),
        conf(null,
            "readPreference", "primary",
            "writeConcern", "Xqw"))
        .build();
  }

  @Test
  public void dbconf() {
    assertEquals("default",
        MongoRx.dbconf("db", conf("mongo", "readConcern", "default")).getString("readConcern"));

    assertEquals("default",
        MongoRx.dbconf("mongodb://localhost", conf("mongo", "readConcern", "default"))
            .getString("readConcern"));

    assertEquals("local",
        MongoRx.dbconf("db",
            conf(null,
                "mongo.readConcern", "default",
                "mongo.db.readConcern", "local"))
            .getString("readConcern"));
  }

  private Config conf(final String scope, final Object... values) {
    ImmutableMap.Builder<String, Object> hash = ImmutableMap.builder();
    String prefix = scope == null ? "" : scope + ".";
    for (int i = 0; i < values.length; i += 2) {
      hash.put(prefix + values[i].toString(), values[i + 1]);
    }
    return ConfigFactory.parseMap(hash.build());
  }
}
