package nxm.sys.test.junit;

import java.beans.Statement;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Pattern;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MidasReference;
import nxm.sys.lib.FileUtil;
import nxm.sys.lib.Midas;
import nxm.sys.lib.NeXtMidas;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Time;
import nxm.sys.lib.Util;

/** This is the parent of all of the other test classes.
    Child test cases should extend this class and provide a constructor
    that takes the following two parameters (String methodName, Midas midas)
    and call this class's constructor with the three parameters as such:
    <pre>
      ...
      // This constructor is required by NeXtMidas Unit Testing framework.
      public CacheTest(String methodName, Midas midas) {
        super("nxm.sys.lib.Cache", methodName, midas);
      }
      -- OR since NeXtMidas 2.7.0, if your test class  follows the following convention:
         Class to Test:    {@code nxm.<optiontree>.<area>.YourClass}
         Test Case Class:  {@code nxm.<optiontree>}.<b>test.junit</b>.{@code <area>}.YourClass<b>Test</b>
         and does not have "test.junit" any where else in the class's package structure.

      // This constructor is required by NeXtMidas Unit Testing framework.
      public YourClassTest(String methodName, Midas midas) {
        super(methodName, midas);
      }
    </pre>
    @author  J. Czechowski IV / Neon Ngo
 */
public abstract class NMTestCase extends TestCase implements MidasReference {
  /** This is the error that is thrown whenever a test case is missing.
      @since NeXtMidas 2.0.0
   */
  public static final AssertionFailedError MISSING_TEST_ERROR = new AssertionFailedError(null);

  /** boolean to run test metrics - defaulted to false
   *  @since 3.9.1
   */
  private static boolean unittestMetrics = false;
  /** This is the name of the class being tested. */
  private String testedClass;

  /** This is the Midas context to use. */
  private Midas midas;

  // private static final Pattern TEST_PACKAGE_REGEX    = Pattern.compile(".test.junit.", Pattern.LITERAL); // Java 5
  private static final Pattern TEST_PACKAGE_REGEX    = Pattern.compile("\\.test\\.junit\\."); // Java 1.4.2
  private static final String  TEST_CLASS_SUFFIX     = "Test";
  private static final int     TEST_CLASS_SUFFIX_LEN = TEST_CLASS_SUFFIX.length();

  /** Normally we always create a NeXtMidas, but this has issue when running limited NOGRAPHICS jar */
  @InternalUseOnly
  public static boolean normalOperation = true;

  protected static boolean sysDatFull = false;
  private static final String DAT_SYS_DIR = "nxm.sys.dat";
  private static final String DAT_TEST_DIR = "nxm.sys.test.dat";
  private static final String DAT_SYS_DIR_SLASH = DAT_SYS_DIR.replace(".", "/");
  private static final String DAT_TEST_DIR_SLASH = DAT_TEST_DIR.replace(".", "/");
  protected static String datDir;
  protected static String datDirSlash;
  protected static String urlSysTest;
  protected static String urlDat;

  static {
    ClassLoader loader = NeXtMidas.class.getClassLoader();

    // Check if main dat folder is sys.dat or sys.test.dat (check apenny.prm location since it gets moved when using nxm.sys.test.dat)
    // Modified so that unit tests running during a Gradle build command can access resources in dat/
    URL urlSysDat = loader.getResource(DAT_SYS_DIR_SLASH + "/apenny.prm");
    URL urlSysTestDat = loader.getResource(DAT_TEST_DIR_SLASH + "/apenny.prm");
    if (urlSysDat != null) {
      sysDatFull = true;
      urlDat = FileUtil.changePathSeparator(urlSysDat.toString(), "/").replace("apenny.prm", ""); // NXM-3755 since NeXtMidas 4.0.1
    }
    else {
      sysDatFull = false;
      urlDat = FileUtil.changePathSeparator(urlSysTestDat.toString(), "/").replace("apenny.prm", ""); // NXM-3755 since NeXtMidas 4.0.1
    }

    // Get test URL - this was modified to be detected during both a Gradle build command and the Gradle build jar - NXM-3755 since NeXtMidas 4.0.1
    URL sysTestURL = loader.getResource("nxm/sys/test/cone5000.csv");
    if (sysTestURL != null) urlSysTest = FileUtil.changePathSeparator(sysTestURL.toString(), "/").replace("cone5000.csv", "");
    else                    urlSysTest = (sysDatFull) ? urlDat.replace("dat/", "test/") : urlDat.replace("dat/", "");

    datDir      = (sysDatFull) ? DAT_SYS_DIR       : DAT_TEST_DIR;
    datDirSlash = (sysDatFull) ? DAT_SYS_DIR_SLASH : DAT_TEST_DIR_SLASH;
  }

