/*
 * Copyright (c) 2016, 2022, 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.
 */

import javax.tools.Diagnostic;
import javax.tools.DiagnosticListener;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;

/*
 * @test
 * @bug 8062389
 * @modules jdk.compiler
 *          jdk.zipfs
 * @summary Nearly exhaustive test of Class.getMethod() and Class.getMethods()
 * @run main/othervm -XX:+IgnoreUnrecognizedVMOptions -XX:-VerifyDependencies PublicMethodsTest
 */
public class PublicMethodsTest {

    public static void main(String[] args) {
        Case c = new Case1();

        int[] diffs = new int[1];
        try (Stream<Map.Entry<int[], Map<String, String>>>
                 expected = expectedResults(c)) {
            diffResults(c, expected)
                .forEach(diff -> {
                    System.out.println(diff);
                    diffs[0]++;
                });
        }

        if (diffs[0] > 0) {
            throw new RuntimeException(
                "There were " + diffs[0] + " differences.");
        }
    }

    // use this to generate .results file for particular case
    public static class Generate {
        public static void main(String[] args) {
            Case c = new Case1();
            dumpResults(generateResults(c))
                .forEach(System.out::println);
        }
    }

    interface Case {
        Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{(.+?)}");

        // possible variants of interface method
        List<String> INTERFACE_METHODS = List.of(
            "", "void m();", "default void m() {}", "static void m() {}"
        );

        // possible variants of class method
        List<String> CLASS_METHODS = List.of(
            "", "public abstract void m();",
            "public void m() {}", "public static void m() {}"
        );

        // template with placeholders parsed with PLACEHOLDER_PATTERN
        String template();

        // map of replacementKey (== PLACEHOLDER_PATTERN captured group #1) ->
        // list of possible replacements
        Map<String, List<String>> replacements();

        // ordered list of replacement keys
        List<String> replacementKeys();

        // names of types occurring in the template
        List<String> classNames();
    }

    static class Case1 implements Case {

        private static final String TEMPLATE = Stream.of(
            "interface I { ${I} }",
            "interface J { ${J} }",
            "interface K extends I, J { ${K} }",
            "abstract class C { ${C} }",
            "abstract class D extends C implements I { ${D} }",
            "abstract class E extends D implements J, K { ${E} }"
        ).collect(joining("\n"));

        private static final Map<String, List<String>> REPLACEMENTS = Map.of(
            "I", INTERFACE_METHODS,
            "J", INTERFACE_METHODS,
            "K", INTERFACE_METHODS,
            "C", CLASS_METHODS,
            "D", CLASS_METHODS,
            "E", CLASS_METHODS
        );

        private static final List<String> REPLACEMENT_KEYS = REPLACEMENTS
            .keySet().stream().sorted().collect(Collectors.toList());

        @Override
        public String template() {
            return TEMPLATE;
        }

        @Override
        public Map<String, List<String>> replacements() {
            return REPLACEMENTS;
        }

        @Override
        public List<String> replacementKeys() {
            return REPLACEMENT_KEYS;
        }

        @Override
        public List<String> classNames() {
            // just by accident, names of classes are equal to replacement keys
            // (this need not be the case in general)
            return REPLACEMENT_KEYS;
        }
    }

    // generate all combinations as a tuple of indexes into lists of
    // replacements. The index of the element in int[] tuple represents the index
    // of the key in replacementKeys() list. The value of the element in int[] tuple
    // represents the index of the replacement string in list of strings in the
    // value of the entry of replacements() map with the corresponding key.
    static Stream<int[]> combinations(Case c) {
        int[] sizes = c.replacementKeys().stream()
                       .mapToInt(key -> c.replacements().get(key).size())
                       .toArray();

        return Stream.iterate(
            new int[sizes.length],
            state -> state != null,
            state -> {
                int[] newState = state.clone();
                for (int i = 0; i < state.length; i++) {
                    if (++newState[i] < sizes[i]) {
                        return newState;
                    }
                    newState[i] = 0;
                }
                // wrapped-around
                return null;
            }
        );
    }

    // given the combination of indexes, return the expanded template
    static String expandTemplate(Case c, int[] combination) {

        // 1st create a map: key -> replacement string
        Map<String, String> map = new HashMap<>(combination.length * 4 / 3 + 1);
        for (int i = 0; i < combination.length; i++) {
            String key = c.replacementKeys().get(i);
            String repl = c.replacements().get(key).get(combination[i]);
            map.put(key, repl);
        }

        return Case.PLACEHOLDER_PATTERN
            .matcher(c.template())
            .replaceAll(match -> map.get(match.group(1)));
    }

