/*
 * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @bug 8042931
 * @summary Checking EnclosingMethod attribute of anonymous/local class.
 * @library /tools/lib /tools/javac/lib ../lib
 * @modules jdk.compiler/com.sun.tools.javac.api
 *          jdk.compiler/com.sun.tools.javac.main
 *          jdk.jdeps/com.sun.tools.classfile
 * @build toolbox.ToolBox InMemoryFileManager TestResult TestBase
 * @run main EnclosingMethodTest
 */

import com.sun.tools.classfile.Attribute;
import com.sun.tools.classfile.ClassFile;
import com.sun.tools.classfile.EnclosingMethod_attribute;

import java.io.File;
import java.io.FilenameFilter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

/**
 * The test checks the enclosing method attribute of anonymous/local classes.
 * The top-level class contains the anonymous and local classes to be tested. The test examines
 * each inner class and determine whether the class should have the EnclosingMethod attribute or not.
 * Golden information about enclosing methods are held in annotation {@code ExpectedEnclosingMethod}.
 *
 * The test assumes that a class must have the EnclosingMethod attribute if the class is annotated or
 * if its parent class is annotated in case of anonymous class. In addition, classes
 * named {@code VariableInitializer} are introduced to test variable initializer cases. These classes
 * must not have the enclosing method attribute, but its anonymous derived class must.
 * After classification of classes, the test checks whether classes contain the correct enclosing
 * method attribute in case of anonymous/local class, or checks whether classes do not contain
 * the EnclosingMethod attribute, otherwise.
 *
 * Test cases:
 *   top-level class as enclosing class:
 *     1. anonymous and local classes in static initializer;
 *     2. anonymous and local classes in instance initializer;
 *     3. anonymous and local classes in lambda;
 *     4. anonymous and local classes in constructor;
 *     5. anonymous and local classes in method;
 *     6. static and instance variable initializer.
 *
 *   inner class as enclosing class:
 *     1. anonymous and local classes in static initializer;
 *     2. anonymous and local classes in instance initializer;
 *     3. anonymous and local classes in lambda;
 *     4. anonymous and local classes in constructor;
 *     5. anonymous and local classes in method;
 *     6. static and instance variable initializer.
 *
 *   enum as enclosing class:
 *     1. anonymous and local classes in static initializer;
 *     2. anonymous and local classes in instance initializer;
 *     3. anonymous and local classes in lambda;
 *     4. anonymous and local classes in constructor;
 *     5. anonymous and local classes in method;
 *     6. static and instance variable initializer.
 *
 *   interface as enclosing class:
 *     1. anonymous and local classes in lambda;
 *     2. anonymous and local classes in static method;
 *     3. anonymous and local classes in default method;
 *     4. static variable initializer.
 *
 *   annotation as enclosing class:
 *     1. anonymous and local classes in lambda;
 *     2. static variable initializer.
 */
public class EnclosingMethodTest extends TestResult {

    private final Map<Class<?>, ExpectedEnclosingMethod> class2EnclosingMethod = new HashMap<>();
    private final Set<Class<?>> noEnclosingMethod = new HashSet<>();

    public EnclosingMethodTest() throws ClassNotFoundException {
        Class<EnclosingMethodTest> outerClass = EnclosingMethodTest.class;
        String outerClassName = outerClass.getSimpleName();
        File testClasses = getClassDir();
        FilenameFilter filter = (dir, name) -> name.matches(outerClassName + ".*\\.class");

        for (File file : testClasses.listFiles(filter)) {
            Class<?> clazz = Class.forName(file.getName().replace(".class", ""));
            if (clazz.isAnonymousClass()) {
                // anonymous class cannot be annotated, information is in its parent class.
                ExpectedEnclosingMethod declaredAnnotation =
                        clazz.getSuperclass().getDeclaredAnnotation(ExpectedEnclosingMethod.class);
                class2EnclosingMethod.put(clazz, declaredAnnotation);
            } else {
                ExpectedEnclosingMethod enclosingMethod = clazz.getDeclaredAnnotation(ExpectedEnclosingMethod.class);
                // if class is annotated and it does not contain information for variable initializer cases,
                // then it must have the enclosing method attribute.
                if (enclosingMethod != null && !clazz.getSimpleName().contains("VariableInitializer")) {
                    class2EnclosingMethod.put(clazz, enclosingMethod);
                } else {
                    noEnclosingMethod.add(clazz);
                }
            }
        }
    }

