/*
 * 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 {
            return new BeanContext(null, new IvmContext(), new ModuleContext("", null, "", new AppContext("", SystemInstance.get(), testClass.getClassLoader(), new IvmContext(), new IvmContext(), false), new IvmContext(), null), testClass, null, null, null, null, null, null, null, null, null, BeanType.MANAGED, 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 {
        }
    }
}
