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

import org.apache.openejb.AppContext;
import org.apache.openejb.BeanContext;
import org.apache.openejb.BeanType;
import org.apache.openejb.ModuleContext;
import org.apache.openejb.SystemException;
import org.apache.openejb.core.ThreadContext;
import org.apache.openejb.core.ivm.naming.IvmContext;
import org.apache.openejb.core.transaction.JtaTransactionPolicyFactory;
import org.apache.openejb.core.transaction.TransactionPolicy;
import org.apache.openejb.core.transaction.TransactionType;
import org.apache.openejb.loader.SystemInstance;
import org.junit.internal.runners.model.ReflectiveCallable;
import org.junit.internal.runners.statements.Fail;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;

import javax.interceptor.Interceptors;
import javax.transaction.TransactionManager;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

/**
 * @version $Rev$ $Date$
 */
public class LocalClientRunner extends BlockJUnit4ClassRunner {

    private final BeanContext deployment;
    private final Class<?> clazz;

    public LocalClientRunner(final Class<?> clazz) throws InitializationError {
        super(clazz);
        deployment = createDeployment(clazz);
        this.clazz = clazz;
    }

    @Override
    protected Statement methodBlock(final FrameworkMethod method) {
        final Object instance = newTestInstance();

        final Test test = new Test(clazz, method.getMethod(), instance, deployment);

        Statement statement = methodInvoker(method, instance);

        statement = wrap(test, statement, RunAs.class, javax.annotation.security.RunAs.class);
        statement = wrap(test, statement, RunTestAs.class, org.apache.openejb.junit.RunTestAs.class);
        statement = wrap(test, statement, Transaction.class, org.apache.openejb.junit.Transaction.class);
        statement = wrap(test, statement, TransactionAttribute.class, javax.ejb.TransactionAttribute.class);

        statement = possiblyExpectingExceptions(method, instance, statement);
        statement = withPotentialTimeout(method, instance, statement);
        statement = withBefores(method, instance, statement);
        statement = withAfters(method, instance, statement);
        return statement;
    }

    private Statement wrap(final Test test, final Statement statement, final Class<? extends AnnotationStatement> clazz, final Class<? extends Annotation> annotation) {

        if (test.has(annotation)) {
            try {
                final Class[] types = {annotation, Statement.class, Test.class};
                final Object[] args = {test.get(annotation), statement, test};
                return clazz.getConstructor(types).newInstance(args);
            } catch (final Exception e) {
                throw new IllegalStateException("Cannot construct " + clazz, e);
            }
        }
        return statement;
    }

    /**
     * Creates a new test instance
     *
     * @return new instance
     */
    private Object newTestInstance() {
        try {
            return new ReflectiveCallable() {
                @Override
                protected Object runReflectiveCall() throws Throwable {
                    return createTest();
                }
            }.run();
        } catch (final Throwable e) {
            return new Fail(e);
        }
    }

    private BeanContext createDeployment(final Class<?> testClass) {
        try {
            final AppContext appContext = new AppContext("", SystemInstance.get(), testClass.getClassLoader(), new IvmContext(), new IvmContext(), false);
            final ModuleContext moduleContext = new ModuleContext("", null, "", appContext, new IvmContext(), null);
            return new BeanContext(null, new IvmContext(), moduleContext, testClass, null, null, null, null, null, null, null, null, null, BeanType.MANAGED, false, false);
        } catch (final SystemException e) {
            throw new IllegalStateException(e);
        }
    }

    public abstract static class AnnotationStatement<A extends Annotation> extends Statement {

        protected final A annotation;
        protected final Statement next;
        protected final Test test;
        protected final BeanContext info;

        protected AnnotationStatement(final A annotation, final Statement next, final Test test) {
            this.annotation = annotation;
            this.next = next;
            this.test = test;
            this.info = test.info;
        }
    }

    private static final class Test {

        public final Class clazz;
        public final Method method;
        public final Object instance;
        public final BeanContext info;

