/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.cloud.test;

import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableMap;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.springframework.util.Assert;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static org.apache.log4j.Level.ALL;
import static org.apache.log4j.Level.DEBUG;
import static org.apache.log4j.Level.ERROR;
import static org.apache.log4j.Level.FATAL;
import static org.apache.log4j.Level.INFO;
import static org.apache.log4j.Level.OFF;

/**
*
* Tracks one or more patterns to determine whether or not they have been
* logged. It uses a streaming approach to determine whether or not a message
* has a occurred to prevent unnecessary memory consumption. Instances of this
* of this class are created using the {@link TestAppenderBuilder}.
*
* To use this class, register a one or more expected patterns by level as part
* of the test setup and retain an reference to the appender instance. After the
* expected logging events have occurred in the test case, call
* {@link TestAppender#assertMessagesLogged()} which will fail the test if any of the
* expected patterns were not logged.
*
*/
public final class TestAppender extends AppenderSkeleton {
    private final static String APPENDER_NAME = "test_appender";
    private final ImmutableMap<Level, Set<PatternResult>> expectedPatternResults;
    private TestAppender(final Map<Level, Set<PatternResult>> expectedPatterns) {
        super();
        expectedPatternResults = ImmutableMap.copyOf(expectedPatterns);
    }
    protected void append(LoggingEvent loggingEvent) {
        checkArgument(loggingEvent != null, "append requires a non-null loggingEvent");
        final Level level = loggingEvent.getLevel();
        checkState(expectedPatternResults.containsKey(level), "level " + level + " not supported by append");
        for (final PatternResult patternResult : expectedPatternResults.get(level)) {
            if (patternResult.getPattern().matcher(loggingEvent.getRenderedMessage()).matches()) {
                patternResult.markFound();
            }
        }
    }

    public void close() {
// Do nothing ...
    }
    public boolean requiresLayout() {
        return false;
    }
    public void assertMessagesLogged() {
        final List<String> unloggedPatterns = new ArrayList<>();
        for (final Map.Entry<Level, Set<PatternResult>> expectedPatternResult : expectedPatternResults.entrySet()) {
            for (final PatternResult patternResults : expectedPatternResult.getValue()) {
                if (!patternResults.isFound()) {
                    unloggedPatterns.add(format("%1$s was not logged for level %2$s",
                            patternResults.getPattern().toString(), expectedPatternResult.getKey()));
                }
            }
        }
        if (!unloggedPatterns.isEmpty()) {
            //Raise an assert
            Assert.isTrue(false, Joiner.on(",").join(unloggedPatterns));
        }
    }

    private static final class PatternResult {
        private final Pattern pattern;
        private boolean foundFlag = false;
        private PatternResult(Pattern pattern) {
            super();
            this.pattern = pattern;
        }
        public Pattern getPattern() {
            return pattern;
        }
        public void markFound() {
        // This operation is thread-safe because the value will only ever be switched from false to true. Therefore,
        // multiple threads mutating the value for a pattern will not corrupt the value ...
            foundFlag = true;
        }
        public boolean isFound() {
            return foundFlag;
        }
        @Override
        public boolean equals(Object thatObject) {
            if (this == thatObject) {
                return true;
            }
            if (thatObject == null || getClass() != thatObject.getClass()) {
                return false;
            }
            PatternResult thatPatternResult = (PatternResult) thatObject;
            return foundFlag == thatPatternResult.foundFlag &&
                    Objects.equal(pattern, thatPatternResult.pattern);
        }
        @Override
        public int hashCode() {
            return Objects.hashCode(pattern, foundFlag);
        }
        @Override
        public String toString() {
            return format("Pattern Result [ pattern: %1$s, markFound: %2$s ]", pattern.toString(), foundFlag);
        }
    }

    public static final class TestAppenderBuilder {
        private final Map<Level, Set<PatternResult>> expectedPatterns;
        public TestAppenderBuilder() {
            super();
            expectedPatterns = new HashMap<>();
            expectedPatterns.put(ALL, new HashSet<PatternResult>());
            expectedPatterns.put(DEBUG, new HashSet<PatternResult>());
            expectedPatterns.put(ERROR, new HashSet<PatternResult>());
            expectedPatterns.put(FATAL, new HashSet<PatternResult>());
            expectedPatterns.put(INFO, new HashSet<PatternResult>());
            expectedPatterns.put(OFF, new HashSet<PatternResult>());
        }
        public TestAppenderBuilder addExpectedPattern(final Level level, final String pattern) {
            checkArgument(level != null, "addExpectedPattern requires a non-null level");
            checkArgument(!isNullOrEmpty(pattern), "addExpectedPattern requires a non-blank pattern");
            checkState(expectedPatterns.containsKey(level), "level " + level + " is not supported by " + getClass().getName());
            expectedPatterns.get(level).add(new PatternResult(Pattern.compile(pattern)));
            return this;
        }
        public TestAppender build() {
            return new TestAppender(expectedPatterns);
        }
    }
    /**
     *
     * Attaches a {@link TestAppender} to a {@link Logger} and ensures that it is the only
     * test appender attached to the logger.
     *
     * @param logger The logger which will be monitored by the test
     * @param testAppender The test appender to attach to {@code logger}
     */
    public static void safeAddAppender(Logger logger, TestAppender testAppender) {
        logger.removeAppender(APPENDER_NAME);
        logger.addAppender(testAppender);
    }
}