/*
 * Logback GELF - zero dependencies Logback GELF appender library.
 * Copyright (C) 2016 Oliver Siegmar
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package de.siegmar.logbackgelf;

import static java.time.Duration.ofMillis;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.LineReader;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.LoggingEvent;

public class GelfEncoderTest {

    private static final String LOGGER_NAME = GelfEncoderTest.class.getCanonicalName();

    private final GelfEncoder encoder = new GelfEncoder();

    @BeforeEach
    public void before() {
        encoder.setContext(new LoggerContext());
        encoder.setOriginHost("localhost");
    }

    @Test
    public void simple() throws IOException {
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final String logMsg = encodeToStr(simpleLoggingEvent(logger, null));

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        basicValidation(jsonNode);

        final LineReader msg =
            new LineReader(new StringReader(jsonNode.get("full_message").textValue()));
        assertEquals("message 1", msg.readLine());
    }

    @Test
    public void newline() {
        encoder.setAppendNewline(true);
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final String logMsg = encodeToStr(simpleLoggingEvent(logger, null));

        assertTrue(logMsg.endsWith(System.lineSeparator()));
    }

    @Test
    public void nestedExceptionShouldNotFail() {
        encoder.setIncludeRootCauseData(true);
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        assertTimeout(ofMillis(400), () -> {
            final String logMsg = encodeToStr(simpleLoggingEvent(logger,
                new IOException(new IOException(new IOException()))));

            assertNotNull(logMsg);
        });
    }

    public static LoggingEvent simpleLoggingEvent(final Logger logger, final Throwable e) {
        return new LoggingEvent(
            LOGGER_NAME,
            logger,
            Level.DEBUG,
            "message {}",
            e,
            new Object[]{1});
    }

    private static void coreValidation(final JsonNode jsonNode) {
        assertEquals("1.1", jsonNode.get("version").textValue());
        assertEquals("localhost", jsonNode.get("host").textValue());
        assertEquals("message 1", jsonNode.get("short_message").textValue());
        assertEquals(7, jsonNode.get("level").intValue());
    }

    public static void basicValidation(final JsonNode jsonNode) {
        coreValidation(jsonNode);
        assertNotNull(jsonNode.get("_thread_name").textValue());
        assertEquals(LOGGER_NAME, jsonNode.get("_logger_name").textValue());
    }

    @Test
    public void exception() throws IOException {
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final String logMsg;
        try {
            throw new IllegalArgumentException("Example Exception");
        } catch (final IllegalArgumentException e) {
            logMsg = encodeToStr(new LoggingEvent(
                LOGGER_NAME,
                logger,
                Level.DEBUG,
                "message {}",
                e,
                new Object[]{1}));
        }

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        basicValidation(jsonNode);

        final LineReader msg =
            new LineReader(new StringReader(jsonNode.get("full_message").textValue()));

        assertEquals("message 1", msg.readLine());
        assertEquals("java.lang.IllegalArgumentException: Example Exception", msg.readLine());
        final String line = msg.readLine();
        assertTrue(line.matches("^\tat de.siegmar.logbackgelf.GelfEncoderTest.exception"
            + "\\(GelfEncoderTest.java:\\d+\\)$"), "Unexpected line: " + line);
    }

    @Test
    public void complex() throws IOException {
        encoder.setIncludeRawMessage(true);
        encoder.setIncludeLevelName(true);
        encoder.addStaticField("foo:bar");
        encoder.setIncludeCallerData(true);
        encoder.setIncludeRootCauseData(true);
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final LoggingEvent event = simpleLoggingEvent(logger, null);

        event.setMDCPropertyMap(ImmutableMap.of("mdc_key", "mdc_value"));

        final String logMsg = encodeToStr(event);

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        basicValidation(jsonNode);
        assertEquals("DEBUG", jsonNode.get("_level_name").textValue());
        assertEquals("bar", jsonNode.get("_foo").textValue());
        assertEquals("mdc_value", jsonNode.get("_mdc_key").textValue());
        assertEquals("message {}", jsonNode.get("_raw_message").textValue());
        assertNull(jsonNode.get("_exception"));
    }

    @Test
    public void customLevelNameKey() throws IOException {
        encoder.setIncludeLevelName(true);
        encoder.setLevelNameKey("Severity");
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final LoggingEvent event = simpleLoggingEvent(logger, null);

        final String logMsg = encodeToStr(event);

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        basicValidation(jsonNode);
        assertEquals("DEBUG", jsonNode.get("_Severity").textValue());
        assertNull(jsonNode.get("_exception"));
    }

    @Test
    public void customLoggerNameKey() throws IOException {
        encoder.setLoggerNameKey("Logger");
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final LoggingEvent event = simpleLoggingEvent(logger, null);

        final String logMsg = encodeToStr(event);

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        coreValidation(jsonNode);
        assertNotNull(jsonNode.get("_thread_name").textValue());
        assertEquals(LOGGER_NAME, jsonNode.get("_Logger").textValue());
        assertNull(jsonNode.get("_exception"));
    }

    @Test
    public void customThreadNameKey() throws IOException {
        encoder.setThreadNameKey("Thread");
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final LoggingEvent event = simpleLoggingEvent(logger, null);

        final String logMsg = encodeToStr(event);

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        coreValidation(jsonNode);
        assertNotNull(jsonNode.get("_Thread").textValue());
        assertEquals(LOGGER_NAME, jsonNode.get("_logger_name").textValue());
        assertNull(jsonNode.get("_exception"));
    }

    @Test
    public void rootExceptionTurnedOff() throws IOException {
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final String logMsg;
        try {
            throw new IOException("Example Exception");
        } catch (final IOException e) {
            logMsg = encodeToStr(simpleLoggingEvent(logger, e));
        }

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);

        assertFalse(jsonNode.has("_exception"));
    }

    @Test
    public void noRootException() throws IOException {
        encoder.setIncludeRootCauseData(true);
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final String logMsg = encodeToStr(simpleLoggingEvent(logger, null));

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);

        assertFalse(jsonNode.has("_exception"));
    }

    @Test
    public void rootExceptionWithoutCause() throws IOException {
        encoder.setIncludeRootCauseData(true);
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final String logMsg;
        try {
            throw new IOException("Example Exception");
        } catch (final IOException e) {
            logMsg = encodeToStr(simpleLoggingEvent(logger, e));
        }

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);

        assertEquals("java.io.IOException", jsonNode.get("_root_cause_class_name").textValue());
        assertEquals("Example Exception", jsonNode.get("_root_cause_message").textValue());
    }

    @Test
    public void rootExceptionWithCause() throws IOException {
        encoder.setIncludeRootCauseData(true);
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final String logMsg;
        try {
            throw new IOException("Example Exception",
                new IllegalStateException("Example Exception 2"));
        } catch (final IOException e) {
            logMsg = encodeToStr(simpleLoggingEvent(logger, e));
        }

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        basicValidation(jsonNode);

        assertEquals("java.lang.IllegalStateException",
            jsonNode.get("_root_cause_class_name").textValue());

        assertEquals("Example Exception 2",
            jsonNode.get("_root_cause_message").textValue());
    }

    @Test
    public void numericValueAsNumber() throws IOException {
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final LoggingEvent event = simpleLoggingEvent(logger, null);

        event.setMDCPropertyMap(ImmutableMap.of("int", "200", "float", "0.00001"));

        final String logMsg = encodeToStr(event);

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        basicValidation(jsonNode);

        assertTrue(logMsg.contains("\"_int\":200"));
        assertTrue(logMsg.contains("\"_float\":0.00001"));
    }

    @Test
    public void numericValueAsString() throws IOException {
        encoder.setNumbersAsString(true);
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final LoggingEvent event = simpleLoggingEvent(logger, null);

        event.setMDCPropertyMap(ImmutableMap.of("int", "200", "float", "0.00001"));

        final String logMsg = encodeToStr(event);

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        basicValidation(jsonNode);

        assertTrue(logMsg.contains("\"_int\":\"200\""));
        assertTrue(logMsg.contains("\"_float\":\"0.00001\""));
    }

    @Test
    public void singleMarker() throws IOException {
        encoder.setLoggerNameKey("Logger");
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final LoggingEvent event = simpleLoggingEvent(logger, null);
        event.setMarker(MarkerFactory.getMarker("SINGLE"));

        final String logMsg = encodeToStr(event);

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        coreValidation(jsonNode);
        assertEquals("SINGLE", jsonNode.get("_marker").textValue());
    }

    @Test
    public void multipleMarker() throws IOException {
        encoder.setLoggerNameKey("Logger");
        encoder.start();

        final LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        final Logger logger = lc.getLogger(LOGGER_NAME);

        final LoggingEvent event = simpleLoggingEvent(logger, null);
        final Marker marker = MarkerFactory.getMarker("FIRST");
        marker.add(MarkerFactory.getMarker("SECOND"));
        event.setMarker(marker);

        final String logMsg = encodeToStr(event);

        final ObjectMapper om = new ObjectMapper();
        final JsonNode jsonNode = om.readTree(logMsg);
        coreValidation(jsonNode);
        assertEquals("FIRST, SECOND", jsonNode.get("_marker").textValue());
    }

    private String encodeToStr(final LoggingEvent event) {
        return new String(encoder.encode(event), StandardCharsets.UTF_8);
    }

}