package org.jooby.jedis;

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

import java.net.URI;

import javax.inject.Provider;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.jooby.Env;
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.google.inject.Key;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.name.Names;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;

import javaslang.control.Try.CheckedRunnable;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Redis.class, JedisPool.class, GenericObjectPoolConfig.class })
public class RedisTest {

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

  @SuppressWarnings("unchecked")
  @Test
  public void defaults() throws Exception {
    Config config = jedisConfig()
        .withValue("db", ConfigValueFactory.fromAnyRef("redis://localhost:6780"));
    new MockUnit(Env.class, Binder.class)
        .expect(unit -> {
          Env env = unit.get(Env.class);
          expect(env.serviceKey()).andReturn(new Env.ServiceKey());
        })
        .expect(unit -> {
          GenericObjectPoolConfig poolConfig = unit
              .mockConstructor(GenericObjectPoolConfig.class);
          poolConfig.setBlockWhenExhausted(true);
          poolConfig
              .setEvictionPolicyClassName("org.apache.commons.pool2.impl.DefaultEvictionPolicy");
          poolConfig.setJmxEnabled(false);
          poolConfig.setJmxNamePrefix("redis-pool");
          poolConfig.setLifo(true);
          poolConfig.setMaxIdle(10);
          poolConfig.setMaxTotal(128);
          poolConfig.setMaxWaitMillis(-1);
          poolConfig.setMinEvictableIdleTimeMillis(1800000L);
          poolConfig.setMinIdle(10);
          poolConfig.setNumTestsPerEvictionRun(3);
          poolConfig.setSoftMinEvictableIdleTimeMillis(1800000L);
          poolConfig.setTestOnBorrow(false);
          poolConfig.setTestOnReturn(false);
          poolConfig.setTestWhileIdle(false);
          poolConfig.setTimeBetweenEvictionRunsMillis(-1);

          JedisPool pool = unit.mockConstructor(
              JedisPool.class,
              new Class[]{GenericObjectPoolConfig.class, URI.class, int.class },
              poolConfig, URI.create("redis://localhost:6780"), 2000);

          AnnotatedBindingBuilder<JedisPool> jpABB = unit.mock(AnnotatedBindingBuilder.class);
          jpABB.toInstance(pool);
          jpABB.toInstance(pool);

          AnnotatedBindingBuilder<Jedis> jABB = unit.mock(AnnotatedBindingBuilder.class);
          expect(jABB.toProvider(isA(Provider.class))).andReturn(jABB);
          expect(jABB.toProvider(isA(Provider.class))).andReturn(jABB);

          Binder binder = unit.get(Binder.class);
          expect(binder.bind(Key.get(JedisPool.class))).andReturn(jpABB);
          expect(binder.bind(Key.get(Jedis.class))).andReturn(jABB);
          expect(binder.bind(Key.get(JedisPool.class, Names.named("db")))).andReturn(jpABB);
          expect(binder.bind(Key.get(Jedis.class, Names.named("db")))).andReturn(jABB);
        })
        .expect(onManaged)
        .run(unit -> {
          new Redis().configure(unit.get(Env.class), config, unit.get(Binder.class));
        });
  }

  @SuppressWarnings("unchecked")
  @Test
  public void shouldGetJedisInstance() throws Exception {
    Config config = jedisConfig()
        .withValue("db", ConfigValueFactory.fromAnyRef("redis://localhost:6780"));
    new MockUnit(Env.class, Binder.class, Jedis.class)
        .expect(unit -> {
          Env env = unit.get(Env.class);
          expect(env.serviceKey()).andReturn(new Env.ServiceKey());
        })
        .expect(unit -> {
          GenericObjectPoolConfig poolConfig = unit
              .mockConstructor(GenericObjectPoolConfig.class);
          poolConfig.setBlockWhenExhausted(true);
          poolConfig
              .setEvictionPolicyClassName(
                  "org.apache.commons.pool2.impl.DefaultEvictionPolicy");
          poolConfig.setJmxEnabled(false);
          poolConfig.setJmxNamePrefix("redis-pool");
          poolConfig.setLifo(true);
          poolConfig.setMaxIdle(10);
          poolConfig.setMaxTotal(128);
          poolConfig.setMaxWaitMillis(-1);
          poolConfig.setMinEvictableIdleTimeMillis(1800000L);
          poolConfig.setMinIdle(10);
          poolConfig.setNumTestsPerEvictionRun(3);
          poolConfig.setSoftMinEvictableIdleTimeMillis(1800000L);
          poolConfig.setTestOnBorrow(false);
          poolConfig.setTestOnReturn(false);
          poolConfig.setTestWhileIdle(false);
          poolConfig.setTimeBetweenEvictionRunsMillis(-1);

          JedisPool pool = unit.mockConstructor(
              JedisPool.class,
              new Class[]{GenericObjectPoolConfig.class, URI.class, int.class },
              poolConfig, URI.create("redis://localhost:6780"), 2000);

          expect(pool.getResource()).andReturn(unit.get(Jedis.class));

          AnnotatedBindingBuilder<JedisPool> jpABB = unit.mock(AnnotatedBindingBuilder.class);
          jpABB.toInstance(pool);
          jpABB.toInstance(pool);

          AnnotatedBindingBuilder<Jedis> jABB = unit.mock(AnnotatedBindingBuilder.class);
          expect(jABB.toProvider(isA(Provider.class))).andReturn(jABB);
          expect(jABB.toProvider(unit.capture(Provider.class))).andReturn(jABB);

          Binder binder = unit.get(Binder.class);
          expect(binder.bind(Key.get(JedisPool.class))).andReturn(jpABB);
          expect(binder.bind(Key.get(Jedis.class))).andReturn(jABB);
          expect(binder.bind(Key.get(JedisPool.class, Names.named("db")))).andReturn(jpABB);
          expect(binder.bind(Key.get(Jedis.class, Names.named("db")))).andReturn(jABB);
        })
        .expect(onManaged)
        .run(unit -> {
          new Redis().configure(unit.get(Env.class), config, unit.get(Binder.class));
        }, unit -> {
          assertEquals(unit.get(Jedis.class), unit.captured(Provider.class).iterator().next()
              .get());
        });
  }

