package org.jooby.pebble;

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

import java.util.Locale;
import java.util.concurrent.CountDownLatch;

import org.jooby.Env;
import org.jooby.Renderer;
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.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.inject.Binder;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.google.inject.binder.LinkedBindingBuilder;
import com.google.inject.multibindings.Multibinder;
import com.mitchellbosecke.pebble.PebbleEngine;
import com.mitchellbosecke.pebble.cache.BaseTagCacheKey;
import com.mitchellbosecke.pebble.loader.ClasspathLoader;
import com.mitchellbosecke.pebble.template.PebbleTemplate;
import com.typesafe.config.Config;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Pebble.class, PebbleEngine.Builder.class, ClasspathLoader.class,
    Multibinder.class, CacheBuilder.class })
public class PebbleTest {

  private Block newEngine = unit -> {
    PebbleEngine.Builder pebble = unit.constructor(PebbleEngine.Builder.class)
        .build();

    expect(pebble.cacheActive(true)).andReturn(pebble);
    expect(pebble.loader(unit.get(ClasspathLoader.class))).andReturn(pebble);
    expect(pebble.extension(isA(XssExt.class))).andReturn(pebble);

    unit.registerMock(PebbleEngine.Builder.class, pebble);
  };

  private Block defLoader = unit -> {
    ClasspathLoader loader = unit.constructor(ClasspathLoader.class).build();
    loader.setPrefix(null);
    loader.setSuffix(".html");
    unit.registerMock(ClasspathLoader.class, loader);
  };

  private Block build = unit -> {
    PebbleEngine.Builder pebble = unit.get(PebbleEngine.Builder.class);
    expect(pebble.build()).andReturn(unit.get(PebbleEngine.class));
  };

  @SuppressWarnings("unchecked")
  private Block bindEngine = unit -> {
    AnnotatedBindingBuilder<PebbleEngine> peABB = unit.mock(AnnotatedBindingBuilder.class);
    peABB.toInstance(unit.get(PebbleEngine.class));

    Binder binder = unit.get(Binder.class);
    expect(binder.bind(PebbleEngine.class)).andReturn(peABB);
  };

  @SuppressWarnings("unchecked")
  private Block renderer = unit -> {
    PebbleRenderer renderer = unit.constructor(PebbleRenderer.class)
        .args(PebbleEngine.class)
        .build(unit.get(PebbleEngine.class));
    unit.mockStatic(Multibinder.class);

    LinkedBindingBuilder<Renderer> rLBB = unit.mock(LinkedBindingBuilder.class);
    rLBB.toInstance(renderer);

    Multibinder<Renderer> rmb = unit.mock(Multibinder.class);
    expect(rmb.addBinding()).andReturn(rLBB);

    expect(Multibinder.newSetBinder(unit.get(Binder.class), Renderer.class)).andReturn(rmb);
  };

  private Block cacheStatic = unit -> {
    unit.mockStatic(CacheBuilder.class);
  };

