/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed 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 libcore.java.lang;

import java.io.ObjectStreamClass;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Arrays;
import libcore.junit.junit3.TestCaseWithRules;
import libcore.junit.util.SwitchTargetSdkVersionRule;
import libcore.junit.util.SwitchTargetSdkVersionRule.TargetSdkVersion;
import libcore.libcore.util.SerializationTester;
import org.junit.Rule;
import org.junit.rules.TestRule;

public class ThrowableTest extends TestCaseWithRules {

    @Rule
    public TestRule switchTargetSdkVersionRule = SwitchTargetSdkVersionRule.getInstance();

    private static class NoStackTraceException extends Exception {
        @Override
        public synchronized Throwable fillInStackTrace() {
            return null;
        }
    }
    public void testNullStackTrace() {
        try {
            throw new NoStackTraceException();
        } catch (NoStackTraceException ex) {
            // We used to throw NullPointerException when printing an exception with no stack trace.
            ex.printStackTrace(new PrintWriter(new StringWriter()));
        }
    }

    public void testNonWritableStackTrace() {
        try {
            // The 4th argument, writableStackTrace, is false...
            throw new SuppressionsThrowable("hi", null, true, false);
        } catch (Throwable th) {
            assertEquals("hi", th.getMessage());

            // We see an empty stack trace.
            assertEquals(0, th.getStackTrace().length);

            // setStackTrace is a no-op.
            th.setStackTrace(new StackTraceElement[] { new StackTraceElement("c", "m", "f", -2) });
            assertEquals(0, th.getStackTrace().length);

            // fillInStackTrace is a no-op.
            th.fillInStackTrace();
            assertEquals(0, th.getStackTrace().length);

            // It's still possible to print an exception with writableStackTrace == false.
            th.printStackTrace(new PrintWriter(new StringWriter()));
        }
    }

    private static class SuppressionsThrowable extends Throwable {
        private static final long serialVersionUID = 202649043897209143L;

        public SuppressionsThrowable(String detailMessage, Throwable throwable,
                boolean enableSuppression, boolean writableStackTrace) {
            super(detailMessage, throwable, enableSuppression, writableStackTrace);
        }
    }

    public void testAddSuppressed() {
        Throwable throwable = new Throwable();
        assertSuppressed(throwable);
        Throwable suppressedA = new Throwable();
        throwable.addSuppressed(suppressedA);
        assertSuppressed(throwable, suppressedA);
        Throwable suppressedB = new Throwable();
        throwable.addSuppressed(suppressedB);
        assertSuppressed(throwable, suppressedA, suppressedB);
    }

    public void testAddDuplicateSuppressed() {
        Throwable throwable = new Throwable();
        Throwable suppressedA = new Throwable();
        throwable.addSuppressed(suppressedA);
        throwable.addSuppressed(suppressedA);
        throwable.addSuppressed(suppressedA);
        assertSuppressed(throwable, suppressedA, suppressedA, suppressedA);
    }

    public void testGetSuppressedReturnsCopy() {
        Throwable throwable = new Throwable();
        Throwable suppressedA = new Throwable();
        Throwable suppressedB = new Throwable();
        throwable.addSuppressed(suppressedA);
        throwable.addSuppressed(suppressedB);
        Throwable[] mutable = throwable.getSuppressed();
        mutable[0] = null;
        mutable[1] = null;
        assertSuppressed(throwable, suppressedA, suppressedB);
    }

    public void testAddSuppressedWithSuppressionDisabled() {
        Throwable throwable = new SuppressionsThrowable("foo", null, false, true);
        assertSuppressed(throwable);
        throwable.addSuppressed(new Throwable());
        assertSuppressed(throwable);
        throwable.addSuppressed(new Throwable());
        assertSuppressed(throwable);
    }

