package tbd;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

/**
 * Utility to enable verification of log messages in tests.
 *
 * Usage is as follows:
 *
 *     //@Test
 *     void shouldWriteLogMessage() {
 *         new TestLogger().with(testLogger -> {
 *             new ClassUnderTest.someCodeThatIncludesLogStatements();
 *
 *             testLogger.contains(Level.INFO, "Log message says: {}", "param to log message");
 *         });
 *     }
 */
public class TestLogger {

    private final Logger logger;
    private final ListAppender<ILoggingEvent> listAppender;

    public TestLogger() {
        this.logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        this.listAppender = new ListAppender<>();
        this.listAppender.start();
        this.logger.addAppender(this.listAppender);
    }

    /**
     * This method wraps the given function with a Logback ListAppender, and provides a
     * reference to that list appender to the calling function. Log messages can then be
     * validated using the provided list appender reference.
     *
     * Example:
     *
     * new TestLogger().with(testLogger -> {
     *     new ClassUnderTest().someCodeThatIncludesLogStatements();
     *
     *     assertTrue(testLogger.contains(Level.DEBUG, "expected log message"));
     * });
     *
     * NOTE:
     *
     * The following example will not work. The test logging needs to be in place
     * before the loggers in the class under test are initialized.
     *
     * ClassUnderTest cut = new ClassUnderTest();
     * new TestLogger().with(testLogger -> {
     *     cut.someCodeThatIncludesLogStatements();
     *
     *     assertTrue(testLogger.contains(Level.DEBUG, "expected log message"));
     * });
     */
    public void with(Consumer<TestLogger> function) {
        try {
            function.accept(this);
        } finally {
            logger.detachAppender(listAppender);
        }
    }


    /**
     * Determines whether a message was logged at the given level, with the given string,
     * and the given parameters.
     */
    public boolean contains(Level level, String message, Object... params) {
        return listAppender
            .list
            .stream()
            .anyMatch(event -> {
                // uncomment for debugging
                //System.out.println(
                //    "message = " + event.getMessage()
                //    + ", args = " + Arrays.deepToString(event.getArgumentArray())
                //    + ", params = " + Arrays.deepToString(params));

                return level.equals(event.getLevel())
                    && message.equals(event.getMessage())
                    // arg array defaults to null and params defaults to empty array
                    && ((event.getArgumentArray() == null && params.length == 0)
                        || Arrays.equals(params, event.getArgumentArray()));
            });
    }

    // TODO: determine if it's worth validating the class of exception logged also.
    // Note that it's not possible to have another method param after the Object... varargs above,
    // so parameter order to a separate containsWithException() [or whatever] method would have
    // to be different than the existing contains() method.


    /**
     * Returns the list of captured ILoggingEvents, for debugging or deeper verification
     * than provided by the contains() method above.
     */
    public List<ILoggingEvent> getLogEvents() {
        return listAppender.list;
    }

}