/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.jooby.test;

import com.google.inject.Binder;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import org.jooby.Env;
import org.jooby.Jooby;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * JUnit4 block runner for Jooby. Internal use only.
 *
 * @author edgar
 */
public class JoobyRunner extends BlockJUnit4ClassRunner {

  private Jooby app;

  private int port;
  private int securePort;

  private Class<?> server;

  public JoobyRunner(final Class<?> klass) throws InitializationError {
    super(klass);
    prepare(klass, null);
  }

  public JoobyRunner(final Class<?> klass, final Class<?> server) throws InitializationError {
    super(klass);
    prepare(klass, server);
  }

  @Override
  protected String getName() {
    if (server != null) {
      return "[" + server.getSimpleName().toLowerCase() + "]";
    }
    return super.getName();
  }

  @Override
  protected String testName(final FrameworkMethod method) {
    if (server != null) {
      return method.getName() + getName();
    }
    return super.testName(method);
  }

  private void prepare(final Class<?> klass, final Class<?> server) throws InitializationError {
    try {
      this.port = port("coverage.port", 9999);
      this.securePort = port("coverage.securePort", 9943);
      this.server = server;
      Class<?> appClass = klass;
      if (!Jooby.class.isAssignableFrom(appClass)) {
        throw new InitializationError("Invalid jooby app: " + appClass);
      }
      int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
      // required by Jetty (processors * 2, 1(http), 1(https), 1(request)
      int maxThreads = processors * 2 + 3;
      Config config = ConfigFactory.empty("test-config")
          .withValue("server.join", ConfigValueFactory.fromAnyRef(false))
          .withValue("server.http.IdleTimeout", ConfigValueFactory.fromAnyRef("5m"))
          .withValue("server.threads.Min", ConfigValueFactory.fromAnyRef(1))
          .withValue("server.threads.Max", ConfigValueFactory.fromAnyRef(maxThreads))
          .withValue("application.port", ConfigValueFactory.fromAnyRef(port))
          .withValue("undertow.ioThreads", ConfigValueFactory.fromAnyRef(2))
          .withValue("undertow.workerThreads", ConfigValueFactory.fromAnyRef(4))
          .withValue("netty.threads.Parent", ConfigValueFactory.fromAnyRef(2));

      if (server != null) {
        config = config.withFallback(ConfigFactory.empty()
            .withValue("server.module", ConfigValueFactory.fromAnyRef(server.getName())));
      }

      app = (Jooby) appClass.newInstance();
      app.throwBootstrapException();
      if (app instanceof ServerFeature) {
        int appport = ((ServerFeature) app).port;
        if (appport > 0) {
          config = config.withValue("application.port", ConfigValueFactory.fromAnyRef(appport));
          this.port = appport;
        }

        int sappport = ((ServerFeature) app).securePort;
        if (sappport > 0) {
          config = config.withValue("application.securePort",
              ConfigValueFactory.fromAnyRef(sappport));
          this.securePort = sappport;
        }
      }
      Config testConfig = config;
      app.use(new Jooby.Module() {
        @Override
        public void configure(final Env mode, final Config config, final Binder binder) {
        }

        @Override
        public Config config() {
          return testConfig;
        }
      });
    } catch (Exception ex) {
      throw new InitializationError(Arrays.asList(ex));
    }
  }

  @Override
  protected Statement withBeforeClasses(final Statement statement) {
    Statement next = super.withBeforeClasses(statement);
    return new Statement() {

      @Override
      public void evaluate() throws Throwable {
        app.start();

        next.evaluate();
      }
    };
  }

  @Override
  protected Object createTest() throws Exception {
    Object test = super.createTest();
    Class<? extends Object> c = test.getClass();
    set(test, c, "port", port);
    set(test, c, "securePort", securePort);

    return test;
  }

  @SuppressWarnings("rawtypes")
  private void set(final Object test, final Class clazz, final String field, final Object value)
      throws Exception {
    try {
      Field f = clazz.getDeclaredField(field);
      f.setAccessible(true);
      f.set(test, value);
    } catch (NoSuchFieldException ex) {
      Class superclass = clazz.getSuperclass();
      if (superclass != Object.class) {
        set(test, superclass, field, value);
      }
    }

  }

  @Override
  protected Statement withAfterClasses(final Statement statement) {
    Statement next = super.withAfterClasses(statement);
    return new Statement() {

      @Override
      public void evaluate() throws Throwable {
        List<Throwable> errors = new ArrayList<Throwable>();
        try {
          next.evaluate();
        } catch (Throwable e) {
          errors.add(e);
        }

        try {
          app.stop();
        } catch (Exception ex) {
          errors.add(ex);
        }
        if (errors.isEmpty()) {
          return;
        }
        if (errors.size() == 1) {
          throw errors.get(0);
        }
        throw new MultipleFailureException(errors);
      }
    };
  }

  private int port(String property, Integer defaultPort) throws IOException {
    String port = System.getProperty(property, defaultPort.toString());
    if (port.equalsIgnoreCase("random")) {
      try (ServerSocket socket = new ServerSocket(0)) {
        return socket.getLocalPort();
      }
    } else {
      return Integer.parseInt(port);
    }
  }

}
