/*
 * 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.junit;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.jboss.arquillian.junit.event.AfterRules;
import org.jboss.arquillian.junit.event.BeforeRules;
import org.jboss.arquillian.junit.event.RulesEnrichment;
import org.jboss.arquillian.test.spi.LifecycleMethodExecutor;
import org.jboss.arquillian.test.spi.TestMethodExecutor;
import org.jboss.arquillian.test.spi.TestResult;
import org.jboss.arquillian.test.spi.TestResult.Status;
import org.jboss.arquillian.test.spi.TestRunnerAdaptor;
import org.jboss.arquillian.test.spi.TestRunnerAdaptorBuilder;
import org.jboss.arquillian.test.spi.execution.SkippedTestExecutionException;
import org.junit.internal.AssumptionViolatedException;
import org.junit.internal.runners.model.MultipleFailureException;
import org.junit.internal.runners.model.ReflectiveCallable;
import org.junit.internal.runners.statements.Fail;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

/**
 * Main Arquillian JUnit runner
 *
 * @author <a href="mailto:aslak@conduct.no">Aslak Knutsen</a>
 * @version $Revision: $
 */
public class Arquillian extends BlockJUnit4ClassRunner
{
   private TestRunnerAdaptor adaptor;

   public Arquillian(Class<?> testClass) throws InitializationError
   {
      super(testClass);
      if(State.isRunningInEclipse()) {
          State.runnerStarted();
      }
   }
   
   @Override
   protected List<FrameworkMethod> getChildren()
   {
      List<FrameworkMethod> children = super.getChildren();
      // Only sort if InOrder is defined, else keep them in original order returned by parent
      boolean hasDefinedOrder = false;
      for(FrameworkMethod method : children)
      {
         if(method.getAnnotation(InSequence.class) != null)
         {
            hasDefinedOrder = true;
         }
      }
      if(hasDefinedOrder)
      {
         List<FrameworkMethod> sorted = new ArrayList<FrameworkMethod>(children);
         Collections.sort(sorted, new InSequenceSorter());
         return sorted;
      }
      return children;
   }

   @Override
   public void run(final RunNotifier notifier)
   {
      if(State.isNotRunningInEclipse()) {
          State.runnerStarted();
      }
      // first time we're being initialized
      if(!State.hasTestAdaptor())   
      {
         // no, initialization has been attempted before and failed, refuse to do anything else
         if(State.hasInitializationException())  
         {
            // failed on suite level, ignore children
            //notifier.fireTestIgnored(getDescription());
            notifier.fireTestFailure(
                  new Failure(getDescription(), 
                        new RuntimeException(
                              "Arquillian has previously been attempted initialized, but failed. See cause for previous exception", 
                              State.getInitializationException())));
         }
         else
         {
            try 
            {
               // ARQ-1742 If exceptions happen during boot
               TestRunnerAdaptor adaptor = TestRunnerAdaptorBuilder.build();
               // don't set it if beforeSuite fails
               adaptor.beforeSuite();
               State.testAdaptor(adaptor);
            } 
            catch (Exception e)  
            {
               // caught exception during BeforeSuite, mark this as failed
               State.caughtInitializationException(e);
               notifier.fireTestFailure(new Failure(getDescription(), e));
            }
         }
      }
      notifier.addListener(new RunListener()
      {
         @Override
         public void testRunFinished(Result result) throws Exception
         {
            State.runnerFinished();
            shutdown();
         }

         private void shutdown()
         {
            try
            {
               if(State.isLastRunner())
               {
                  try
                  {
                     if(adaptor != null)
                     {
                        adaptor.afterSuite();
                        adaptor.shutdown();
                     }
                  }
                  finally
                  {
                     State.clean();
                  }
               }
               adaptor = null;
            }
            catch (Exception e)
            {
               throw new RuntimeException("Could not run @AfterSuite", e);
            }
         }
      });
      // initialization ok, run children
      if(State.hasTestAdaptor())
      {
         adaptor = State.getTestAdaptor();
         super.run(notifier);
      }
   }

