/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2001-2003 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: XMLTestCase.java 6379 2006-10-31 03:28:59Z ekuns $
 */
package org.exolab.castor.tests.framework;

import junit.framework.TestCase;

import org.exolab.castor.tests.framework.testDescriptor.CallMethod;
import org.exolab.castor.tests.framework.testDescriptor.UnitTestCase;
import org.exolab.castor.tests.framework.testDescriptor.types.FailureStepType;
import org.exolab.castor.tests.framework.testDescriptor.types.TypeType;
import org.exolab.castor.tests.framework.testDescriptor.Configuration;
import org.exolab.castor.tests.framework.testDescriptor.ConfigurationType;
import org.exolab.castor.tests.framework.testDescriptor.FailureType;
import org.exolab.castor.tests.framework.testDescriptor.ListenerType;

import org.exolab.castor.tests.framework.testDescriptor.Value;
import org.exolab.castor.util.NestedIOException;

import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.MarshalListener;
import org.exolab.castor.xml.UnmarshalListener;

import org.xml.sax.InputSource;

import java.io.InputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Enumeration;

import java.lang.reflect.Method;

/**
 * This class encapsulates all the common logic to run the test patterns for
 * Castor XML. Basically it handle the marshalling/marshalling/comparing. This
 * is used to factor the common code for the source generator test and the
 * mapping/introspection tests as only the setup differ in the test patterns.
 * <p>
 * This class is not complete and expects to be extended.
 *
 * @author <a href="mailto:gignoux@kernelcenter.org">Sebastien Gignoux</a>
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
 * @version $Revision: 6379 $ $Date: 2006-04-26 15:14:53 -0600 (Wed, 26 Apr 2006) $
 */
public abstract class XMLTestCase extends TestCase {

    /** True if we desire a lot of info on what happen. */
    protected static boolean _verbose;
    /** True if we dump the stack trace for any exception that occurs during testing. */
    protected static boolean _printStack;

    static {
        String v = System.getProperty(TestCaseAggregator.VERBOSE_PROPERTY);
        _verbose = (v!=null && v.equals(Boolean.TRUE.toString()));

        v = System.getProperty(TestCaseAggregator.PRINT_STACK_TRACE);
        _printStack = (v!=null && v.equals(Boolean.TRUE.toString()));
    }

    /** Name of the test suite to which this test belongs. */
    protected String               _suiteName;
    /** The name of the root class for this test.  Must be set by a concrete class
     * if a test with a random object is used. */
    protected String               _rootClassName;
    /** The root class for this test.  Must be set by a concrete class. */
    protected Class                _rootClass;
    /** If true, the dumpFields() function has been implemented in the root class. */
    protected boolean              _hasDump;
    /** The name of the mapping file to use in a Marshalling Framework Test Case.
     * Must be set by a concrete class if a test with a reference document is used. */
    protected Mapping              _mapping;
    /** A listener for marshalling. */
    protected Object               _listener;
    /** Type of listener test -- Marshal, Unmarshal or Both. */
    protected TypeType             _listenerType;
    /** Gold file for listener. */
    protected String               _listenerGoldFile;
    /** The Configuration the Marshalling Framework. */
    protected Configuration        _configuration;
    /** Name of this test. */
    protected final String         _name;
    /** The unit test case this class represent. */
    protected final UnitTestCase   _unitTest;
    /** Place where the temporary file have to be put. */
    protected final File           _outputRootFile;
    /** True if the test needs to be skipped. */
    protected final boolean        _skip;
    /** The failure object that is not null if the test is expected to fail. */
    protected final FailureType    _failure;
    /** Used only to retrieve the classloader. */
    protected final CastorTestCase _test;

    /**
     * Instantiate a new XMLTestCase with the given name.
     *
     * @param name the name of this test case.
     */
    public XMLTestCase(String name) {
        super(name);
        _name = name;
        throw new IllegalArgumentException("You cannot use the name-only constructor");
    }

