/*
 * 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 2002-2003 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: CastorTestCase.java 6787 2007-01-29 06:00:49Z ekuns $
 */
package org.castor.xmlctf;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.JarFile;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

import org.castor.xmlctf.util.FileServices;
import org.exolab.castor.tests.framework.testDescriptor.MarshallingTest;
import org.exolab.castor.tests.framework.testDescriptor.OnlySourceGenerationTest;
import org.exolab.castor.tests.framework.testDescriptor.SchemaTest;
import org.exolab.castor.tests.framework.testDescriptor.SourceGeneratorTest;
import org.exolab.castor.tests.framework.testDescriptor.TestDescriptor;
import org.exolab.castor.tests.framework.testDescriptor.TestDescriptorChoice;
import org.exolab.castor.tests.framework.testDescriptor.UnitTestCase;
import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.ValidationException;

/**
 * Abstracts a test case in the CTF (Castor Test Framework). A CTF test case can be driven by a
 * directory or by a JAR file.
 *
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
 * @version $Revision: 6787 $ $Date: 2004-03-08 17:23:25 -0700 (Mon, 08 Mar 2004) $
 */
public class CastorTestCase extends TestCase {

  /** An unknown type of Castor test case. */
  public static final short UNKNOWN = -1;
  /** A directory-based Castor test case. */
  public static final short DIRECTORY = 0;
  /** A JAR-based Castor test case. */
  public static final short JAR = 1;

  /** Name of the resource for the test descriptor XML document. */
  public static final String TEST_DESCRIPTOR = "TestDescriptor.xml";
  /** Name of the resource for the test descriptor XML document if a JAR file is used. */
  private static final String TEST_DESCRIPTOR_JAR = "META-INF/TestDescriptor.xml";
  /** File separator for this system. */
  private static final String FILE_SEPARATOR = System.getProperty("file.separator");
  /** Java version of the JVM we are running in. */
  private static final float JAVA_VERSION =
      Float.parseFloat(System.getProperty("java.specification.version"));
  /** True if we desire a lot of information on what is happening during the test. */
  private static final boolean VERBOSE;

  static {
    String v = System.getProperty(TestCaseAggregator.VERBOSE_PROPERTY);
    VERBOSE = (v != null && v.equals("true"));
    v = null;
  }

  /**
   * True if we dump the stack trace that is generated from any validation exception or marshal
   * exception caused by reading the XML Test Description.
   */
  private static boolean _printStack;

  static {
    String v = System.getProperty(TestCaseAggregator.PRINT_STACK_TRACE);
    _printStack = (v != null && v.equals("true"));
    v = null;
  }

  /** Indicates whether or not the output root directory has been compiled. */
  private boolean _compiled = false;
  /** Class loader to use for the jar. */
  private ClassLoader _loader;
  /** The test descriptor from the jar. */
  private TestDescriptor _testDescriptor;
  /** The file that contains the tests. This can either be a directory or a jar file. */
  private final File _testFile;
  /** The Type of the test (directory or jar). */
  private final short _type;
  /** Place where the temporary files and other output are created. */
  private final File _outputRootFile;
  /** String containing the directory path from the test root to here. */
  private final String _directoryToHere;

  /**
   * Constructs a CTF test case given only a test case name.
   * 
   * @param name the name of the test case
   */
  public CastorTestCase(final String name) {
    super(name);
    _testFile = null;
    _outputRootFile = null;
    _directoryToHere = "";
    _type = UNKNOWN;
  }

  /**
   * Constructs a CTF test case given a File (either a JAR file or a directory) and a directory
   * where temporary files will be placed. The test case name will be derived from the file (JAR or
   * directory) name.
   *
   * @param file Either a directory containing TestDescriptor.xml or a JAR file containing
   *        META-INF/TestDescriptor.xml
   * @param directoryToHere directory path leading to the current test.
   * @param outputRoot Directory where temporary files and output will go.
   */
  public CastorTestCase(final File file, final String directoryToHere, final String outputRoot) {
    super(directoryToHere);
    _directoryToHere = directoryToHere;

    if (file.isDirectory()) {
      _type = DIRECTORY;
      _outputRootFile = new File(outputRoot + FILE_SEPARATOR);
    } else {
      _type = JAR;
      try {
        new JarFile(file);
        String fileName = file.getName();
        fileName = fileName.substring(0, fileName.lastIndexOf("."));
        _outputRootFile = new File(outputRoot + FILE_SEPARATOR + fileName);
      } catch (java.util.zip.ZipException e) {
        throw new IllegalStateException(file.getAbsolutePath() + " is not a valid JAR file.");
      } catch (java.io.IOException ie) {
        throw new IllegalStateException(file.getAbsolutePath() + " is not a valid JAR file.");
      }
    }

    // Append to our current class loader the directory or JAR containing our test case
    try {
      URL[] urlList = {file.toURL()};
      _loader = new URLClassLoader(urlList, this.getClass().getClassLoader());
    } catch (MalformedURLException urle) {
      // should never happen--> failure before
      urle.printStackTrace();
    }
    _testFile = file;
    _outputRootFile.mkdirs();
  }

