package nxm.sys.test.junit.lib;

import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import nxm.sys.inc.Chainable;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.KeyObjectNames;
import nxm.sys.inc.Keyable;
import nxm.sys.lib.Args;
import nxm.sys.lib.Data;
import nxm.sys.lib.KeyObject;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.Midas;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;
import nxm.sys.test.junit.NMTestCase;


/** This will test the KeyObject class.
 *
 *  @author  J. Czechowski IV
 */
public final class KeyObjectTest extends NMTestCase {
  private static boolean DEBUG = false; // since 3.6.0

  /** The value of KeyObject.BE_ANNOYING. */ private static final boolean BE_ANNOYING = false;

  /** public constructor to allow running this test case directly with JUnit. */
  public KeyObjectTest (String methodName) { this(methodName, null); }

  /** Creates a new instance of the class.
      @param methodName The name of the method being called.
      @param midas      The midas context to use when running the tests.
   */
  public KeyObjectTest (String methodName, Midas midas) {
    super(methodName, midas);
  }

  @InternalUseOnly("Since added in NeXtMidas 3.6.0")
  public static final void setDebug (boolean val) { DEBUG = val; }

  @InternalUseOnly("Since added in NeXtMidas 3.6.0")
  public static final boolean getDebug () { return DEBUG; }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // INNER TEST CLASSES
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** A test class. */
  public static class TestObj {
    public static final Object staticFinal = "STATIC FINAL";
    public Object publicVar = "PUBLIC VAR";
    private Object value;

    public void setValue(Object[] value){ this.value = value[0]; }
    public void setValue(String value)  { this.value = value; }
    public void setValue(TestObj value) { this.value = value; }
    public Object getValue() { return value; }
  } // end TestObj inner static class

  /** Another test class. */
  public static class KeyableTestObj extends TestObj implements Keyable {
    private static final String KEY = "MYKEY";
    private Object keyedVal = "MYVALUE";

    @Override
    public String toString () {
      return "KeyableTestObj(key="+KEY+", value="+keyedVal+")";
    }

    public Object getKey(String key) {
      if (key.equals(KEY)) { return keyedVal; }
      return null;
    }

    public String[] getKeys() {
      return new String[] { KEY };
    }

    public Object setKey(String key, Object value) {
      if (key.equals(KEY)) { keyedVal = value; }
      return value;
    }
  }

  /** Yet another test class. */
  public static class ChainableTestClass extends KeyableTestObj implements Chainable {
    private Object next;
    private Object prev;

    /** Creates a new instance.
        @param next The next link.
        @param prev The prev link.
     */
    public ChainableTestClass(Object next, Object prev) {
      this.next = next;
      this.prev = prev;
    }

    @Override
    public String toString () {
      return "ChainableTestClass(next="+next+", prev="+prev+", super="+super.toString()+")";
    }

    public Object getNextLink() { return next; }
    public Object getPrevLink() { return prev; }
  }

  /** A simple class for testing abbreviations. */
  public static class AbbrTestClass {
    private int     minVal    = -100;
    private int     maxVal    = 100;
    private float   freq      = 60.0f;
    private float   freqDiff  = 1.0f;
    private double  angle     = 0.0;
    private Data    data      = new Data(new int[] { 0, 1, 2 });
    private short   shortVal  = (short)0;
    private byte    byteVal   = (byte)0;
    private char    charVal   = 'A';
    private boolean boolVal   = false;
    private String  strVal    = "ABC";

    public void setMinVal(int val) { minVal = val; }
    public void setMaxVal(int val) { maxVal = val; }
    public int  getMinVal() { return minVal; }
    public int  getMaxVal() { return maxVal; }

    public void  setFrequency(float val)           { freq     = val; }
    public void  setFrequencyDifference(float val) { freqDiff = val; }
    public float getFrequency()                    { return freq;     }
    public float getFrequencyDifference()          { return freqDiff; }

