/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010, Red Hat, Inc., and individual 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.cdi.tck;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.enterprise.context.spi.Context;
import javax.enterprise.inject.AmbiguousResolutionException;
import javax.enterprise.inject.UnsatisfiedResolutionException;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.util.TypeLiteral;
import javax.inject.Inject;

import org.jboss.arquillian.testng.Arquillian;
import org.jboss.cdi.tck.api.Configuration;
import org.jboss.cdi.tck.impl.ConfigurationFactory;
import org.jboss.cdi.tck.util.BeanLookupUtils;
import org.jboss.cdi.tck.util.DependentInstance;

/**
 * Abstract CDI TCK test.
 */
public abstract class AbstractTest extends Arquillian {

    @Inject
    protected BeanManager beanManager;

    /**
     * Note that TCK uses Arquillian Servlet protocol and so the test class is instantiated/enriched from within the WAR module.
     * This affects most of the {@link BeanManager} methods functionality. For instance the method
     * {@link BeanManager#getBeans(Type, Annotation...)} returns the set of beans which are available for injection in the
     * module or library containing the class into which the {@link BeanManager} was injected (simplified).
     *
     * @return the current {@link BeanManager}
     */
    protected BeanManager getCurrentManager() {
        return beanManager;
    }

    protected byte[] passivate(Object instance) throws IOException {
        return getCurrentConfiguration().getBeans().passivate(instance);
    }

    protected Object activate(byte[] bytes) throws IOException, ClassNotFoundException {
        return getCurrentConfiguration().getBeans().activate(bytes);
    }

    protected void setContextActive(Context context) {
        getCurrentConfiguration().getContexts().setActive(context);
    }

    protected void setContextInactive(Context context) {
        getCurrentConfiguration().getContexts().setInactive(context);
    }

    protected void destroyContext(Context context) {
        getCurrentConfiguration().getContexts().destroyContext(context);
    }

    protected Configuration getCurrentConfiguration() {
        return ConfigurationFactory.get();
    }

    /**
     * Checks if all annotations are in a given set of annotations
     *
     * @param annotations The annotation set
     * @param requiredAnnotationTypes The annotations to match
     * @return True if match, false otherwise
     */
    protected boolean annotationSetMatches(Set<? extends Annotation> annotations,
            Class<? extends Annotation>... requiredAnnotationTypes) {
        Set<Class<? extends Annotation>> annotationsTypeSet = new HashSet<Class<? extends Annotation>>();
        for (Annotation annotation : annotations) {
            annotationsTypeSet.add(annotation.annotationType());
        }
        return typeSetMatches(annotationsTypeSet, requiredAnnotationTypes);
    }

    /**
     * @param annotations The annotation set
     * @param requiredAnnotationTypes The required annotations
     * @return <code>true</code> if the specified set matches required annotations, <code>false</code> otherwise
     */
    protected boolean annotationSetMatches(Set<? extends Annotation> annotations, Annotation... requiredAnnotations) {
        List<Annotation> requiredAnnotationList = new ArrayList<Annotation>();
        return requiredAnnotations.length == annotations.size() && annotations.containsAll(requiredAnnotationList);
    }

    protected boolean rawTypeSetMatches(Set<Type> types, Class<?>... requiredTypes) {
        Set<Type> typesRawSet = new HashSet<Type>();
        for (Type type : types) {
            if (type instanceof Class<?>) {
                typesRawSet.add(type);
            } else if (type instanceof ParameterizedType) {
                typesRawSet.add(((ParameterizedType) type).getRawType());
            }
        }
        return typeSetMatches(typesRawSet, requiredTypes);
    }

    protected boolean typeSetMatches(Collection<? extends Type> types, Type... requiredTypes) {
        List<Type> typeList = Arrays.asList(requiredTypes);
        return requiredTypes.length == types.size() && types.containsAll(typeList);
    }

    protected <T> Bean<T> getUniqueBean(Class<T> type, Annotation... bindings) {
        Set<Bean<T>> beans = getBeans(type, bindings);
        return resolveUniqueBean(type, beans);
    }

    protected <T> Bean<T> getUniqueBean(TypeLiteral<T> type, Annotation... bindings) {
        Set<Bean<T>> beans = getBeans(type, bindings);
        return resolveUniqueBean(type.getType(), beans);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected <T> Set<Bean<T>> getBeans(Class<T> type, Annotation... bindings) {
        return (Set) getCurrentManager().getBeans(type, bindings);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected <T> Set<Bean<T>> getBeans(TypeLiteral<T> type, Annotation... bindings) {
        return (Set) getCurrentManager().getBeans(type.getType(), bindings);
    }

    protected <T> T getContextualReference(Class<T> beanType, Annotation... qualifiers) {
        return BeanLookupUtils.<T>getContextualReference(getCurrentManager(), beanType, qualifiers);
    }

    protected <T> T getContextualReference(TypeLiteral<T> beanType, Annotation... qualifiers) {
        return BeanLookupUtils.<T>getContextualReference(getCurrentManager(), beanType, qualifiers);
    }

    protected <T> T getContextualReference(String name, Class<T> beanType) {
        return BeanLookupUtils.<T>getContextualReference(getCurrentManager(), name, beanType);
    }

    protected <T> DependentInstance<T> newDependentInstance(Class<T> beanType, Annotation... qualifiers) {
        return new DependentInstance<T>(getCurrentManager(), beanType, qualifiers);
    }

    private <T> Bean<T> resolveUniqueBean(Type type, Set<Bean<T>> beans) {
        if (beans.size() == 0) {
            throw new UnsatisfiedResolutionException("Unable to resolve any beans of " + type);
        } else if (beans.size() > 1) {
            throw new AmbiguousResolutionException("More than one bean available (" + beans + ")");
        }
        return beans.iterator().next();
    }

    /**
     * Extracted from test harness. We need this since testng expected exceptions feature is not working with exception cause -
     * see org.testng.internal.Invoker.isExpectedException(Throwable, ExpectedExceptionsHolder).
     *
     * @param throwableType
     * @param throwable
     * @return <code>true</code> if throwable type is assignable from specified throwable or any cause in stack (works
     *         recursively), <code>false</code> otherwise
     */
    protected boolean isThrowablePresent(Class<? extends Throwable> throwableType, Throwable throwable) {
        if (throwable == null) {
            return false;
        } else if (throwableType.isAssignableFrom(throwable.getClass())) {
            return true;
        } else {
            return isThrowablePresent(throwableType, throwable.getCause());
        }
    }

}
