/*
 * Copyright (c) 2023, 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 8304400
 * @summary Test source launcher running Java programs spanning multiple files
 * @modules jdk.compiler/com.sun.tools.javac.launcher
 * @run junit MultiFileSourceLauncherTests
 */

import static org.junit.jupiter.api.Assertions.*;

import com.sun.tools.javac.launcher.Fault;
import java.nio.file.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.*;

class MultiFileSourceLauncherTests {
    @Test
    void testHelloWorldInTwoCompilationUnits(@TempDir Path base) throws Exception {
        var hello = Files.writeString(base.resolve("Hello.java"),
                    """
                    public class Hello {
                      public static void main(String... args) {
                        System.out.println("Hello " + new World("Terra"));
                        System.out.println(Hello.class.getResource("Hello.java"));
                        System.out.println(Hello.class.getResource("World.java"));
                      }
                    }
                    """);
        Files.writeString(base.resolve("World.java"),
                    """
                    record World(String name) {}
                    """);

        var run = Run.of(hello);
        assertLinesMatch(
                """
                Hello World[name=Terra]
                \\Qfile:\\E.+\\QHello.java\\E
                \\Qfile:\\E.+\\QWorld.java\\E
                """.lines(),
                run.stdOut().lines());
        assertTrue(run.stdErr().isEmpty(), run.toString());
        assertNull(run.exception(), run.toString());
    }

    @Test
    void testLoadingOfEnclosedTypes(@TempDir Path base) throws Exception {
        var hello = Files.writeString(base.resolve("Hello.java"),
                    """
                    public class Hello {
                      public static void main(String... args) throws Exception {
                        System.out.println(Class.forName("World$Core"));
                        System.out.println(Class.forName("p.q.Unit$Fir$t"));
                        System.out.println(Class.forName("p.q.Unit$123$Fir$t$$econd"));
                        System.out.println(Class.forName("$.$.$"));
                      }
                    }
                    """);
        Files.writeString(base.resolve("World.java"),
                    """
                    record World(String name) {
                      record Core() {}
                    }
                    """);
        var pq = Files.createDirectories(base.resolve("p/q"));
        Files.writeString(pq.resolve("Unit.java"),
                    """
                    package p.q;
                    record Unit() {
                      record Fir$t() {
                        record $econd() {}
                      }
                    }
                    """);
        Files.writeString(pq.resolve("Unit$123.java"),
                    """
                    package p.q;
                    record Unit$123() {
                      record Fir$t() {
                        record $econd() {}
                      }
                    }
                    """);
        var $$ = Files.createDirectories(base.resolve("$/$"));
        Files.writeString($$.resolve("$.java"),
                    """
                    package $.$;
                    record $($ $) {}
                    """);

        var run = Run.of(hello);
        assertAll("Run -> " + run,
                () -> assertLinesMatch(
                        """
                        class World$Core
                        class p.q.Unit$Fir$t
                        class p.q.Unit$123$Fir$t$$econd
                        class $.$.$
                        """.lines(),
                        run.stdOut().lines()),
                () -> assertTrue(run.stdErr().isEmpty()),
                () -> assertNull(run.exception())
        );
    }

    @Test
    void testMultiplePackages(@TempDir Path base) throws Exception {
        var packageA = Files.createDirectories(base.resolve("a"));
        var hello = Files.writeString(packageA.resolve("Hello.java"),
                    """
                    package a;
                    import b.World;
                    public class Hello {
                      public static void main(String... args) {
                        System.out.println("Hello " + new World("in package b"));
                      }
                    }
                    """);
        var packageB = Files.createDirectories(base.resolve("b"));
        Files.writeString(packageB.resolve("World.java"),
                    """
                    package b;
                    public record World(String name) {}
                    """);

        var run = Run.of(hello);
        assertLinesMatch(
                """
                Hello World[name=in package b]
                """.lines(),
                run.stdOut().lines());
        assertTrue(run.stdErr().isEmpty(), run.toString());
        assertNull(run.exception(), run.toString());
    }

    @Test
    void testMissingSecondUnit(@TempDir Path base) throws Exception {
        var program = Files.writeString(base.resolve("Program.java"),
                    """
                    public class Program {
                      public static void main(String... args) {
                        System.out.println("Hello " + new MissingSecondUnit());
                      }
                    }
                    """);

        var run = Run.of(program);
        assertTrue(run.stdOut().isEmpty(), run.toString());
        assertLinesMatch(
                """
                %s:3: error: cannot find symbol
                    System.out.println("Hello " + new MissingSecondUnit());
                                                      ^
                  symbol:   class MissingSecondUnit
                  location: class Program
                1 error
                """.formatted(program.toString())
                        .lines(),
                run.stdErr().lines(),
                run.toString());
        assertTrue(run.exception() instanceof Fault);
    }