  /** default constructor */
  public NMTestCase () {}

  /** Creates a new TestCase.
      @param testedClass The name of the class being tested (e.g. <code>"java.lang.Integer"</code>).
      @param methodName  The name of the test method that will be run (e.g. <code>"testByteValue"</code>).
      @param midas       The Midas context to use
   */
  public NMTestCase (String testedClass, String methodName, Midas midas) {
    super(methodName);
    this.testedClass = testedClass;
    if (normalOperation) this.midas       = (midas != null)? midas : NeXtMidas.getGlobalInstance().getMidas();
  }

  /** Create a new TestCase. Uses the package and class naming described in the
      class description to figure out name of tested class.
     @param methodName The name of the test that will be run.
     @param midas      The Midas context to use
     @throws RuntimeException if test class does not follow naming convention.
     @see #NMTestCase(String, String, Midas)
     @since NeXtMidas 2.7.0
   */
  public NMTestCase (String methodName, Midas midas) {
    this(null, methodName, midas);
    String thisClassName = getClass().getName();
    String testedClassName = TEST_PACKAGE_REGEX.matcher(thisClassName).replaceAll(".");
    if (testedClassName.endsWith(TEST_CLASS_SUFFIX)) {
      testedClassName = testedClassName.substring(0, testedClassName.length()-TEST_CLASS_SUFFIX_LEN);
    }
    else {
      throw new RuntimeException("Test class '"+thisClassName
          +"' does not follow naming convention. See NMTestCase documentation.");
    }
    this.testedClass = testedClassName;
  }

  /** @return the midas context to run the tests with. */
  public Midas getMidasContext () {
    return this.midas;
  }

  /** @since 3.5.1 */
  @Override
  public Midas getMidas () {
    return this.midas;
  }

  /** @return the name of the tested class. */
  public String getTestedClass () {
    return this.testedClass;
  }

  /** Returns a string describing the test. <br>
      Implementation Note: <i>{@link TestCase#toString()}</i> typically returns a
                           string in the form <code>testMethod(testClass)</code>
                           and this method will transform that into the form
                           <code>class.method(...)</code>.
      @return A string describing the test.
   */
  @Override
  public String toString () {
    String methodName = super.toString();                                   /* testFoo(...) */

    methodName = methodName.substring(4,methodName.indexOf('('));           /* testFoo(..) -> Foo */
    if (methodName.length() > 1) {
      methodName = Character.toLowerCase(methodName.charAt(0))
                 + methodName.substring(1);                                 /* Foo -> foo */
    } else {
      methodName = methodName.toLowerCase();
    }

    return this.testedClass + "." + methodName + "(..)";
  }

  /** Display info message using existing Midas context (if set).
      This will include the test name (method) currently being executed.
      @param msg message to display
      @since NeXtMidas 3.1.0
   */
  protected void info (CharSequence msg) {
    if (midas != null) midas.info(msg+" ["+getName()+']');
    else               Shell.info(msg+" ["+getName()+"].");
  }

  /** Display warning message using existing Midas context (if set).
      This will include the test name (method) currently being executed.
      @param msg message to display
      @since NeXtMidas 3.3.0
   */
  protected void warn (CharSequence msg) {
    if (midas != null) midas.warning(msg+" ["+getName()+']');
    else               Shell.warning(msg+" ["+getName()+"].");
  }

  /** Checks two objects for equality with checks for null. */
  private static boolean equals (Object a, Object b) {
    return (a == b)
        || ((a != null) && (b != null) && a.equals(b));
  }

  /** This will build a test set for the given test class. This is the same as:
      <code>buildTestSet(midas,Class.forName(testClass))</code>
      @param midas     The midas context to use when running the tests.
      @param testClass The name of the test class to use.
      @return The set of tests for the given class.
      @throws ClassNotFoundException If the named class can not be found.
      @see #buildTestSet(Midas,Class)
   */
  public static NMTestCase[] buildTestSet (final Midas midas, final String testClass)
                                           throws ClassNotFoundException {
    return buildTestSet(midas, Class.forName(testClass));
  }


