/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 */

package org.apache.avalon.excalibur.testcase;

import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import junit.framework.TestResult;

import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.component.ComponentManager;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.DefaultContext;
import org.apache.avalon.framework.logger.ConsoleLogger;
import org.apache.avalon.framework.logger.Logger;
import org.apache.avalon.framework.service.ServiceManager;
import org.apache.excalibur.fortress.ContainerManager;
import org.apache.excalibur.fortress.DefaultContainerManager;
import org.apache.excalibur.fortress.util.ContextBuilder;
import org.apache.excalibur.fortress.util.ContextManager;

/**
 * JUnit TestCase for Avalon Components based on Fortress Container 
 * Architecture. This class extends the JUnit TestCase class to setup
 * an environment which makes it possible to easily test Avalon components. 
 * Compared to {@link org.apache.avalon.excalibur.testcase.ExcaliburTestCase} 
 * this class offers support for the current fortress container and therefore 
 * supports {@link org.apache.avalon.framework.service.Serviceable} and
 * {@link org.apache.avalon.framework.service.ServiceManager}, while allowing
 * an easy migration path. 
 * <p/>
 * The following methods and instance variables are exposed for convenience 
 * testing:
 * <p/>
 * <dl>
 *   <dt>getServiceManager()</dt>
 *   <dd>
 *     This method variable contains an initialized ServiceManager
 *     object which can be used to lookup Components configured in 
 *     the test configuration file. (see below).
 *   </dd>
 *   <dt>getComponentManager()</dt>
 *   <dd>
 *     This method gives access to an initialized ComponentManager
 *     which supports the legacy Component interface.
 *   </dd>
 *   <dt>getLogger()</dt>
 *   <dd>
 *     This method returns the default framework logger for this 
 *     test case.  It is the container manager's logger.
 *   </dd>
 * </dl>
 * <p/>
 * This implementation is based on the ExcaliburTestCase and the configuration 
 * for this test case is comparable. The difference comes when defining roles:
 * <p/>
 * <pre><code>
 *  &lt;roles&gt;
 *    &lt;role name="org.apache.excalibur.datasource.DataSourceComponent"&gt;
 *      <i>&lt;!-- ServiceSelector --&gt;</i>
 *      &lt;component 
 *        shorthand="db1" 
 *        class="org.apache.excalibur.datasource.JdbcDataSource"
 *        handler="org.apache.excalibur.fortress.handler.ThreadSafeComponentHandler"&gt;
 *      &lt;/component&gt;
 *      &lt;component 
 *        shorthand="db2" 
 *        class="org.apache.excalibur.datasource.JdbcDataSource"
 *        handler="org.apache.excalibur.fortress.handler.ThreadSafeComponentHandler"&gt;
 *      &lt;/component&gt;
 *    &lt;/role&gt;
 *    &lt;role name="com.xyz.avalon.serial.SerialReader"&gt;
 *      &lt;component 
 *        shorthand="serial-reader" 
 *        class="com.xyz.avalon.serial.DefaultSerialReader"&gt;
 *      &lt;/component&gt;
 *    &lt;/role&gt;
 *  &gt;/roles>
 * </code></pre>
 * <p/>
 * The main difference is the additional <code>component</code> tag and 
 * <code>handler</code> attribute. Also not that the attribute
 * <code>default-class</code> is now named <code>class</code>.
 * <p/>
 *
 * @author <a href="mailto:giacomo@apache.org">Giacomo Pati</a>
 * @author <a href="mailto:mschier@earthlink.net">Marc Schier</a>
 * @version $Id $
 */
public class FortressTestCase extends TestCase
{
    /** 
     * The container manager's logger 
     */
    private Logger m_logger = new ConsoleLogger();

    /**
     * The manager for the container
     */
    private ContainerManager m_containerManager = null;

    /**
     * The context manager of the container
     */
    private ContextManager m_contextManager = null;

    /** 
     * The test methods in a list and keyed by class
     */
    private static HashMap m_tests = new HashMap();

    private FortressTestContainer m_container = null;

