/*
 * JBoss, Home of Professional Open Source
 * Copyright 2009 Red Hat Inc. and/or its affiliates and other contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * Licensed 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.jboss.arquillian.testenricher.ejb;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.List;
import java.util.logging.Logger;

import javax.ejb.EJB;
import javax.naming.Context;
import javax.naming.NamingException;

import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.core.spi.Validate;
import org.jboss.arquillian.test.spi.TestEnricher;

/**
 * Enricher that provide EJB class and setter method injection.
 *
 * @author <a href="mailto:aknutsen@redhat.com">Aslak Knutsen</a>
 * @version $Revision: $
 */
public class EJBInjectionEnricher implements TestEnricher
{
   private static final String ANNOTATION_NAME = "javax.ejb.EJB";

   private static final Logger log = Logger.getLogger(TestEnricher.class.getName());

   @Inject
   private Instance<Context> contextInst;

   /*
    * (non-Javadoc)
    *
    * @see org.jboss.arquillian.spi.TestEnricher#enrich(org.jboss.arquillian.spi .Context, java.lang.Object)
    */
   public void enrich(Object testCase)
   {
      if (SecurityActions.isClassPresent(ANNOTATION_NAME))
      {
         try
         {
            if(createContext() != null)
            {
               injectClass(testCase);
            }
         }
         catch(Exception e)
         {
            log.throwing(EJBInjectionEnricher.class.getName(), "enrich", e);
         }
      }
   }

   /*
    * (non-Javadoc)
    *
    * @see org.jboss.arquillian.spi.TestEnricher#resolve(org.jboss.arquillian.spi .Context, java.lang.reflect.Method)
    */
   public Object[] resolve(Method method)
   {
      return new Object[method.getParameterTypes().length];
   }

   /**
    * Obtains all field in the specified class which contain the specified annotation
    *
    * @param clazz
    * @param annotation
    * @return
    * @throws IllegalArgumentException If either argument is not specified
    */
   // TODO Hack, this leaks out privileged operations outside the package. Extract out properly.
   protected List<Field> getFieldsWithAnnotation(final Class<?> clazz, final Class<? extends Annotation> annotation)
         throws IllegalArgumentException
   {
      // Precondition checks
      if (clazz == null)
      {
         throw new IllegalArgumentException("clazz must be specified");
      }
      if (annotation == null)
      {
         throw new IllegalArgumentException("annotation must be specified");
      }

      // Delegate to the privileged operations
      return SecurityActions.getFieldsWithAnnotation(clazz, annotation);
   }

   protected void injectClass(Object testCase)
   {
      try
      {
         @SuppressWarnings("unchecked")
         Class<? extends Annotation> ejbAnnotation = (Class<? extends Annotation>) SecurityActions.getThreadContextClassLoader().loadClass(ANNOTATION_NAME);

         List<Field> annotatedFields = SecurityActions.getFieldsWithAnnotation(
               testCase.getClass(),
               ejbAnnotation);

         for (Field field : annotatedFields)
         {
            if (field.get(testCase) == null) // only try to lookup fields that are not already set
            {
               EJB fieldAnnotation = (EJB) field.getAnnotation(ejbAnnotation);
               try
               {
                  String mappedName = fieldAnnotation.mappedName();;
                  String beanName = fieldAnnotation.beanName();
                  String lookup = attemptToGet31LookupField(fieldAnnotation);

                  String[] jndiNames = resolveJNDINames(field.getType(), mappedName, beanName, lookup);
                  Object ejb = lookupEJB(jndiNames);
                  field.set(testCase, ejb);
               }
               catch (Exception e)
               {
                  log.fine("Could not lookup " + fieldAnnotation + ", other Enrichers might, move on. Exception: "
                        + e.getMessage());
               }
            }
         }

         List<Method> methods = SecurityActions.getMethodsWithAnnotation(testCase.getClass(), ejbAnnotation);

         for (Method method : methods)
         {
            if (method.getParameterTypes().length != 1)
            {
               throw new RuntimeException("@EJB only allowed on single argument methods");
            }
            if (!method.getName().startsWith("set"))
            {
               throw new RuntimeException("@EJB only allowed on 'set' methods");
            }
            EJB parameterAnnotation = null; // method.getParameterAnnotations()[0]
            for (Annotation annotation : method.getParameterAnnotations()[0])
            {
               if (EJB.class.isAssignableFrom(annotation.annotationType()))
               {
                  parameterAnnotation = (EJB) annotation;
               }
            }

            // Default values of the annotation attributes.
            String mappedName = null;
            String beanName = null;
            String lookup = null;

            if (parameterAnnotation != null)
            {
               mappedName = parameterAnnotation.mappedName();
               beanName = parameterAnnotation.beanName();
               lookup = attemptToGet31LookupField(parameterAnnotation);
            }

            String[] jndiNames = resolveJNDINames(method.getParameterTypes()[0], mappedName, beanName, lookup);
            Object ejb = lookupEJB(jndiNames);
            method.invoke(testCase, ejb);
         }

      }
      catch (Exception e)
      {
         throw new RuntimeException("Could not inject members", e);
      }
   }