    public void test() throws TestFailedException {
        try {
            testEnclosingMethodAttribute();
            testLackOfEnclosingMethodAttribute();
        } finally {
            checkStatus();
        }
    }

    private void testLackOfEnclosingMethodAttribute() {
        for (Class<?> clazz : noEnclosingMethod) {
            try {
                addTestCase("Class should not have EnclosingMethod attribute : " + clazz);
                ClassFile classFile = readClassFile(clazz);
                checkEquals(countEnclosingMethodAttributes(classFile),
                        0l, "number of the EnclosingMethod attribute in the class is zero : "
                                + classFile.getName());
            } catch (Exception e) {
                addFailure(e);
            }
        }
    }

    private void testEnclosingMethodAttribute() {
        class2EnclosingMethod.forEach((clazz, enclosingMethod) -> {
            try {
                String info = enclosingMethod.info() + " "
                        + (clazz.isAnonymousClass() ? "anonymous" : "local");
                addTestCase(info);
                printf("Testing test case : %s\n", info);
                ClassFile classFile = readClassFile(clazz);
                String className = clazz.getName();
                checkEquals(countEnclosingMethodAttributes(classFile), 1l,
                        "number of the EnclosingMethod attribute in the class is one : "
                                + clazz);
                EnclosingMethod_attribute attr = (EnclosingMethod_attribute)
                        classFile.getAttribute(Attribute.EnclosingMethod);

                if (!checkNotNull(attr, "the EnclosingMethod attribute is not null : " + className)) {
                    // stop checking, attr is null. test case failed
                    return;
                }
                checkEquals(classFile.constant_pool.getUTF8Value(attr.attribute_name_index),
                        "EnclosingMethod",
                        "attribute_name_index of EnclosingMethod attribute in the class : " + className);
                checkEquals(attr.attribute_length, 4,
                        "attribute_length of EnclosingMethod attribute in the class : " + className);
                String expectedClassName = enclosingMethod.enclosingClazz().getName();
                checkEquals(classFile.constant_pool.getClassInfo(attr.class_index).getName(),
                        expectedClassName, String.format(
                        "enclosing class of EnclosingMethod attribute in the class %s is %s",
                                className, expectedClassName));

                String expectedMethodName = enclosingMethod.enclosingMethod();
                if (expectedMethodName.isEmpty()) {
                    // class does not have an enclosing method
                    checkEquals(attr.method_index, 0, String.format(
                            "enclosing method of EnclosingMethod attribute in the class %s is null", className));
                } else {
                    String methodName = classFile.constant_pool.getNameAndTypeInfo(attr.method_index).getName();
                    checkTrue(methodName.startsWith(expectedMethodName), String.format(
                            "enclosing method of EnclosingMethod attribute in the class %s" +
                                    " is method name %s" +
                                    ", actual method name is %s",
                            className, expectedMethodName, methodName));
                }
            } catch (Exception e) {
                addFailure(e);
            }
        });
    }

    private long countEnclosingMethodAttributes(ClassFile classFile) {
        return Stream.of(classFile.attributes.attrs)
                .filter(x -> x instanceof EnclosingMethod_attribute)
                .count();
    }

    @Retention(RetentionPolicy.RUNTIME)
    public @interface ExpectedEnclosingMethod {
        String info();
        Class<?> enclosingClazz();
        String enclosingMethod() default "";
    }

    public static void main(String[] args) throws ClassNotFoundException, TestFailedException {
        new EnclosingMethodTest().test();
    }

    // Test cases: enclosing class is a top-level class
    static {
        // anonymous and local classes in static initializer
        @ExpectedEnclosingMethod(
                info = "EnclosingStaticInitialization in EnclosingMethodTest",
                enclosingClazz = EnclosingMethodTest.class
        )
        class EnclosingStaticInitialization {
        }
        new EnclosingStaticInitialization() {
        };
    }