    public void   setAngle(double val) { angle = val; }
    public void   setData(Data val)    { data = val; }
    public double getAngle() { return angle; }
    public Data   getData()  { return data; }


    public void setShortVal(short val)  { shortVal = val; }
    public void setByteVal(byte val)    { byteVal  = val; }
    public void setCharVal(char val)    { charVal  = val; }
    public void setBoolVal(boolean val) { boolVal  = val; }
    public void setStrVal(String val)   { strVal   = val; }
    public short   getShortVal()  { return shortVal; }
    public byte    getByteVal()   { return byteVal;  }
    public char    getCharVal()   { return charVal;  }
    public boolean getBoolVal()   { return boolVal;  }
    public String  getStrVal()    { return strVal;   }
  }

  /* Class for testing ambiguous members. */
  public static class AmbiguousTestClass {
    // Make an ambiguous member and see which it chooses
    String page = null;
    public void setpage() { page = "page"; } // ... should warn and run one
    public void setPage() { page = "Page"; } // Ambiguous method for setKey...

    // BEGIN since 3.1.1 to test BUG 2570 (support for other JDKs like JDK7)
    String subRecord = null;
    public void setSR(String val)        { subRecord = "setSR " +val; }
    @KeyObjectNames({"setSubRec"}) // suppress acronym "SR" to disambiguate with {@link #setSR()}
    public void setSubRec(String val)    { subRecord = "setSubRec " +val; }
    @KeyObjectNames({"setSubRecord"}) // suppress acronym "SR" to disambiguate with {@link #setSR()}
    public void setSubRecord(String val) { subRecord = "setSubRecord "+val; } // <-- NOTE: this makes test fails under Java 5 & 6 before fix to BUG 2570
    public String getSubRecord()         { return subRecord; }
    // END   since 3.1.1 to test BUG 2570

    public String getPage() { return page; } // Non-ambiguous get to return value
  } // end AmbiguousTestClass inner static class

  // Class for testing setKeys with Table input
  public class SetKeysTestClass {
    // Make a set and a get that mirror a LayerShape problem we had to
    // call setColorTable(Table tab)

    String methodType = null;
    public SetKeysTestClass () {
      Table laytab = new Table("{COLORTABLE={X=1,Y=2}}");
      KeyObject.setKeys(this,laytab,null,0);
    }
    public void setColorTable(nxm.sys.lib.Table tab) { methodType = "set"; }
    public nxm.sys.lib.Table getColorTable()         { methodType = "get"; return new nxm.sys.lib.Table(); }
    public String getMethodType () { return methodType; }
  }

  /** Class for testing more ambiguous members.
   */
  public static class AmbiguousTestClass2 {
    private double xstart;

    /** Set the axis start of MPlot and its main layers. */
    public void setXStart (double value) {
      xstart = value;
    }

    /** Get the X Axis Start from the base layer. */
    public double getXStart () {
      return xstart;
    }

    /** Get the X Axis Start for the given layer. */
    public double getXStart (int layer) {
      return layer * 1000;
    }
  }

  /** Class for testing private child class
   *  method declared in abstract parent. This
   *  class is the parent
   *
   */
  public abstract class AbstractParentFoo extends KeyableTestObj {

    public abstract Object getFoo();
    private String getPrivateInParent() {
      return "privateInParent";
    }
  }