  @Test
  public void basic() throws Exception {
    Locale locale = Locale.getDefault();
    new MockUnit(Env.class, Config.class, Binder.class, PebbleEngine.class)
        .expect(defLoader)
        .expect(newEngine)
        .expect(env("dev", locale))
        .expect(cacheStatic)
        .expect(cache("pebble.cache", null))
        .expect(cache(0))
        .expect(cache("pebble.tagCache", null))
        .expect(tagCache(0))
        .expect(locale(locale))
        .expect(build)
        .expect(bindEngine)
        .expect(renderer)
        .run(unit -> {
          new Pebble()
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void prefixAsRoot() throws Exception {
    Locale locale = Locale.getDefault();
    new MockUnit(Env.class, Config.class, Binder.class, PebbleEngine.class)
        .expect(defLoader)
        .expect(newEngine)
        .expect(env("dev", locale))
        .expect(cacheStatic)
        .expect(cache("pebble.cache", null))
        .expect(cache(0))
        .expect(cache("pebble.tagCache", null))
        .expect(tagCache(0))
        .expect(locale(locale))
        .expect(build)
        .expect(bindEngine)
        .expect(renderer)
        .run(unit -> {
          new Pebble("/", ".html")
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void emptyPrefix() throws Exception {
    Locale locale = Locale.getDefault();
    new MockUnit(Env.class, Config.class, Binder.class, PebbleEngine.class)
        .expect(defLoader)
        .expect(newEngine)
        .expect(env("dev", locale))
        .expect(cacheStatic)
        .expect(cache("pebble.cache", null))
        .expect(cache(0))
        .expect(cache("pebble.tagCache", null))
        .expect(tagCache(0))
        .expect(locale(locale))
        .expect(build)
        .expect(bindEngine)
        .expect(renderer)
        .run(unit -> {
          new Pebble("", ".html")
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void noEmptyPrefix() throws Exception {
    Locale locale = Locale.getDefault();
    new MockUnit(Env.class, Config.class, Binder.class, PebbleEngine.class)
        .expect(unit -> {
          ClasspathLoader loader = unit.constructor(ClasspathLoader.class).build();
          loader.setPrefix("views");
          loader.setSuffix(".html");
          unit.registerMock(ClasspathLoader.class, loader);
        })
        .expect(newEngine)
        .expect(env("dev", locale))
        .expect(cacheStatic)
        .expect(cache("pebble.cache", null))
        .expect(cache(0))
        .expect(cache("pebble.tagCache", null))
        .expect(tagCache(0))
        .expect(locale(locale))
        .expect(build)
        .expect(bindEngine)
        .expect(renderer)
        .run(unit -> {
          new Pebble("/views", ".html")
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void proddef() throws Exception {
    Locale locale = Locale.getDefault();
    new MockUnit(Env.class, Config.class, Binder.class, PebbleEngine.class)
        .expect(defLoader)
        .expect(newEngine)
        .expect(env("prod", locale))
        .expect(cacheStatic)
        .expect(cache("pebble.cache", null))
        .expect(cache(100))
        .expect(cache("pebble.tagCache", null))
        .expect(tagCache(100))
        .expect(locale(locale))
        .expect(build)
        .expect(bindEngine)
        .expect(renderer)
        .run(unit -> {
          new Pebble()
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void doWithFull() throws Exception {
    Locale locale = Locale.getDefault();
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit(Env.class, Config.class, Binder.class, PebbleEngine.class)
        .expect(defLoader)
        .expect(newEngine)
        .expect(env("dev", locale))
        .expect(cacheStatic)
        .expect(cache("pebble.cache", null))
        .expect(cache(0))
        .expect(cache("pebble.tagCache", null))
        .expect(tagCache(0))
        .expect(locale(locale))
        .expect(build)
        .expect(bindEngine)
        .expect(renderer)
        .run(unit -> {
          new Pebble(".html").doWith((b, c) -> {
            assertNotNull(b);
            assertNotNull(c);
            latch.countDown();
          })
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
    latch.await();
  }

  @Test
  public void doWith1() throws Exception {
    Locale locale = Locale.getDefault();
    CountDownLatch latch = new CountDownLatch(1);
    new MockUnit(Env.class, Config.class, Binder.class, PebbleEngine.class)
        .expect(defLoader)
        .expect(newEngine)
        .expect(env("dev", locale))
        .expect(cacheStatic)
        .expect(cache("pebble.cache", null))
        .expect(cache(0))
        .expect(cache("pebble.tagCache", null))
        .expect(tagCache(0))
        .expect(locale(locale))
        .expect(build)
        .expect(bindEngine)
        .expect(renderer)
        .run(unit -> {
          new Pebble().doWith(b -> {
            assertNotNull(b);
            latch.countDown();
          })
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
    latch.await();
  }

  @SuppressWarnings({"unchecked", "rawtypes" })
  @Test
  public void prod() throws Exception {
    Locale locale = Locale.getDefault();
    new MockUnit(Env.class, Config.class, Binder.class, PebbleEngine.class)
        .expect(defLoader)
        .expect(newEngine)
        .expect(env("prod", locale))
        .expect(cache("pebble.cache", "maximumSize=200"))
        .expect(cacheStatic)
        .expect(cacheBuilder(200))
        .expect(cache("pebble.tagCache", "maximumSize=100"))
        .expect(unit -> {
          Cache<BaseTagCacheKey, Object> cache = unit.mock(Cache.class);

          CacheBuilder cachebuilder = unit.mock(CacheBuilder.class);
          expect(CacheBuilder.from("maximumSize=100")).andReturn(cachebuilder);
          expect(cachebuilder.build()).andReturn(cache);

          PebbleEngine.Builder pebble = unit.get(PebbleEngine.Builder.class);
          expect(pebble.tagCache(cache)).andReturn(pebble);
        })
        .expect(locale(locale))
        .expect(build)
        .expect(bindEngine)
        .expect(renderer)
        .run(unit -> {
          new Pebble()
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @SuppressWarnings({"unchecked", "rawtypes" })
  private Block cacheBuilder(final int maxSize) {
    return unit -> {
      Cache<Object, PebbleTemplate> cache = unit.mock(Cache.class);
      unit.registerMock(Cache.class, cache);

      CacheBuilder cachebuilder = unit.mock(CacheBuilder.class);
      expect(CacheBuilder.from("maximumSize=" + maxSize)).andReturn(cachebuilder);
      expect(cachebuilder.build()).andReturn(cache);

      PebbleEngine.Builder pebble = unit.get(PebbleEngine.Builder.class);
      expect(pebble.templateCache(cache)).andReturn(pebble);
    };
  }

  private Block locale(final Locale locale) {
    return unit -> {
      PebbleEngine.Builder pebble = unit.get(PebbleEngine.Builder.class);
      expect(pebble.defaultLocale(locale)).andReturn(pebble);
    };
  }

  private Block cache(final String path, final String value) {
    return unit -> {
      Config conf = unit.get(Config.class);
      boolean has = value != null;
      expect(conf.hasPath(path)).andReturn(has);
      if (has) {
        expect(conf.getString(path)).andReturn(value);
      }
    };
  }

  private Block env(final String name, final Locale locale) {
    return unit -> {
      Env env = unit.get(Env.class);
      expect(env.name()).andReturn(name);
      expect(env.locale()).andReturn(locale);
    };
  }

  @SuppressWarnings({"unchecked", "rawtypes" })
  private Block cache(final int size) {
    return unit -> {
      Cache<Object, PebbleTemplate> cache = unit.mock(Cache.class);

      CacheBuilder cachebuilder = unit.mock(CacheBuilder.class);
      expect(CacheBuilder.newBuilder()).andReturn(cachebuilder);
      expect(cachebuilder.maximumSize(size)).andReturn(cachebuilder);
      expect(cachebuilder.build()).andReturn(cache);

      PebbleEngine.Builder pebble = unit.get(PebbleEngine.Builder.class);
      expect(pebble.templateCache(cache)).andReturn(pebble);
    };
  }

  @SuppressWarnings({"unchecked", "rawtypes" })
  private Block tagCache(final int size) {
    return unit -> {
      Cache<BaseTagCacheKey, Object> cache = unit.mock(Cache.class);

      CacheBuilder cachebuilder = unit.mock(CacheBuilder.class);
      expect(CacheBuilder.newBuilder()).andReturn(cachebuilder);
      expect(cachebuilder.maximumSize(size)).andReturn(cachebuilder);
      expect(cachebuilder.build()).andReturn(cache);

      PebbleEngine.Builder pebble = unit.get(PebbleEngine.Builder.class);
      expect(pebble.tagCache(cache)).andReturn(pebble);
    };
  }
}