    @Test
    void testSecondUnitWithSyntaxError(@TempDir Path base) throws Exception {
        var program = Files.writeString(base.resolve("Program.java"),
                    """
                    public class Program {
                      public static void main(String... args) {
                        System.out.println("Hello " + new BrokenSecondUnit());
                      }
                    }
                    """);
        var broken = Files.writeString(base.resolve("BrokenSecondUnit.java"),
                    """
                    record BrokenSecondUnit {}
                    """);

        var run = Run.of(program);
        assertTrue(run.stdOut().isEmpty(), run.toString());
        assertLinesMatch(
                """
                %s:1: error: '(' expected
                >> MORE LINES >>
                """.formatted(broken.toString())
                   .lines(),
                run.stdErr().lines(),
                run.toString());
        assertTrue(run.exception() instanceof Fault);
    }

    @Test
    void onlyJavaFilesReferencedByTheProgramAreCompiled(@TempDir Path base) throws Exception {
        var prog = Files.writeString(base.resolve("Prog.java"),
                    """
                    class Prog {
                      public static void main(String... args) {
                        Helper.run();
                      }
                    }
                    """);
        Files.writeString(base.resolve("Helper.java"),
                    """
                    class Helper {
                      static void run() {
                        System.out.println("Hello!");
                      }
                    }
                    """);

        var old = Files.writeString(base.resolve("OldProg.java"),
                    """
                    class OldProg {
                      public static void main(String... args) {
                        Helper.run()
                      }
                    }
                    """);

        var run = Run.of(prog);
        assertAll("Run := " + run,
                () -> assertLinesMatch(
                        """
                        Hello!
                        """.lines(), run.stdOut().lines()),
                () -> assertTrue(run.stdErr().isEmpty()),
                () -> assertNull(run.exception()));

        var fail = Run.of(old);
        assertAll("Run := " + fail,
                () -> assertTrue(fail.stdOut().isEmpty()),
                () -> assertLinesMatch(
                        """
                        %s:3: error: ';' expected
                            Helper.run()
                                        ^
                        1 error
                        """.formatted(old).lines(), fail.stdErr().lines()),
                () -> assertNotNull(fail.exception()));
    }

    @Test
    void classesDeclaredInSameFileArePreferredToClassesInOtherFiles(@TempDir Path base) throws Exception {
        var prog = Files.writeString(base.resolve("Prog.java"),
                """
                class Helper {
                  static void run() {
                    System.out.println("Same file.");
                  }
                }
                public class Prog {
                  public static void main(String... args) {
                    Helper.run();
                  }
                }
                """);
        Files.writeString(base.resolve("Helper.java"),
                """
                class Helper {
                  static void run() {
                    System.out.println("Other file.");
                  }
                }
                """);

        var run = Run.of(prog);
        assertAll("Run := " + run,
                () -> assertLinesMatch(
                        """
                        Same file.
                        """.lines(), run.stdOut().lines()),
                () -> assertTrue(run.stdErr().isEmpty()),
                () -> assertNull(run.exception()));
    }

    @Test
    void duplicateDeclarationOfClassFails(@TempDir Path base) throws Exception {
        var prog = Files.writeString(base.resolve("Prog.java"),
                    """
                    class Prog {
                      public static void main(String... args) {
                        Helper.run();
                        Aux.cleanup();
                      }
                    }
                    class Aux {
                      static void cleanup() {}
                    }
                    """);
        var helper = Files.writeString(base.resolve("Helper.java"),
                    """
                    class Helper {
                      static void run() {}
                    }
                    class Aux {
                      static void cleanup() {}
                    }
                    """);


        var fail = Run.of(prog);
        assertAll("Run := " + fail,
                () -> assertTrue(fail.stdOut().isEmpty()),
                () -> assertLinesMatch(
                        """
                        %s:4: error: duplicate class: Aux
                        class Aux {
                        ^
                        1 error
                        """.formatted(helper).lines(), fail.stdErr().lines()),
                () -> assertNotNull(fail.exception()));
    }
}