    /**
     * Instantiates a new test case that represents the given UnitTestCase.
     *
     * @param test the reference to the jar/directory
     * @param unit the UnitTestCase that wraps the configuration for this XML Test case.
     * @param outputRoot the directory that contains the files needed for the test
     */
    public XMLTestCase(CastorTestCase test, UnitTestCase unit, File outputRoot) {
        super(unit.getName());
        _name           = unit.getName();
        _unitTest       = unit;
        _outputRootFile = outputRoot;
        _skip           = unit.getSkip();
        _failure        = unit.getFailure();
        _test           = test;
    }

    /**
     * Instantiates a new test cases that has the same configuration of the given
     * test case.
     *
     * @param name the name of the test case
     * @param tc the XML test case that hold the configuration for this test case
     */
    public XMLTestCase(String name, XMLTestCase tc) {
        super(name);
        _name           = tc._name;
        _unitTest       = tc._unitTest;
        _outputRootFile = tc._outputRootFile;
        _skip           = tc._skip;
        _failure        = tc._failure;
        _test           = tc._test;
    }

    /**
     * Sets the name of the test suite to which this test case belongs to.
     *
     * @param suiteName the name of the test suite.
     *
     */
    public void setTestSuiteName(String suiteName) {
        _suiteName = suiteName;
    }

    /**
     * Returns the name of the test suite to which this test case belongs to.
     * @return the name of the test suite to which this test case belongs to.
     */
    public String getTestSuiteName() {
        return _suiteName;
    }

    /**
     * Called when a test case throws an Exception. Returns true if we expected
     * this test to fail (and if this was the correct Exception class, if one is
     * specified, and if this was the correct test step for the failure to
     * occur, if one was specified).
     *
     * @param exception
     *            the Exception that was thrown
     * @param checkStep
     *            the test step that threw an Exception
     * @return true if we pass the test (because we expected this Exception)
     */
    protected boolean checkExceptionWasExpected(Exception exception, FailureStepType checkStep) {
        if (_printStack) {
            exception.printStackTrace();
        }
        // If we're not expecting any failure, then we're not expecting any Exception
        if (_failure == null || !_failure.getContent()) {
            return false;
        }
        if (checkStep == null) {
            fail("CTF coding failure ... checkStep should never be null");
        }

        // We expect failure.  Is this the failure we expected?

        String          exceptionName = _failure.getException();
        FailureStepType expectedStep  = _failure.getFailureStep();

        // If no specific step or Exception, then any failure is OK
        if (exceptionName == null && expectedStep == null) {
            return true;
        }

        // If we're expecting an exception, make sure we got the right one
        Exception ex = exception;
        if (ex instanceof NestedIOException) {
            ex = ((NestedIOException)ex).getException();
        }

        if (exceptionName != null) {
            try {
                Class expected = Class.forName(exceptionName);
                if (!expected.isAssignableFrom(ex.getClass())) {
                    String complaint = "Received Exception: '" + ex.getClass().getName()
                            + ": " + ex + "' but expected: '" + exceptionName + "'.";

                    StringWriter sw = new StringWriter();
                    PrintWriter pw = new PrintWriter(sw);
                    pw.print(complaint);
                    if (_verbose) {
                        pw.println("Stacktrace for the Exception that was thrown:");
                        ex.printStackTrace(pw);
                    }
                    pw.flush();
                    System.out.println(sw.toString());

                    fail(complaint);
                }

            } catch (ClassNotFoundException cnfex) {
                fail("The Exception specified: '" + exceptionName + "' cannot be found in the CLASSPATH");
            }
        }

        // If we're expecting to fail at a specific step, make sure this is that step
        if (expectedStep != null && !expectedStep.equals(checkStep)) {
            fail("We expected to fail at test step '" + expectedStep.toString()
                 + "' but actually failed at step '" + checkStep.toString() + "'");
        }

        // We got the expected failure!  Return true.
        return true;
    }

