/*
 * Copyright (c) 2023, 2024, 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 java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

/**
 * @test
 * @bug 8329420
 * @summary test execution priority and behavior of main methods
 * @run main InstanceMainTest
 */
public class InstanceMainTest extends TestHelper {

    private static final String[] SOURCES = new String[] {
            // static dominating with args
            """
            class MainClass {
                static void main() {
                    throw new AssertionError();
                }
                static void main(String[] args) {
                }
            }
            """,

            // instance dominating static
            """
            class MainClass {
                void main(String[] args) {
                }
                static void main() {
                    throw new AssertionError();
                }
            }
            """,

            // instance dominating with args
            """
            class MainClass {
                void main() {
                    throw new AssertionError();
                }
                void main(String[] args) {
                }
            }
            """,

            // instance no args
            """
            class MainClass {
                void main() {
                }
            }
            """,

            // unnamed class static dominating with args
            """
            static void main() {
                throw new AssertionError();
            }
            static void main(String[] args) {
            }
            """,

            // main with args dominating main without args
            """
            void main(String[] args) {
            }
            static void main() {
                throw new AssertionError();
            }
            """,

            // unnamed class instance dominating with args
            """
            void main() {
                throw new AssertionError();
            }
            void main(String[] args) {
            }
            """,

            // unnamed class instance main no args
            """
            void main() {
            }
            """,

            // instance main with args dominating super
            """
            class MainClass extends SuperClass {
                void main() {
                    throw new AssertionError();
                }
            }
            class SuperClass {
                void main(String[] args) {
                }
            }
            """,

            // super instance main with args dominating
            """
            public class MainClass extends Super {
            }

            class Super {
                public void main(String... args) {
                }

                public void main() {
                    throw new AssertionError();
                }
            }
            """,

            // ignore super instance main
            """
            public class MainClass extends Super {
                public static void main(String... args) {
                }
            }

            class Super {
                public static void main(String... args) {
                    throw new AssertionError();
                }
            }
            """,

            // enum main
            """
            enum MainClass {
                A;

                public static void main() {
                }
            }
            """,

            // record main
            """
            record MainClass() {
                 static void main() {
                     System.out.println("Done!");
                 }
            }
            """,
            // interface main
            """
            interface MainClass {
                 static void main() {
                     System.out.println("Done!");
                 }
            }
            """
    };

    private static void testMethodOrder() throws Exception {
        for (String source : SOURCES) {
            performTest(source, true, tr -> {
                if (!tr.isOK()) {
                    System.err.println(source);
                    System.err.println(tr);
                    throw new AssertionError();
                }
            });
        }
    }

    record TestCase(String sourceCode, boolean enablePreview, List<String> expectedOutput) {

        public TestCase(String sourceCode, List<String> expectedOutput) {
            this(sourceCode, true, expectedOutput);
        }

    }

    private static final TestCase[] EXECUTION_ORDER = new TestCase[] {
            new TestCase("""
                     public class MainClass {
                         public MainClass() {
                             System.out.println("Constructor called!");
                         }
                         public static void main() {
                             System.out.println("main called!");
                         }
                     }
                     """,
                    List.of("main called!")),
            new TestCase("""
                     public class MainClass {
                         public MainClass() {
                             System.out.println("Constructor called!");
                         }
                         public void main() {
                             System.out.println("main called!");
                         }
                     }
                     """,
                    List.of("Constructor called!", "main called!"))
    };

    private static void testExecutionOrder() throws Exception {
        for (TestCase testCase : EXECUTION_ORDER) {
            performTest(testCase.sourceCode, testCase.enablePreview(), tr -> {
                if (!Objects.equals(testCase.expectedOutput, tr.testOutput)) {
                    throw new AssertionError("Unexpected output, " +
                            "expected: " + testCase.expectedOutput +
                            ", actual: " + tr.testOutput);
                }
            });
        }
    }