  /** This will build a test set for the given test class. <br>
      <br>
      This will use reflection to find all test methods in the given class (a
      test method is any method that takes in zero arguments and has a name
      starting with <code>"test"</code>. It will then instantiate the class as
      appropriate using the class's single-argument ({@link String})
      constructor.<br>
      <br>
      This method works similar to the {@link TestSuite#TestSuite(Class)}
      constructor, except that the tests remain individual instances of
      {@link NMTestCase} (which is necessary for setting the Midas context
      to be used) and are not grouped together in a {@link TestSuite}.

      @param midas     The midas context to use when running the tests.
      @param testClass The test class to use.

      @return The set of tests for the given class.

      @see TestSuite
   */
  public static NMTestCase[] buildTestSet (final Midas midas, final Class<?> testClass) {
    final Class<?>[]            constType = {String.class, Midas.class};
    final ArrayList<NMTestCase> tests     = new ArrayList<>();
    final Method[]              methods   = testClass.getMethods();

    try {
      Constructor<?> cnst  = testClass.getConstructor(constType);
      NMTestCase  instance = (NMTestCase)cnst.newInstance(new Object[] { "", midas });

      for (Method method : methods) {
        final String methodName = method.getName();

        if (methodName.startsWith("test")) {
          if (method.getParameterTypes().length == 0) {
            instance = (NMTestCase)cnst.newInstance(new Object[] { methodName, midas });
            tests.add(instance);
          }
        }
      }
      tests.addAll(listMissingTests(midas, Class.forName(instance.getTestedClass()), testClass));

      Collections.sort(tests, new Comparator<NMTestCase>() {
        @Override
        public int compare (NMTestCase a, NMTestCase b) {
          return a.getName().compareTo(b.getName());
        }
      });

    } catch(NoSuchMethodException ex) {
      tests.add(new InstantiationErrorTest(testClass.toString()+"(String, Midas)", "error", midas,ex));
    } catch(Exception exception) {
      midas.printStackTrace(exception);
      tests.add(new InstantiationErrorTest(testClass.toString(), "error", midas, exception));
    }

    return tests.toArray(new NMTestCase[tests.size()]);
  }

  /** Creates a list of the missing tests.
      @param midas The Midas context.
      @param clazz The tested class.
      @param tests The JUnit test class.
      @return The list of missing tests as a set of test cases.
      @since NeXtMidas 2.0.0
   */
  public static List<MissingTest> listMissingTests (Midas midas, Class<?> clazz, Class<?> tests) {
    final Method[]          clazzMethods = clazz.getDeclaredMethods();
    final Method[]          testMethods  = tests.getMethods();
    final List<MissingTest> missingTests = new ArrayList<>();
    final Set<String>       foundNames   = new HashSet<>();

    for (Method m : testMethods) {
      foundNames.add(m.getName());
    }

    for (Method clazzMethod : clazzMethods) {
      if (Modifier.isPublic(clazzMethod.getModifiers())) {
        String testName = "test" + capitalize(clazzMethod.getName());

        if (!foundNames.contains(testName)) {
          missingTests.add(new MissingTest(clazz.getName(), testName, midas));
          foundNames.add(testName);
        }
      }
    }
    return missingTests;
  }

  /** Capitalizes the first letter in a string.
      @param str The string.
      @return The capitalized string.
      @since NeXtMidas 2.0.0
   */
  private static String capitalize (String str) {
    if ((str == null) || str.isEmpty()) return str;
    return "" + Character.toUpperCase(str.charAt(0)) + str.substring(1);
  }

  /** This will test to verify that an exception of the given type is thrown
      when the provided method is executed.
      @param expected   The class of the exception that is expected.
      @param targetObj  The argument to execute the method on.
      @param methodName The name of the method to execute.
      @param arguments  The array of arguments for the method.
      @see #assertException(Class,Statement)
   */
  public static void assertException (Class<?> expected, Object targetObj,
                                      String methodName, Object[] arguments) {
    assertException(expected, new Statement(targetObj, methodName, arguments));
  }

  /** This will test to verify that an exception of the given type is thrown
      when the provided method is executed.
      @param message    A description of the test.
      @param expected   The class of the exception that is expected.
      @param targetObj  The argument to execute the method on.
      @param methodName The name of the method to execute.
      @param arguments  The array of arguments for the method.
      @see #assertException(String,Class,Statement)
   */
  public static void assertException (String message, Class<?> expected, Object targetObj,
                                      String methodName, Object[] arguments) {
    assertException(message, expected, new Statement(targetObj, methodName, arguments));
  }