    {
        // anonymous and local classes in instance initializer
        @ExpectedEnclosingMethod(
                info = "EnclosingInitialization in EnclosingMethodTest",
                enclosingClazz = EnclosingMethodTest.class
        )
        class EnclosingInitialization {
        }
        new EnclosingInitialization() {
        };
    }

    Runnable lambda = () -> {
        // anonymous and local classes in lambda
        @ExpectedEnclosingMethod(
                info = "EnclosingLambda in EnclosingMethodTest",
                enclosingMethod = "lambda",
                enclosingClazz = EnclosingMethodTest.class
        )
        class EnclosingLambda {
        }
        new EnclosingLambda() {
        };
    };

    EnclosingMethodTest(int i) {
        // anonymous and local classes in constructor
        @ExpectedEnclosingMethod(
                info = "EnclosingConstructor in EnclosingMethodTest",
                enclosingMethod = "<init>",
                enclosingClazz = EnclosingMethodTest.class
        )
        class EnclosingConstructor {
        }
        new EnclosingConstructor() {
        };
    }

    void method() {
        // anonymous and local classes in method
        @ExpectedEnclosingMethod(
                info = "EnclosingMethod in EnclosingMethodTest",
                enclosingMethod = "method",
                enclosingClazz = EnclosingMethodTest.class
        )
        class EnclosingMethod {
        }
        new EnclosingMethod() {
        };
    }

    @ExpectedEnclosingMethod(
            info = "VariableInitializer in EnclosingMethodTest",
            enclosingClazz = EnclosingMethodTest.class
    )
    static class VariableInitializer {
    }

    // static variable initializer
    private static final VariableInitializer cvi = new VariableInitializer() {
    };

    // instance variable initializer
    private final VariableInitializer ivi = new VariableInitializer() {
    };

    // Test cases: enclosing class is an inner class
    public static class notEnclosing01 {
        static {
            // anonymous and local classes in static initializer
            @ExpectedEnclosingMethod(
                    info = "EnclosingStaticInitialization in notEnclosing01",
                    enclosingClazz = notEnclosing01.class
            )
            class EnclosingStaticInitialization {
            }
            new EnclosingStaticInitialization() {
            };
        }

        {
            // anonymous and local classes in instance initializer
            @ExpectedEnclosingMethod(
                    info = "EnclosingInitialization in notEnclosing01",
                    enclosingClazz = notEnclosing01.class
            )
            class EnclosingInitialization {
            }
            new EnclosingInitialization() {
            };
        }

        Runnable lambda = () -> {
            // anonymous and local classes in lambda
            @ExpectedEnclosingMethod(
                    info = "EnclosingLambda in notEnclosing01",
                    enclosingMethod = "lambda",
                    enclosingClazz = notEnclosing01.class
            )
            class EnclosingLambda {
            }
            new EnclosingLambda() {
            };
        };

        notEnclosing01() {
            // anonymous and local classes in constructor
            @ExpectedEnclosingMethod(
                    info = "EnclosingConstructor in notEnclosing01",
                    enclosingMethod = "<init>",
                    enclosingClazz = notEnclosing01.class
            )
            class EnclosingConstructor {
            }
            new EnclosingConstructor() {
            };
        }

        void method() {
            // anonymous and local classes in method
            @ExpectedEnclosingMethod(
                    info = "EnclosingMethod in notEnclosing01",
                    enclosingMethod = "method",
                    enclosingClazz = notEnclosing01.class
            )
            class EnclosingMethod {
            }
            new EnclosingMethod() {
            };
        }

        @ExpectedEnclosingMethod(
                info = "VariableInitializer in notEnclosing01",
                enclosingClazz = notEnclosing01.class
        )
        static class VariableInitializer {
        }

        // static variable initializer
        private static final VariableInitializer cvi = new VariableInitializer() {
        };

        // instance variable initializer
        private final VariableInitializer ivi = new VariableInitializer() {
        };
    }