    public void testAddSuppressedSelf() {
        Throwable throwable = new Throwable();
        try {
            throwable.addSuppressed(throwable);
            fail();
        } catch (IllegalArgumentException expected) {
        }
    }

    public void testAddSuppressedNull() {
        Throwable throwable = new Throwable();
        try {
            throwable.addSuppressed(null);
            fail();
        } catch (NullPointerException expected) {
        }
    }

    public void testPrintStackTraceWithCause() {
        Throwable throwable = newThrowable("Throwable", "A", "B");
        throwable.initCause(newThrowable("Cause", "A", "B", "C", "D"));

        assertEquals("java.lang.Throwable: Throwable\n"
                + "\tat ClassB.doB(ClassB.java:1)\n"
                + "\tat ClassA.doA(ClassA.java:0)\n"
                + "Caused by: java.lang.Throwable: Cause\n"
                + "\tat ClassD.doD(ClassD.java:3)\n"
                + "\tat ClassC.doC(ClassC.java:2)\n"
                + "\t... 2 more\n", printStackTraceToString(throwable));
    }

    public void testPrintStackTraceWithCauseAndSuppressed() {
        Throwable throwable = newThrowable("Throwable", "A", "B");
        throwable.initCause(newThrowable("Cause", "A", "B", "C", "D"));
        throwable.addSuppressed(newThrowable("Suppressed", "A", "B", "E", "F"));
        throwable.addSuppressed(newThrowable("Suppressed", "A", "B", "G", "H"));

        assertEquals("java.lang.Throwable: Throwable\n"
                + "\tat ClassB.doB(ClassB.java:1)\n"
                + "\tat ClassA.doA(ClassA.java:0)\n"
                + "\tSuppressed: java.lang.Throwable: Suppressed\n"
                + "\t\tat ClassF.doF(ClassF.java:3)\n"
                + "\t\tat ClassE.doE(ClassE.java:2)\n"
                + "\t\t... 2 more\n"
                + "\tSuppressed: java.lang.Throwable: Suppressed\n"
                + "\t\tat ClassH.doH(ClassH.java:3)\n"
                + "\t\tat ClassG.doG(ClassG.java:2)\n"
                + "\t\t... 2 more\n"
                + "Caused by: java.lang.Throwable: Cause\n"
                + "\tat ClassD.doD(ClassD.java:3)\n"
                + "\tat ClassC.doC(ClassC.java:2)\n"
                + "\t... 2 more\n", printStackTraceToString(throwable));
    }

    public void testPrintStackTraceWithEverything() {
        Throwable throwable = newThrowable("Throwable", "A", "B");
        Throwable cause = newThrowable("Cause", "A", "B", "C", "D");
        Throwable suppressed = newThrowable("Suppressed", "A", "B", "E", "F");

        throwable.addSuppressed(suppressed);
        suppressed.addSuppressed(newThrowable("Suppressed/Suppressed", "A", "B", "E", "G"));
        suppressed.initCause(newThrowable("Suppressed/Cause", "A", "B", "E", "H"));

        throwable.initCause(cause);
        cause.addSuppressed(newThrowable("Cause/Suppressed", "A", "B", "C", "I"));
        cause.initCause(newThrowable("Cause/Cause", "A", "B", "C", "J"));

        assertEquals("java.lang.Throwable: Throwable\n"
                + "\tat ClassB.doB(ClassB.java:1)\n"
                + "\tat ClassA.doA(ClassA.java:0)\n"
                + "\tSuppressed: java.lang.Throwable: Suppressed\n"
                + "\t\tat ClassF.doF(ClassF.java:3)\n"
                + "\t\tat ClassE.doE(ClassE.java:2)\n"
                + "\t\t... 2 more\n"
                + "\t\tSuppressed: java.lang.Throwable: Suppressed/Suppressed\n"
                + "\t\t\tat ClassG.doG(ClassG.java:3)\n"
                + "\t\t\t... 3 more\n"
                + "\tCaused by: java.lang.Throwable: Suppressed/Cause\n"
                + "\t\tat ClassH.doH(ClassH.java:3)\n"
                + "\t\t... 3 more\n"
                + "Caused by: java.lang.Throwable: Cause\n"
                + "\tat ClassD.doD(ClassD.java:3)\n"
                + "\tat ClassC.doC(ClassC.java:2)\n"
                + "\t... 2 more\n"
                + "\tSuppressed: java.lang.Throwable: Cause/Suppressed\n"
                + "\t\tat ClassI.doI(ClassI.java:3)\n"
                + "\t\t... 3 more\n"
                + "Caused by: java.lang.Throwable: Cause/Cause\n"
                + "\tat ClassJ.doJ(ClassJ.java:3)\n"
                + "\t... 3 more\n", printStackTraceToString(throwable));
    }