  /** This will test to verify that an exception of the given type is thrown
      when the provided statement is executed.
      @param expected The class of the exception that is expected.
      @param statment The statement to execute.
      @see #assertException(String,Class,Statement)
   */
  public static void assertException (Class<?> expected, Statement statment) {
    assertException(null, expected, statment);
  }

  /** This will test to verify that an exception of the given type is thrown
      when the provided statement is executed.
      @param message  A description of the test.
      @param expected The class of the exception that is expected.
      @param statment The statement to execute.
   */
  public static void assertException (String message, Class<?> expected, Statement statment) {
    assertException(message,expected,statment,null);
  }

  /** This will test to verify that an exception of the given type is thrown
      when the provided statement is executed.
      @param expected The class of the exception that is expected.
      @param task     The task to execute.
   */
  public static void assertException (Class<?> expected, Runnable task) {
    assertException(null,expected,task);
  }

  /** This will test to verify that an exception of the given type is thrown
      when the provided statement is executed.
      @param message  A description of the test.
      @param expected The class of the exception that is expected.
      @param task     The task to execute.
   */
  public static void assertException (String message, Class<?> expected, Runnable task) {
    assertException(message,expected,null,task);
  }

  private static void assertException (String message, Class<?> expected, Statement statment, Runnable task) {
    Exception actual = null;
    String    msg    = (message == null)? "" : message + " ";

    try {
      if (task == null) statment.execute();
      else              task.run();
    }
    catch (Exception e) {
      actual = e;
    }

    if (actual == null) {
      fail(msg + "expected exception:<"+expected.getName()+"> but no exception caught");
    }
    if(!(expected.isInstance(actual))) {
      fail(msg + "expected exception:<"+expected.getName()+"> but caught:<"+actual+">");
    }
  }

  /** This will test to verify that an exception is not thrown
      when the provided method is executed.
      @param targetObj  The argument to execute the method on.
      @param methodName The name of the method to execute.
      @param arguments  The array of arguments for the method.
      @see #assertNoException(Statement)
   */
  public static void assertNoException (Object targetObj, String methodName, Object[] arguments) {
    assertNoException(new Statement(targetObj, methodName, arguments));
  }

  /** This will test to verify that an exception is not thrown
      when the provided method is executed.
      @param message    A description of the test.
      @param targetObj  The argument to execute the method on.
      @param methodName The name of the method to execute.
      @param arguments  The array of arguments for the method.
      @see #assertNoException(String,Statement)
   */
  public static void assertNoException (String message, Object targetObj,
                                        String methodName, Object[] arguments) {
    assertNoException(message, new Statement(targetObj, methodName, arguments));
  }

  /** This will test to verify that an exception is not thrown
      when the provided statement is executed.
      @param statment The statement to execute.
      @see #assertNoException(String,Statement)
   */
  public static void assertNoException (Statement statment) {
    assertNoException(null, statment);
  }

  /** This will test to verify that an exception is not thrown
      when the provided statement is executed.
      @param message  A description of the test.
      @param statment The statement to execute.
   */
  public static void assertNoException (String message, Statement statment) {
    String msg = (message == null)? "" : message + " ";

    try {
      statment.execute();
    }
    catch (Exception e) {
      fail(msg+"expected no exceptions but caught:<"+e.getClass().getName()+">");
    }
  }

  public static void assertTrue (boolean test) { assertEquals(true, test); }

  public static void assertFalse (boolean test) { assertEquals(false, test); }

  public static void assertTrue (String message, boolean test) {
    assertEquals(message, true, test);
  }

  public static void assertFalse (String message, boolean test) {
    assertEquals(message, false, test);
  }

  /** Asserts that two double values are identical. <br>
      <br>
      Use {@link #assertEquals(double,double,double)} to specify a tolerance.
      @param expected The expected value.
      @param actual   The actual value.
   */
  public static void assertEquals (double expected, double actual) {
    assertEquals(expected, actual, 0.0d);
  }

  /** Asserts that two long values are identical. <br>
      <br>
      Use {@link #assertEquals(double,double,double)} to specify a tolerance.
      @param expected The expected value.
     @param actual   The actual value.
   */
  public static void assertEquals (long expected, long actual) {
    assertEquals(expected, actual, 0.0d);
  }

