package org.jooby.scanner;

import app.ns.AbsController;
import app.ns.AbsFoo;
import app.ns.FooApp;
import app.ns.FooController;
import app.ns.FooImpl;
import app.ns.FooModule;
import app.ns.FooSub;
import app.ns.GuavaService;
import app.ns.GuiceModule;
import app.ns.IFoo;
import app.ns.NamedFoo;
import app.ns.SingletonFoo;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Service;
import com.google.common.util.concurrent.ServiceManager;
import com.google.inject.Binder;
import com.google.inject.Module;
import com.google.inject.binder.AnnotatedBindingBuilder;
import com.typesafe.config.Config;
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner;
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
import org.jooby.Env;
import org.jooby.Jooby;
import org.jooby.Registry;
import org.jooby.Router;
import org.jooby.mvc.Path;
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;

import javax.inject.Provider;
import java.util.Arrays;
import java.util.List;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Scanner.class, FastClasspathScanner.class, FooModule.class, ServiceManager.class})
public class ScannerTest {

  private Block routes = unit -> {
    Env env = unit.get(Env.class);
    expect(env.router()).andReturn(unit.get(Router.class));
  };

  private Block runtimeProcessors = unit -> {
    expect(unit.get(Config.class).getInt("runtime.processors")).andReturn(1);
  };