  @SuppressWarnings("unchecked")
  @Test
  public void jedisConfigOverride() throws Exception {
    Config config = jedisConfig()
        .withValue("db", ConfigValueFactory.fromAnyRef("redis://localhost:6780"))
        .withValue("jedis.db.maxTotal", ConfigValueFactory.fromAnyRef(8));
    new MockUnit(Env.class, Binder.class)
        .expect(unit -> {
          Env env = unit.get(Env.class);
          expect(env.serviceKey()).andReturn(new Env.ServiceKey());
        })
        .expect(unit -> {
          GenericObjectPoolConfig poolConfig = unit
              .mockConstructor(GenericObjectPoolConfig.class);
          poolConfig.setBlockWhenExhausted(true);
          poolConfig
              .setEvictionPolicyClassName(
                  "org.apache.commons.pool2.impl.DefaultEvictionPolicy");
          poolConfig.setJmxEnabled(false);
          poolConfig.setJmxNamePrefix("redis-pool");
          poolConfig.setLifo(true);
          poolConfig.setMaxIdle(10);
          poolConfig.setMaxTotal(8);
          poolConfig.setMaxWaitMillis(-1);
          poolConfig.setMinEvictableIdleTimeMillis(1800000L);
          poolConfig.setMinIdle(10);
          poolConfig.setNumTestsPerEvictionRun(3);
          poolConfig.setSoftMinEvictableIdleTimeMillis(1800000L);
          poolConfig.setTestOnBorrow(false);
          poolConfig.setTestOnReturn(false);
          poolConfig.setTestWhileIdle(false);
          poolConfig.setTimeBetweenEvictionRunsMillis(-1);

          JedisPool pool = unit.mockConstructor(
              JedisPool.class,
              new Class[]{GenericObjectPoolConfig.class, URI.class, int.class },
              poolConfig, URI.create("redis://localhost:6780"), 2000);

          AnnotatedBindingBuilder<JedisPool> jpABB = unit.mock(AnnotatedBindingBuilder.class);
          jpABB.toInstance(pool);
          jpABB.toInstance(pool);

          AnnotatedBindingBuilder<Jedis> jABB = unit.mock(AnnotatedBindingBuilder.class);
          expect(jABB.toProvider(isA(Provider.class))).andReturn(jABB);
          expect(jABB.toProvider(isA(Provider.class))).andReturn(jABB);

          Binder binder = unit.get(Binder.class);
          expect(binder.bind(Key.get(JedisPool.class))).andReturn(jpABB);
          expect(binder.bind(Key.get(Jedis.class))).andReturn(jABB);
          expect(binder.bind(Key.get(JedisPool.class, Names.named("db")))).andReturn(jpABB);
          expect(binder.bind(Key.get(Jedis.class, Names.named("db")))).andReturn(jABB);
        })
        .expect(onManaged)
        .run(unit -> {
          new Redis().configure(unit.get(Env.class), config, unit.get(Binder.class));
        });
  }

  @Test
  public void defaultConfig() throws Exception {
    assertEquals(jedisConfig(), new Redis().config());
  }

  private Config jedisConfig() {
    return ConfigFactory.parseResources(getClass(), "jedis.conf");
  }
}