    /**
     * Marshals the object with the configuration of the test.
     *
     * @param object the object to marshall.
     * @param fileName the name of the file where to marshall the object.
     * @return a file containing marshalled output.
     */
    protected File testMarshal(Object object, String fileName) {
        verbose("--> Marshaling to: '" + fileName + "'");

        File marshalOutput = new File(_outputRootFile, fileName);

        try {
            Marshaller marshaller = createMarshaler(marshalOutput);
            marshaller.marshal(object);
        } catch (Exception e) {
            if (!checkExceptionWasExpected(e, FailureStepType.MARSHAL_TO_DISK)) {
                fail("Exception marshaling to disk " + e);
            }
            return null;
        }

        if (_failure != null && _failure.getContent() && _failure.getFailureStep() != null &&
             _failure.getFailureStep().equals(FailureStepType.MARSHAL_TO_DISK)) {
            fail("Unmarshaling was expected to fail, but succeeded");
        }
        return marshalOutput;
    }

    private Marshaller createMarshaler(File marshalOutput) throws Exception {
        Marshaller marshaller = new Marshaller(new FileWriter(marshalOutput));

        //-- Configuration for marshaler?  Use config from unit test case if available
        Configuration config = _unitTest.getConfiguration();
        if (config == null) {
            config = _configuration;
        }

        if (config != null) {
            ConfigurationType marshal = config.getMarshal();
            invokeEnumeratedMethods(marshaller, marshal);
        }//-- config != null

        if (_mapping != null) {
            marshaller.setMapping(_mapping);
        }

        if (_listener != null && _listener instanceof MarshalListener
                && _listenerType.getType() != TypeType.UNMARSHAL_TYPE) {
            marshaller.setMarshalListener((MarshalListener)_listener);
        }
        return marshaller;
    }

    /**
     * Unmarshals the given file with the configuration of that test. If we
     * unmarshal null, complain and fail the test case.
     *
     * @param file the file containing the xml document to unmarshall.
     * @return the result of the unmarshalling of the given file.
     * @throws Exception if anything goes wrong during the test
     */
    protected Object testUnmarshal(File file) throws Exception {
        verbose("--> Unmarshaling '" + file.getName() + "'\n");
        InputStream inputStream = new FileInputStream(file);
        Object unmarshaledObject = testUnmarshal(inputStream);
        inputStream.close();
        assertNotNull("Unmarshaling '" + file.getName() + "' results in a NULL object.",
                      unmarshaledObject);
        return unmarshaledObject;
    }

    /**
     * Unmarshals the given input stream with the configuration of that test.
     *
     * @param stream
     *            the input stream containing the xml document to unmarshall.
     * @return the result of the unmarshalling of the given file, null if
     *         anything went wrong.
     * @throws Exception if anything goes wrong during the test
     */
    protected Object testUnmarshal(InputStream stream) throws Exception {
        Object result = null;
        final Unmarshaller unmar = createUnmarshaler();
        result = unmar.unmarshal(new InputSource(stream));
        return result;
    }

    private Unmarshaller createUnmarshaler() throws Exception {
        //-- Configuration for unmarshaler?  Use config from unit test case if available
        Configuration config = _unitTest.getConfiguration();
        if (config == null) {
            config = _configuration;
        }

        if (config != null) {
            ConfigurationType unmarshal = config.getUnmarshal();
            invokeEnumeratedMethods(unmarshal, unmarshal);
        }//-- config != null

        final Unmarshaller unmar;
        if (_mapping != null) {
            unmar = new Unmarshaller(_mapping);
        } else {
            if (_test.getClassLoader() != null) {
                unmar = new Unmarshaller(_rootClass, _test.getClassLoader());
            } else {
                unmar = new Unmarshaller(_rootClass);
            }
        }

        if (_listener != null && _listener instanceof UnmarshalListener
                && _listenerType.getType() != TypeType.MARSHAL_TYPE) {
            unmar.setUnmarshalListener((UnmarshalListener)_listener);
        }

        unmar.setDebug(_verbose);

        return unmar;
    }