  @Test
  public void newScanner() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .run(unit -> {
          new Scanner()
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void newScannerWithSpec() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(runtimeProcessors)
        .expect(scanResult("test.pkg"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .run(unit -> {
          new Scanner("test.pkg")
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void scanController() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns"))
        .expect(routes)
        .expect(annotations(Path.class, FooController.class.getName()))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.use(FooController.class)).andReturn(null);
        })
        .run(unit -> {
          new Scanner()
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void scanApp() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class, FooApp.class.getName()))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.use(isA(FooApp.class))).andReturn(routes);
        })
        .run(unit -> {
          new Scanner()
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @SuppressWarnings("unchecked")
  @Test
  public void scanAnnotation() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns", "javax.inject", "com.google.inject.name"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(annotations(javax.inject.Named.class, NamedFoo.class.getName()))
        .expect(annotations(com.google.inject.name.Named.class))
        .expect(unit -> {
          Binder binder = unit.get(Binder.class);

          AnnotatedBindingBuilder<NamedFoo> abb = unit.mock(AnnotatedBindingBuilder.class);
          abb.asEagerSingleton();
          expect(binder.bind(NamedFoo.class)).andReturn(abb);

          Env env = unit.get(Env.class);
          expect(env.lifeCycle(NamedFoo.class)).andReturn(env);
        })
        .run(unit -> {
          new Scanner()
              .scan(javax.inject.Named.class)
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @SuppressWarnings("unchecked")
  @Test
  public void scanSingleton() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns", "javax.inject", "com.google.inject"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(annotations(javax.inject.Singleton.class, SingletonFoo.class.getName()))
        .expect(annotations(com.google.inject.Singleton.class))
        .expect(unit -> {
          Binder binder = unit.get(Binder.class);

          AnnotatedBindingBuilder<SingletonFoo> abb = unit.mock(AnnotatedBindingBuilder.class);
          abb.asEagerSingleton();
          expect(binder.bind(SingletonFoo.class)).andReturn(abb);

          Env env = unit.get(Env.class);
          expect(env.lifeCycle(SingletonFoo.class)).andReturn(env);
        })
        .run(unit -> {
          new Scanner()
              .scan(javax.inject.Singleton.class)
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @SuppressWarnings("unchecked")
  @Test
  public void scanImplements() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(implementing(IFoo.class, FooImpl.class.getName()))
        .expect(unit -> {
          Binder binder = unit.get(Binder.class);

          AnnotatedBindingBuilder<FooImpl> abb = unit.mock(AnnotatedBindingBuilder.class);
          abb.asEagerSingleton();
          expect(binder.bind(FooImpl.class)).andReturn(abb);

          Env env = unit.get(Env.class);
          expect(env.lifeCycle(FooImpl.class)).andReturn(env);
        })
        .run(unit -> {
          new Scanner()
              .scan(IFoo.class)
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @SuppressWarnings("unchecked")
  @Test
  public void scanSubclass() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(subClassesOf(AbsFoo.class, FooSub.class.getName()))
        .expect(unit -> {
          Binder binder = unit.get(Binder.class);

          AnnotatedBindingBuilder<FooSub> abb = unit.mock(AnnotatedBindingBuilder.class);
          abb.asEagerSingleton();
          expect(binder.bind(FooSub.class)).andReturn(abb);

          Env env = unit.get(Env.class);
          expect(env.lifeCycle(FooSub.class)).andReturn(env);
        })
        .run(unit -> {
          new Scanner()
              .scan(AbsFoo.class)
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void scanGuiceModule() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns", "com.google.inject"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(implementing(Module.class, GuiceModule.class.getName()))
        .expect(unit -> {
          Binder binder = unit.get(Binder.class);
          expect(binder.bind(GuiceModule.class)).andReturn(null);
        })
        .run(unit -> {
          new Scanner()
              .scan(Module.class)
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @SuppressWarnings("unchecked")
  @Test
  public void scanGuavaService() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class, Registry.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns", "com.google.common.util.concurrent"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(implementing(Service.class, GuavaService.class.getName()))
        .expect(unit -> {
          Binder binder = unit.get(Binder.class);
          AnnotatedBindingBuilder<GuavaService> abb = unit.mock(AnnotatedBindingBuilder.class);
          abb.asEagerSingleton();
          expect(binder.bind(GuavaService.class)).andReturn(abb);

          AnnotatedBindingBuilder<ServiceManager> abbsm = unit.mock(AnnotatedBindingBuilder.class);
          expect(abbsm.toProvider(unit.capture(Provider.class))).andReturn(abbsm);
          expect(binder.bind(ServiceManager.class)).andReturn(abbsm);

          Env env = unit.get(Env.class);
          expect(env.onStart(unit.capture(Throwing.Consumer.class))).andReturn(env);
          expect(env.onStop(unit.capture(Throwing.Runnable.class))).andReturn(env);
        })
        .expect(unit -> {
          GuavaService service = unit.mock(GuavaService.class);
          Registry registry = unit.get(Registry.class);
          expect(registry.require(GuavaService.class)).andReturn(service);

          ServiceManager sm = unit.constructor(ServiceManager.class)
              .build(Lists.newArrayList(service));

          unit.registerMock(ServiceManager.class, sm);

          expect(sm.startAsync()).andReturn(sm);
          sm.awaitHealthy();

          expect(sm.stopAsync()).andReturn(sm);
          sm.awaitStopped();
        })
        .run(unit -> {
          new Scanner()
              .scan(Service.class)
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        }, unit -> {
          unit.captured(Throwing.Consumer.class).iterator().next().accept(unit.get(Registry.class));

          assertEquals(unit.get(ServiceManager.class),
              unit.captured(Provider.class).iterator().next().get());

          unit.captured(Throwing.Runnable.class).iterator().next().run();
        });
  }

  @Test
  public void scanEmptyGuavaService() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class, Registry.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns", "com.google.common.util.concurrent"))
        .expect(routes)
        .expect(annotations(Path.class))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(implementing(Service.class))
        .run(unit -> {
          new Scanner()
              .scan(Service.class)
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void scanShouldCreateJustOneController() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns"))
        .expect(routes)
        .expect(
            annotations(Path.class, FooController.class.getName(), FooController.class.getName()))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .expect(unit -> {
          Router routes = unit.get(Router.class);
          expect(routes.use(FooController.class)).andReturn(null);
        })
        .run(unit -> {
          new Scanner()
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @Test
  public void scanShouldIgnoreAbsClasses() throws Exception {
    new MockUnit(Env.class, Config.class, Binder.class, Router.class)
        .expect(ns("app.ns"))
        .expect(runtimeProcessors)
        .expect(scanResult("app.ns"))
        .expect(routes)
        .expect(
            annotations(Path.class, AbsController.class.getName()))
        .expect(subClassesOf(Jooby.class))
        .expect(appclass(ScannerTest.class.getName()))
        .run(unit -> {
          new Scanner()
              .configure(unit.get(Env.class), unit.get(Config.class), unit.get(Binder.class));
        });
  }

  @SuppressWarnings("rawtypes")
  private Block annotations(final Class type, final String... names) {
    return annotations(type, Arrays.asList(names));
  }

  @SuppressWarnings("rawtypes")
  private Block annotations(final Class type, final List<String> classes) {
    return unit -> {
      ScanResult result = unit.get(ScanResult.class);
      expect(result.getNamesOfClassesWithAnnotation(type)).andReturn(classes);
    };
  }

  @SuppressWarnings("rawtypes")
  private Block implementing(final Class type, final String... names) {
    return implementing(type, Arrays.asList(names));
  }

  @SuppressWarnings("rawtypes")
  private Block implementing(final Class type, final List<String> classes) {
    return unit -> {
      ScanResult result = unit.get(ScanResult.class);
      expect(result.getNamesOfClassesImplementing(type))
          .andReturn(classes);
    };
  }

  @SuppressWarnings("rawtypes")
  private Block subClassesOf(final Class type, final String... names) {
    return subClassesOf(type, Arrays.asList(names));
  }

  @SuppressWarnings("rawtypes")
  private Block subClassesOf(final Class type, final List<String> classes) {
    return unit -> {
      ScanResult result = unit.get(ScanResult.class);
      expect(result.getNamesOfSubclassesOf(type))
          .andReturn(classes);
    };
  }

  private Block ns(final String pkg) {
    return unit -> {
      Config conf = unit.get(Config.class);
      expect(conf.getString("application.ns")).andReturn(pkg);
    };
  }

  private Block appclass(final String main) {
    return unit -> {
      Config conf = unit.get(Config.class);
      expect(conf.getString("application.class")).andReturn(main);
    };
  }

  private Block scanResult(final Object... spec) {
    return unit -> {
      FastClasspathScanner scanner = unit.constructor(FastClasspathScanner.class)
          .args(String[].class)
          .build(spec);

      ScanResult result = unit.mock(ScanResult.class);

      expect(scanner.scan(2)).andReturn(result);

      unit.registerMock(ScanResult.class, result);
    };
  }
}