   protected String attemptToGet31LookupField(EJB annotation) throws IllegalAccessException,
         InvocationTargetException
   {
      String lookup = null;
      try
      {
         Method m = EJB.class.getMethod("lookup");
         lookup = String.valueOf(m.invoke(annotation));
      }
      catch (NoSuchMethodException e)
      {
         // No op, running on < 3.1 EJB lib
      }
      return lookup;
   }

   /**
    * Resolves the JNDI name of the given field.
    *
    * If <tt>mappedName</tt>, <tt>lookup</tt> or <tt>beanName</tt> are specified, they're used to resolve JNDI name. Otherwise, default policy
    * applies.
    *
    * If more than one of the <tt>mappedName</tt>, <tt>lookup</tt> and <tt>beanName</tt> {@link EJB} annotation attributes is specified at the same time, an {@link IllegalStateException}
    * will be thrown.
    *
    * @param fieldType annotated field which JNDI name should be resolved.
    * @param mappedName Value of {@link EJB}'s <tt>mappedName</tt> attribute.
    * @param beanName Value of {@link EJB}'s <tt>beanName</tt> attribute.
    * @param lookup Value of {@link EJB}'s <tt>lookup</tt> attribute.
    *
    * @return possible JNDI names which should be looked up to access the proper object.
    */
   protected String[] resolveJNDINames(Class<?> fieldType, String mappedName, String beanName, String lookup)
   {

      MessageFormat msg = new MessageFormat(
            "Trying to resolve JNDI name for field \"{0}\" with mappedName=\"{1}\" and beanName=\"{2}\"");
      log.finer(msg.format(new Object[] {fieldType, mappedName, beanName}));

      Validate.notNull(fieldType, "EJB enriched field cannot to be null.");

      boolean isMappedNameSet = hasValue(mappedName);
      boolean isBeanNameSet = hasValue(beanName);
      boolean isLookupSet = hasValue(lookup);

      if (isMoreThanOneValueTrue(isMappedNameSet, isBeanNameSet, isLookupSet))
      {
         throw new IllegalStateException(
               "Only one of the @EJB annotation attributes 'mappedName', 'lookup' and 'beanName' can be specified at the same time.");
      }

      String[] jndiNames;

      // If set, use only mapped name or bean name to lookup the EJB.
      if (isMappedNameSet)
      {
         jndiNames = new String[] {mappedName};
      }
      else if (isLookupSet)
      {
         jndiNames = new String[] {lookup};
      }
      else if (isBeanNameSet)
      {
         jndiNames = new String[] {"java:module/" + beanName + "!" + fieldType.getName()};
      }
      else
      {
          jndiNames = getJndiNamesForAnonymousEJB(fieldType);

      }
      return jndiNames;
   }

   protected String[] getJndiNamesForAnonymousEJB(Class<?> fieldType)
   {
      String[] jndiNames;
      // TODO: These names are not spec compliant; fieldType needs to be a bean type here, but usually is just an interface of a bean. These seldom work.
      jndiNames = new String[] {
            "java:global/test.ear/test/" + fieldType.getSimpleName() + "Bean",
            "java:global/test.ear/test/" + fieldType.getSimpleName(),
            "java:global/test/" + fieldType.getSimpleName(),
            "java:global/test/" + fieldType.getSimpleName() + "Bean",
            "java:global/test/" + fieldType.getSimpleName() + "/no-interface",
            "test/" + fieldType.getSimpleName() + "Bean/local",
            "test/" + fieldType.getSimpleName() + "Bean/remote",
            "test/" + fieldType.getSimpleName() + "/no-interface",
            fieldType.getSimpleName() + "Bean/local",
            fieldType.getSimpleName() + "Bean/remote",
            fieldType.getSimpleName() + "/no-interface",
            // WebSphere Application Server Local EJB default binding
            "ejblocal:" + fieldType.getCanonicalName(),
            // WebSphere Application Server Remote EJB default binding
            fieldType.getCanonicalName()};
      return jndiNames;
   }

   protected Object lookupEJB(String[] jndiNames) throws Exception
   {
      // TODO: figure out test context ?
      Context initcontext = createContext();

      for (String jndiName : jndiNames)
      {
         try
         {
            return initcontext.lookup(jndiName);
         }
         catch (NamingException e)
         {
            // no-op, try next
         }
      }
      throw new NamingException("No EJB found in JNDI, tried the following names: " + joinJndiNames(jndiNames));
   }

   protected Context createContext() throws Exception
   {
      return contextInst.get();
   }

   // Simple helper for printing the jndi names
   private String joinJndiNames(String[] strings)
   {
      StringBuilder sb = new StringBuilder();

      for (String string : strings)
      {
         sb.append(string).append(", ");
      }
      return sb.toString();
   }

   /**
    * Helper method that checks if the given String has a non-empty value.
    *
    * @param string String to be checked.
    * @return true if <tt>string</tt> is not null and has non-empty value; false otherwise.
    */
   private boolean hasValue(String string)
   {
      if (string != null && (!string.trim().equals("")))
      {
         return true;
      }
      else
      {
         return false;
      }
   }

   private boolean isMoreThanOneValueTrue(boolean... values) {
		boolean trueFound = false;
		for (boolean value : values) {
			if (value) {
				if (trueFound) {
					return true;
				}
				trueFound = true;
			}
		}
		return false;
	}
}