  /** Class for testing private child class
   *  method declared in abstract parent. This
   *  class is the child
   *
   */
  private class PrivateChildFoo extends AbstractParentFoo {
    private String getPrivateInParent() {
      return "privateInChild";
    }
    public String getFoo() {
      return "FOO!";
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // TEST SETUP/CLEANUP
  //
  //  CTO
  //   |   getNextLink()               getKey("DATA")          get value "Z"
  //   +--------------------> KEYABLE -----------------> DATA -----------------> 2
  //   |   getKey("MYKEY")             get value [4]           get value "ALT"
  //   +--------------------> ARRAY -------------------> LIST -----------------> "ALT"
  //   |   getValue()                  getKey("MYKEY")
  //   +--------------------> KTO ---------------------> "MYVALUE"
  //                           |       staticFinal
  //                           +-----------------------> "STATIC FINAL"
  //                           |       publicVar
  //                           +-----------------------> "PUBLIC VAR"
  //
  //////////////////////////////////////////////////////////////////////////////////////////////////
  private Data                sampleData;
  private List<String>        sampleList;
  private Keyable             sampleKeyable;
  private KeyableTestObj      sampleKeyable2;
  private Object[]            sampleArray;
  private Table               sampleTable;
  private TestObj             sampleTestObj;
  private AbbrTestClass       sampleAbbrs;
  private AbbrTestClass       sampleAbbrs2;
  private ChainableTestClass  sampleChainable;
  private ChainableTestClass  sampleChainable2;
  private AmbiguousTestClass  sampleAmbig;
  private AmbiguousTestClass2 sampleAmbig2;
  private PrivateChildFoo     samplePrivateFoo;

  public void setUp () throws Exception {
    super.setUp();
    sampleData        = new Data(new int[] { 0, 1, 2 });
    sampleList        = new ArrayList<>(Arrays.asList(new String[] { "ALT", "LAT", "LON" }));
    sampleKeyable     = new KeyVector(16);
    sampleKeyable2    = new KeyableTestObj();
    sampleArray       = new Object[] { "ZERO", "ONE", "TWO", "THREE", sampleList };
    sampleTable       = new Table();
    sampleTestObj     = new TestObj();
    sampleAbbrs       = new AbbrTestClass();
    sampleAbbrs2      = new AbbrTestClass();
    sampleChainable   = new ChainableTestClass(sampleKeyable,null);
    sampleChainable2  = new ChainableTestClass(sampleAbbrs2,null);
    sampleAmbig       = new AmbiguousTestClass();
    sampleAmbig2      = new AmbiguousTestClass2();
    samplePrivateFoo  = new PrivateChildFoo();

    sampleKeyable.setKey("DATA", sampleData);
    sampleChainable.setKey("MYKEY",sampleArray);
    sampleChainable.setValue(sampleKeyable2);
    sampleTable.setKey("STR","ABC");
    sampleTable.setKey("VEC",sampleData);
    samplePrivateFoo.setKey("MYKEY",sampleArray);
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // TEST METHODS
  //////////////////////////////////////////////////////////////////////////////////////////////////
  public void testAmbiguous() {
    // Since setPage is entered with correct case, we should always use it.
    KeyObject.setKey(sampleAmbig,    "setPage", null);
    assertEquals("setKey(sampleAmbig,'setPage', null)", "Page",sampleAmbig.getPage());

    // Since setpage is entered with correct case, we should always use it.
    KeyObject.setKey(sampleAmbig,    "setpage", null);
    assertEquals("setKey(sampleAmbig,'setpage', null)", "page", sampleAmbig.getPage());

    // Prior to NeXtMidas 3.7.1 this was a warning rather than an error... after many, many years
    // it is time to make it an error...
    //info("!!!!! It is ok to see warnings below for 'KeyObject: Ambiguous.. SETPAGE...' !!!!!");
    //KeyObject.setKey(sampleAmbig,    "SETPAGE", null);
    //assertEquals("setKey(sampleAmbig,'SETPAGE',null)", "PAGE", sampleAmbig.getPage().toUpperCase());
    assertException("setKey(sampleAmbig,'SETPAGE',null)",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleAmbig, "SETPAGE",  null});

    // since 3.1.1: begin test BUG 2570 (additional JDKs support such as Java 7)
    KeyObject.setKey(sampleAmbig,    "SR", "Java 7");
    assertEquals("setKey(sampleAmbig,'SR', 'Java 7')", "setSR Java 7", sampleAmbig.getSubRecord());
    KeyObject.setKey(sampleAmbig,    "SubRec", "JDK 7");
    assertEquals("setKey(sampleAmbig,'SubRec',1)",  "setSubRec JDK 7", sampleAmbig.getSubRecord());
    KeyObject.setKey(sampleAmbig,    "SubRecord", "Shows Problem with JDK 5/6");
    assertEquals("setKey(sampleAmbig,'SubRecord', 'Shows Problem with JDK 5/6')",
                                     "setSubRecord Shows Problem with JDK 5/6", sampleAmbig.getSubRecord());
    // end test BUG 2570 (since 3.1.1)
  }

  public void testAmbiguous2 () {
    KeyObject.setKey(sampleAmbig2, "XS", 2000000);
    assertEquals("setKey(sampleAmbig2,'XS', 2000000)", 2000000d, sampleAmbig2.getXStart());
  }

  public void testSetKeys() {
    SetKeysTestClass sampleSetKeys = new SetKeysTestClass();
    assertEquals("setKeys with get/set with table input","set",sampleSetKeys.getMethodType());
  }

  public void testGetKey() {
    // Check the values.
    assertEquals("getKey(sampleChainable,'MYKEY.4.ALT')",
                 "ALT",          KeyObject.getKey(sampleChainable, "MYKEY.4.ALT"));

    assertEquals("getKey(sampleChainable,'VALUE.MYKEY')",
                 "MYVALUE",      KeyObject.getKey(sampleChainable, "VALUE.MYKEY"));

    assertEquals("getKey(sampleChainable,'VALUE.STATICFINAL')",
                 "STATIC FINAL", KeyObject.getKey(sampleChainable, "VALUE.STATICFINAL"));

    // Check the values for a Table.
    assertEquals("getKey(sampleTable,'STR')",    "ABC",      KeyObject.getKey(sampleTable, "STR"));
    assertEquals("getKey(sampleTable,'VEC')",    sampleData, KeyObject.getKey(sampleTable, "VEC"));
    assertEquals("getKey(sampleTable,'VEC(1)')", "1",        KeyObject.getKey(sampleTable,"VEC(1)").toString());

    assertException("getKey(sampleTable,'STR(1)')",
                    Exception.class, KeyObject.class, "getKey",
                    new Object[] { sampleTable, "STR(1)" } );

    // Check array size
    final Integer size = Integer.valueOf(sampleArray.length);
    assertEquals("getKey(array,'GETSIZE')", size, KeyObject.getKey(sampleArray, "GETSIZE"));
    assertEquals("getKey(array,'SIZE')",    size, KeyObject.getKey(sampleArray, "SIZE"));
    assertEquals("getKey(array,'getSize')", size, KeyObject.getKey(sampleArray, "getSize"));

    // Check for warnings about use of public variables (deprecated if KeyObject.BE_ANNOYING is true).
    assertEquals("getKey(sampleChainable,'VALUE.PUBLICVAR')", "PUBLIC VAR",
                 KeyObject.getKey(sampleChainable, "VALUE.PUBLICVAR"));
    if (BE_ANNOYING) {
      // TODO: figure out way to capture warning message for above test to verify;
      //       without using System.setOut(..), since that doesn't work and gives SecurityException in Java Web Start
      //assertTrue("getKey(sampleChainable,'VALUE.PUBLICVAR') caused warning message.", (out.getByteCount()>0)));
    }

    // Try to get something that isn't there. -- This should fail.
    assertException("setKey(to,'SNARF','0')",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleTestObj, "SNARF", null });

    // Test the handling of null object
    assertEquals("getKey(null,'MINV')",  null, KeyObject.getKey(null, "MINV"));

    // Test the handling of abbreviations and acronyms
    assertEquals("getKey(sampleAbbrs,'MINV')",      Integer.valueOf(-100), KeyObject.getKey(sampleAbbrs, "MINV"));
    assertEquals("getKey(sampleAbbrs,'MAXV')",      Integer.valueOf( 100), KeyObject.getKey(sampleAbbrs, "MAXV"));
    assertEquals("getKey(sampleAbbrs,'FD')",        Float.valueOf(1.0f),   KeyObject.getKey(sampleAbbrs, "FD"));
    assertEquals("getKey(sampleAbbrs,'ANGLE')",     Double.valueOf(0.0),   KeyObject.getKey(sampleAbbrs, "ANGLE"));
    assertEquals("getKey(sampleAbbrs,'freq')",      Float.valueOf(60.0f),  KeyObject.getKey(sampleAbbrs, "freq"));
    assertEquals("getKey(sampleAbbrs,'Frequency')", Float.valueOf(60.0f),  KeyObject.getKey(sampleAbbrs, "Frequency"));
    assertEquals("getKey(sampleAbbrs,'DATA(1)')",   "1",  Objects.toString(KeyObject.getKey(sampleAbbrs, "DATA(1)")));
  }