   /**
    * Override to allow test methods with arguments
    */
   @Override
   protected void validatePublicVoidNoArgMethods(Class<? extends Annotation> annotation, boolean isStatic, List<Throwable> errors)
   {
      List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(annotation);
      for (FrameworkMethod eachTestMethod : methods)
      {
         eachTestMethod.validatePublicVoid(isStatic, errors);
      }
   }
      
   /*
    * Override BeforeClass/AfterClass and Before/After handling.
    * 
    * Let super create the Before/After chain against a EmptyStatement so our newly created Statement
    * only contains the method that are of interest to us(@Before..etc). 
    * They can then optionally be executed if we get expected callback.
    * 
    */
      
   @Override
   protected Statement withBeforeClasses(final Statement originalStatement)
   {
      final Statement onlyBefores = super.withBeforeClasses(new EmptyStatement());
      return new Statement() 
      {
         @Override
         public void evaluate() throws Throwable
         {
            adaptor.beforeClass(
                  Arquillian.this.getTestClass().getJavaClass(), 
                  new StatementLifecycleExecutor(onlyBefores));
            originalStatement.evaluate();
         }
      };
   }
   
   @Override
   protected Statement withAfterClasses(final Statement originalStatement)
   {
      final Statement onlyAfters = super.withAfterClasses(new EmptyStatement());
      return new Statement() 
      {
         @Override
         public void evaluate() throws Throwable
         {
            multiExecute
            (
               originalStatement,
               new Statement() { @Override public void evaluate() throws Throwable 
               {
                   adaptor.afterClass(
                        Arquillian.this.getTestClass().getJavaClass(), 
                        new StatementLifecycleExecutor(onlyAfters));
               }}
            );
         }
      };
   }   

   @Override
   protected Statement withBefores(final FrameworkMethod method, final Object target, final Statement originalStatement)
   {
      final Statement onlyBefores = super.withBefores(method, target, new EmptyStatement());
      return new Statement()
      {
         @Override
         public void evaluate() throws Throwable
         {
             adaptor.before(
                  target,
                  method.getMethod(),
                  new StatementLifecycleExecutor(onlyBefores));
            originalStatement.evaluate();
         }
      };
   }

   @Override
   protected Statement withAfters(final FrameworkMethod method, final Object target, final Statement originalStatement)
   {
      final Statement onlyAfters = super.withAfters(method, target, new EmptyStatement());
      return new Statement()
      {
         @Override
         public void evaluate() throws Throwable
         {
            multiExecute
            (
               originalStatement,
               new  Statement() { @Override public void evaluate() throws Throwable
               {
                  adaptor.after(
                          target,
                          method.getMethod(),
                          new StatementLifecycleExecutor(onlyAfters));
               }}
            );
         }
      };
   }
   @Override
   @SuppressWarnings("deprecation")
   protected Statement methodBlock(final FrameworkMethod method) {
       Object temp;
       try {
           temp = new ReflectiveCallable() {
               @Override
               protected Object runReflectiveCall() throws Throwable {
                   return createTest();
               }
           }.run();
       } catch (Throwable e) {
           return new Fail(e);
       }
       final Object test = temp;
       try
       {
           Method withRules = BlockJUnit4ClassRunner.class.getDeclaredMethod("withRules",
                   FrameworkMethod.class, Object.class, Statement.class);
           withRules.setAccessible(true);

           Statement statement = methodInvoker(method, test);
           statement = possiblyExpectingExceptions(method, test, statement);
           statement = withPotentialTimeout(method, test, statement);

           Statement arounds = withBefores(method, test, statement);
           arounds = withAfters(method, test, arounds);
           final Statement stmtWithLifecycle = arounds;

           adaptor.fireCustomLifecycle(
               new RulesEnrichment(test, getTestClass(), method.getMethod(), LifecycleMethodExecutor.NO_OP));

           final Statement stmtWithRules = (Statement)withRules.invoke(this, method, test, arounds);
           return new Statement() {

               @Override
               public void evaluate() throws Throwable {
                   State.caughtExceptionAfterJunit(null);
                   final AtomicInteger integer = new AtomicInteger();
                   List<Throwable> exceptions = new ArrayList<Throwable>();

                   try
                   {
                       adaptor.fireCustomLifecycle(new BeforeRules(test, getTestClass(), stmtWithRules, method.getMethod(),
                           new LifecycleMethodExecutor()
                           {
                               @Override
                               public void invoke() throws Throwable
                               {
                                   integer.incrementAndGet();
                                   stmtWithRules.evaluate();
                               }
                           }));
                       // If AroundRules (includes lifecycles) were not executed, invoke only lifecycles+test
                       if(integer.get() == 0) {
                           try {
                               stmtWithLifecycle.evaluate();
                           } catch(Throwable t) {
                               State.caughtExceptionAfterJunit(t);
                               throw t;
                           }
                       }
                   } catch(Throwable t) {
                       State.caughtExceptionAfterJunit(t);
                       exceptions.add(t);
                   }
                   finally {
                       try {
                           adaptor.fireCustomLifecycle(new AfterRules(test, method.getMethod(), LifecycleMethodExecutor.NO_OP));
                       } catch(Throwable t) {
                           State.caughtExceptionAfterJunit(t);
                           exceptions.add(t);
                       }
                   }
                   if(exceptions.isEmpty())
                   {
                      return;
                   }
                   if(exceptions.size() == 1)
                   {
                      throw exceptions.get(0);
                   }
                   throw new MultipleFailureException(exceptions);
               }
           };
       } catch(Exception e) {
           throw new RuntimeException("Could not create statement", e);
       }
    }