    public void testSetStackTraceWithNullElement() {
        Throwable throwable = new Throwable();
        try {
            throwable.setStackTrace(new StackTraceElement[]{ null });
            fail();
        } catch (NullPointerException expected) {
        }
    }

    public void testCauseSerialization() {
        String s = "aced0005737200136a6176612e6c616e672e5468726f7761626c65d5c635273977b8cb0300034c0"
                + "00563617573657400154c6a6176612f6c616e672f5468726f7761626c653b4c000d64657461696c4"
                + "d6573736167657400124c6a6176612f6c616e672f537472696e673b5b000a737461636b547261636"
                + "574001e5b4c6a6176612f6c616e672f537461636b5472616365456c656d656e743b78707371007e0"
                + "00071007e000574000543617573657572001e5b4c6a6176612e6c616e672e537461636b547261636"
                + "5456c656d656e743b02462a3c3cfd22390200007870000000047372001b6a6176612e6c616e672e5"
                + "37461636b5472616365456c656d656e746109c59a2636dd8502000449000a6c696e654e756d62657"
                + "24c000e6465636c6172696e67436c61737371007e00024c000866696c654e616d6571007e00024c0"
                + "00a6d6574686f644e616d6571007e0002787000000003740006436c6173734474000b436c6173734"
                + "42e6a617661740003646f447371007e000900000002740006436c6173734374000b436c617373432"
                + "e6a617661740003646f437371007e000900000001740006436c6173734274000b436c617373422e6"
                + "a617661740003646f427371007e000900000000740006436c6173734174000b436c617373412e6a6"
                + "17661740003646f41787400095468726f7761626c657571007e0007000000027371007e000900000"
                + "001740006436c6173734274000b436c617373422e6a617661740003646f427371007e00090000000"
                + "0740006436c6173734174000b436c617373412e6a617661740003646f4178";
        Throwable throwable = newThrowable("Throwable", "A", "B");
        throwable.initCause(newThrowable("Cause", "A", "B", "C", "D"));
        assertSerialized(throwable, s);
    }

