/*
 * 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.logging.log4j.core.impl;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketPermission;
import java.nio.channels.ServerSocketChannel;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

import javax.xml.bind.DatatypeConverter;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.jackson.Log4jJsonObjectMapper;
import org.apache.logging.log4j.core.jackson.Log4jXmlObjectMapper;
import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 *
 */
public class ThrowableProxyTest {

    public static class AlwaysThrowsError {
        static {
            if (true) {
                throw new Error("I always throw an Error when initialized");
            }
        }
    }

    static class Fixture {
        @JsonProperty
        ThrowableProxy proxy = new ThrowableProxy(new IOException("test"));
    }

    private ThrowableProxy deserialize(final byte[] binary) throws IOException, ClassNotFoundException {
        final ByteArrayInputStream inArr = new ByteArrayInputStream(binary);
        final ObjectInputStream in = new ObjectInputStream(inArr);
        return (ThrowableProxy) in.readObject();
    }

    private byte[] serialize(final ThrowableProxy proxy) throws IOException {
        final ByteArrayOutputStream arr = new ByteArrayOutputStream();
        final ObjectOutputStream out = new ObjectOutputStream(arr);
        out.writeObject(proxy);
        return arr.toByteArray();
    }

    private void testIoContainer(final ObjectMapper objectMapper ) throws IOException {
        final Fixture expected = new Fixture();
        final String s = objectMapper.writeValueAsString(expected);
        final Fixture actual = objectMapper.readValue(s, Fixture.class);
        assertEquals(expected.proxy.getName(), actual.proxy.getName());
        assertEquals(expected.proxy.getMessage(), actual.proxy.getMessage());
        assertEquals(expected.proxy.getLocalizedMessage(), actual.proxy.getLocalizedMessage());
        assertEquals(expected.proxy.getCommonElementCount(), actual.proxy.getCommonElementCount());
        assertArrayEquals(expected.proxy.getExtendedStackTrace(), actual.proxy.getExtendedStackTrace());
        assertEquals(expected.proxy, actual.proxy);
    }

    @Test
    public void testIoContainerAsJson() throws IOException {
        testIoContainer(new Log4jJsonObjectMapper());
    }

    @Test
    public void testIoContainerAsXml() throws IOException {
        testIoContainer(new Log4jXmlObjectMapper());
    }

    /**
     * Attempts to instantiate a class that cannot initialize and then logs the stack trace of the Error. The logger
     * must not fail when using {@link ThrowableProxy} to inspect the frames of the stack trace.
     */
    @Test
    public void testLogStackTraceWithClassThatCannotInitialize() {
        try {
            // Try to create the object, which will always fail during class initialization
            final AlwaysThrowsError error = new AlwaysThrowsError();

            // If the error was not triggered then fail
            fail("Test did not throw expected error: " + error);
        } catch (final Throwable e) {
            // Print the stack trace to System.out for informational purposes
            // System.err.println("### Here's the stack trace that we'll log with log4j ###");
            // e.printStackTrace();
            // System.err.println("### End stack trace ###");

            final Logger logger = LogManager.getLogger(getClass());

            // This is the critical portion of the test. The log message must be printed without
            // throwing a java.lang.Error when introspecting the AlwaysThrowError class in the
            // stack trace.
            logger.error(e.getMessage(), e);
            logger.error(e);
        }
    }

    @Test
    public void testLogStackTraceWithClassThatWillCauseSecurityException() throws IOException {
        class SimplePolicy extends Policy {

            private final Permissions permissions;

            public SimplePolicy(final Permissions permissions) {
                this.permissions = permissions;
            }

            @Override
            public PermissionCollection getPermissions(final CodeSource codesource) {
                return permissions;
            }

        }

        final SecurityManager sm = System.getSecurityManager();
        try {
            final Permissions permissions = new Permissions();

            // you know, for binding
            permissions.add(new SocketPermission("localhost:9300", "listen,resolve"));

            /**
             * the JUnit test runner uses reflection to invoke the test; while leaving this
             * permission out would display the same issue, it's clearer to grant this
             * permission and show the real issue that would arise
             */
            // TODO: other JDKs might need a different permission here
            permissions.add(new RuntimePermission("accessClassInPackage.sun.reflect"));

            // for restoring the security manager after test execution
            permissions.add(new RuntimePermission("setSecurityManager"));

            Policy.setPolicy(new SimplePolicy(permissions));
            System.setSecurityManager(new SecurityManager());
            ServerSocketChannel.open().socket().bind(new InetSocketAddress("localhost", 9300));
            ServerSocketChannel.open().socket().bind(new InetSocketAddress("localhost", 9300));
            fail("expected a java.net.BindException");
        } catch (final BindException e) {
            new ThrowableProxy(e);
        } finally {
            // restore the security manager
            System.setSecurityManager(sm);
        }
    }