  /** Asserts that two double values are identical. <br>
      <br>
      Use {@link #assertEquals(String,double,double,double)} to specify a tolerance.
      @param message  A description of the test.
      @param expected The expected value.
      @param actual   The actual value.
   */
  public static void assertEquals (String message, double expected, double actual) {
    assertEquals(message, expected, actual, 0.0d);
  }

  /** Asserts that two float values are identical. <br>
      <br>
      Use {@link #assertEquals(float,float,float)} to specify a tolerance.
      @param expected The expected value.
      @param actual   The actual value.
   */
  public static void assertEquals (float expected, float actual) {
    assertEquals(expected, actual, 0.0f);
  }

  /** Asserts that two float values are identical. <br>
      <br>
      Use {@link #assertEquals(String,float,float,float)} to specify a tolerance.
      @param message  A description of the test.
      @param expected The expected value.
      @param actual   The actual value.
   */
  public static void assertEquals (String message, float expected, float actual) {
    assertEquals(message, expected, actual, 0.0f);
  }

  /**
   * Asserts that two Numbers (i.e. subclasses of Number) are equal to
   * within a positive delta
   *
   * @param message  A description of the test.
   * @param expected The expected value.
   * @param actual   The actual value.
   * @param delta    The maximum delta between expected and actual for which
   *                 both numbers are still considered equal.
   * @since NeXtMidas 3.7.2
   */
  @InternalUseOnly("For JUnit tests")
  public static <N extends Number> void assertNumberEquals (String message,
      N expected, N actual, float delta) {
    if ((expected instanceof Double) || (expected instanceof Float) ||
        (actual instanceof Double) || (actual instanceof Float)) {
      double _exp   = expected.doubleValue();
      double _act   = actual.doubleValue();
      assertEquals(message,_exp,_act,delta);
    }
    else if ((expected instanceof Long) || (expected instanceof Integer) ||
        (actual instanceof Long) || (actual instanceof Integer)) {
      long _exp   = expected.longValue();
      long _act   = actual.longValue();
      assertEquals(message,_exp,_act,delta);
    }
    else {
      assertEquals(message,expected,actual);
    }
  }

  public static void assertEquals (String message, String expected, String actual) {
    if (!equals(expected, actual)) {
      throw new StringAssertionFailedError(message, expected, actual);
    }
  }

  /** Asserts that an error message is present.
      @param message  A description of the test.
      @param expected Some of the text expected in the warning message.
      @param actual   The actual output.
   */
  public static void assertErrorMsg (String message, String expected, String actual) {
    assertMsg("ERROR:", message, expected, actual);
  }

  /** Asserts that a warning message is present.
      @param message  A description of the test.
      @param expected Some of the text expected in the warning message.
      @param actual   The actual output.
   */
  public static void assertWarnMsg (String message, String expected, String actual) {
    assertMsg("WARN:", message, expected, actual);
  }

  /** Asserts that an informational message is present.
      @param message  A description of the test.
      @param expected Some of the text expected in the warning message.
      @param actual   The actual output.
   */
  public static void assertInfoMsg (String message, String expected, String actual) {
    assertMsg("INFO:", message, expected, actual);
  }

  /** Asserts that a warning message is present.
      @param message  A description of the test.
      @param expected Some of the text expected in the warning message.
      @param actual   The actual output.
   */
  private static void assertMsg (String type, String message, String expected, String actual) {
    if ((expected == null) || expected.isEmpty()) {
      if ((actual == null) || actual.isEmpty()) {
        return;
      }
    }
    else {
      int off = 0;
      while (off >= 0) {
        int     i  = actual.indexOf(type, off);    if (i < 0) break;
        int     j  = actual.indexOf(expected, i);
        int     k  = actual.indexOf('\n',     i);
        if ((0 <= i) && (i < j) && ((j < k) || (k < 0))) {
          return; // found it
        }
        off = k;
      }
    }
    throw new StringAssertionFailedError(message, type+"..."+expected+"...", actual);
  }

  /** Compares two objects using {@link Util#equals}. This allows some differences in types, as long
      as the values are equal.
      @param expected The expected value.
      @param actual   The actual value.
      @since NeXtMidas 2.5.0
   */
  public static void assertSimilar (Object expected, Object actual) {
    assertSimilar(null,expected,actual);
  }