    public void testSuppressedSerialization() {
        String s = "aced0005737200136a6176612e6c616e672e5468726f7761626c65d5c635273977b8cb0300044c0"
                + "00563617573657400154c6a6176612f6c616e672f5468726f7761626c653b4c000d64657461696c4"
                + "d6573736167657400124c6a6176612f6c616e672f537472696e673b5b000a737461636b547261636"
                + "574001e5b4c6a6176612f6c616e672f537461636b5472616365456c656d656e743b4c00147375707"
                + "0726573736564457863657074696f6e737400104c6a6176612f7574696c2f4c6973743b787071007"
                + "e000574000a53657269616c697a65647572001e5b4c6a6176612e6c616e672e537461636b5472616"
                + "365456c656d656e743b02462a3c3cfd22390200007870000000027372001b6a6176612e6c616e672"
                + "e537461636b5472616365456c656d656e746109c59a2636dd8502000449000a6c696e654e756d626"
                + "5724c000e6465636c6172696e67436c61737371007e00024c000866696c654e616d6571007e00024"
                + "c000a6d6574686f644e616d6571007e0002787000000001740006436c6173734274000b436c61737"
                + "3422e6a617661740003646f427371007e000900000000740006436c6173734174000b436c6173734"
                + "12e6a617661740003646f41737200136a6176612e7574696c2e41727261794c6973747881d21d99c"
                + "7619d03000149000473697a657870000000017704000000017371007e000071007e001474000a537"
                + "570707265737365647571007e0007000000047371007e000900000003740006436c6173734474000"
                + "b436c617373442e6a617661740003646f447371007e000900000002740006436c6173734374000b4"
                + "36c617373432e6a617661740003646f437371007e000900000001740006436c6173734274000b436"
                + "c617373422e6a617661740003646f427371007e000900000000740006436c6173734174000b436c6"
                + "17373412e6a617661740003646f41737200266a6176612e7574696c2e436f6c6c656374696f6e732"
                + "4556e6d6f6469666961626c654c697374fc0f2531b5ec8e100200014c00046c69737471007e00047"
                + "872002c6a6176612e7574696c2e436f6c6c656374696f6e7324556e6d6f6469666961626c65436f6"
                + "c6c656374696f6e19420080cb5ef71e0200014c0001637400164c6a6176612f7574696c2f436f6c6"
                + "c656374696f6e3b78707371007e0012000000007704000000007871007e002b787878";
        Throwable throwable = newThrowable("Serialized", "A", "B");
        throwable.addSuppressed(newThrowable("Suppressed", "A", "B", "C", "D"));
        assertSerialized(throwable, s);
    }

    public void testDisableSuppressionSerialization() {
        String s = "aced0005737200356c6962636f72652e6a6176612e6c616e672e5468726f7761626c65546573742"
                + "45375707072657373696f6e735468726f7761626c6502cff43b5390d137020000787200136a61766"
                + "12e6c616e672e5468726f7761626c65d5c635273977b8cb0300044c000563617573657400154c6a6"
                + "176612f6c616e672f5468726f7761626c653b4c000d64657461696c4d6573736167657400124c6a6"
                + "176612f6c616e672f537472696e673b5b000a737461636b547261636574001e5b4c6a6176612f6c6"
                + "16e672f537461636b5472616365456c656d656e743b4c00147375707072657373656445786365707"
                + "4696f6e737400104c6a6176612f7574696c2f4c6973743b787070740003666f6f7572001e5b4c6a6"
                + "176612e6c616e672e537461636b5472616365456c656d656e743b02462a3c3cfd223902000078700"
                + "00000007078";
        Throwable throwable = new SuppressionsThrowable("foo", null, false, true);
        throwable.setStackTrace(new StackTraceElement[0]);
        new SerializationTester<Throwable>(throwable, s) {
            @Override protected boolean equals(Throwable a, Throwable b) {
                return printStackTraceToString(a).equals(printStackTraceToString(b));
            }
            @Override protected void verify(Throwable deserialized) {
                // the suppressed exception is silently discarded
                deserialized.addSuppressed(newThrowable("Suppressed"));
                assertSuppressed(deserialized);
            }
        }.test();
    }