   @Override
   protected Statement methodInvoker(final FrameworkMethod method, final Object test)
   {
      return new Statement()
      {
         @Override
         public void evaluate() throws Throwable
         {
            TestResult result = adaptor.test(new TestMethodExecutor()
            {
               @Override
               public void invoke(Object... parameters) throws Throwable
               {
                  try
                  {
                     method.invokeExplosively(test, parameters); 
                  } 
                  catch (Throwable e) 
                  {
                     // Force a way to return the thrown Exception from the Container to the client. 
                     State.caughtTestException(e);
                     throw e;
                  }
               }
               
               public Method getMethod()
               {
                  return method.getMethod();
               }
               
               public Object getInstance()
               {
                  return test;
               }
            });
            Throwable throwable = result.getThrowable();
            if(throwable != null)
            {
               if (result.getStatus() == Status.SKIPPED)
               {
                   if (throwable instanceof SkippedTestExecutionException)
                   {
                       result.setThrowable(new AssumptionViolatedException(throwable.getMessage()));    
                   }
               }
               throw result.getThrowable();
            }
         }
      };
   }

   /**
    * A helper to safely execute multiple statements in one.<br/>
    * 
    * Will execute all statements even if they fail, all exceptions will be kept. If multiple {@link Statement}s
    * fail, a {@link MultipleFailureException} will be thrown.
    *
    * @author <a href="mailto:aslak@redhat.com">Aslak Knutsen</a>
    * @version $Revision: $
    */
   private void multiExecute(Statement... statements) throws Throwable 
   {
      List<Throwable> exceptions = new ArrayList<Throwable>();
      for(Statement command : statements) 
      {
         try
         {
            command.evaluate();
         } 
         catch (Throwable e) 
         {
            exceptions.add(e);
         }
      }
      if(exceptions.isEmpty())
      {
         return;
      }
      if(exceptions.size() == 1)
      {
         throw exceptions.get(0);
      }
      throw new MultipleFailureException(exceptions);
   }
   
   private static class EmptyStatement extends Statement
   {
      @Override
      public void evaluate() throws Throwable
      {
      }
   }
 
   private static class StatementLifecycleExecutor implements LifecycleMethodExecutor
   {
      private Statement statement;
      
      public StatementLifecycleExecutor(Statement statement)
      {
         this.statement = statement;
      }
      
      public void invoke() throws Throwable
      {
         statement.evaluate();
      }
   }
}