    private static final TestCase[] EXECUTION_ERRORS = new TestCase[] {
            new TestCase("""
                     public class MainClass {
                         public MainClass() {
                             System.out.println("Constructor called!");
                             if (true) throw new Error();
                         }
                         public void main(String... args) {
                             System.out.println("main called!");
                         }
                     }
                     """,
                    List.of("Constructor called!",
                            "Exception in thread \"main\" java.lang.Error",
                            "\tat MainClass.<init>(MainClass.java:4)")),
            new TestCase("""
                     public class MainClass {
                         public MainClass() {
                             System.out.println("Constructor called!");
                             if (true) throw new Error();
                         }
                         public void main() {
                             System.out.println("main called!");
                         }
                     }
                     """,
                    List.of("Constructor called!",
                            "Exception in thread \"main\" java.lang.Error",
                            "\tat MainClass.<init>(MainClass.java:4)")),
            new TestCase("""
                     public class MainClass {
                         static int idx;
                         public MainClass() {
                             System.out.println("Constructor called!");
                             if (idx++ == 0) throw new Error();
                         }
                         public void main(String... args) {
                             System.out.println("main called!");
                         }
                         public void main() {
                             System.out.println("main called!");
                         }
                     }
                     """,
                    List.of("Constructor called!",
                            "Exception in thread \"main\" java.lang.Error",
                            "\tat MainClass.<init>(MainClass.java:5)")),
            new TestCase("""
                     public class MainClass {
                         static {
                             System.out.println("static init called!");
                             if (true) throw new Error();
                         }
                         public static void main(String... args) {
                             System.out.println("main called!");
                         }
                     }
                     """,
                    false,
                    List.of("static init called!",
                            "Exception in thread \"main\" java.lang.Error",
                            "\tat MainClass.<clinit>(MainClass.java:4)")),
            new TestCase("""
                     public class MainClass {
                         static {
                             System.out.println("static init called!");
                             if (true) throw new Error();
                         }
                         public static void main(String... args) {
                             System.out.println("main called!");
                         }
                     }
                     """,
                    true,
                    List.of("static init called!",
                            "Exception in thread \"main\" java.lang.Error",
                            "\tat MainClass.<clinit>(MainClass.java:4)")),
            new TestCase("""
                     public class MainClass {
                         static {
                             System.out.println("static init called!");
                             if (true) throw new Error();
                         }
                         public void main(String... args) {
                             System.out.println("main called!");
                         }
                     }
                     """,
                    true,
                    List.of("static init called!",
                            "Exception in thread \"main\" java.lang.Error",
                            "\tat MainClass.<clinit>(MainClass.java:4)")),
    };

    private static void testExecutionErrors() throws Exception {
        for (TestCase testCase : EXECUTION_ERRORS) {
            performTest(testCase.sourceCode, testCase.enablePreview(), tr -> {
                for (int i = 0; i < testCase.expectedOutput.size(); i++) {
                    if (i >= tr.testOutput.size() ||
                            !Objects.equals(testCase.expectedOutput.get(i),
                                    tr.testOutput.get(i))) {
                        throw new AssertionError("Unexpected output, " +
                                "expected: " + testCase.expectedOutput +
                                ", actual: " + tr.testOutput +
                                ", failed comparison at index: " + i);
                    }
                }
            });
        }
    }

    private static void performTest(String source, boolean enablePreview, Consumer<TestResult> validator) throws Exception {
        Path mainClass = Path.of("MainClass.java");
        Files.writeString(mainClass, source);
        var version = System.getProperty("java.specification.version");
        var previewRuntime = enablePreview ? "--enable-preview" : "-DtestNoPreview";
        var previewCompile = enablePreview ? "--enable-preview" : "-XDtestNoPreview";
        var trSource = doExec(javaCmd, previewRuntime, "--source", version, "MainClass.java");
        validator.accept(trSource);
        compile(previewCompile, "--source", version, "MainClass.java");
        String cp = mainClass.toAbsolutePath().getParent().toString();
        var trCompile = doExec(javaCmd, previewRuntime, "--class-path", cp, "MainClass");
        validator.accept(trCompile);
    }

    public static void main(String... args) throws Exception {
        testMethodOrder();
        testExecutionOrder();
        testExecutionErrors();
    }
}