  public void testSetKey() {
    // Alter the values.
    KeyObject.setKey(sampleChainable, "MYKEY.1",     "1");
    KeyObject.setKey(sampleChainable, "MYKEY.4.ALT", "ALTITUDE");
    KeyObject.setKey(sampleChainable, "VALUE.MYKEY", "NEWVALUE");

    assertEquals("getKey(cto,'MYKEY.1')",
                 "1",        KeyObject.getKey(sampleChainable, "MYKEY.1"));
    assertEquals("getKey(cto,'MYKEY.4.ALT')",
                 "ALTITUDE", KeyObject.getKey(sampleChainable, "MYKEY.4.ALT"));
    assertEquals("getKey(cto,'MYKEY.MYKEY')",
                 "NEWVALUE", KeyObject.getKey(sampleChainable, "VALUE.MYKEY"));

    // TODO: figure out way to capture warning message below tests to verify (when BE_ANNOYING == true);
    //       without using System.setOut(..), since that doesn't work and gives SecurityException in Java Web Start
    // Check for warnings about use of public variables (deprecated if KeyObject.BE_ANNOYING is true).
    final String newValue = "setting public field is/will be DEPRECATED";
    KeyObject.setKey(sampleTestObj, "PUBLICVAR", newValue);
    assertEquals("setKey(to,'PUBLICVAR,'"+newValue+"') ", newValue, KeyObject.getKey(sampleTestObj, "PUBLICVAR"));
    if (BE_ANNOYING) {
      // TODO: assert warning message
    }

    // 2011-12-23 NTN: this does not look to be working?, setting public field in with a get/SetValue().publicvar
    KeyObject.setKey(sampleChainable, "VALUE.PUBLICVAR", newValue);
    // assertEquals("setKey(cto,'VALUE.PUBLICVAR,'"+newValue+"') ", newValue, KeyObject.getKey(sampleChainable, "PUBLICVAR"));
    if (BE_ANNOYING) {
      // TODO: assert warning message
    }

    // Try setting a "static final" -- this should fail.
    assertException("setKey(to,'STATICFINAL','0')",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleTestObj, "STATICFINAL",   "0" });

