package nxm.sys.intr;

import java.util.ArrayList;
import java.util.Hashtable;

import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestListener;
import junit.framework.TestResult;
import junit.framework.TestSuite;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.TextFile;
import nxm.sys.test.junit.NMTestCase;

/** This will run a set of unit tests.
    @author J. Czechowski IV / Neon Ngo
*/
public class Unittest extends Intrinsic {
  /** ("INFO: "|"WARN: "|etc.) + ("..PASS"|"..FAIL"|etc.) */
  public static final int OUTPUT_OVERHEAD_LENGTH = 6 + 6;
  /** This is the total length of an output message minus overhead (info|warn) and (pass|fail). */
  public static final int OUTPUT_LENGTH = (80 - OUTPUT_OVERHEAD_LENGTH); // total - <overhead length>

  private static final String ERR_FLAG = " <--"; // Marks errors to make them easy to find.
  private boolean   showError;     // Indicates if the details on errors should be shown.
  private boolean   showFailure;   // Indicates if the details on failures should be shown.
  private boolean   showTrace;     // Indicates if a stack trace should be shown on error/failure.
  private String    results;       // The results parameter to write a summary of the tests to.
  private int       tests;         // The number of tests that have been run.
  private int       failures;      // The number of tests ending in a failure.
  private int       errors;        // The number of tests ending in an error.
  private int       broken;        // The number of tests marked broken (known DR) that are skipped.
  private int       missing;       // The number of methods for which there are no test cases.
  private TestSuite testSuite;     // The set of tests to run.
  private int       nextTest;      // The index into testSet indicating the next test to process.

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

  public int open () {
    String toTest = MA.getQ("OPT", "SYS").trim();
    boolean isSingleClass = (toTest.indexOf(".") >= 0);

    showError   = MA.getState("/ERROR",   true);
    showFailure = MA.getState("/FAILURE", true);
    showTrace   = MA.getState("/ST",      false);
    results     = MA.getS("RES").trim();

    tests       = 0;
    failures    = 0;
    errors      = 0;
    broken      = 0;
    missing     = 0;
    nextTest    = 0;

    if (isSingleClass) {
      if ((toTest.indexOf(".test.junit.") < 0) && (toTest.startsWith("nxm."))) {
        int index = toTest.indexOf('.', 4); // Index of the dot between opt and area
        toTest = toTest.substring(0, index) + ".test.junit" + toTest.substring(index) + "Test";
      }

      testSuite = new TestSuite();
      testSuite.addTest(buildTestSuite(toTest));
    } else {
      testSuite = buildOptionTestSuite(toTest);
    }

    return NORMAL;
  }

  public int process () {
    int status = NORMAL;

    if (nextTest >= testSuite.testCount()) {
      status =  FINISH; // All tests have been run

    } else {
      NMTestResult results = new NMTestResult();
      results.addListener(new NMTestListener());

      String msg = "Testing " + testSuite.testAt(nextTest) + " ";

      if (verbose) {
        M.info(msg);
      }

      testSuite.testAt(nextTest).run(results);

      if (verbose) {
        msg = "    Results of testing " + testSuite.testAt(nextTest);
      }

      if (results.wasSuccessful()) {
        M.info(StringUtil.padRight(msg,OUTPUT_LENGTH,'.') + "..PASS");
      } else {
        M.warning(StringUtil.padRight(msg,OUTPUT_LENGTH,'.') + "..FAIL"+ERR_FLAG);
      }

      tests    += results.runCount();
      failures += results.failureCount();
      errors   += results.errorCount();
      missing  += results.missingCount();

      nextTest++;
    }
    return status;
  }

  public int close () {
    String summary = "Ran " + tests + " JUnit tests, " + (failures + errors) + " failed. " + "("
                   + broken + " known drs, " + errors + " exceptions, " + missing + " missing)";

    String message;
    int    status;

    if ((failures == 0) && (errors == 0)) {
      message = "PASSED ALL TESTS";
      status  = 1;
    } else {
      message = "FAILED SOME TESTS";
      status  = -1;
    }

    if ((results != null) && (results.length() > 0)) {
      nxm.sys.lib.Table res = new nxm.sys.lib.Table();

      /* For compatibility with CONFIDENCE. */
      res.put("MSG",           message);
      res.put("STATUS",        status);
      /* Additional information. */
      res.put("SUMMARY",       summary);
      res.put("TEST_COUNT",    tests);
      res.put("FAILURE_COUNT", (failures + errors));
      res.put("ERROR_COUNT",   errors);
      res.put("BROKEN_COUNT",  broken);
      res.put("MISSING_COUNT", missing);

      MR.put(results, res);

    } else {
      if ((failures == 0) && (errors == 0)) {
        M.info(summary);
      } else {
        M.warning(summary);
      }
    }

    return NORMAL;
  }