  public ClassLoader getClassLoader() {
    return _loader;
  }

  public File getTestFile() {
    return _testFile;
  }

  public short getType() {
    return _type;
  }

  public String getDirectoryToHere() {
    return _directoryToHere;
  }

  public File getOutputRootFile() {
    return _outputRootFile;
  }

  /**
   * Returns a boolean that when true indicates the output directory has been compiled. This is
   * useful for preventing the compilation of a directory multiple times when more than one test
   * case exists in a given directory.
   *
   * @return true when the output root directory has already been compiled.
   */
  public boolean isDirectoryCompiled() {
    return _compiled;
  } // -- isDirectoryCompiled

  /**
   * Sets the ClassLoader to use for loading the resources for this test case.
   *
   * @param loader the class loader to use
   */
  public void setClassLoader(final ClassLoader loader) {
    _loader = loader;
  }

  /**
   * Sets a flag to indicate the output directory has been compiled. This prevents compiling a
   * directory multiple times unnecessarily when more than one test case exists in a given
   * directory.
   *
   * @param compiled true if the output directory for this test case has been compiled
   */
  public void setDirectoryCompiled(final boolean compiled) {
    _compiled = compiled;
  } // -- setDirectoryCompiled

  /**
   * Assembles and returns a test suite containing all known tests.
   *
   * @return A non-null test suite if we can load the test descriptor
   */
  public Test suite() {
    final InputStream descriptor;
    if (_type == JAR) {
      descriptor = _loader.getResourceAsStream(TEST_DESCRIPTOR_JAR);
    } else {
      descriptor = _loader.getResourceAsStream(TEST_DESCRIPTOR);
    }

    if (descriptor == null) {
      verbose("test '" + _testFile.getName() + "' has no TestDescriptor.xml");
      return null;
    }

    try {
      _testDescriptor = TestDescriptor.unmarshal(new InputStreamReader(descriptor));
    } catch (ValidationException ve) {
      verbose("Error reading: " + _testFile.getAbsolutePath());
      verbose("-> " + ve.toString());
      if (_printStack) {
        ve.printStackTrace(System.out);
      }
      fail(ve.toString());
    } catch (MarshalException me) {
      verbose("Error reading: " + _testFile.getAbsolutePath());
      verbose("-> " + me.toString());
      if (_printStack) {
        me.printStackTrace(System.out);
      }
      fail(me.toString());
    } finally {
      try {
        descriptor.close();
      } catch (IOException e) {
        // ignore
      }
    }

    if (_testDescriptor.hasMinimumJavaVersion()) {
      // Get minimum Java version & convert to our Canonical form
      float minVersion = _testDescriptor.getMinimumJavaVersion();
      if (minVersion >= 5F && minVersion < 10F) {
        minVersion = 1.0F + (minVersion / 10F);
      }
      if (JAVA_VERSION < minVersion) {
        verbose("-->Test requires at minimum Java " + minVersion + ", but we are running Java "
            + JAVA_VERSION);
        verbose("-->Skipping the test");
        return null;
      }
    }

    if (_testDescriptor.hasMaximumJavaVersion()) {
      // Get maximum Java version & convert to our Canonical form
      float maxVersion = _testDescriptor.getMaximumJavaVersion();
      if (maxVersion >= 5F && maxVersion < 10F) {
        maxVersion = 1.0F + (maxVersion / 10F);
      }
      if (JAVA_VERSION > maxVersion) {
        verbose("-->Test is designed to run up to Java " + maxVersion + ", but we are running Java "
            + JAVA_VERSION);
        verbose("-->Skipping the test");
        return null;
      }
    }

    final String suiteName = _directoryToHere + _testDescriptor.getName();
    final TestSuite suite = new TestSuite(suiteName);
    verbose("Creating '" + suiteName + "' test suite");

    TestDescriptorChoice choice = _testDescriptor.getTestDescriptorChoice();
    MarshallingTest marshallingTests = choice.getMarshallingTest();
    SourceGeneratorTest sourceGenTests = choice.getSourceGeneratorTest();
    SchemaTest schemaTests = choice.getSchemaTest();
    OnlySourceGenerationTest genOnlyTests = choice.getOnlySourceGenerationTest();

    if (marshallingTests != null) {
      setUpMarshallingTests(suiteName, suite, marshallingTests);
    }
    if (sourceGenTests != null) {
      setUpSourceGeneratorTests(suiteName, suite, sourceGenTests);
    }
    if (schemaTests != null) {
      setUpSchemaTests(suiteName, suite, schemaTests);
    }
    if (genOnlyTests != null) {
      setUpGenerationOnlyTests(suiteName, suite, genOnlyTests);
    }

    return suite;
  }