  /** Compares two objects using {@link Util#equals}. This allows some differences in types, as long
      as the values are equal.
      @param message  A description of the test.
      @param expected The expected value.
      @param actual   The actual value.
      @since NeXtMidas 2.5.0
   */
  public static void assertSimilar (String message, Object expected, Object actual) {
    if (!Util.equals(expected,actual)) {
      throw new AssertionFailedError(format(message,expected,actual));
    }
  }

  public static String format (String message, Object expected, Object actual) {
    return ((message != null)? (message + " "): "")
        + "expected:<" + format(expected) + ">"
        + "but was:<"  + format(actual)   + ">";
  }

  /** Formats the  object for printing. This is ONLY used by the "format" method.
      @param obj The object to format.
      @return The formatted string for the object.
   */
  private static String format (Object obj) {
    if (obj == null) return "null";
    if (obj instanceof String   ) return "\"" + obj + "\"";
    if (obj instanceof Double   ) return obj.toString();
    if (obj instanceof Float    ) return obj.toString() + "f";
    if (obj instanceof Long     ) return obj.toString() + "L";
    if (obj instanceof Integer  ) return obj.toString();
    if (obj instanceof Short    ) return "(short)" + obj;
    if (obj instanceof Byte     ) return "(byte)"  + obj;
    if (obj instanceof Character) return "\'"  + obj + "\'";
    if (obj instanceof Boolean  ) return obj.toString();
    return obj.getClass().getName() + ": " + obj.toString();
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Array tests
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Asserts that size and values of two arrays are exactly equal.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, double[] expected, double[] actual) {
    assertArrayEquals(msg, expected, actual, 0.0d);
  }

  /** Asserts that size and values of two arrays are equal with a tolerance.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
      @param tol The tolerance.
   */
  public static void assertArrayEquals (String msg, double[] expected, double[] actual, double tol) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i], tol);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i], tol);
        }
      }
    }
  }

  /** Asserts that size and values of two arrays are exactly equal.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, float[] expected, float[] actual) {
    assertArrayEquals(msg, expected, actual, 0.0f);
  }

  /** Asserts that size and values of two arrays are equal with a tolerance.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
      @param tol The tolerance.
   */
  public static void assertArrayEquals (String msg, float[] expected, float[] actual, float tol) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i], tol);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i], tol);
        }
      }
    }
  }

  /** Asserts that size and values of two arrays are equal.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, long[] expected, long[] actual) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i]);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i]);
        }
      }
    }
  }

  /** Asserts that size and values of two arrays are equal.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, int[] expected, int[] actual) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i]);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i]);
        }
      }
    }
  }

  /** Asserts that size and values of two arrays are equal.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, short[] expected, short[] actual) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i]);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i]);
        }
      }
    }
  }

  /** Asserts that size and values of two arrays are equal.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, byte[] expected, byte[] actual) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i]);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i]);
        }
      }
    }
  }

  /** Asserts that size and values of two arrays are equal.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, char[] expected, char[] actual) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i]);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i]);
        }
      }
    }
  }

  /** Asserts that size and values of two arrays are equal.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, boolean[] expected, boolean[] actual) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i]);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i]);
        }
      }
    }
  }

  /** Asserts that size and values of two arrays are equal. This does an assertEquals(..)
      for each element in the array. It does not check sub-arrays or anything like that.
      @param msg A description of the test.
      @param expected The expected value.
      @param actual The actual value.
   */
  public static void assertArrayEquals (String msg, Object[] expected, Object[] actual) {
    if (msg == null) {
      if (expected == null) {
        assertNull(actual);
      } else {
        assertNotNull(actual);
        assertEquals(expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(expected[i], actual[i]);
        }
      }
    } else {
      if (expected == null) {
        assertNull(msg + ": array is null", actual);
      } else {
        assertNotNull(msg + ": array is not null", actual);
        assertEquals(msg + ": array length", expected.length, actual.length);
        for (int i = 0; i < expected.length; i++) {
          assertEquals(msg + ": value at index "+i, expected[i], actual[i]);
        }
      }
    }
  }