    // Test cases: enclosing class is an interface
    public interface notEnclosing02 {
        Runnable lambda = () -> {
            // anonymous and local classes in lambda
            @ExpectedEnclosingMethod(
                    info = "EnclosingLambda in notEnclosing02",
                    enclosingMethod = "lambda",
                    enclosingClazz = notEnclosing02.class
            )
            class EnclosingLambda {
            }
            new EnclosingLambda() {
            };
        };

        static void staticMethod() {
            // anonymous and local classes in static method
            @ExpectedEnclosingMethod(
                    info = "EnclosingMethod in notEnclosing02",
                    enclosingMethod = "staticMethod",
                    enclosingClazz = notEnclosing02.class
            )
            class EnclosingMethod {
            }
            new EnclosingMethod() {
            };
        }

        default void defaultMethod() {
            // anonymous and local classes in default method
            @ExpectedEnclosingMethod(
                    info = "EnclosingMethod in notEnclosing02",
                    enclosingMethod = "defaultMethod",
                    enclosingClazz = notEnclosing02.class
            )
            class EnclosingMethod {
            }
            new EnclosingMethod() {
            };
        }

        @ExpectedEnclosingMethod(
                info = "VariableInitializer in notEnclosing02",
                enclosingClazz = notEnclosing02.class
        )
        static class VariableInitializer {
        }

        // static variable initializer
        VariableInitializer cvi = new VariableInitializer() {
        };
    }

    // Test cases: enclosing class is an enum
    public enum notEnclosing03 {;

        static {
            // anonymous and local classes in static initializer
            @ExpectedEnclosingMethod(
                    info = "EnclosingStaticInitialization in notEnclosing03",
                    enclosingClazz = notEnclosing03.class
            )
            class EnclosingStaticInitialization {
            }
            new EnclosingStaticInitialization() {
            };
        }

        {
            // anonymous and local classes in instance initializer
            @ExpectedEnclosingMethod(
                    info = "EnclosingInitialization in notEnclosing03",
                    enclosingClazz = notEnclosing03.class
            )
            class EnclosingInitialization {
            }
            new EnclosingInitialization() {
            };
        }

        Runnable lambda = () -> {
            // anonymous and local classes in lambda
            @ExpectedEnclosingMethod(
                    info = "EnclosingLambda in notEnclosing03",
                    enclosingMethod = "lambda",
                    enclosingClazz = notEnclosing03.class
            )
            class EnclosingLambda {
            }
            new EnclosingLambda() {
            };
        };

        notEnclosing03() {
            // anonymous and local classes in constructor
            @ExpectedEnclosingMethod(
                    info = "EnclosingConstructor in notEnclosing03",
                    enclosingMethod = "<init>",
                    enclosingClazz = notEnclosing03.class
            )
            class EnclosingConstructor {
            }
            new EnclosingConstructor() {
            };
        }

        void method() {
            // anonymous and local classes in method
            @ExpectedEnclosingMethod(
                    info = "EnclosingMethod in notEnclosing03",
                    enclosingMethod = "method",
                    enclosingClazz = notEnclosing03.class
            )
            class EnclosingMethod {
            }
            new EnclosingMethod() {
            };
        }

        @ExpectedEnclosingMethod(
                info = "VariableInitializer in notEnclosing03",
                enclosingClazz = notEnclosing03.class
        )
        static class VariableInitializer {
        }

        // static variable initializer
        private static final VariableInitializer cvi = new VariableInitializer() {
        };

        // instance variable initializer
        private final VariableInitializer ivi = new VariableInitializer() {
        };
    }

    // Test cases: enclosing class is an annotation
    public @interface notEnclosing04 {
        Runnable lambda = () -> {
            // anonymous and local classes in lambda
            @ExpectedEnclosingMethod(
                    info = "EnclosingLambda in notEnclosing04",
                    enclosingMethod = "lambda",
                    enclosingClazz = notEnclosing04.class
            )
            class EnclosingLambda {
            }
            new EnclosingLambda() {
            };
        };

        @ExpectedEnclosingMethod(
                info = "VariableInitializer in notEnclosing04",
                enclosingClazz = notEnclosing04.class
        )
        static class VariableInitializer {
        }

        // static variable initializer
        VariableInitializer cvi = new VariableInitializer() {
        };
    }
}