    public void testEnableSuppressionSerialization() {
        String s = "aced0005737200356c6962636f72652e6a6176612e6c616e672e5468726f7761626c65546573742"
                + "45375707072657373696f6e735468726f7761626c6502cff43b5390d137020000787200136a61766"
                + "12e6c616e672e5468726f7761626c65d5c635273977b8cb0300044c000563617573657400154c6a6"
                + "176612f6c616e672f5468726f7761626c653b4c000d64657461696c4d6573736167657400124c6a6"
                + "176612f6c616e672f537472696e673b5b000a737461636b547261636574001e5b4c6a6176612f6c6"
                + "16e672f537461636b5472616365456c656d656e743b4c00147375707072657373656445786365707"
                + "4696f6e737400104c6a6176612f7574696c2f4c6973743b787070740003666f6f7572001e5b4c6a6"
                + "176612e6c616e672e537461636b5472616365456c656d656e743b02462a3c3cfd223902000078700"
                + "0000000737200266a6176612e7574696c2e436f6c6c656374696f6e7324556e6d6f6469666961626"
                + "c654c697374fc0f2531b5ec8e100200014c00046c69737471007e00057872002c6a6176612e75746"
                + "96c2e436f6c6c656374696f6e7324556e6d6f6469666961626c65436f6c6c656374696f6e1942008"
                + "0cb5ef71e0200014c0001637400164c6a6176612f7574696c2f436f6c6c656374696f6e3b7870737"
                + "200136a6176612e7574696c2e41727261794c6973747881d21d99c7619d03000149000473697a657"
                + "870000000007704000000007871007e000f78";
        Throwable throwable = new SuppressionsThrowable("foo", null, true, true);
        throwable.setStackTrace(new StackTraceElement[0]);
        new SerializationTester<Throwable>(throwable, s) {
            @Override protected boolean equals(Throwable a, Throwable b) {
                return printStackTraceToString(a).equals(printStackTraceToString(b));
            }
            @Override protected void verify(Throwable deserialized) {
                // the suppressed exception is permitted
                Throwable suppressed = newThrowable("Suppressed");
                deserialized.addSuppressed(suppressed);
                assertSuppressed(deserialized, suppressed);
            }
        }.test();
    }

    /**
     * Detect issue that caused b/109930347
     *
     * <p>Due to a bug in the default serialVersionUID calculation that is used for apps with
     * targetSdkVersion <= 23 changes in {@link Throwable} can affect the default SUID for
     * subclasses of {@link Throwable}.
     *
     * <p>This test protects against changes in Throwable (e.g. removing Android specific patches)
     * that would cause a change in the default SUID of Throwable subclasses in apps with
     * targetSdkVersion <= 23. It does so by checking the default SUID for a Throwable subclass,
     * computed as by an app with targetSdkVersion <= 23, against a known good value.
     */
    @TargetSdkVersion(23)
    public void testThrowableSubclassSerialVersionUIDComputation_target23() {
        ObjectStreamClass objectStreamClass = ObjectStreamClass.lookup(ThrowableSubclass.class);
        assertEquals(
            "SerialVersionUID computation for Throwable subclass is broken for targetSdkVersion 23",
            -1036450421582688704L, objectStreamClass.getSerialVersionUID());
    }

    public static class ThrowableSubclass extends Throwable {
        public ThrowableSubclass(String message) {
            super(message);
        }
    }

    private void assertSerialized(final Throwable throwable, String golden) {
        new SerializationTester<Throwable>(throwable, golden) {
            @Override protected boolean equals(Throwable a, Throwable b) {
                return printStackTraceToString(a).equals(printStackTraceToString(b));
            }
        }.test();
    }

    private Throwable newThrowable(String message, String... stackTraceElements) {
        StackTraceElement[] array = new StackTraceElement[stackTraceElements.length];
        for (int i = 0; i < stackTraceElements.length; i++) {
            String s = stackTraceElements[i];
            array[stackTraceElements.length - 1 - i]
                    = new StackTraceElement("Class" + s, "do" + s, "Class" + s + ".java", i);
        }
        Throwable result = new Throwable(message);
        result.setStackTrace(array);
        return result;
    }

    private String printStackTraceToString(Throwable throwable) {
        StringWriter writer = new StringWriter();
        throwable.printStackTrace(new PrintWriter(writer));
        return writer.toString();
    }

    private void assertSuppressed(Throwable throwable, Throwable... expectedSuppressed) {
        assertEquals(Arrays.asList(throwable.getSuppressed()), Arrays.asList(expectedSuppressed));
    }
}