// JC4: I added this and then decided not to use it... if ever we want it, we can just put it back in.
//
//  /** Asserts that size and values of two arrays are equal. This does an assertEquals(..) for each
//      element in the array. It does not check sub-arrays or anything like that.
//      @param msg A description of the test.
//      @param expected The expected value.
//      @param actual The actual value.
//   */
//  public static void assertArrayEquals (String msg, Object expected, Object actual) {
//    assertArrayEquals(msg,expected,actual,0.0);
//  }
//
//  /** Asserts that size and values of two arrays are equal. This does an assertEquals(..) for each
//      element in the array. It does not check sub-arrays or anything like that.
//      @param msg A description of the test.
//      @param expected The expected value.
//      @param actual The actual value.
//      @param tol Tolerance used only if type is double[] or float[].
//   */
//  public static void assertArrayEquals (String msg, Object expected, Object actual, double tol) {
//         if (expected==null  &&  msg==null) assertNull(actual);
//    else if (expected==null  &&  msg!=null) assertNull(msg+": array is null", actual);
//    else if (expected instanceof double[] ) assertArrayEquals(msg, (double[] )expected, (double[] )actual, tol);
//    else if (expected instanceof float[]  ) assertArrayEquals(msg, (float[]  )expected, (float[]  )actual, (float)tol);
//    else if (expected instanceof long[]   ) assertArrayEquals(msg, (long[]   )expected, (long[]   )actual);
//    else if (expected instanceof int[]    ) assertArrayEquals(msg, (int[]    )expected, (int[]    )actual);
//    else if (expected instanceof short[]  ) assertArrayEquals(msg, (short[]  )expected, (short[]  )actual);
//    else if (expected instanceof byte[]   ) assertArrayEquals(msg, (byte[]   )expected, (byte[]   )actual);
//    else if (expected instanceof char[]   ) assertArrayEquals(msg, (char[]   )expected, (char[]   )actual);
//    else if (expected instanceof boolean[]) assertArrayEquals(msg, (boolean[])expected, (boolean[])actual);
//    else if (expected instanceof Object[] ) assertArrayEquals(msg, (Object[] )expected, (Object[] )actual);
//    else throw new RuntimeException("Error: unknown type "+actual.getClass());
//  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Nested Classes
  //////////////////////////////////////////////////////////////////////////////////////////////////

  /** A test that always fails and reports a that the class couldn't be instantiated. */
  public static class InstantiationErrorTest extends NMTestCase {
    private Throwable t;

    /** default constructor */
    public InstantiationErrorTest () {}

    private InstantiationErrorTest (String clazz, String name, Midas midas, Throwable t) {
      super(clazz, name, midas);
      this.t = t;
    }

    @Override
    public void runTest () {
      if (getTestedClass() == null && "testNothing".equals(super.getName()));
      else throw new RuntimeException("Unable to instantiate "+getTestedClass(),t);
    }

    /** This test method is empty because it was added to make the InstantiationErrorTest class
     *  compatible with specific gradle builds (ex. thin-gradle build).
     *  @since NeXtMidas 4.1.1
     */
    public void testNothing() {}
  }

  /** A test that always fails and reports that a test method is missing. */
  public static class MissingTest extends NMTestCase {

    /** default constructor */
    public MissingTest () {}

    private MissingTest (String clazz, String name, Midas midas) {
      super(clazz, name, midas);
    }

    @Override
    public void runTest () {
      if ("testNothing".equals(super.getName()));
      else throw MISSING_TEST_ERROR;
    }

    /** This test method is empty because it was added to make the MissingTest class
     *  compatible with specific gradle builds (ex. thin-gradle build).
     *  @since NeXtMidas 4.1.1
     */
    public void testNothing() {}
  }

  /** An output stream that simply counts the number of bytes written to it. <br>
      <br>
      This should be used as follows:
      <pre>
        OutputTrap  out    = new OutputTrap();
        OutputTrap  err    = new OutputTrap();
        PrintStream stdout = System.out;
        PrintStream stderr = System.err;

        try {
          System.setOut(out);
          System.setErr(err);

          // Do something

        } catch(RuntimeException e) {
          throw e;
        } finally {
          System.setOut(stdout);
          System.setErr(stderr);
        }

        // Check for proper number of bytes written
      </pre>
   */
  public static class OutputTrap extends PrintStream {
    private final ByteCounter os;

    public OutputTrap () {
      this(new ByteCounter());
    }

    private OutputTrap (ByteCounter out) {
      super(out);
      this.os = out;
    }

    /** @return the number of bytes written to this output stream. */
    public long getByteCount () {
      return os.getByteCount();
    }
  }

  /** An output stream that simply counts the number of bytes written to it. */
  private static class ByteCounter extends OutputStream {
    private long byteCount = 0; // The number of bytes written.

    @Override
    public void write (int b) {
      byteCount++;
    }

    /** Gets the number of bytes written to this output stream. */
    public long getByteCount() {
      return byteCount;
    }
  }

  private static final int PASSING_LENGTH = 24;
  private static final int EXTRA_CHARS    = 8;
  private static final int PASSING_LEN    = Math.max(EXTRA_CHARS*3, PASSING_LENGTH);
  private static final int MAX_DELTA      = PASSING_LENGTH-EXTRA_CHARS*2;

  @SuppressWarnings("serial")
  private static class StringAssertionFailedError extends AssertionFailedError {
    private final String message;
    private final String expected;
    private final String actual;

    public StringAssertionFailedError (String message, String expected, String actual) {
      super(message);
      this.message  = message;
      this.expected = expected;
      this.actual   = actual;
    }

    @Override
    public String getMessage () {
      String exp = ((expected == null)? "": expected);
      String act = ((actual   == null)? "": actual);

      if (exp.length() > PASSING_LEN) {
        int start = -1;
        int end   = -1;
        int elen  = exp.length();
        int alen  = act.length();
        int len   = Math.min(elen,alen);

        for (int i = 0; i < len; i++) {
          if (exp.charAt(i) != act.charAt(i)) {
            if (start == -1) start = i;
            end = i;
          }
        }
        if (start == -1) { start = 0; end = len; }
        if (end-start > MAX_DELTA) end = start + MAX_DELTA;

        start -= EXTRA_CHARS;
        end   += EXTRA_CHARS;

        int estart = Math.max(0,start);
        int astart = Math.max(0,start);
        int eend   = Math.min(end,elen);
        int aend   = Math.min(end,alen);

        String edots1 = (estart == 0   )? "": "...";
        String adots1 = (astart == 0   )? "": "...";
        String edots2 = (eend   == elen)? "": "...";
        String adots2 = (aend   == alen)? "": "...";

        exp = edots1 + exp.subSequence(estart,eend) + edots2;
        act = adots1 + act.subSequence(astart,aend) + adots2;
      }
      return NMTestCase.format(message, ((expected == null)? null: exp),
                                        ((actual   == null)? null: act));
    }
  }

  public static void runMetrics(Runnable r, String name, int count) {
    long start = System.currentTimeMillis();
    //do the loop here
    for(int i = 0; i<count;i++) {
      r.run();
    }
    long result = (System.currentTimeMillis() - start);

    System.out.println(name + " took " + result + " milliseconds");
  }

  /** This will determine if the unittest will be running in "metrics" mode
   *  @param message Message to log if running metrics
   *  @return true if in metrics mode
   *  @since 3.9.1
   */
  public static boolean isUnittestMetrics (String message) {
    if(unittestMetrics) {
     Midas.log(message, Midas.WARNING);
     return true;
    }
    return false;
  }

  /** Set whether to run metrics on unittests or not
   *  @param unittestMetrics the unittestMetrics to set
   *  @since 3.9.1
   */
  public static void setUnittestMetrics (boolean unittestMetrics) {
    NMTestCase.unittestMetrics = unittestMetrics;
  }

  /** Send in a thread that will be run and will catch the output.
   *  Then it will compare the output to the expected output.
   *  @param consumer The consumer to execute
   *  @param expected The expected message to be printed to the terminal
   *  @param param The parameter for the consumer
   *  @return true if the expected was printed
   *  @since 3.9.1
   */
  public boolean isMessagePrinted(Consumer<Object> consumer, String expected, Object param) {
    boolean isSuccessful = false;
    Midas midas = getMidas();
    // The stream to steal the terminal output
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // set the terminal output to use the custom stream - setter returns the original to save for later
    OutputStream old = midas.getTerminal().setOutputStream(baos);
    // execute lambda with given parameter
    consumer.accept(param);
    // did the expected string match what was printed to the terminal?
    int count =0;
    while(baos.size()==0 && count<5) {
      Time.sleep(1);
      count++;
    }
    isSuccessful = expected.equals(baos.toString());
    // reset the output stream for the terminal
    midas.getTerminal().setOutputStream(old);
    // if failed then return false
    if (!isSuccessful) return isSuccessful;
    // the following lines will make sure that everything is back to the original.
    baos.reset();
    midas.warning("This is a test message");
    return "".equals(baos.toString());
  }

}