        private Test(final Class clazz, final Method method, final Object instance, final BeanContext info) {
            this.clazz = clazz;
            this.method = method;
            this.instance = instance;
            this.info = info;
        }

        private <A extends Annotation> boolean has(final Class<A> a) {
            return method.isAnnotationPresent(a) || clazz.isAnnotationPresent(a);
        }

        private <A extends Annotation> A get(final Class<A> annotationClass) {
            A annotation = method.getAnnotation(annotationClass);

            if (annotation == null) {
                annotation = (A) clazz.getAnnotation(annotationClass);
            }

            return annotation;
        }
    }

    public static class RunAs extends AnnotationStatement<javax.annotation.security.RunAs> {

        public RunAs(final javax.annotation.security.RunAs annotation, final Statement next, final Test test) {
            super(annotation, next, test);
        }

        public void evaluate() throws Throwable {
            info.setRunAs(annotation.value());
            final ThreadContext context = new ThreadContext(info, null);
            final ThreadContext old = ThreadContext.enter(context);
            try {
                next.evaluate();
            } finally {
                ThreadContext.exit(old);
            }
        }
    }

    public static class RunTestAs extends AnnotationStatement<org.apache.openejb.junit.RunTestAs> {

        public RunTestAs(final org.apache.openejb.junit.RunTestAs annotation, final Statement next, final Test test) {
            super(annotation, next, test);
        }

        public void evaluate() throws Throwable {
            info.setRunAs(annotation.value());
            final ThreadContext context = new ThreadContext(info, null);
            final ThreadContext old = ThreadContext.enter(context);
            try {
                next.evaluate();
            } finally {
                ThreadContext.exit(old);
            }
        }
    }

    public static class TransactionAttribute extends AnnotationStatement<javax.ejb.TransactionAttribute> {

        public TransactionAttribute(final javax.ejb.TransactionAttribute annotation, final Statement next, final Test test) {
            super(annotation, next, test);
        }

        public void evaluate() throws Throwable {
            final TransactionManager transactionManager = SystemInstance.get().getComponent(TransactionManager.class);
            final JtaTransactionPolicyFactory factory = new JtaTransactionPolicyFactory(transactionManager);
            final TransactionType transactionType = TransactionType.get(annotation.value());
            // This creates *and* begins the transaction
            final TransactionPolicy policy = factory.createTransactionPolicy(transactionType);
            try {
                next.evaluate();
            } catch (final Throwable t) {
                if (!isApplicationException(t)) {
                    policy.setRollbackOnly();
                }
            } finally {
                policy.commit();
            }
        }

        private boolean isApplicationException(final Throwable t) {
            if (t.getClass().isAnnotationPresent(javax.ejb.ApplicationException.class)) {
                return true;
            }
            if (t instanceof Error) {
                return false;
            }
            if (t instanceof RuntimeException) {
                return false;
            }
            return true;
        }
    }

    public static class Transaction extends AnnotationStatement<org.apache.openejb.junit.Transaction> {

        public Transaction(final org.apache.openejb.junit.Transaction annotation, final Statement next, final Test test) {
            super(annotation, next, test);
        }

        public void evaluate() throws Throwable {
            final TransactionManager transactionManager = SystemInstance.get().getComponent(TransactionManager.class);
            final JtaTransactionPolicyFactory factory = new JtaTransactionPolicyFactory(transactionManager);

            // This creates *and* begins the transaction
            final TransactionPolicy policy = factory.createTransactionPolicy(TransactionType.RequiresNew);
            try {
                next.evaluate();
            } finally {

                if (annotation.rollback()) {
                    policy.setRollbackOnly();
                }

                policy.commit();
            }
        }
    }

    public static class Interceptorss extends AnnotationStatement<Interceptors> {
        public Interceptorss(final Interceptors annotation, final Statement next, final Test test) {
            super(annotation, next, test);
        }

        public void evaluate() throws Throwable {
        }
    }
}