  /** This will build the test set for the specified option.
      @param option The options to put in the test set.
      @return The test set.
   */
  public TestSuite buildOptionTestSuite (String option) {
    ArrayList<String> testClasses = new ArrayList<>();
    TestSuite testSuite     = new TestSuite();
    option = option.toLowerCase();
    final String testPath   = Shell.getOptPath(option, "TEST");
    final String configFile = FileName.addPathRoot(FileName.addPathDir(testPath, "junit"), "unittest_config.tbl"); // fix for Bug 2054
    final String optUpperCase = option.toUpperCase();

    TextFile tf = new TextFile(M, configFile); // fix for Bug 2054
    if (!tf.exists()) {
      String msg = "No unit test configuration file found for option " + optUpperCase;
      M.info(StringUtil.padRight(msg, OUTPUT_LENGTH, '.') + "..MISS");
    }
    else if (!tf.open(BaseFile.INPUT|BaseFile.NOABORT)) {
      String msg = "Error in loading unit test configuration for option " + optUpperCase;
      M.warning(StringUtil.padRight(msg, OUTPUT_LENGTH, '.') + "..FAIL");
      errors++;
    }

    String line = tf.read();
    while (line != null) {
      line = line.trim();

      if ((!line.startsWith("!")) && (line.length() > 0)) {
        final int indexEq = line.indexOf('=');

        String name   = null;
        String params = null;

        if (indexEq < 0) {
          name   = line;
          params = null;
        }
        else {
          name   = line.substring(0, (indexEq)).trim();
          params = line.substring(indexEq+1).trim().toUpperCase();
        }

        if ((params == null) || (params.length() == 0)) {
          final String className = "nxm." + option + ".test.junit." + name;
          testClasses.add(className);
        }
        else if (params.equals("BROKEN")) {
          final int    index     = name.lastIndexOf("Test");
          final String className = (index <= 0)? ("nxm." + option + name):
            ("nxm." + option + name.substring(0,index));

          String msg = className + ": Test skipped - known DR. ";

          M.info(StringUtil.padRight(msg, OUTPUT_LENGTH, '.') + "..BROK");
          broken++;
        }
        else {
          M.error("Unknown test specifier (" + line + ") expected '" + name + "' or '" + name + "=BROKEN'.");
        }
      }

      line = tf.read();
    } // end while loop
    tf.close();

    for (String className : testClasses) {
      TestSuite test = buildTestSuite(className);
      testSuite.addTest(test);
    }

    return testSuite;
  }

  /** This will build the test set for the specified options.
      @param className The class name
      @return The test set.
   */
  public TestSuite buildTestSuite(String className) {
    TestSuite suite  = new TestSuite(className);

    try {
      NMTestCase[] set = NMTestCase.buildTestSet(M, className);

      for(int i = 0; i < set.length; i++) {
        suite.addTest(set[i]);
      }

      if (set.length > 0) {
        String name  = set[0].toString();
        int    index = name.lastIndexOf('(');

        if (index > 0) {
          name  = name.substring(0, index);
          index = name.lastIndexOf('.');

          if (index > 0) {
            name = name.substring(0, index);
          }
        }

        suite.setName(name);
      }


    } catch (ClassNotFoundException exception) {
      String msg = "Unable to instantiate test class " + className + ". ";

      M.warning(StringUtil.padRight(msg, OUTPUT_LENGTH, '.') + "..FAIL");
      errors++;
    }

    return suite;
  }


  /** This is the {@link TestListener} that will be used when running the unit
      tests to record the appropriate statistics about the test.
   */
  protected class NMTestListener implements TestListener {
    /** This is the set of all active tests. */
    protected Hashtable<Test,NMTestResult> activeTests;

    /** Creates a new instance of NMTestRunner. */
    public NMTestListener () {
      activeTests = new Hashtable<>();
    }

    public void addError (Test test, Throwable error) {
      NMTestResult testResult = activeTests.get(test);
      testResult.addError(test, error);

      if (showError) {
        M.printStackTrace("TEST ERROR", error, showTrace);
      }
    }

    public void addFailure (Test test, AssertionFailedError error) {
      NMTestResult testResult = activeTests.get(test);
      testResult.addFailure(test, error);

      if ((error != NMTestCase.MISSING_TEST_ERROR) && showFailure) {
        M.printStackTrace("TEST FAILURE", error, showTrace);
      }
    }

    public void endTest (Test test) {
      NMTestResult testResult = activeTests.remove(test);

      String name = test.toString().substring(((NMTestCase)test).getTestedClass().length()+1);
      String msg = "    Testing " + name + " ";

      if (test instanceof NMTestCase.MissingTest) {
        msg = "    Testing " + name + ": No test method found ";
        if (verbose) M.info(StringUtil.padRight(msg,OUTPUT_LENGTH,'.')+"..MISS");
      }
      else if (testResult.wasSuccessful()) {
        if (verbose) M.info(StringUtil.padRight(msg,OUTPUT_LENGTH,'.')+"..PASS");
      }
      else {
        M.warning(StringUtil.padRight(msg,OUTPUT_LENGTH,'.')+"..FAIL");
      }
    }

    public void startTest (Test test) {
      activeTests.put(test, new NMTestResult());
    }
  }

  /** Minor extension to {@link TestResult} to allow it to be used to indicate
      when tests are missing.
   */
  private static class NMTestResult extends TestResult {
    private int fMissing = 0;
    public void addFailure (Test test, AssertionFailedError t) {
      if (t == NMTestCase.MISSING_TEST_ERROR) {
        fMissing++;
      } else {
        super.addFailure(test, t);
      }
    }
    private int missingCount () {
      return fMissing;
    }
  }
}