    // DO NOT REMOVE THIS COMMENT:
    // UNCOMMENT WHEN GENERATING SERIALIZED THROWABLEPROXY FOR #testSerializationWithUnknownThrowable
    // public static class DeletedException extends Exception {
    // private static final long serialVersionUID = 1L;
    //
    // public DeletedException(String msg) {
    // super(msg);
    // }
    // };

    @Test
    public void testSerialization() throws Exception {
        final Throwable throwable = new IllegalArgumentException("This is a test");
        final ThrowableProxy proxy = new ThrowableProxy(throwable);
        final byte[] binary = serialize(proxy);
        final ThrowableProxy proxy2 = deserialize(binary);

        assertEquals(proxy.getName(), proxy2.getName());
        assertEquals(proxy.getMessage(), proxy2.getMessage());
        assertEquals(proxy.getCauseProxy(), proxy2.getCauseProxy());
        assertArrayEquals(proxy.getExtendedStackTrace(), proxy2.getExtendedStackTrace());
    }

    @Test
    public void testSerialization_getExtendedStackTraceAsString() throws Exception {
        final Throwable throwable = new IllegalArgumentException("This is a test");
        final ThrowableProxy proxy = new ThrowableProxy(throwable);
        final byte[] binary = serialize(proxy);
        final ThrowableProxy proxy2 = deserialize(binary);

        assertEquals(proxy.getExtendedStackTraceAsString(), proxy2.getExtendedStackTraceAsString());
    }

    @Test
	public void testSerialization_getExtendedStackTraceAsStringWithNestedThrowableDepth1() throws Exception {
		final Throwable throwable = new RuntimeException(new IllegalArgumentException("This is a test"));
		testSerialization_getExtendedStackTraceAsStringWithNestedThrowable(throwable);
	}

    @Test
	public void testSerialization_getExtendedStackTraceAsStringWithNestedThrowableDepth2() throws Exception {
		final Throwable throwable = new RuntimeException(
				new IllegalArgumentException("This is a test", new IOException("level 2")));
		testSerialization_getExtendedStackTraceAsStringWithNestedThrowable(throwable);
	}

    @Test
	public void testSerialization_getExtendedStackTraceAsStringWithNestedThrowableDepth3() throws Exception {
		final Throwable throwable = new RuntimeException(new IllegalArgumentException("level 1",
				new IOException("level 2", new IllegalStateException("level 3"))));
		testSerialization_getExtendedStackTraceAsStringWithNestedThrowable(throwable);
	}

    private void testSerialization_getExtendedStackTraceAsStringWithNestedThrowable(final Throwable throwable) throws Exception {
        final ThrowableProxy proxy = new ThrowableProxy(throwable);
        final byte[] binary = serialize(proxy);
        final ThrowableProxy proxy2 = deserialize(binary);

        assertEquals(proxy.getExtendedStackTraceAsString(), proxy2.getExtendedStackTraceAsString());
    }

    @Test
    public void testSerializationWithUnknownThrowable() throws Exception {

        final String msg = "OMG I've been deleted!";

        // DO NOT DELETE THIS COMMENT:
        // UNCOMMENT TO RE-GENERATE SERIALIZED EVENT WHEN UPDATING THIS TEST.
        // final Exception thrown = new DeletedException(msg);
        // final ThrowableProxy proxy = new ThrowableProxy(thrown);
        // final byte[] binary = serialize(proxy);
        // String base64 = DatatypeConverter.printBase64Binary(binary);
        // System.out.println("final String base64 = \"" + base64.replaceAll("\r\n", "\\\\r\\\\n\" +\r\n\"") + "\";");

        final String base64 = " ";

        final byte[] binaryDecoded = DatatypeConverter.parseBase64Binary(base64);
        final ThrowableProxy proxy2 = deserialize(binaryDecoded);

        assertEquals(this.getClass().getName() + "$DeletedException", proxy2.getName());
        assertEquals(msg, proxy2.getMessage());
    }

    @Test
    public void testStack() {
        final Map<String, ThrowableProxy.CacheEntry> map = new HashMap<>();
        final Stack<Class<?>> stack = new Stack<>();
        final Throwable throwable = new IllegalStateException("This is a test");
        final ThrowableProxy proxy = new ThrowableProxy(throwable);
        final ExtendedStackTraceElement[] callerPackageData = proxy.toExtendedStackTrace(stack, map, null,
                throwable.getStackTrace());
        assertNotNull("No package data returned", callerPackageData);
    }

