/*
 * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved.
 */
package net.snowflake.client.log;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.AppenderBase;
import net.snowflake.client.category.TestCategoryCore;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.experimental.categories.Category;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A class for testing {@link SLF4JLogger}
 */
@Category(TestCategoryCore.class)
public class SLF4JLoggerIT extends AbstractLoggerIT
{
  /**
   * {@link SLF4JLogger} instance that will be tested in this class
   */
  private static final SLF4JLogger LOGGER =
      new SLF4JLogger(SLF4JLoggerIT.class);

  /**
   * Used for storing the log level set on the logger instance before starting
   * the tests. Once the tests are run, this log level will be restored.
   */
  private static Level logLevelToRestore;

  /**
   * This is the logger instance internally used by the SLF4JLogger instance.
   * LoggerFactory returns the cached instance if an instance by the same name
   * was created before. The name used for creating SLF4JLogger instance is also
   * used here to get the same internal logger instance.
   * <p>
   * SLF4JLogger doesn't expose methods to set logging level and other
   * configurations, hence direct access to the internal logger is required.
   */
  private static final Logger internalLogger = (Logger) LoggerFactory.getLogger(
      SLF4JLoggerIT.class);

  /**
   * Used for storing whether additive property was set on the logger instance
   * before starting the tests. Once the tests are run, additivity will be
   * restored if it was previously enabled.
   * <p>
   * If additivity is enabled, {@link Appender}s in the parent loggers are also
   * invoked.
   */
  private static boolean additivityToRestore = true;

  /**
   * Used for storing any appenders present in the internal logger before
   * starting the tests. They will be restored after running the tests.
   */
  private static final List<Appender<ILoggingEvent>> appendersToRestore =
      new ArrayList<>();

  /**
   * This appender will be added to the internal logger to get logged messages.
   */
  private final Appender<ILoggingEvent> testAppender = new TestAppender();

  /**
   * Message last logged using SLF4JLogger.
   */
  private String lastLogMessage = null;

  /**
   * Level at which last message was logged using SLF4JLogger.
   */
  private Level lastLogMessageLevel = null;

  @BeforeClass
  public static void oneTimeSetUp()
  {
    logLevelToRestore = internalLogger.getLevel();
    additivityToRestore = internalLogger.isAdditive();

    appendersToRestore.clear();

    // Get all existing appenders and restore them once testing is done.
    Iterator<Appender<ILoggingEvent>> itr = internalLogger.iteratorForAppenders();
    while (itr.hasNext())
    {
      appendersToRestore.add(itr.next());
    }

    // Remove existing appenders to avoid unnecessary logging
    appendersToRestore.forEach(internalLogger::detachAppender);

    // Disable running appenders in parent loggers to avoid unnecessary logging
    internalLogger.setAdditive(false);
  }

  @AfterClass
  public static void oneTimeTearDown()
  {
    // Restore original configuration
    internalLogger.setLevel(logLevelToRestore);
    internalLogger.setAdditive(additivityToRestore);

    internalLogger.detachAndStopAllAppenders();

    appendersToRestore.forEach(internalLogger::addAppender);
  }

  @Before
  public void setUp()
  {
    super.setUp();

    if (!testAppender.isStarted())
    {
      testAppender.start();
    }

    internalLogger.addAppender(testAppender);
  }

  @After
  public void tearDown()
  {
    internalLogger.detachAppender(testAppender);
  }

  @Override
  void logMessage(LogLevel level, String message, Object... args)
  {
    switch (level)
    {
      case ERROR:
        LOGGER.error(message, args);
        break;
      case WARNING:
        LOGGER.warn(message, args);
        break;
      case INFO:
        LOGGER.info(message, args);
        break;
      case DEBUG:
        LOGGER.debug(message, args);
        break;
      case TRACE:
        LOGGER.trace(message, args);
        break;
    }
  }

  @Override
  void setLogLevel(LogLevel level)
  {
    internalLogger.setLevel(toLogBackLevel(level));
  }

  @Override
  String getLoggedMessage()
  {
    return this.lastLogMessage;
  }

  @Override
  LogLevel getLoggedMessageLevel()
  {
    return fromLogBackLevel(this.lastLogMessageLevel);
  }

  @Override
  void clearLastLoggedMessageAndLevel()
  {
    this.lastLogMessage = null;
    this.lastLogMessageLevel = null;
  }

  /**
   * Converts log levels in {@link LogLevel} to appropriate levels in
   * {@link Level}.
   */
  private Level toLogBackLevel(LogLevel level)
  {
    switch (level)
    {
      case ERROR:
        return Level.ERROR;
      case WARNING:
        return Level.WARN;
      case INFO:
        return Level.INFO;
      case DEBUG:
        return Level.DEBUG;
      case TRACE:
        return Level.TRACE;
    }

    return Level.TRACE;
  }

  /**
   * Converts log levels in {@link Level} to appropriate levels in
   * {@link LogLevel}.
   */
  private LogLevel fromLogBackLevel(Level level)
  {
    if (Level.ERROR.equals(level))
    {
      return LogLevel.ERROR;
    }
    if (Level.WARN.equals(level))
    {
      return LogLevel.WARNING;
    }
    if (Level.INFO.equals(level))
    {
      return LogLevel.INFO;
    }
    if (Level.DEBUG.equals(level))
    {
      return LogLevel.DEBUG;
    }
    if (Level.TRACE.equals(level) || Level.ALL.equals(level))
    {
      return LogLevel.TRACE;
    }

    throw new IllegalArgumentException(String.format(
        "Specified log level '%s' not supported", level.toString()));
  }

  /**
   * An appender that will be used for getting messages logged by a
   * {@link Logger} instance
   */
  private class TestAppender extends AppenderBase<ILoggingEvent>
  {
    @Override
    public void append(ILoggingEvent event)
    {
      // Assign the log message and it's level to the outer class instance
      // variables so that it can see the messages logged
      lastLogMessage = event.getFormattedMessage();
      lastLogMessageLevel = event.getLevel();
    }
  }
}