    /**
     * Invokes all requested methods on the object we are supplied
     *
     * @param objectInvoked the object on which to invoke the methods
     * @param config configuration object listing the methods we are to call
     * @throws java.lang.Exception if anything goes wrong during the test
     */
    private void invokeEnumeratedMethods(Object objectInvoked, ConfigurationType config) throws java.lang.Exception {
        if (config == null) {
            return;
        }

        Enumeration methods = config.enumerateCallMethod();
        //-- For each method defined, we invoke it on marshaller just created.
        while (methods.hasMoreElements()) {
            CallMethod method = (CallMethod) methods.nextElement();
            //-- search for the method to invoke
            Method toBeinvoked = getInvokeMethod(method.getName(), method.getValue());
            //-- construct the objects representing the arguments of the method
            Object[] arguments = getArguments(method.getValue());
            toBeinvoked.invoke(objectInvoked, arguments);
        }
    }

    /**
     * Initialize listeners for marshalling/unmarshalling
     *
     * @param listener the listener to initialize
     * @throws ClassNotFoundException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    protected void initializeListeners(ListenerType listener) throws ClassNotFoundException,
                                                                     IllegalAccessException,
                                                                     InstantiationException {
        String listenerClassName = listener.getClassName();
        if (listenerClassName == null || listenerClassName.length() == 0) {
            throw new IllegalArgumentException("ClassName must be provided for Listener element.");
        }

        final Class listenerClass;
        if (_test.getClassLoader() != null) {
            listenerClass = _test.getClassLoader().loadClass(listenerClassName);
        } else {
            listenerClass = Thread.currentThread().getContextClassLoader().loadClass(listenerClassName);
        }

        Object o = listenerClass.newInstance();
        if (o instanceof UnmarshalListener || o instanceof MarshalListener) {
            _listener = o;
        } else {
            _listener = null;
        }

        _listenerGoldFile = listener.getGoldFile();
        _listenerType = listener.getType();
    }

    /**
     * Returns an instance of the object model hardcoded in the given
     * ObjectModelBuilder.
     *
     * @param builderName the name of the class used as a builder
     * @return an instance of the object model hardcoded in the given
     *         ObjectModelBuilder.
     * @throws java.lang.Exception if anything goes wrong during the test
     */
    protected Object buildObjectModel(String builderName) throws java.lang.Exception {
        Class builderClass = null;
        if (_test.getClassLoader() != null) {
            builderClass = _test.getClassLoader().loadClass(builderName);
        } else {
            builderClass = this.getClass().getClassLoader().loadClass(builderName);
        }
        ObjectModelBuilder builder = (ObjectModelBuilder)builderClass.newInstance();
        return builder.buildInstance();
    }

    private Method getInvokeMethod(String methodName, Value[] values) throws ClassNotFoundException,
                                                                     NoSuchMethodException {
        if (methodName == null) {
            throw new IllegalArgumentException("The name of the method to invoke is null");
        }
        Class[] argumentsClass = null;

        //-- the value object represent the arguments
        //--of the method if any
        if (values != null && values.length > 0) {
            argumentsClass = new Class[values.length];
            for (int i = 0; i < values.length; i++) {
                Value value = values[i];
                argumentsClass[i] = CTFUtils.getClass(value.getType(), _test.getClassLoader());
            }
        }
        return Marshaller.class.getMethod(methodName, argumentsClass);
    }

    private Object[] getArguments(Value[] values) throws ClassNotFoundException, MarshalException {
        Object[] result = new Object[values.length];
        for (int i = 0; i < values.length; i++) {
            Value value = values[i];
            result[i] = CTFUtils.instantiateObject(value.getType(), value.getContent(), _test.getClassLoader());
        }
        return result;
    }

    /**
     * print the message if in verbose mode.
     * @param message the message to print
     */
    protected void verbose(String message) {
        if (_verbose) {
            System.out.println(message);
        }
    }

}