    /**
     * compile expanded template into a ClassLoader that sees compiled classes
     */
    static TestClassLoader compile(String source) throws CompileException {
        JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
        if (javac == null) {
            throw new AssertionError("No Java compiler tool found.");
        }

        ErrorsCollector errorsCollector = new ErrorsCollector();
        StandardJavaFileManager standardJavaFileManager =
            javac.getStandardFileManager(errorsCollector, Locale.ROOT,
                                         Charset.forName("UTF-8"));
        try {
            standardJavaFileManager.setLocation(StandardLocation.CLASS_PATH, List.of());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        TestFileManager testFileManager = new TestFileManager(
            standardJavaFileManager, source);

        JavaCompiler.CompilationTask javacTask;
        try {
            javacTask = javac.getTask(
                null, // use System.err
                testFileManager,
                errorsCollector,
                null,
                null,
                List.of(testFileManager.getJavaFileForInput(
                    StandardLocation.SOURCE_PATH,
                    TestFileManager.TEST_CLASS_NAME,
                    JavaFileObject.Kind.SOURCE))
            );
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

        javacTask.call();

        if (errorsCollector.hasError()) {
            throw new CompileException(errorsCollector.getErrors());
        }

        return new TestClassLoader(ClassLoader.getSystemClassLoader(),
                                   testFileManager);
    }

    static class CompileException extends Exception {
        CompileException(List<Diagnostic<?>> diagnostics) {
            super(diagnostics.stream()
                             .map(diag -> diag.toString())
                             .collect(Collectors.joining("\n")));
        }
    }

    static class TestFileManager
        extends ForwardingJavaFileManager<StandardJavaFileManager> {
        static final String TEST_CLASS_NAME = "Test";

        private final String testSource;
        private final Map<String, ClassFileObject> classes = new HashMap<>();

        TestFileManager(StandardJavaFileManager fileManager, String source) {
            super(fileManager);
            testSource = "public class " + TEST_CLASS_NAME + " {}\n" +
                         source; // the rest of classes are package-private
        }

        @Override
        public JavaFileObject getJavaFileForInput(Location location,
                                                  String className,
                                                  JavaFileObject.Kind kind)
        throws IOException {
            if (location == StandardLocation.SOURCE_PATH &&
                kind == JavaFileObject.Kind.SOURCE &&
                TEST_CLASS_NAME.equals(className)) {
                return new SourceFileObject(className, testSource);
            }
            return super.getJavaFileForInput(location, className, kind);
        }

        private static class SourceFileObject extends SimpleJavaFileObject {
            private final String source;

            SourceFileObject(String className, String source) {
                super(
                    URI.create("memory:/src/" +
                               className.replace('.', '/') + ".java"),
                    Kind.SOURCE
                );
                this.source = source;
            }

            @Override
            public CharSequence getCharContent(boolean ignoreEncodingErrors) {
                return source;
            }
        }

        @Override
        public JavaFileObject getJavaFileForOutput(Location location,
                                                   String className,
                                                   JavaFileObject.Kind kind,
                                                   FileObject sibling)
        throws IOException {
            if (kind == JavaFileObject.Kind.CLASS) {
                ClassFileObject cfo = new ClassFileObject(className);
                classes.put(className, cfo);
                return cfo;
            }
            return super.getJavaFileForOutput(location, className, kind, sibling);
        }

        private static class ClassFileObject extends SimpleJavaFileObject {
            final String className;
            ByteArrayOutputStream byteArrayOutputStream;

            ClassFileObject(String className) {
                super(
                    URI.create("memory:/out/" +
                               className.replace('.', '/') + ".class"),
                    Kind.CLASS
                );
                this.className = className;
            }

            @Override
            public OutputStream openOutputStream() throws IOException {
                return byteArrayOutputStream = new ByteArrayOutputStream();
            }

            byte[] getBytes() {
                if (byteArrayOutputStream == null) {
                    throw new IllegalStateException(
                        "No class file written for class: " + className);
                }
                return byteArrayOutputStream.toByteArray();
            }
        }

        byte[] getClassBytes(String className) {
            ClassFileObject cfo = classes.get(className);
            return (cfo == null) ? null : cfo.getBytes();
        }
    }

    static class ErrorsCollector implements DiagnosticListener<JavaFileObject> {
        private final List<Diagnostic<?>> errors = new ArrayList<>();

        @Override
        public void report(Diagnostic<? extends JavaFileObject> diagnostic) {
            if (diagnostic.getKind() == Diagnostic.Kind.ERROR) {
                errors.add(diagnostic);
            }
        }

        boolean hasError() {
            return !errors.isEmpty();
        }

        List<Diagnostic<?>> getErrors() {
            return errors;
        }
    }

    static class TestClassLoader extends ClassLoader implements Closeable {
        private final TestFileManager fileManager;

        public TestClassLoader(ClassLoader parent, TestFileManager fileManager) {
            super(parent);
            this.fileManager = fileManager;
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] classBytes = fileManager.getClassBytes(name);
            if (classBytes == null) {
                throw new ClassNotFoundException(name);
            }
            return defineClass(name, classBytes, 0, classBytes.length);
        }

        @Override
        public void close() throws IOException {
            fileManager.close();
        }
    }

    static Map<String, String> generateResult(Case c, ClassLoader cl) {
        return
            c.classNames()
             .stream()
             .map(cn -> {
                 try {
                     return Class.forName(cn, false, cl);
                 } catch (ClassNotFoundException e) {
                     throw new RuntimeException("Class not found: " + cn, e);
                 }
             })
             .flatMap(clazz -> Stream.of(
                 Map.entry(clazz.getName() + ".gM", generateGetMethodResult(clazz)),
                 Map.entry(clazz.getName() + ".gMs", generateGetMethodsResult(clazz))
             ))
             .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    static String generateGetMethodResult(Class<?> clazz) {
        try {
            Method m = clazz.getMethod("m");
            return m.getDeclaringClass().getName() + "." + m.getName();
        } catch (NoSuchMethodException e) {
            return "-";
        }
    }

    static String generateGetMethodsResult(Class<?> clazz) {
        return Stream.of(clazz.getMethods())
                     .filter(m -> m.getDeclaringClass() != Object.class)
                     .map(m -> m.getDeclaringClass().getName()
                               + "." + m.getName())
                     .collect(Collectors.joining(", ", "[", "]"));
    }

    static Stream<Map.Entry<int[], Map<String, String>>> generateResults(Case c) {
        return combinations(c)
            .flatMap(comb -> {
                String src = expandTemplate(c, comb);
                try {
                    try (TestClassLoader cl = compile(src)) {
                        // compilation was successful -> generate result
                        return Stream.of(Map.entry(
                            comb,
                            generateResult(c, cl)
                        ));
                    } catch (CompileException e) {
                        // ignore uncompilable combinations
                        return Stream.empty();
                    }
                } catch (IOException ioe) {
                    // from TestClassLoader.close()
                    throw new UncheckedIOException(ioe);
                }
            });
    }

    static Stream<Map.Entry<int[], Map<String, String>>> expectedResults(Case c) {
        try {
            BufferedReader r = new BufferedReader(new InputStreamReader(
                c.getClass().getResourceAsStream(
                    c.getClass().getSimpleName() + ".results"),
                "UTF-8"
            ));

            return parseResults(r.lines())
                .onClose(() -> {
                    try {
                        r.close();
                    } catch (IOException ioe) {
                        throw new UncheckedIOException(ioe);
                    }
                });
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    static Stream<Map.Entry<int[], Map<String, String>>> parseResults(
        Stream<String> lines
    ) {
        return lines
            .map(l -> l.split(Pattern.quote("#")))
            .map(lkv -> Map.entry(
                Stream.of(lkv[0].split(Pattern.quote(",")))
                      .mapToInt(Integer::parseInt)
                      .toArray(),
                Stream.of(lkv[1].split(Pattern.quote("|")))
                      .map(e -> e.split(Pattern.quote("=")))
                      .collect(toMap(ekv -> ekv[0], ekv -> ekv[1]))
            ));
    }

    static Stream<String> dumpResults(
        Stream<Map.Entry<int[], Map<String, String>>> results
    ) {
        return results
            .map(le ->
                     IntStream.of(le.getKey())
                              .mapToObj(String::valueOf)
                              .collect(joining(","))
                     + "#" +
                     le.getValue().entrySet().stream()
                       .map(e -> e.getKey() + "=" + e.getValue())
                       .collect(joining("|"))
            );
    }

    static Stream<String> diffResults(
        Case c,
        Stream<Map.Entry<int[], Map<String, String>>> expectedResults
    ) {
        return expectedResults
            .flatMap(exp -> {
                int[] comb = exp.getKey();
                Map<String, String> expected = exp.getValue();

                String src = expandTemplate(c, comb);
                Map<String, String> actual;
                try {
                    try (TestClassLoader cl = compile(src)) {
                        actual = generateResult(c, cl);
                    } catch (CompileException ce) {
                        return Stream.of(src + "\n" +
                                         "got compilation error: " + ce);
                    }
                } catch (IOException ioe) {
                    // from TestClassLoader.close()
                    return Stream.of(src + "\n" +
                                     "got IOException: " + ioe);
                }

                if (actual.equals(expected)) {
                    return Stream.empty();
                } else {
                    Map<String, String> diff = new HashMap<>(expected);
                    diff.entrySet().removeAll(actual.entrySet());
                    return Stream.of(
                        diff.entrySet()
                            .stream()
                            .map(e -> "expected: " + e.getKey() + ": " +
                                      e.getValue() + "\n" +
                                      "  actual: " + e.getKey() + ": " +
                                      actual.get(e.getKey()) + "\n")
                            .collect(joining("\n", src + "\n\n", "\n"))
                    );
                }
            });
    }
}