    // Try setting a parameter when given two arguments -- this should fail.
    assertException("setKey(to,'PUBLICVAR',{'0','1'})",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleTestObj, "PUBLICVAR",   new Object[] { "0", "1" } });

    // Try setting an array value when given three arguments -- this should fail.
    assertException("setKey(to,'0',{'0','1'})",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleArray, "0",   new Object[] { "0", "1" } });

    // Try setting an array value when given invalid index -- this should fail.
    assertException("setKey(to,'FOO', '0')",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleArray, "FOO", "0" });
    assertException("setKey(to,'5000', '0')",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleArray, "5000", "0" });

    // Try setting an array size -- this should fail.
    assertException("setKey(to,'SIZE', '14')",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleArray, "SIZE", "14" });

    // Test the handling of ambiguous abbreviations and acronyms -- this should fail.
    assertException("setKey(sampleAbbrs,'MV','0')",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleAbbrs, "MV",   "0" });

    // Test abbreviation longer than 4 characters -- this should fail.
    assertException("setKey(sampleAbbrs,'FREQU','0')",
                    Exception.class, KeyObject.class, "setKey",
                    new Object[] {sampleAbbrs, "FREQU",   "0" });

    // Test conversions                  VAR NAME    SET TO                     EXPECTED FROM GET
    Object[][] convData = {new Object[] {"MINV",     "-1",                      Integer.valueOf(-1) },
                           new Object[] {"MAXV",     "1",                       Integer.valueOf(1)  },
                           new Object[] {"FD",       "1000",                    Float.valueOf(1000f)},
                           new Object[] {"FR",       "10",                      Float.valueOf(10f)  },
                           new Object[] {"FRE",      "20",                      Float.valueOf(20f)  },
                           new Object[] {"FREQ",     "30",                      Float.valueOf(30f)  },

                           new Object[] {"FD",       Double.valueOf(2000d),     Float.valueOf(2000f)},
                           new Object[] {"FD",       Long.valueOf(3000L),       Float.valueOf(3000f)},

                           new Object[] {"ANGLE",    Byte.valueOf((byte)1),     Double.valueOf(1.0) },
                           new Object[] {"ANGLE",    Short.valueOf((short)2),   Double.valueOf(2.0) },
                           new Object[] {"ANGLE",    Integer.valueOf(3),        Double.valueOf(3.0) },
                           new Object[] {"ANGLE",    Long.valueOf(4),           Double.valueOf(4.0) },
                           new Object[] {"ANGLE",    Float.valueOf(5.0f),       Double.valueOf(5.0) },
                           new Object[] {"ANGLE",    Double.valueOf(6.0),       Double.valueOf(6.0) },
                           new Object[] {"ANGLE",    Character.valueOf('A'),    Double.valueOf(65.0)},

                           new Object[] {"SHORTVAL", Double.valueOf(10.0),      Short.valueOf((short)10)},
                           new Object[] {"BYTEVAL",  Double.valueOf(10.0),      Byte.valueOf((byte)10)  },
                           new Object[] {"CHARVAL",  Integer.valueOf((int)'Z'), Character.valueOf('Z')  },
                           new Object[] {"BOOLVAL",  "YES",                     Boolean.TRUE        },
                           new Object[] {"STRVAL",   Double.valueOf(10.0),      "10.0"              }};

    for (int i = 0; i < convData.length; i++) {
      String name = convData[i][0].toString();
      Object obj  = convData[i][1];
      Object exp  = convData[i][2];

      KeyObject.setKey(sampleAbbrs, name, obj);

      assertEquals("getKey(sampleAbbrs,"+name+") = "+exp, exp, KeyObject.getKey(sampleAbbrs, name));
    }
  }

  /** @since 3.5.0 */
  public void testFindMembers() {
    // since 3.5.0 test case for BUG 2793 (this should NOT cause an exception)
    KeyObject.findMembers(Table.class, "size", null, false);

    // begin test case for BUG 2794 (since 3.5.0)
    final int testDurationTime = 3; // in seconds
    final int numThreads = 10;
    final ExecutorService threadPool = Executors.newFixedThreadPool(numThreads);
    final ArrayList<Runnable> tasks = new ArrayList<>();

    // add tests
    for (int ii=0; ii<10000*testDurationTime; ii++) { // 10k x 5 tasks x testDurationTime (3) = 150K tasks
      tasks.add(new KOTestTask(Table.class));
      tasks.add(new KOTestTask(Data.class));
      tasks.add(new KOTestTask(KeyObject.class));
      tasks.add(new KOTestTask(Args.class));
      tasks.add(new KOTestTask(Shell.class));
    }

    // info("DEBUG: running "+ tasks.size() + " test cases in thread pool for up to "+ testDurationTime +" seconds...");
    // run tests using thread pool
    for (Runnable task : tasks) {
      threadPool.execute(task);
    }

    // wait up to specified duration for tasks in thread pool to complete, then stop
    try {
      threadPool.awaitTermination(testDurationTime, TimeUnit.SECONDS);
    } catch (InterruptedException ie) {
      warn("Interrupted while waiting for testing task in thread pool to complete. "+ie);
    }
    threadPool.shutdown();

    info("KeyObject.findMembers(): numPassed=" + taskCounter + " numFailed=" + failedCounter
        + " numThreads=" + numThreads + " numTasks="+ tasks.size() + " testDuration=" + testDurationTime);

    final boolean lowPerformanceDetected = taskCounter.intValue() < (numThreads * testDurationTime * 5);
    if (lowPerformanceDetected || DEBUG) { // since 3.6.0 for warn about Java performance issue (e.g. Java 7u80) (Bug 2906)
      if (lowPerformanceDetected) {
        warn("This JVM seems to have some performance issue with KeyObject, reflection, and multiple-threads. Java version: " +
             Shell.getProperty("java.version") + ", vendor: " + Shell.getProperty("java.vendor") + ", arch: " + Shell.getProperty("os.arch"));
      }
      for (int ii = 0; ii < 12; ii++) { // print stats once per second for next 12 additional seconds
        info(" numPassed = " + taskCounter  +" numFailed = " + failedCounter + " total duration = " + (testDurationTime + ii) + " secs");
        Time.pause(1);
      }
    }

    assertEquals("number of exceptions/failed tasks", 0, failedCounter.intValue());
    assertTrue("number of passed tasks <= numTasks",   taskCounter.intValue() <= tasks.size()); // since 3.5.4
    assertTrue("number of passed tasks >= numThreads", taskCounter.intValue() >= numThreads);   // simplified check in 3.5.4

    threadPool.shutdownNow();
    // end test case for BUG 2794 (since 3.5.0)
  }

  /** Runnable to test BUG 2552 (since 3.7.1).
   *  Author: Greg Harris
   *  This method tests an extended abstract class public method.
   *  This method in the child is called from a private child class
   *  object.
   * */
  public void testFoo() {
    // Checks that the getKey method works for samplePrivateFoo object
    assertEquals("getKey(samplePrivateFoo,'MYKEY.4.ALT')",
        "ALT", KeyObject.getKey(samplePrivateFoo, "MYKEY.4.ALT"));

    // Checks that the return is what it is expected to be
    assertEquals("FOO!", samplePrivateFoo.getFoo());
    assertEquals("privateInChild", samplePrivateFoo.getPrivateInParent());
  }

  private AtomicInteger taskCounter   = new AtomicInteger();
  private AtomicInteger failedCounter = new AtomicInteger();

  /** Runnable to test BUG 2794 (since 3.5.0). */
  private class KOTestTask implements Runnable {
    private final Class<?> clazz;

    public KOTestTask(Class<?> clazz) {
      this.clazz = clazz;
    }

    public void run() {
      boolean staticOnly = false;
      ArrayList<Member> methods = new ArrayList<>();
      String name = null;
      Member lastMember = null;
      try {
        for (Method method : clazz.getMethods()) {
          name = method.getName();
          lastMember = method;
          Member[] result = KeyObject.findMembers(clazz, name, null, staticOnly);
          for (Member member : result) {
            methods.add(member);
          }
        }
        ArrayList<Member> fields = new ArrayList<>();
        for (Field field : clazz.getFields()) {
          name = field.getName();
          lastMember = field;
          Member[] result = KeyObject.findMembers(clazz, name, null, staticOnly);
          for (Member member : result) {
            fields.add(member);
          }
        }
        taskCounter.incrementAndGet();   // increment task/test succeeded counter
      } catch (Exception e) {
        failedCounter.incrementAndGet(); // increment task/test failed counter
        warn("Got unexpected exception: at name = " + name + " at " + lastMember + " exception = " + e);
//        fail("Got exception: " + e + " class = " + clazz + " at method/field name = "+ name + " -> " + lastMember);
      }
    }
  } // end inner class KOTestTask

}
