
/*
 * Janino - An embedded Java[TM] compiler
 *
 * Copyright (c) 2016 Arno Unkrig. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
 * following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
 *       following disclaimer.
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
 *       following disclaimer in the documentation and/or other materials provided with the distribution.
 *    3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
 *       products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.codehaus.janino.tests;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.codehaus.commons.compiler.CompileException;
import org.codehaus.commons.nullanalysis.Nullable;
import org.codehaus.janino.ClassLoaderIClassLoader;
import org.codehaus.janino.IClassLoader;
import org.codehaus.janino.Java.AbstractCompilationUnit;
import org.codehaus.janino.Parser;
import org.codehaus.janino.Scanner;
import org.codehaus.janino.SimpleCompiler;
import org.codehaus.janino.UnitCompiler;
import org.codehaus.janino.util.ClassFile;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public // SUPPRESS CHECKSTYLE Javadoc:9999
class GithubPullRequestsTest {

    @Before public void
    setUp() throws Exception {

        // Optionally print class file disassemblies to the console.
        if (Boolean.parseBoolean(System.getProperty("disasm"))) {
            Logger scl = Logger.getLogger(UnitCompiler.class.getName());
            for (Handler h : scl.getHandlers()) {
                h.setLevel(Level.FINEST);
            }
            scl.setLevel(Level.FINEST);
        }
    }

    /**
     * <a href="https://github.com/janino-compiler/janino/pull/10">Replace if condition with
     * literal if possible to simplify if statement</a>
     */
    @Test public void
    testPullRequest10a() throws Exception {

        GithubPullRequestsTest.assertDisassemblyDoesNotContain(
            (
                ""
                + "public\n"
                + "class Foo { \n"
                + "\n"
                + "    public static String\n"
                + "    meth() {\n"
                + "\n"
                + "        boolean test = true;\n"
                + "        if (test) {\n"
                + "            return \"true\";\n"
                + "        } else {\n"
                + "            return \"FAIL\";\n"
                + "        }\n"
                + "    }\n"
                + "}\n"
            ),
            "FAIL"
        );
    }

    @Test public void
    testPullRequest10b() throws Exception {

        GithubPullRequestsTest.assertDisassemblyDoesNotContain(
            (
                ""
                + "public\n"
                + "class Foo {\n"
                + "\n"
                + "    public static String\n"
                + "    meth() {\n"
                + "\n"
                + "        boolean test = true;\n"
                + "        int     test2 = 1;\n"
                + "        if (test) {\n"
                + "            return \"true\";\n"
                + "        } else {\n"
                + "            return \"FAIL\";\n"
                + "        }\n"
                + "    }\n"
                + "}\n"
            ),
            "FAIL"
        );
    }

    @Test public void
    testPullRequest10c() throws Exception {

        GithubPullRequestsTest.assertDisassemblyDoesNotContain(
            (
                ""
                + "public\n"
                + "class Foo { \n"
                + "\n"
                + "    public static String\n"
                + "    meth() {\n"
                + "\n"
                + "        boolean test = true, test1 = false;\n"
                + "        int test3 = 1;\n"
                + "        if (test) {\n"
                + "            return \"true\";\n"
                + "        } else {\n"
                + "            return \"FAIL\";\n"
                + "        }\n"
                + "    }\n"
                + "}\n"
            ),
            "FAIL"
        );
    }

    @Test public void
    testPullRequest10d() throws Exception {

        GithubPullRequestsTest.assertDisassemblyDoesNotContain(
            (
                ""
                + "public\n"
                + "class Foo { \n"
                + "\n"
                + "    public static String\n"
                + "    meth() {\n"
                + "\n"
                + "        boolean test = true, test1 = false;\n"
                + "        boolean test3 = test;\n"
                + "        if (test) {\n"
                + "            return \"true\";\n"
                + "        } else {\n"
                + "            return \"FAIL\";\n"
                + "        }\n"
                + "    }\n"
                + "}\n"
            ),
            "FAIL"
        );
    }

    @Test public void
    testPullRequest10e() throws Exception {

        GithubPullRequestsTest.assertDisassemblyContainsAllOf(
            (
                ""
                + "public\n"
                + "class Foo { \n"
                + "\n"
                + "    public static String\n"
                + "    meth() {\n"
                + "\n"
                + "        boolean test = true, test1 = false;\n"
                + "        boolean test3 = (test = false);\n"
                + "        if (test) {\n"
                + "            return \"SUCCEED1\";\n"
                + "        } else {\n"
                + "            return \"SUCCEED2\";\n"
                + "        }\n"
                + "    }\n"
                + "}\n"
            ),
            "SUCCEED1", "SUCCEED2"
        );
    }

    /**
     * There <em>are</em> initializers with side effects between the offending variable declarator and the IF statement.
     */
    @Test public void
    testPullRequest10f() throws Exception {

        GithubPullRequestsTest.assertDisassemblyContainsAllOf(
            (
                ""
                + "public\n"
                + "class Foo { \n"
                + "\n"
                + "    public static String\n"
                + "    meth() {\n"
                + "\n"
                + "        boolean test = true, test1 = false;\n"
                + "        Object o = new Object();\n"
                + "        if (test) {\n"
                + "            return \"SUCCEED1\";\n"
                + "        } else {\n"
                + "            return \"SUCCEED2\";\n"
                + "        }\n"
                + "    }\n"
                + "}\n"
            ),
            "SUCCEED1", "SUCCEED2"
        );
    }

    /**
     * There <em>are</em> initializers with side effects between the offending variable declarator and the IF statement,
     * but the variable declarator is FINAL.
     */
    @Test public void
    testPullRequest10g() throws Exception {

        GithubPullRequestsTest.assertDisassemblyDoesNotContain(
            (
                ""
                + "public\n"
                + "class Foo { \n"
                + "\n"
                + "    public static String\n"
                + "    meth() {\n"
                + "\n"
                + "        final boolean test = true, test1 = false;\n"
                + "        Object o = new Object();\n"
                + "        if (test) {\n"
                + "            return \"true\";\n"
                + "        } else {\n"
                + "            return \"FAIL\";\n"
                + "        }\n"
                + "    }\n"
                + "}\n"
            ),
            "FAIL"
        );
    }

    private static void
    assertDisassemblyContainsAllOf(String cu, String... infixes) throws Exception {
        String s = GithubPullRequestsTest.compileAndDisassemble(cu);
        for (String infix : infixes) {
            Assert.assertTrue("Disassembly does not contain \"" + infix + "\": " + s, s.contains(infix));
        }
    }

    private static void
    assertDisassemblyDoesNotContain(String cu, String... infixes) throws Exception {
        String s = GithubPullRequestsTest.compileAndDisassemble(cu);
        for (String infix : infixes) {
            Assert.assertFalse("Disassembly contains \"" + infix + "\": " + s, s.contains(infix));
        }
    }

    private static String
    compileAndDisassemble(String cu)
    throws CompileException, IOException, Exception {
        AbstractCompilationUnit
        acu = new Parser(new Scanner(null, new StringReader(cu))).parseAbstractCompilationUnit();

        IClassLoader          icl        = new ClassLoaderIClassLoader(GithubPullRequestsTest.class.getClassLoader());
        UnitCompiler          uc         = new UnitCompiler(acu, icl);
        Collection<ClassFile> classFiles = new ArrayList<>();

        uc.compileUnit(
            false, // debugSource
            false, // debugLines
            false, // debugVars
            classFiles
        );

        Assert.assertEquals(1, classFiles.size());
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        classFiles.iterator().next().store(baos);

        String s = GithubPullRequestsTest.disassemble(new ByteArrayInputStream(baos.toByteArray()));
        return s;
    }

    private static String
    disassemble(InputStream is) throws Exception {
        Class<?> dc = GithubPullRequestsTest.class.getClassLoader().loadClass("de.unkrig.jdisasm.Disassembler");

        Object       d  = dc.getConstructor().newInstance();             // Disassembler d = new Disassemlber();
        StringWriter sw = new StringWriter();
        dc.getDeclaredMethod("setOut", Writer.class).invoke(d, sw);      // dd.setOut(sw);
        dc.getDeclaredMethod("disasm", InputStream.class).invoke(d, is); // d.disasm(is);
        return sw.toString();
    }

    @Test public void
    testPullRequest148() throws Exception {
        String cu = (
            ""
            + "public class MyClass {\n"
            + "  private boolean b1;\n"
            + "\n"
            + "  public MyClass(boolean b1) {\n"
            + "    this.b1 = b1;\n"
            + "  }\n"
            + "\n"
            + "  public Object apply(Object i) {\n"
            + "    final int value_0;\n"
            + "    // The bug still exist if the if condition is 'true'. Here we use a variable\n"
            + "    // to make the test more robust, in case the compiler can eliminate the else branch.\n"
            + "    if (b1) {\n"
            + "    } else {\n"
            + "      int field_0 = 1;\n"
            + "    }\n"
            + "    // The second if-else is necessary to trigger the bug.\n"
            + "    if (b1) {\n"
            + "    } else {\n"
            + "      // The bug disappear if it's an int variable.\n"
            + "      long field_1 = 2;\n"
            + "    }\n"
            + "    value_0 = 1;\n"
            + "\n"
            + "    // The second final variable is necessary to trigger the bug.\n"
            + "    final int value_2;\n"
            + "    if (b1) {\n"
            + "    } else {\n"
            + "      int field_2 = 3;\n"
            + "    }\n"
            + "    value_2 = 2;\n"
            + "\n"
            + "    return null;\n"
            + "  }\n"
            + "}\n"
        );

        SimpleCompiler sc = new SimpleCompiler();
        sc.cook(cu);
        Class<?> c = sc.getClassLoader().loadClass("MyClass");
        Object   o = c.getConstructor(boolean.class).newInstance(true);
        Assert.assertEquals(null, c.getDeclaredMethod("apply", Object.class).invoke(o, new Object()));
    }

    /**
     * Asserts that <var>actual</var> equals one of the <var>expected</var>.
     */
    public static void
    assertEqualsOneOf(String message, Object[] expected, Object actual) {

        for (Object e : expected) {
            if (GithubPullRequestsTest.equals(e, actual)) return;
        }

        GithubPullRequestsTest.failNotEquals(message, Arrays.toString(expected), actual);
    }

    private static boolean
    equals(@Nullable Object o1, @Nullable Object o2) { return o1 == null ? o2 == null : o1.equals(o2); }

    private static void
    failNotEquals(String message, Object expected, Object actual) {
        Assert.fail(message + " " + "expected:<" + expected + "> but was:<" + actual + ">");
    }
}
