/*
 * 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 6787 2007-01-29 06:00:49Z ekuns $
 */
package org.castor.xmlctf;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;

import junit.framework.TestCase;

import org.castor.xmlctf.util.CTFUtils;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.tests.framework.testDescriptor.CallMethod;
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.UnitTestCase;
import org.exolab.castor.tests.framework.testDescriptor.Value;
import org.exolab.castor.tests.framework.testDescriptor.types.FailureStepType;
import org.exolab.castor.tests.framework.testDescriptor.types.TypeType;
import org.exolab.castor.util.NestedIOException;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.MarshalListener;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.XMLContext;
import org.xml.sax.InputSource;

/**
 * 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: 6787 $ $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. */
  public 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;
  /** The internal Castor XML context. */
  private XMLContext _xmlContext;

  /**
   * Instantiate a new XMLTestCase with the given name.
   *
   * @param name the name of this test case.
   */
  public XMLTestCase(final String name) {
    super(name);
    _name = cleanup(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.
   */
  public XMLTestCase(final CastorTestCase test, final UnitTestCase unit) {
    super(unit.getName());
    _name = cleanup(unit.getName());
    _unitTest = unit;
    _skip = unit.getSkip();
    _failure = unit.getFailure();
    _test = test;
    _outputRootFile = test.getOutputRootFile();
    _xmlContext = null;
  }

  /**
   * 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(final String name, final XMLTestCase tc) {
    super(name);
    _name = cleanup(tc._name);
    _unitTest = tc._unitTest;
    _outputRootFile = tc._outputRootFile;
    _skip = tc._skip;
    _failure = tc._failure;
    _test = tc._test;
    _xmlContext = null;
  }

  protected abstract void setUp() throws Exception;

  protected abstract void tearDown() throws Exception;

  public void setXMLContext(XMLContext xmlContext) {
    _xmlContext = xmlContext;
  }

  public XMLContext getXMLContext() {
    return _xmlContext;
  }

  /**
   * Returns a version of the input name that is suitable for JUnit test case or test suite use.
   * Apparently, JUnit truncates test case names after encountering certain characters, so we scrub
   * those characters from the test case or test suite name.
   * <p>
   * We also translate all whitespace to blanks, remove all leading and trailing whitespace, and
   * collapse consecutive whitespace to a single blank.
   *
   * @param name the input name
   * @return a name suitable for JUnit test case or test suite use.
   */
  static String cleanup(final String name) {
    return name.replaceAll("[\\*()\\t\\n\\r\\f]", " ").replaceAll(" {2,}", " ").trim();
  }

  /**
   * 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(final String suiteName) {
    _suiteName = cleanup(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(final Exception exception,
      final 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(final Object object, final 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(final File marshalOutput) throws Exception {
    getXMLContext().getInternalContext().getXMLClassDescriptorResolver().cleanDescriptorCache();
    Marshaller marshaller = getXMLContext().createMarshaller();
    marshaller.setWriter(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();
      List returnValues = invokeEnumeratedMethods(marshaller, marshal);
      returnValues.clear(); // We don't care about the return values
    } // -- config != null

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

    if (_listener != null && _listener instanceof MarshalListener
        && _listenerType != TypeType.UNMARSHAL) {
      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(final File file) throws Exception {
    verbose("--> Unmarshaling '" + file.getName() + "'\n");
    try (InputStream inputStream = new FileInputStream(file)) {
      Object unmarshaledObject = testUnmarshal(inputStream);
      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(final 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;
    }

    final Unmarshaller unmar;
    if (_mapping != null) {
      // J unmar = new Unmarshaller(_mapping);
      unmar = getXMLContext().createUnmarshaller();
      unmar.setMapping(_mapping);
    } else {
      if (_test.getClassLoader() != null) {
        // J unmar = new Unmarshaller(_rootClass, _test.getClassLoader());
        unmar = getXMLContext().createUnmarshaller();
        unmar.setClassLoader(_test.getClassLoader());
        unmar.setClass(_rootClass);
      } else {
        // J unmar = new Unmarshaller(_rootClass);
        unmar = getXMLContext().createUnmarshaller();
        unmar.setClass(_rootClass);
      }
    }

    if (_listener != null && _listener instanceof org.exolab.castor.xml.UnmarshalListener
        && _listenerType != TypeType.MARSHAL) {
      unmar.setUnmarshalListener((org.exolab.castor.xml.UnmarshalListener) _listener);
    }

    if (_listener != null && _listener instanceof org.castor.xml.UnmarshalListener
        && _listenerType != TypeType.MARSHAL) {
      unmar.setUnmarshalListener((org.castor.xml.UnmarshalListener) _listener);
    }

    unmar.setDebug(_verbose);

    if (config != null) {
      ConfigurationType unmarshal = config.getUnmarshal();
      List returnValues = invokeEnumeratedMethods(unmar, unmarshal);
      returnValues.clear(); // We don't care about the return values
    }

    return unmar;
  }

  /**
   * Invokes all requested methods on the object we are supplied. Keeps track of the objects
   * returned from each invocation and returns a List containing all of the return values. Some of
   * the return values may be null. This is not an error!
   *
   * @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
   */
  protected List invokeEnumeratedMethods(final Object objectInvoked, final ConfigurationType config)
      throws java.lang.Exception {
    final List returnValues = new LinkedList();

    if (config == null) {
      return returnValues;
    }

    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(objectInvoked.getClass(), method.getName(), method.getValue());
      // -- construct the objects representing the arguments of the method
      Object[] arguments = getArguments(method.getValue());
      // -- Invoke the method and keep a copy of the returned object
      returnValues.add(toBeinvoked.invoke(objectInvoked, arguments));
    }

    return returnValues;
  }

  /**
   * Initialize listeners for marshalling/unmarshalling
   *
   * @param listener the listener to initialize
   * @throws ClassNotFoundException
   * @throws IllegalAccessException
   * @throws InstantiationException
   */
  protected void initializeListeners(final 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 org.castor.xml.UnmarshalListener
        || o instanceof org.exolab.castor.xml.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(final 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(final Class dataBinder, final String methodName,
      final 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 represents 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 dataBinder.getMethod(methodName, argumentsClass);
  }

  private Object[] getArguments(final 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(final String message) {
    if (_verbose) {
      System.out.println(message);
    }
  }

}