    //------------------------------- FortressTestCase constructors
    /**
     * @see TestCase#TestCase(String)
     */
    public FortressTestCase(final String name)
    {
        super(name);

        ArrayList methodList =
            (ArrayList) FortressTestCase.m_tests.get(this.getClass());

        final Method[] methods = this.getClass().getDeclaredMethods();

        if (null == methodList)
        {
            methodList = new ArrayList(methods.length);

            for (int i = 0; i < methods.length; i++)
            {
                String methodName = methods[i].getName();
                if (methodName.startsWith("test")
                    && (Modifier.isPublic(methods[i].getModifiers()))
                    && (methods[i].getReturnType().equals(Void.TYPE))
                    && (methods[i].getParameterTypes().length == 0))
                {
                    methodList.add(methodName);
                }
            }

            FortressTestCase.m_tests.put(this.getClass(), methodList);
        }
    }

    //---------------------------- overridden methods in TestCase
    /**
     * @see TestCase#run(TestResult)
     */
    final public void run(TestResult result)
    {
        final ArrayList methodList =
            (ArrayList) FortressTestCase.m_tests.get(this.getClass());

        if (null == methodList || methodList.isEmpty())
        {
            return; // The test was already run!  NOTE: this is a hack.
        }

        try
        {
            if (this instanceof Initializable)
            {
                ((Initializable) this).initialize();
            }

            this.prepare();

            Iterator tests = methodList.iterator();

            while (tests.hasNext())
            {
                String methodName = (String) tests.next();
                this.setName(methodName);

                if (this.getLogger().isDebugEnabled())
                {
                    this.getLogger().debug("");
                    this.getLogger().debug(
                        "========================================");
                    this.getLogger().debug("  begin test: " + methodName);
                    this.getLogger().debug(
                        "========================================");
                }

                super.run(result);

                if (this.getLogger().isDebugEnabled())
                {
                    this.getLogger().debug(
                        "========================================");
                    this.getLogger().debug("  end test: " + methodName);
                    this.getLogger().debug(
                        "========================================");
                    this.getLogger().debug("");
                }
            }

        }
        catch (Exception e)
        {
            System.out.println(e);
            e.printStackTrace();
            result.addError(this, e);
        }
        finally
        {
            this.done();

            if (this instanceof Disposable)
            {
                try
                {
                    ((Disposable) this).dispose();
                }
                catch (Exception e)
                {
                    result.addFailure(
                        this,
                        new AssertionFailedError("Disposal Error"));
                }
            }
        }

        methodList.clear();
        FortressTestCase.m_tests.put(this.getClass(), methodList);
    }

    //------------------------- FortressTestCase instance access methods
    /** 
     * Gives access to the Component Manager object 
     * for this test case's container.
     * @since Aug 9, 2002
     * 
     * @return ComponentManager
     *  The ComponentManager that can be used in the test
     *  case to gain access to Avalon Components.
     */
    protected final ComponentManager getComponentManager()
    {
        return m_container.getComponentManager();
    }

    /** 
     * Gives access to the Service Manager object 
     * for this test case's container.
     * @since Aug 9, 2002
     * 
     * @return ServiceManager
     *  The ServiceManager that can be used in the test
     *  case to gain access to Avalon Components.
     */
    protected final ServiceManager getServiceManager()
    {
        return m_container.getServiceManager();
    }

    /** 
     * Gives access to the logger object for this test
     * case to log information during testing to.
     * @since Aug 9, 2002
     * 
     * @return Logger
     *  The logger object that can be used in the test
     *  case to log information to.
     */
    protected final Logger getLogger()
    {
        return m_logger;
    }

    //------------------------- FortressTestCase specific implementation
    /**
     * Initializes the Unit test in that it loads the
     * xtest file from the classes package directory folder.
     * The configuration file is determined by the class name 
     * plus .xtest appended, all '.' replaced by '/' and loaded 
     * as a resource via classpath.
     * @since Aug 9, 2002
     * 
     * @throws Exception
     *  In the case an error occurs reading the configuration
     *  file.
     */
    protected void prepare() throws Exception
    {
        final String resourceName =
            getClass().getName().replace('.', '/') + ".xtest";
        final URL resource =
            getClass().getClassLoader().getResource(resourceName);

        if (resource != null)
        {
            getLogger().debug("Loading resource " + resourceName);
            prepare(resource.openStream());
        }
        else
        {
            getLogger().debug("Resource not found " + resourceName);
        }
    }

