/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira.junit
 */
package net.ranides.assira.junit;

import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.IntPredicate;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * LogObserver is working only inside unit tests.
 * It is collection all log messages emmitted by slf4j.
 * It works because assira.junit provides its own slf4j logger implementation: {@link org.slf4j.impl.ObserveLogger}
 *
 * To use LogObserver be sure, that there is no other slf4j logger implementations in classpath
 *
 * You can enable log collection by setting system properties:
 *  "assira.junit.debug"            - messages are printed to console
 *  "assira.junit.logger.observe"   - messages are collected
 *  "assira.junit.logger.level"     - set minimum level of collected messages ("debug" by default)
 *                                    allowed values: trace, debug, info, warn, warning, error
 *
 * You can change "debug" and "observe" states at runtime, by calling static methods: "debug" and "reset".
 *
 * It is impossible to change "level" of messages accepted by logger.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public final class LogObserver {

    /**
     * We use ConcurrentLinkedQueue because we want to avoid blocking on log operations.
     * If we decided to use collection synchronized by locks, then all multithreading operations in unit test
     * would be degenerated to serial list of steps one after one, by every log operation.
     */
    private static final Queue<LogMessage> TARGET = new ConcurrentLinkedQueue<>();
    
    private static final boolean DEBUG = "true".equals(System.getProperty("assira.junit.debug"));
    
    private static final boolean ENABLED = "true".equals(System.getProperty("assira.junit.logger.observe"));
    
    private static volatile boolean debug = DEBUG;
    
    private static volatile boolean enabled = ENABLED;
        
    private LogObserver() {
        /* utility class */
    }

    /**
     * Adds new message to collected list, if observer is enabled. Prints message, if debug is enabled.
     * It does nothing in other cases.
     *
     * Method is not supposed to be called directly. It is used by internal {@link org.slf4j.impl.ObserveLogger}.
     *
     * @param level level
     * @param message message
     * @param error error
     */
    @SuppressWarnings("PMD.SystemPrintln")
    public static void append(int level, String message, Throwable error) {
        if(enabled) {
            TARGET.add(new LogMessage(level, message, error));
        }
        if(debug) {
            System.err.println(message);
            if(null != error) {
                error.printStackTrace(System.err);
            }
        }
    }

    /**
     * Turns on/off printing messages to console.
     *
     * @param value value
     */
    public static void debug(boolean value) {
        debug = value;
    }

    /**
     * Turns on/off collecting of messages emmited by slf4j.
     *
     * This method always clears already collected list of messages!
     *
     * @param enabled enabled
     */
    public static void reset(boolean enabled) {
        TARGET.clear();
        LogObserver.enabled = enabled;
    }

    /**
     * Returns snaphshot of collected messages.
     * It is copy of internal buffer.
     *
     * @return list
     */
    public static List<LogMessage> list() {
        return new ArrayList<>(TARGET);
    }

    /**
     * Returns stream with collected messages.
     *
     * @return stream
     */
    public static Stream<LogMessage> stream() {
        return TARGET.stream();
    }

    /**
     * Checks if there is a message matching specified criteria.
     * Equivalent to stream().anyMatch(filter)
     *
     * @param filter filter
     * @return bool
     */
    public static boolean match(Predicate<LogMessage> filter) {
        return stream().anyMatch(filter);
    }

    /**
     * Returns stream with messages with text matching specified criteria.
     *
     * @param filter filter
     * @return stream
     */
    public static Stream<LogMessage> forText(Predicate<CharSequence> filter) {
        return stream().filter(m -> filter.test(m.text()));
    }

    /**
     * Returns stream with messages with specified text.
     * It uses simple comparison, not regular expression match.
     *
     * @param text text
     * @return stream
     */
    public static Stream<LogMessage> forText(CharSequence text) {
        return forText(String.valueOf(text)::equals);
    }

    /**
     * Returns stream with messages with level matching specified criteria
     *
     * @param filter filter
     * @return stream
     */
    public static Stream<LogMessage> forLevel(IntPredicate filter) {
        return stream().filter(m -> filter.test(m.level()));
    }

    /**
     * Returns stream with messages with level greater or equal to provided
     *
     * @param level level
     * @return stream
     */
    public static Stream<LogMessage> forLevel(int level) {
        return forLevel(m -> m >= level);
    }
    
}
