/*
 * 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 org.apache.pulsar.structuredeventlog.slf4j;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.BufferedReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.WriterAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.layout.JsonLayout;
import org.apache.pulsar.structuredeventlog.Event;
import org.apache.pulsar.structuredeventlog.EventGroup;
import org.apache.pulsar.structuredeventlog.EventResources;
import org.apache.pulsar.structuredeventlog.StructuredEventLog;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

public class StructuredEventLogTest {
    private static final String APPENDER_NAME = "stevlogtest";
    StringWriter writer;

    @BeforeMethod
    public void setupLog4j() throws Exception {
        LoggerContext context = LoggerContext.getContext(false);
        Configuration config = context.getConfiguration();

        writer = new StringWriter();


        LoggerConfig logConfig = config.getLoggerConfig(APPENDER_NAME);
        for (Map.Entry<String, Appender> a : logConfig.getAppenders().entrySet()) {
            if (a.getKey().equals(APPENDER_NAME)) {
                a.getValue().stop();
            }
        }
        logConfig.removeAppender(APPENDER_NAME);

        JsonLayout layout = JsonLayout.newBuilder().setEventEol(true).setCompact(true).setProperties(true).build();
        Appender appender = WriterAppender.createAppender(layout, null, writer, "stevlogtest", false, true);
        appender.start();
        logConfig.addAppender(appender, null, null);
        logConfig.setLevel(Level.DEBUG);
    }

    @Test
    public void testTraceId() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        Event e = log.newRootEvent();
        e.newChildEvent().log("child");
        e.log("parent");

        log.newRootEvent().log("second");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("child"));
        assertThat(logged.get(1).get("message"), equalTo("parent"));
        assertThat(logged.get(2).get("message"), equalTo("second"));

        assertThat(contextMapField(logged.get(0), "traceId"),
                   equalTo(contextMapField(logged.get(1), "traceId")));
        assertThat(contextMapField(logged.get(0), "traceId"),
                   not(equalTo(contextMapField(logged.get(2), "traceId"))));
    }

    @Test
    public void testParentId() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        Event e1 = log.newRootEvent();
        Event e2 = e1.newChildEvent();
        e2.newChildEvent().log("child2");
        e2.log("child1");
        e1.log("parent");

        log.newRootEvent().log("second");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("child2"));
        assertThat(logged.get(1).get("message"), equalTo("child1"));
        assertThat(logged.get(2).get("message"), equalTo("parent"));
        assertThat(logged.get(3).get("message"), equalTo("second"));

        assertThat(contextMapField(logged.get(0), "parentId"), not(nullValue()));
        assertThat(contextMapField(logged.get(0), "parentId"),
                   equalTo(contextMapField(logged.get(1), "id")));
        assertThat(contextMapField(logged.get(1), "parentId"),
                   equalTo(contextMapField(logged.get(2), "id")));
        assertThat(contextMapField(logged.get(2), "parentId"), nullValue());
        assertThat(contextMapField(logged.get(3), "parentId"), nullValue());
    }

    @Test
    public void testResources() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        EventResources res = log.newEventResources()
            .resource("r1", "v1")
            .resource("r2", () -> "v2");
        Event e1 = log.newRootEvent()
            .resources(res)
            .resource("r3", "v3")
            .resource("r4", () -> "v4");
        Event e2 = e1.newChildEvent().resource("r5", "v5");
        e2.newChildEvent().resource("r6", "v6").log("child2");
        e2.log("child1");
        e1.log("parent");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("child2"));
        assertThat(logged.get(1).get("message"), equalTo("child1"));
        assertThat(logged.get(2).get("message"), equalTo("parent"));

        assertThat(contextMapField(logged.get(0), "r6"), equalTo("v6"));
        assertThat(contextMapField(logged.get(0), "r5"), equalTo("v5"));
        assertThat(contextMapField(logged.get(0), "r4"), equalTo("v4"));
        assertThat(contextMapField(logged.get(0), "r3"), equalTo("v3"));
        assertThat(contextMapField(logged.get(0), "r2"), equalTo("v2"));
        assertThat(contextMapField(logged.get(0), "r1"), equalTo("v1"));

        assertThat(contextMapField(logged.get(1), "r6"), nullValue());
        assertThat(contextMapField(logged.get(1), "r5"), equalTo("v5"));
        assertThat(contextMapField(logged.get(1), "r4"), equalTo("v4"));
        assertThat(contextMapField(logged.get(1), "r3"), equalTo("v3"));
        assertThat(contextMapField(logged.get(1), "r2"), equalTo("v2"));
        assertThat(contextMapField(logged.get(1), "r1"), equalTo("v1"));

        assertThat(contextMapField(logged.get(2), "r6"), nullValue());
        assertThat(contextMapField(logged.get(2), "r5"), nullValue());
        assertThat(contextMapField(logged.get(2), "r4"), equalTo("v4"));
        assertThat(contextMapField(logged.get(2), "r3"), equalTo("v3"));
        assertThat(contextMapField(logged.get(2), "r2"), equalTo("v2"));
        assertThat(contextMapField(logged.get(2), "r1"), equalTo("v1"));
    }

    @Test
    public void testResourcesNullTest() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        EventResources res = log.newEventResources()
            .resource(null, "v1")
            .resource("r1", null)
            .resource("r2", () -> null);
        Event e1 = log.newRootEvent()
            .resources(res)
            .resource(null, "v2")
            .resource("r3", null)
            .resource("r4", () -> null);
        e1.newChildEvent()
            .resource(null, "v3")
            .resource("r5", null)
            .log("child1");
        e1.log("parent");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("child1"));
        assertThat(logged.get(1).get("message"), equalTo("parent"));

        assertThat(contextMapField(logged.get(0), "r5"), equalTo("null"));
        assertThat(contextMapField(logged.get(0), "r4"), equalTo("null"));
        assertThat(contextMapField(logged.get(0), "r3"), equalTo("null"));
        assertThat(contextMapField(logged.get(0), "r2"), equalTo("null"));
        assertThat(contextMapField(logged.get(0), "r1"), equalTo("null"));
        assertThat(contextMapField(logged.get(0), "null"), equalTo("v3"));

        assertThat(contextMapField(logged.get(1), "r5"), nullValue());
        assertThat(contextMapField(logged.get(1), "r4"), equalTo("null"));
        assertThat(contextMapField(logged.get(1), "r3"), equalTo("null"));
        assertThat(contextMapField(logged.get(1), "r2"), equalTo("null"));
        assertThat(contextMapField(logged.get(1), "r1"), equalTo("null"));
        assertThat(contextMapField(logged.get(1), "null"), equalTo("v2"));
    }

    @Test
    public void testAttributes() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        Event e1 = log.newRootEvent()
            .attr("a1", "v1")
            .attr("a2", () -> "v2");
        Event e2 = e1.newChildEvent().attr("a3", "v3");
        e2.newChildEvent().resource("a4", "v4").log("child2");
        e2.log("child1");
        e1.log("parent");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("child2"));
        assertThat(logged.get(1).get("message"), equalTo("child1"));
        assertThat(logged.get(2).get("message"), equalTo("parent"));

        assertThat(contextMapField(logged.get(0), "a4"), equalTo("v4"));
        assertThat(contextMapField(logged.get(0), "a3"), nullValue());
        assertThat(contextMapField(logged.get(0), "a2"), nullValue());
        assertThat(contextMapField(logged.get(0), "a1"), nullValue());

        assertThat(contextMapField(logged.get(1), "a4"), nullValue());
        assertThat(contextMapField(logged.get(1), "a3"), equalTo("v3"));
        assertThat(contextMapField(logged.get(1), "a2"), nullValue());
        assertThat(contextMapField(logged.get(1), "a1"), nullValue());

        assertThat(contextMapField(logged.get(2), "a4"), nullValue());
        assertThat(contextMapField(logged.get(2), "a3"), nullValue());
        assertThat(contextMapField(logged.get(2), "a2"), equalTo("v2"));
        assertThat(contextMapField(logged.get(2), "a1"), equalTo("v1"));
    }

    @Test
    public void testAttributedNullTest() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        log.newRootEvent()
            .attr(null, "v1")
            .attr("a1", null)
            .attr("a2", () -> null)
            .log("msg");

        log.newRootEvent()
            .attr(null, "v1")
            .attr("a1", null)
            .attr("a2", () -> null)
            .log("msg");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("msg"));

        assertThat(contextMapField(logged.get(0), "null"), equalTo("v1"));
        assertThat(contextMapField(logged.get(0), "a1"), equalTo("null"));
        assertThat(contextMapField(logged.get(0), "a2"), equalTo("null"));
    }

    @Test
    public void testInfoLevel() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        log.newRootEvent().log("info1");
        log.newRootEvent().atInfo().log("info2");
        log.newRootEvent().atError().atInfo().log("info3");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("info1"));
        assertThat(logged.get(1).get("message"), equalTo("info2"));
        assertThat(logged.get(2).get("message"), equalTo("info3"));

        assertThat(logged.get(0).get("level"), equalTo("INFO"));
        assertThat(logged.get(1).get("level"), equalTo("INFO"));
        assertThat(logged.get(2).get("level"), equalTo("INFO"));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testInfoLevelException() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        log.newRootEvent().exception(new Throwable("cause1")).log("info1");
        log.newRootEvent().atInfo().exception(new Throwable("cause2")).log("info2");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("info1"));

        assertThat(((Map<String, Object>) logged.get(0).get("thrown")).get("message"), equalTo("cause1"));
        assertThat(logged.get(1).get("message"), equalTo("info2"));
        assertThat(((Map<String, Object>) logged.get(1).get("thrown")).get("message"), equalTo("cause2"));
    }

    @Test
    public void testWarnLevel() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        log.newRootEvent().atWarn().log("warn1");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("warn1"));
        assertThat(logged.get(0).get("level"), equalTo("WARN"));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testWarnLevelException() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        log.newRootEvent().atWarn().exception(new Throwable("cause1")).log("warn1");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("warn1"));
        assertThat(((Map<String, Object>) logged.get(0).get("thrown")).get("message"), equalTo("cause1"));
    }

    @Test
    public void testErrorLevel() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        log.newRootEvent().atError().log("error1");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("error1"));
        assertThat(logged.get(0).get("level"), equalTo("ERROR"));
    }

    @SuppressWarnings("unchecked")
    @Test
    public void testErrorLevelException() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;

        log.newRootEvent().atError().exception(new Throwable("cause1")).log("error1");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("error1"));
        assertThat(((Map<String, Object>) logged.get(0).get("thrown")).get("message"), equalTo("cause1"));
    }


    @Test
    public void testTimedEvent() throws Exception {
        MockClock clock = new MockClock();
        Slf4jStructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;
        log.clock = clock;
        Event e = log.newRootEvent().timed();
        clock.advanceTime(1234, TimeUnit.MILLISECONDS);
        e.log("timed");

        List<Map<String, Object>> logged = getLogged();
        assertThat(logged.get(0).get("message"), equalTo("timed"));
        assertThat(contextMapField(logged.get(0), "startTimestamp"), equalTo("1970-01-02T03:46:40Z"));
        assertThat(contextMapField(logged.get(0), "durationMs"), equalTo("1234"));
    }

    @EventGroup(component = "foobar")
    public enum Events {
        TEST_EVENT
    }

    @Test
    public void testEventGroups() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;
        log.newRootEvent().log(Events.TEST_EVENT);

        List<Map<String, Object>> logged = getLogged();
        System.out.println(logged);
        assertThat(logged.get(0).get("message"), equalTo("TEST_EVENT"));
        assertThat(logged.get(0).get("loggerName"), equalTo("stevlog.foobar.TEST_EVENT"));
        assertThat(contextMapField(logged.get(0), "component"), equalTo("foobar"));
    }

    public enum BareEvents {
        BARE_EVENT
    }

    @Test
    public void testBareEnum() throws Exception {
        StructuredEventLog log = Slf4jStructuredEventLog.INSTANCE;
        log.newRootEvent().log(BareEvents.BARE_EVENT);

        List<Map<String, Object>> logged = getLogged();
        System.out.println(logged);
        assertThat(logged.get(0).get("message"), equalTo("BARE_EVENT"));
        assertThat(logged.get(0).get("loggerName"), equalTo("stevlog.BARE_EVENT"));
        assertThat(contextMapField(logged.get(0), "component"), nullValue());
    }

    @SuppressWarnings("unchecked")
    private Object contextMapField(Map<String, Object> map, String field) {
        return ((Map<String, Object>) map.get("contextMap")).get(field);
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> getLogged() throws Exception {
        List<Map<String, Object>> logged = new ArrayList<>();
        ObjectMapper o = new ObjectMapper();
        try (BufferedReader r = new BufferedReader(new StringReader(writer.toString()))) {
            String line = r.readLine();
            while (line != null) {
                Map<String, Object> log = o.readValue(line, Map.class);
                if (log.get("loggerName").toString().startsWith("stevlog")) {
                    logged.add(log);
                }
                line = r.readLine();
            }
        }
        return logged;
    }

    private static class MockClock extends Clock {
        AtomicLong ticker = new AtomicLong(100000000);

        void advanceTime(int duration, TimeUnit unit) {
            ticker.addAndGet(unit.toMillis(duration));
        }

        @Override
        public Instant instant() {
            return Instant.ofEpochMilli(ticker.get());
        }

        @Override
        public ZoneId getZone() {
            return ZoneId.of("UTC");
        }

        @Override
        public Clock withZone(ZoneId zoneId) {
            return this;
        }
    }
}

