package org.jooby.mongodb;

import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.name.Names;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.client.MongoDatabase;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import org.jooby.Env;
import org.jooby.Env.ServiceKey;
import org.jooby.test.MockUnit;
import org.jooby.test.MockUnit.Block;
import org.jooby.funzy.Throwing;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Mongodb.class, MongoClient.class})
public class MongodbTest {

  private Config $mongodb = ConfigFactory.parseResources(getClass(), "mongodb.conf");

  @SuppressWarnings("unchecked")
  MockUnit.Block mongodb = unit -> {
    AnnotatedBindingBuilder<MongoClientURI> mcuABB = unit.mock(AnnotatedBindingBuilder.class);
    mcuABB.toInstance(isA(MongoClientURI.class));
    mcuABB.toInstance(isA(MongoClientURI.class));

    MongoClient client = unit.constructor(MongoClient.class)
        .args(MongoClientURI.class)
        .build(isA(MongoClientURI.class));

    MongoDatabase db = unit.mock(MongoDatabase.class);
    expect(client.getDatabase("mydb")).andReturn(db);

    unit.registerMock(MongoClient.class, client);

    AnnotatedBindingBuilder<MongoClient> mcABB = unit.mock(AnnotatedBindingBuilder.class);
    mcABB.toInstance(client);
    mcABB.toInstance(client);

    AnnotatedBindingBuilder<MongoDatabase> dbABB = unit.mock(AnnotatedBindingBuilder.class);
    dbABB.toInstance(db);
    dbABB.toInstance(db);

    Binder binder = unit.get(Binder.class);
    expect(binder.bind(Key.get(MongoClientURI.class))).andReturn(mcuABB);
    expect(binder.bind(Key.get(MongoClientURI.class, Names.named("mydb")))).andReturn(mcuABB);

    expect(binder.bind(Key.get(MongoClient.class))).andReturn(mcABB);
    expect(binder.bind(Key.get(MongoClient.class, Names.named("mydb")))).andReturn(mcABB);

    expect(binder.bind(Key.get(MongoDatabase.class))).andReturn(dbABB);
    expect(binder.bind(Key.get(MongoDatabase.class, Names.named("mydb")))).andReturn(dbABB);

    Env env = unit.get(Env.class);
    expect(env.onStop(unit.capture(Throwing.Runnable.class))).andReturn(env);
  };

  @Test
  public void defaults() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.getConfig("mongodb")).andReturn($mongodb.getConfig("mongodb"));
          expect(config.hasPath("mongodb.db")).andReturn(false);
          expect(config.getString("db")).andReturn("mongodb://127.0.0.1/mydb");
        })
        .expect(serviceKey(new ServiceKey()))
        .expect(mongodb)
        .expect(unit -> {
          MongoClient client = unit.get(MongoClient.class);
          client.close();
        })
        .run(unit -> {
          Mongodb mongodb = new Mongodb();
          mongodb.configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        }, unit -> {
          unit.captured(Throwing.Runnable.class).iterator().next().run();
        });
  }

  @Test(expected = IllegalArgumentException.class)
  public void shouldFaileWhenDbIsMissing() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.getConfig("mongodb")).andReturn($mongodb.getConfig("mongodb"));
          expect(config.hasPath("mongodb.db")).andReturn(false);
          expect(config.getString("db")).andReturn("mongodb://127.0.0.1");
        })
        .expect(mongodb)
        .run(unit -> {
          Mongodb mongodb = new Mongodb();
          mongodb.configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void defaultsWithCustomOption() throws Exception {

    new MockUnit(Env.class, Config.class, Binder.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.getConfig("mongodb"))
              .andReturn($mongodb.getConfig("mongodb"));
          expect(config.hasPath("mongodb.db")).andReturn(true);
          expect(config.getConfig("mongodb.db")).andReturn(ConfigFactory.empty()
              .withValue("connectionsPerHost", ConfigValueFactory.fromAnyRef(50)));
          expect(config.getString("db")).andReturn("mongodb://127.0.0.1/mydb");
        })
        .expect(serviceKey(new ServiceKey()))
        .expect(mongodb)
        .run(unit -> {
          new Mongodb()
              .options((options, config) -> {
                assertEquals(50, options.build().getConnectionsPerHost());
              })
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void defaultsConfig() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class)
        .run(unit -> {
          assertEquals($mongodb, new Mongodb().config());
        });
  }

  @Test
  public void defaultsWithOptions() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class)
        .expect(unit -> {
          Config config = unit.get(Config.class);
          expect(config.getConfig("mongodb")).andReturn($mongodb.getConfig("mongodb"));
          expect(config.hasPath("mongodb.db")).andReturn(false);
          expect(config.getString("db")).andReturn("mongodb://127.0.0.1/mydb");
        })
        .expect(serviceKey(new ServiceKey()))
        .expect(mongodb)
        .run(unit -> {
          new Mongodb()
              .options((options, config) -> {
                options.connectTimeout(3000);
              })
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  private Block serviceKey(final ServiceKey serviceKey) {
    return unit -> {
      Env env = unit.get(Env.class);
      expect(env.serviceKey()).andReturn(serviceKey);
    };
  }

}
