package nxm.sys.test.junit.lib;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import org.junit.After;
import org.junit.Before;

import nxm.sys.lib.CoreIO;
import nxm.sys.lib.LogUtilities;
import nxm.sys.lib.LogUtilities.EnhancedLogRecord;
import nxm.sys.lib.Midas;
import nxm.sys.lib.NeXtMidas;
import nxm.sys.lib.SampleFormatter;
import nxm.sys.lib.Shell;
import nxm.sys.test.junit.NMTestCase;

/**
 * This tests the functionality of LogUtilities.
 * @author Kevin Esterheld
 * @since NeXtMidas 3.9.1
 *
 */
public class LogUtilitiesTest extends NMTestCase {

  private static Logger log = Logger.getLogger(LogUtilitiesTest.class.getName());

  // This will collect the logs records that are logged for asserts.
  // The log record is what is sent to the handler which is then sent to the
  // formatter.
  // This test will not test that the formatter is working correctly.
  private TempHandler tempHandler;

  private ConsoleHandler customConsoleHandler;

  private String optionsSave;

  private Level previousLevel;

  /**
   * Public constructor to allow running this test case directly with JUnit
   * @param methodName
   */
  public LogUtilitiesTest (String methodName) {
    this(methodName, null);

  }

  /**
   * Constructor is required by NeXtMidas Unit Testing Framework
   * @param methodName
   * @param midas
   */
  public LogUtilitiesTest (String methodName, Midas midas) {
    super(methodName, midas);
  }


  @Before
  public void setUp() {
    log.setUseParentHandlers(false);
    previousLevel = log.getLevel();
    log.setLevel(Level.ALL);
    customConsoleHandler = new ConsoleHandler();
    // Use the NeXtMidas Sample Formatter for example purposes only.
    customConsoleHandler.setFormatter(new SampleFormatter());
    tempHandler = new TempHandler();
    log.addHandler(tempHandler);
    log.addHandler(customConsoleHandler);
    optionsSave = Shell.getMidasContext().io.getOptions();
  }

  /** Tets that Demand Logger loaded successfully */
  public void testGetDemandLogger () {
    Method demandLogger = LogUtilities.getDemandLogger();
    boolean successful = demandLogger != null;
    String assertText = "LogUtilities was able to load the demandLogger which in JDK 17+ requires " +
                        "jvm option --add-opens java.logging/java.util.logging=ALL-UNNAMED";
    assertTrue(assertText,successful);
  }

  /**
   * This will test the logger and the logging levels are respected with the options.
   */
  public void testlog () {
    Shell.getMidasContext().io.setOptions("+CLASSBASEDLOGGERSHOULDUSEMIDASLOGGER");
    // The following two logs will not go to the java logger as we have the option set to "+CLASSBASEDLOGGERSHOULDUSEMIDASLOGGER"
    Midas.log("Calling Midas.log with io option +CLASSBASEDLOGGERSHOULDUSEMIDASLOGGER", Midas.WARNING);
    LogUtilities.log(Level.WARNING, "Calling LogUtilities.log directly with io option +CLASSBASEDLOGGERSHOULDUSEMIDASLOGGER");

    // Switch to use the java logger
    Shell.getMidasContext().io.setOptions("-CLASSBASEDLOGGERSHOULDUSEMIDASLOGGER|+MIDASLOGGERSHOULDUSELOGUTILITIES");

    // The following logs will go to the java logger and assert statements may be done to be sure it is all going correctly.
    Midas.log("Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", Midas.WARNING);
    Integer lineNum = getLineNumber();
    LogRecord capturedLog = getTestCapturedLog();
    assertEquals("The level should be warning",Level.WARNING, capturedLog.getLevel());
    assertEquals("Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    Midas.log("Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", Midas.INFO);
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level shoud be info",Level.INFO, capturedLog.getLevel());
    assertEquals("Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    Midas.log("Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", Midas.DEPRECATE);
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be info",Level.INFO, capturedLog.getLevel());
    assertEquals("DEPRECATE:Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    Midas.log("Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", Midas.ERROR);
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be server",Level.SEVERE, capturedLog.getLevel());
    assertEquals("Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    Midas.log("Calling Midas.log with io option +MIDASLOGGERSHOULDUSELOGUTILITIES (Interactive Message doesn't go to the java logger)", Midas.INTERACTIVE);
    capturedLog = getTestCapturedLog();
    assertNull("Interactive messages do not go to the java logger",capturedLog);

    LogUtilities.log(Level.WARNING, "Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES");
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be warning",Level.WARNING, capturedLog.getLevel());
    assertEquals("Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    LogUtilities.log(Level.INFO, "Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES");
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be info",Level.INFO, capturedLog.getLevel());
    assertEquals("Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    LogUtilities.log(Level.SEVERE, "Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES");
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be serve",Level.SEVERE, capturedLog.getLevel());
    assertEquals("Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    LogUtilities.log(Level.FINE, "Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES");
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be fine",Level.FINE, capturedLog.getLevel());
    assertEquals("Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    LogUtilities.log(Level.FINER, "Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES");
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be finer",Level.FINER, capturedLog.getLevel());
    assertEquals("Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    LogUtilities.log(Level.FINEST, "Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES");
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be finest",Level.FINEST, capturedLog.getLevel());
    assertEquals("Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

    // Lets set the log minimum to INFO
    log.setLevel(Level.INFO);

    LogUtilities.log(Level.FINE, "Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES");
    capturedLog = getTestCapturedLog();
    assertNull("This should not be logged",capturedLog);

    LogUtilities.log(Level.INFO, "Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES");
    lineNum = getLineNumber();
    capturedLog = getTestCapturedLog();
    assertEquals("The level should be info",Level.INFO, capturedLog.getLevel());
    assertEquals("Calling LogUtilities.log directly with io option +MIDASLOGGERSHOULDUSELOGUTILITIES", capturedLog.getMessage());
    assertEquals("The line number should be included in the log record", lineNum,((EnhancedLogRecord)capturedLog).getSourceLineNumber());

  }

  private int getLineNumber() {
    StackTraceElement[] elements = Thread.currentThread().getStackTrace();
    boolean skippedThisOne = false;

    for(StackTraceElement element : elements) {
        String methodName = element.getMethodName();
        int lineNum = element.getLineNumber();
        if(skippedThisOne) {
            return lineNum-1;
        }
        if(methodName.equals("getLineNumber")) {
          skippedThisOne = true;
        }
    }
    return -1;
  }

  @After
  public void tearDown () {
    Shell.getMidasContext().io.setOptions(optionsSave);
    log.setUseParentHandlers(true);
    log.removeHandler(tempHandler);
    log.removeHandler(customConsoleHandler);
    log.setLevel(previousLevel);
  }

  private LogRecord getTestCapturedLog () {
    if(tempHandler.records.size()<=0) return null;
    return tempHandler.records.remove(0);
  }

  private static final class TempHandler extends Handler {
    public ArrayList<LogRecord> records = new ArrayList<>(); // the collected log records

    /** Creates a new instance. */
    TempHandler () {
      super.setLevel(Level.ALL);
    }

    @Override public void close () { }
    @Override public void flush () { }

    @Override
    public void publish (LogRecord record) {
      records.add(record);
    }
  }
}