    /**
     * Initializes the Unit test. A common way to supply a 
     * InputStream is to overwrite the initialize() method
     * in the sub class, do there whatever is needed to get 
     * the right InputStream object supplying a conformant 
     * xtest configuartion and pass it to this initialize 
     * method. the mentioned initialize method is also the 
     * place to set a different logging priority to the member
     * variable m_logPriority.
     * @since Aug 9, 2002
     *
     * @param testconf 
     *  The configuration file is passed as a <code>InputStream</code>
     */
    protected void prepare(final InputStream testconf) throws Exception
    {
        getLogger().debug("FortressTestCase preparing...");

        final DefaultConfigurationBuilder builder =
            new DefaultConfigurationBuilder();
        final Configuration conf = builder.build(testconf);
        final String annotation = conf.getChild("annotation").getValue(null);

        Context context = setupContext(conf.getChild("context"));
        final ContextBuilder contextBuilder = new ContextBuilder(context);

        // container class is the stage container to test
        contextBuilder.setContainerClass(FortressTestContainer.class.getName());
        contextBuilder.setContextDirectory("./");
        contextBuilder.setWorkDirectory("./");
        contextBuilder.setLoggerCategory(null);
        contextBuilder.setCommandQueue(null);

        contextBuilder.setContextClassLoader(
            Thread.currentThread().getContextClassLoader());

        contextBuilder.setContainerConfiguration(conf.getChild("components"));
        contextBuilder.setLoggerManagerConfiguration(conf.getChild("logkit"));
        contextBuilder.setRoleManagerConfiguration(conf.getChild("roles"));

        m_contextManager =
            new ContextManager(contextBuilder.getContext(), null);

        // initialize the context manager
        m_contextManager.initialize();
        // then set the context manager to be used by the container's manager
        m_containerManager =
            new DefaultContainerManager(m_contextManager, null);
        // init the manager
        m_containerManager.initialize();

        m_container = (FortressTestContainer) m_containerManager.getContainer();
        m_logger = m_container.getLoggerManager().getLoggerForCategory("test");

        if ((null != annotation) && !("".equals(annotation)))
        {
            m_logger.info(annotation);
        }
    }

    /**
     * Disposes the <code>ContainerManager</code> and
     * <code>ContextManager</code>.
     * @since Aug 9, 2002
     */
    final private void done()
    {
        if (null != m_containerManager)
        {
            m_containerManager.dispose();
        }
        if (null != m_contextManager)
        {
            m_contextManager.dispose();
        }
    }

    /**
     * Exctract the base class name of a class name.
     * @since Aug 9, 2002
     * 
     * @param clazz
     *  The clazz for which we want to return the base 
     *  class name (stripped of the inner class name)
     * @return String
     *  The base class name of the class
     */
    private String getBaseClassName(Class clazz)
    {
        String name = clazz.getName();
        int pos = name.lastIndexOf('.');
        if (pos >= 0)
        {
            name = name.substring(pos + 1);
        }
        return name;
    }

    /**
     * set up a context according to the xtest configuration 
     * specifications context element.
     * A method addContext(DefaultContext context) is called 
     * here to enable subclasses to put additional objects 
     * into the context programmatically.
     * @since Aug 9, 2002
     * 
     * @param configuration
     *  the configuration from which to generate the context
     */
    private final Context setupContext(final Configuration configuration)
        throws Exception
    {
        //FIXME(GP): This method should setup the Context object according to the
        //           configuration spec. not yet completed
        final DefaultContext context = new DefaultContext();
        final Configuration[] confs = configuration.getChildren("entry");
        for (int i = 0; i < confs.length; i++)
        {
            final String key = confs[i].getAttribute("name");
            final String value = confs[i].getAttribute("value", null);
            if (value == null)
            {
                String clazz = confs[i].getAttribute("class");
                Object obj =
                    this
                        .getClass()
                        .getClassLoader()
                        .loadClass(clazz)
                        .newInstance();
                context.put(key, obj);
                if (getLogger().isInfoEnabled())
                    getLogger().info(
                        "ExcaliburTestCase: added an instance of class "
                            + clazz
                            + " to context entry "
                            + key);
            }
            else
            {
                context.put(key, value);
                if (getLogger().isInfoEnabled())
                    getLogger().info(
                        "ExcaliburTestCase: added value \""
                            + value
                            + "\" to context entry "
                            + key);
            }
        }
        addContext(context);
        return context;
    }

    /**
     * This method may be overwritten by subclasses to put 
     * additional objects into the context programmatically.
     * @since Aug 9, 2002
     * 
     * @param context
     *  The context in which to put additional information.
     */
    protected void addContext(DefaultContext context)
    {
    }
}