    /**
     * Asserts that LOG4J2-834 is solved by constructing a ThrowableProxy over a RuntimeException object thrown at a
     * unloaded known class (already compiled and available as a test resource:
     * org.apache.logging.log4j.core.impl.ForceNoDefClassFoundError.class).
     */
    @Test
    public void testStackWithUnloadableClass() throws Exception {
        final Stack<Class<?>> stack = new Stack<>();
        final Map<String, ThrowableProxy.CacheEntry> map = new HashMap<>();

        final String runtimeExceptionThrownAtUnloadableClass_base64 = "rO0ABXNyABpqYXZhLmxhbmcuUnVudGltZUV4Y2VwdGlvbp5fBkcKNIPlAgAAeHIAE2phdmEubGFuZy5FeGNlcHRpb27Q/R8+GjscxAIAAHhyABNqYXZhLmxhbmcuVGhyb3dhYmxl1cY1Jzl3uMsDAANMAAVjYXVzZXQAFUxqYXZhL2xhbmcvVGhyb3dhYmxlO0wADWRldGFpbE1lc3NhZ2V0ABJMamF2YS9sYW5nL1N0cmluZztbAApzdGFja1RyYWNldAAeW0xqYXZhL2xhbmcvU3RhY2tUcmFjZUVsZW1lbnQ7eHBxAH4ABnB1cgAeW0xqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnQ7AkYqPDz9IjkCAAB4cAAAAAFzcgAbamF2YS5sYW5nLlN0YWNrVHJhY2VFbGVtZW50YQnFmiY23YUCAARJAApsaW5lTnVtYmVyTAAOZGVjbGFyaW5nQ2xhc3NxAH4ABEwACGZpbGVOYW1lcQB+AARMAAptZXRob2ROYW1lcQB+AAR4cAAAAAZ0ADxvcmcuYXBhY2hlLmxvZ2dpbmcubG9nNGouY29yZS5pbXBsLkZvcmNlTm9EZWZDbGFzc0ZvdW5kRXJyb3J0AB5Gb3JjZU5vRGVmQ2xhc3NGb3VuZEVycm9yLmphdmF0AARtYWlueA==";
        final byte[] binaryDecoded = DatatypeConverter
                .parseBase64Binary(runtimeExceptionThrownAtUnloadableClass_base64);
        final ByteArrayInputStream inArr = new ByteArrayInputStream(binaryDecoded);
        final ObjectInputStream in = new ObjectInputStream(inArr);
        final Throwable throwable = (Throwable) in.readObject();
        final ThrowableProxy subject = new ThrowableProxy(throwable);

        subject.toExtendedStackTrace(stack, map, null, throwable.getStackTrace());
    }

    /**
     * Tests LOG4J2-934.
     */
    @Test
    public void testCircularSuppressedExceptions() {
        final Exception e1 = new Exception();
        final Exception e2 = new Exception();
        e2.addSuppressed(e1);
        e1.addSuppressed(e2);
        LogManager.getLogger().error("Error", e1);
    }

    @Test
	public void testSuppressedExceptions() {
		final Exception e = new Exception("Root exception");
		e.addSuppressed(new IOException("Suppressed #1"));
		e.addSuppressed(new IOException("Suppressed #2"));
		LogManager.getLogger().error("Error", e);
		final ThrowableProxy proxy = new ThrowableProxy(e);
		final String extendedStackTraceAsString = proxy.getExtendedStackTraceAsString();
		assertTrue(extendedStackTraceAsString.contains("\tSuppressed: java.io.IOException: Suppressed #1"));
		assertTrue(extendedStackTraceAsString.contains("\tSuppressed: java.io.IOException: Suppressed #1"));
	}

    @Test
	public void testCauseSuppressedExceptions() {
		final Exception cause = new Exception("Nested exception");
		cause.addSuppressed(new IOException("Suppressed #1"));
		cause.addSuppressed(new IOException("Suppressed #2"));
		LogManager.getLogger().error("Error", new Exception(cause));
		final ThrowableProxy proxy = new ThrowableProxy(new Exception("Root exception", cause));
		final String extendedStackTraceAsString = proxy.getExtendedStackTraceAsString();
		assertTrue(extendedStackTraceAsString.contains("\tSuppressed: java.io.IOException: Suppressed #1"));
		assertTrue(extendedStackTraceAsString.contains("\tSuppressed: java.io.IOException: Suppressed #1"));
	}

    /**
     * Tests LOG4J2-934.
     */
    @Test
    public void testCircularSuppressedNestedException() {
        final Exception e1 = new Exception();
        final Exception e2 = new Exception(e1);
        e2.addSuppressed(e1);
        e1.addSuppressed(e2);
        LogManager.getLogger().error("Error", e1);
    }

    /**
     * .
     */
    @Test
    public void testCircularCauseExceptions() {
        final Exception e1 = new Exception();
        final Exception e2 = new Exception(e1);
        e1.initCause(e2);
        LogManager.getLogger().error("Error", e1);
    }
}