  /**
   * Loops over all Marshalling tests from our TestDescriptor.xml, configures each test and adds it
   * to our suite.
   *
   * @param suiteName Test Suite name
   * @param suite the Test Suite to add all unit tests to
   * @param mar a collection of Marshalling Unit Tests
   */
  private void setUpMarshallingTests(final String suiteName, final TestSuite suite,
      final MarshallingTest mar) {
    for (int i = 0; i < mar.getUnitTestCaseCount(); ++i) {
      UnitTestCase tc = mar.getUnitTestCase(i);
      MarshallingFrameworkTestCase mftc = new MarshallingFrameworkTestCase(this, tc, mar);
      mftc._configuration = mar.getConfiguration();
      mftc.setTestSuiteName(suiteName);
      suite.addTest(mftc.suite());
    }
  }

  /**
   * Loops over all Source Generation tests from our TestDescriptor.xml, configures each test and
   * adds it to our suite.
   *
   * @param suiteName Test Suite name
   * @param suite the Test Suite to add all unit tests to
   * @param sg a collection of Source Generation Unit Tests
   */
  private void setUpSourceGeneratorTests(final String suiteName, final TestSuite suite,
      final SourceGeneratorTest sg) {
    for (int i = 0; i < sg.getUnitTestCaseCount(); ++i) {
      UnitTestCase tc = sg.getUnitTestCase(i);
      SourceGeneratorTestCase sgtc = new SourceGeneratorTestCase(this, tc, sg);
      sgtc.setTestSuiteName(suiteName);
      suite.addTest(sgtc.suite());
    }
  }

  /**
   * Loops over all Schema tests from our TestDescriptor.xml, configures each test and adds it to
   * our suite.
   *
   * @param suiteName Test Suite name
   * @param suite the Test Suite to add all unit tests to
   * @param schemaTest a collection of Schema Unit Tests
   */
  private void setUpSchemaTests(final String suiteName, final TestSuite suite,
      final SchemaTest schemaTest) {
    for (int i = 0; i < schemaTest.getUnitTestCaseCount(); i++) {
      UnitTestCase tc = schemaTest.getUnitTestCase(i);
      // Little trick: getUnitTestCaseChoice should not be null at this point
      String name = tc.getUnitTestCaseChoice().getSchema();
      if (name.equals("*")) {
        File[] list = _testFile.listFiles();
        for (int j = 0; j < list.length; ++j) {
          String fileName = list[j].getName();
          // FIXME: It would be better to use a file filter and to make
          // sure our SchemaReader can read this file
          if (fileName.endsWith(FileServices.XSD)) {
            makeIndividualSchemaTest(suiteName, suite, tc, fileName);
          }
        }
      } else {
        makeIndividualSchemaTest(suiteName, suite, tc, name);
      }
    }
  }

  /**
   * Loops over all Only-Source-Generation tests from our TestDescriptor.xml, configures each test
   * and adds it to our suite.
   *
   * @param suiteName Test Suite name
   * @param suite the Test Suite to add all unit tests to
   * @param sg a collection of Source Generation Unit Tests
   */
  private void setUpGenerationOnlyTests(final String suiteName, final TestSuite suite,
      final OnlySourceGenerationTest sg) {
    for (int i = 0; i < sg.getUnitTestCaseCount(); ++i) {
      UnitTestCase tc = sg.getUnitTestCase(i);
      OnlySourceGenerationTestCase sgtc = new OnlySourceGenerationTestCase(this, tc, sg);
      sgtc.setTestSuiteName(suiteName);
      suite.addTest(sgtc.suite());
    }
  }

  /**
   * Makes an individual Schema test and adds it to our Test Suite.
   *
   * @param suiteName Test Suite name
   * @param suite the Test Suite to add all unit tests to
   * @param tc our Test Case
   * @param name Schema name
   */
  private void makeIndividualSchemaTest(final String suiteName, final TestSuite suite,
      final UnitTestCase tc, final String name) {
    tc.setName(suiteName + '#' + name);
    SchemaTestCase stc = new SchemaTestCase(this, tc);
    stc.setSchemaName(name);
    suite.addTest(stc);
  }

  /**
   * Prints the provided message if verbose is true.
   *
   * @param message The message to display if verbose is true.
   */
  private void verbose(final String message) {
    if (VERBOSE) {
      System.out.println(message);
    }
  }

}
