// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.groovy.compiler;

import com.intellij.compiler.CompilerConfiguration;
import com.intellij.compiler.ParallelCompilationOption;
import com.intellij.compiler.server.BuildManager;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.executors.DefaultRunExecutor;
import com.intellij.execution.impl.DefaultJavaProgramRunner;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessListener;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.execution.runners.ProgramRunner;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.compiler.CompilerMessage;
import com.intellij.openapi.compiler.CompilerMessageCategory;
import com.intellij.openapi.compiler.options.ExcludeEntryDescription;
import com.intellij.openapi.compiler.options.ExcludesConfiguration;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.advanced.AdvancedSettings;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.project.IntelliJProjectConfiguration;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.PsiTestUtil;
import com.intellij.testFramework.TestLoggerFactory;
import com.intellij.util.ThrowableRunnable;
import com.intellij.util.containers.ContainerUtil;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.incremental.groovy.JpsGroovycRunner;
import org.jetbrains.jps.model.java.compiler.ProcessorConfigProfile;
import org.jetbrains.org.objectweb.asm.ClassReader;
import org.jetbrains.org.objectweb.asm.ClassVisitor;
import org.jetbrains.org.objectweb.asm.Opcodes;
import org.jetbrains.plugins.groovy.GroovyProjectDescriptors;
import org.jetbrains.plugins.groovy.RepositoryTestLibrary;
import org.jetbrains.plugins.groovy.TestLibrary;
import org.junit.Assert;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Function;

public abstract class GroovyCompilerTest extends GroovyCompilerTestCase {
  @Override
  protected boolean runInDispatchThread() {
    return false;
  }

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    Files.deleteIfExists(TestLoggerFactory.getTestLogDir().resolve("../log/build-log/build.log"));
    Logger.getInstance(GroovyCompilerTest.class).info(getTestStartMessage());
    addGroovyLibrary(getModule());
  }

  protected abstract TestLibrary getGroovyLibrary();

  @Override
  protected final void addGroovyLibrary(Module to) {
    getGroovyLibrary().addTo(to);
  }

  public void testPlainGroovy() throws Throwable {
    myFixture.addFileToProject("A.groovy", "println '239'");
    assertEmpty("Expected zero compilation errors", make());
    assertOutput("A", "239");
  }

  public void testJavaDependsOnGroovy() throws Throwable {
    myFixture.addClass(
      "public class Foo {" + "public static void main(String[] args) { " + "  System.out.println(new Bar().foo());" + "}" + "}");
    myFixture.addFileToProject("Bar.groovy", "class Bar {" + "  def foo() {" + "    239" + "  }" + "}");
    make();
    assertOutput("Foo", "239");
  }

  public void testCorrectFailAndCorrect() throws Exception {
    myFixture.addClass(
      "public class Foo {" + "public static void main(String[] args) { " + "  System.out.println(new Bar().foo());" + "}" + "}");
    final String barText = "class Bar {" + "  def foo() { 239  }" + "}";
    final PsiFile file = myFixture.addFileToProject("Bar.groovy", barText);
    make();
    assertOutput("Foo", "239");

    setFileText(file, "class Bar {}");
    GroovyCompilerTestCase.shouldFail(make());

    setFileText(file, barText);
    make();
    assertOutput("Foo", "239");
  }

  public void testRenameToJava() throws Throwable {
    myFixture.addClass(
      "public class Foo {" + "public static void main(String[] args) { " + "  System.out.println(new Bar().foo());" + "}" + "}");

    final PsiFile bar =
      myFixture.addFileToProject("Bar.groovy", "public class Bar {" + "public int foo() { " + "  return 239;" + "}" + "}");

    make();
    assertOutput("Foo", "239");

    setFileName(bar, "Bar.java");

    make();
    assertOutput("Foo", "239");
  }

  public void testTransitiveJavaDependency() throws Throwable {
    final VirtualFile ifoo = myFixture.addClass("public interface IFoo { int foo(); }").getContainingFile().getVirtualFile();
    myFixture.addClass("public class Foo implements IFoo {" + "  public int foo() { return 239; }" + "}");
    final PsiFile bar = myFixture.addFileToProject("Bar.groovy", "class Bar {" +
                                                                 "Foo foo\n" +
                                                                 "public static void main(String[] args) { " +
                                                                 "  System.out.println(new Foo().foo());" +
                                                                 "}" +
                                                                 "}");
    assertEmpty("Expected zero compilation errors", make());
    assertOutput("Bar", "239");

    touch(ifoo);
    touch(bar.getVirtualFile());

    //assertTrue(assertOneElement(make()).contains("WARNING: Groovyc stub generation failed"));
    assertEmpty("Expected zero compilation errors", make());
    assertOutput("Bar", "239");
  }

  protected boolean isRebuildExpectedAfterChangeInJavaClassExtendedByGroovy() {
    return false;
  }

  public void testTransitiveJavaDependencyThroughGroovy() throws IOException {
    PsiFile iFoo = myFixture.addClass("public class IFoo { void foo() {} }").getContainingFile();
    myFixture.addFileToProject("Foo.groovy", """
      class Foo {
        static IFoo f
        public int foo() { return 239; }
      }
    """);
    PsiFile bar = myFixture.addFileToProject("Bar.groovy", """
      class Bar extends Foo {
        public static void main(String[] args) {
          System.out.println(new Foo().foo());
        }
      }
    """);
    assertEmpty("Expected zero compilation errors", make());

    touch(iFoo.getVirtualFile());
    touch(bar.getVirtualFile());

    // in 2.4:
    // - the Foo is not well-formed (IFoo doesn't exist), so we throw NCDFE;
    // - NCDFE forces loading Foo class node from Foo.groovy file;
    // - Foo.groovy is added to the current compile session;
    // => no chunk rebuild
    //
    // in 2.5:
    // - the Foo is loaded as decompiled node;
    // - when the Bar stub is being written on the disk, it throws NCDFE when trying to resolve IFoo;
    // => chunk rebuild
    //see also org.codehaus.groovy.control.ClassNodeResolver#tryAsLoaderClassOrScript
    if (isRebuildExpectedAfterChangeInJavaClassExtendedByGroovy()) {
      assertEquals(chunkRebuildMessage("Groovy stub generator"), ContainerUtil.map(make(), CompilerMessage::getMessage));
    } else {
      assertEmpty("Expected zero compilation errors", make());
    }

  }

  public void testTransitiveGroovyDependency() throws Throwable {
    PsiFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo {} ");
    PsiFile bar = myFixture.addFileToProject("Bar.groovy", "class Bar extends Foo {}");
    PsiFile goo = myFixture.addFileToProject("Goo.groovy", "class Goo extends Bar {}");
    assertEmpty("Expected zero compilation errors", make());

    touch(foo.getVirtualFile());
    touch(goo.getVirtualFile());
    assertEmpty("Expected zero compilation errors", make());
  }

  public void testTransitiveDependencyViaAnnotation() throws IOException {
    PsiFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo {}");
    myFixture.addFileToProject("Bar.groovy", "class Bar { Bar plugin(@DelegatesTo(Foo) c) {} }");
    PsiFile goo = myFixture.addFileToProject("Goo.groovy", "@groovy.transform.CompileStatic class Goo { def x(Bar bar) { bar.plugin {} } }");
    assertEmpty("Expected zero compilation errors", make());

    touch(foo.getVirtualFile());
    touch(goo.getVirtualFile());

    assertEmpty("Expected zero compilation errors", make());
  }

  public void testJavaDependsOnGroovyEnum() throws Throwable {
    myFixture.addFileToProject("Foo.groovy", "enum Foo { FOO }");
    myFixture.addClass("class Bar { Foo f; }");
    assertEmpty("Expected zero compilation errors", make());
  }

  public void testDeleteTransitiveJavaClass() throws Throwable {
    myFixture.addClass("public interface IFoo { int foo(); }");
    myFixture.addClass("public class Foo implements IFoo {" + "  public int foo() { return 239; }" + "}");
    final PsiFile bar = myFixture.addFileToProject("Bar.groovy", """
      class Bar {\
      Foo foo
      public static void main(String[] args) { \
        System.out.println(new Foo().foo());\
      }\
      }""");
    assertEmpty("Expected zero compilation errors", make());
    assertOutput("Bar", "239");

    deleteClassFile("IFoo");
    touch(bar.getVirtualFile());

    //assertTrue(assertOneElement(make()).contains("WARNING: Groovyc stub generation failed"));
    assertEmpty("Expected zero compilation errors", make());
    assertOutput("Bar", "239");
  }

  public void testGroovyDependsOnGroovy() throws Throwable {
    myFixture.addClass("public class JustToMakeGroovyGenerateStubs {}");
    myFixture.addFileToProject("Foo.groovy", "class Foo { }");
    final PsiFile bar = myFixture.addFileToProject("Bar.groovy", """
      class Bar {\
      def foo(Foo f) {}
      public static void main(String[] args) { \
        System.out.println(239);\
      }\
      }""");
    assertEmpty("Expected zero compilation errors", make());
    assertOutput("Bar", "239");

    touch(bar.getVirtualFile());

    assertEmpty("Expected zero compilation errors", make());
    assertOutput("Bar", "239");
  }

  public String getTestStartMessage() { return "Starting " + getClass().getName() + " " + getName(); }

  @Override
  protected void runTestRunnable(@NotNull ThrowableRunnable<Throwable> testRunnable) throws Throwable {
    try {
      super.runTestRunnable(testRunnable);
    }
    catch (Throwable e) {
      printLogs();
      throw e;
    }
  }

  private void printLogs() throws IOException {
    DefaultGroovyMethods.println(this, "Idea log");
    TestLoggerFactory.dumpLogToStdout(getTestStartMessage());

    Path makeLog = TestLoggerFactory.getTestLogDir().resolve("../log/build-log/build.log");
    if (Files.exists(makeLog)) {
      DefaultGroovyMethods.println(this, "\n\nServer Log:");
      DefaultGroovyMethods.println(this, Files.readString(makeLog));
    }

    System.out.flush();
  }

  public void testMakeInTests() throws Throwable {
    setupTestSources();
    myFixture.addFileToProject("tests/Super.groovy", "class Super {}");
    assertEmpty("Expected zero compilation errors", make());

    myFixture.addFileToProject("tests/Sub.groovy", """
      class Sub {
        Super xxx() {}
        static void main(String[] args) {\
          println 'hello'\
        }\
      }""");

    myFixture.addFileToProject("tests/Java.java", "public class Java {}");

    assertEmpty("Expected zero compilation errors", make());
    assertOutput("Sub", "hello");
  }

  public void testTestsDependOnProduction() throws Throwable {
    setupTestSources();
    myFixture.addFileToProject("src/com/Bar.groovy", "package com\nclass Bar {}");
    myFixture.addFileToProject("src/com/ToGenerateStubs.java", "package com;\npublic class ToGenerateStubs {}");
    myFixture.addFileToProject("tests/com/BarTest.groovy", "package com\nclass BarTest extends Bar {}");
    assertEmpty("Expected zero compilation errors", make());
  }

  public void testStubForGroovyExtendingJava() throws Exception {
    PsiFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo extends Goo { }");
    myFixture.addFileToProject("Goo.groovy", "class Goo extends Main { void bar() { println 'hello' } }");
    PsiClass main = myFixture.addClass("public class Main { public static void main(String[] args) { new Goo().bar(); } }");
    assertEmpty("Expected zero compilation errors", make());

    if (AdvancedSettings.getBoolean("compiler.unified.ic.implementation")) {
      long oldFooStamp = findClassFile("Foo").lastModified();
      long oldGooStamp = findClassFile("Goo").lastModified();
      long oldMainStamp = findClassFile("Main").lastModified();

      touch(foo.getVirtualFile());
      touch(main.getContainingFile().getVirtualFile());
      GroovyCompilerTestCase.shouldSucceed(make());

      Assert.assertNotEquals(oldFooStamp, findClassFile("Foo").lastModified());
      Assert.assertNotEquals(oldMainStamp, findClassFile("Main").lastModified());
      Assert.assertNotEquals(oldGooStamp, findClassFile("Goo").lastModified());
    }
    else {
      touch(foo.getVirtualFile());
      touch(main.getContainingFile().getVirtualFile());
      if (isRebuildExpectedAfterChangeInJavaClassExtendedByGroovy()) {
        assertEquals(chunkRebuildMessage("Groovy stub generator"), ContainerUtil.map(make(), CompilerMessage::getMessage));
      } else {
        assertEmpty("Expected zero compilation errors", make());
      }
    }
  }

  public void testDontApplyTransformsFromSameModule() throws Exception {
    addTransform();

    myFixture.addClass("public class JavaClassToGenerateStubs {}");

    assertEmpty("Expected zero compilation errors", make());
  }

  private void addTransform() throws IOException {
    myFixture.addFileToProject("Transf.java", """
      import org.codehaus.groovy.ast.*;
      import org.codehaus.groovy.control.*;
      import org.codehaus.groovy.transform.*;
      @GroovyASTTransformation(phase = CompilePhase.CONVERSION)
      public class Transf implements ASTTransformation {
        public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {
          ModuleNode module = (ModuleNode)nodes[0];
          for (ClassNode clazz : module.getClasses()) {
    
            if (clazz.getName().contains("Bar")) {
              module.addStaticStarImport("Foo", ClassHelper.makeWithoutCaching(Foo.class));
            }
          }
            //throw new RuntimeException("In class " + nodes[0]);
        }
      }
    """);

    myFixture.addFileToProject("Foo.java", """
      public class Foo {
      public static int autoImported() { return 239; }
      }""");

    CompilerConfiguration.getInstance(getProject()).addResourceFilePattern("*.ASTTransformation");

    myFixture.addFileToProject("META-INF/services/org.codehaus.groovy.transform.ASTTransformation", "Transf");
  }

  public void testApplyTransformsFromDependencies() throws Exception {
    addTransform();

    myFixture.addFileToProject("dependent/Bar.groovy", """
      class Bar {
        static Object zzz = autoImported()
        static void main(String[] args) {
          println zzz
        }
      }""");

    myFixture.addFileToProject("dependent/AJavaClass.java", "class AJavaClass {}");

    Module dep = addDependentModule();

    addGroovyLibrary(dep);

    assertEmpty("Expected zero compilation errors", make());
    assertOutput("Bar", "239", dep);
  }

  public void testIndirectDependencies() throws Exception {
    myFixture.addFileToProject("dependent1/Bar1.groovy", "class Bar1 {}");
    myFixture.addFileToProject("dependent2/Bar2.groovy", "class Bar2 extends Bar1 {}");
    PsiFile main = myFixture.addFileToProject("Main.groovy", "class Main extends Bar2 {}");

    Module dep1 = addModule("dependent1", true);
    Module dep2 = addModule("dependent2", true);
    ModuleRootModificationUtil.addDependency(dep2, dep1);
    ModuleRootModificationUtil.addDependency(getModule(), dep2);

    addGroovyLibrary(dep1);
    addGroovyLibrary(dep2);

    assertEmpty("Expected zero compilation errors", make());

    touch(main.getVirtualFile());
    assertEmpty("Expected zero compilation errors", make());
  }

  public void testExtendFromGroovyAbstractClass() {
    myFixture.addFileToProject("Super.groovy", "abstract class Super {}");
    myFixture.addFileToProject("AJava.java", "public class AJava {}");
    assertEmpty("Expected zero compilation errors", make());

    myFixture.addFileToProject("Sub.groovy", "class Sub extends Super {}");
    assertEmpty("Expected zero compilation errors", make());
  }

  public void test1_7InnerClass() throws Exception {
    myFixture.addFileToProject("Foo.groovy", """
      class Foo {
        static class Bar {}
      }
    """);
    PsiFile javaFile = myFixture.addFileToProject("AJava.java", "public class AJava extends Foo.Bar {}");
    assertEmpty("Expected zero compilation errors", make());

    touch(javaFile.getVirtualFile());
    assertEmpty("Expected zero compilation errors", make());
  }

public void testRecompileDependentClass() throws Exception {
    PsiFile cloud = myFixture.addFileToProject("Cloud.groovy", """
      class Cloud {
        def accessFooProperty(Foo c) {
          c.prop = 2
        }
      }
    """);
    myFixture.addFileToProject("Foo.groovy", """
      class Foo {
        def withGooParameter(Goo x) {}
      }
    """);
    PsiFile goo = myFixture.addFileToProject("Goo.groovy", "class Goo {}");

  assertEmpty("Expected zero compilation errors", make());

    touch(cloud.getVirtualFile());
    touch(goo.getVirtualFile());
  assertEmpty("Expected zero compilation errors", make());
  }

public void testRecompileExpressionReferences() throws Exception {
    PsiFile rusCon = myFixture.addFileToProject("RusCon.groovy", """
      interface RusCon {
        Closure foo = { Seq.foo() }
      }
    """);
    myFixture.addFileToProject("Seq.groovy", """
      class Seq implements RusCon {
        static def foo() { }
      }
    """);
  assertEmpty("Expected zero compilation errors", make());

    touch(rusCon.getVirtualFile());
  assertEmpty("Expected zero compilation errors", make());
  }

public void testRecompileImportedClass() throws Exception {
    PsiFile bar = myFixture.addFileToProject("pack/Bar.groovy", """
      package pack
      import pack.Foo
      class Bar {}
    """);
    myFixture.addFileToProject("pack/Foo.groovy", """
      package pack
      class Foo extends Goo {
      }
    """);
    PsiFile goo = myFixture.addFileToProject("pack/Goo.groovy", """
      package pack
      class Goo {}
    """);

  assertEmpty("Expected zero compilation errors", make());

    touch(bar.getVirtualFile());
    touch(goo.getVirtualFile());
  assertEmpty("Expected zero compilation errors", make());
  }

public void testRecompileDependentClassesWithOnlyOneChanged() throws Exception {
    PsiFile bar = myFixture.addFileToProject("Bar.groovy", """
      class Bar {
        Foo f
      }
    """);
    myFixture.addFileToProject("Foo.groovy", """
      class Foo extends Bar {
      }
    """);

  assertEmpty("Expected zero compilation errors", make());

    touch(bar.getVirtualFile());
  assertEmpty("Expected zero compilation errors", make());
  }

  public void testDollarGroovyInnerClassUsagesInStubs() throws Exception {
    PsiClass javaFile = myFixture.addClass("""
      public class JavaClass {
        public static class InnerJavaClass {}
      }
    """);
    myFixture.addFileToProject("WithInner.groovy", """
      class WithInner {
        static class Inner {}
      }
    """);
    assertEmpty("Expected zero compilation errors", make());

    myFixture.addFileToProject("Usage.groovy", """
      class Usage {
        def foo(WithInner.Inner i) {}
        def foo(JavaClass.InnerJavaClass i) {}
      }
    """);

    touch(javaFile.getContainingFile().getVirtualFile());
    assertEmpty("Expected zero compilation errors", make());
  }

  public void testDollarGroovyInnerClassUsagesInStubs2() throws Exception {
    myFixture.addClass(" public class JavaClass { } ");
    myFixture.addFileToProject("WithInner.groovy", """
      class WithInner {
        static class Inner {}
      }
    """);

    myFixture.addFileToProject("Usage.groovy", """
      class Usage {
        def foo(WithInner.Inner i) {}
      }
    """);
    assertEmpty("Expected zero compilation errors", make());
  }

  public void testGroovyAnnotations() {
    myFixture.addClass("public @interface Anno { Class<?>[] value(); }");
    myFixture.addFileToProject("Foo.groovy", "@Anno([String]) class Foo {}");
    myFixture.addFileToProject("Bar.java", "class Bar extends Foo {}");

    assertEmpty("Expected zero compilation errors", make());
  }

  public void test_with_annotation_processing_enabled() {
    ProcessorConfigProfile profile = (ProcessorConfigProfile)CompilerConfiguration.getInstance(getProject()).getAnnotationProcessingConfiguration(getModule());
    profile.setEnabled(true);
    profile.setObtainProcessorsFromClasspath(true);

    myFixture.addFileToProject("Foo.groovy", "class Foo {}");

    assertEmpty("Expected zero compilation errors", make());
  }

public void testGenericStubs() {
    myFixture.addFileToProject("Foo.groovy", "class Foo { List<String> list }");
    myFixture.addFileToProject("Bar.java", "class Bar {{ for (String s : new Foo().getList()) { s.hashCode(); } }}");
  assertEmpty("Expected zero compilation errors", make());
  }

  public void testDuplicateClassDuringCompilation() throws Exception {
    VirtualFile base = myFixture.addFileToProject("p/Base.groovy", "package p; class Base { }").getVirtualFile();
    myFixture.addFileToProject("p/Indirect.groovy", """
      package p
      class Indirect {
        private static class Inner { Base b }
      
        private Indirect.Inner foo(Indirect.Inner g1, Inner g2, Base b) {}
      }
     """).getVirtualFile();
    VirtualFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo { p.Indirect foo() {} }").getVirtualFile();
    assertEmpty("Expected zero compilation errors", make());

    touch(foo);
    touch(base);
    assertEmpty("Expected zero compilation errors", make());
  }

  public void testDontRecompileUnneeded() throws IOException {
    myFixture.addFileToProject("Base.groovy", "class Base { }");
    VirtualFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo extends Base { }").getVirtualFile();
    myFixture.addFileToProject("Bar.groovy", "class Bar extends Foo { }");
    VirtualFile main = myFixture.addFileToProject("Main.groovy", "class Main extends Bar { }").getVirtualFile();
    assertEmpty("Expected zero compilation errors", make());
    long oldBaseStamp = findClassFile("Base").lastModified();
    long oldMainStamp = findClassFile("Main").lastModified();

    touch(main);
    touch(foo);
    assertEmpty("Expected zero compilation errors", make());
    Assert.assertNotEquals(oldMainStamp, findClassFile("Main").lastModified());
    Assert.assertEquals(oldBaseStamp, findClassFile("Base").lastModified());
  }

  public void test_changed_groovy_refers_to_java_which_refers_to_changed_groovy_and_fails_in_stub_generator() throws IOException {
    PsiFile used = myFixture.addFileToProject("Used.groovy", "class Used { }");
    PsiFile java = myFixture.addFileToProject("Java.java", "class Java { void foo(Used used) {} }");
    VirtualFile main = myFixture.addFileToProject("Main.groovy", "class Main extends Java {  }").getVirtualFile();
    assertEmpty("Expected zero compilation errors", make());

    touch(used.getVirtualFile());
    touch(main);
    if (isRebuildExpectedAfterChangesInGroovyWhichUseJava()) {
      assertEquals(chunkRebuildMessage("Groovy stub generator"), ContainerUtil.map(make(), CompilerMessage::getMessage));
    } else {
      assertEmpty("Expected zero compilation errors", make());
    }

    setFileText(used, "class Used2 {}");
    GroovyCompilerTestCase.shouldFail(make());
    assertNull(findClassFile("Used"));

    setFileText(used, "class Used3 {}");
    setFileText(java, "class Java { void foo(Used3 used) {} }");
    assertEmpty("Expected zero compilation errors", make());

    assertNull(findClassFile("Used2"));
  }

  protected abstract List<String> chunkRebuildMessage(String builder);

  public void test_changed_groovy_refers_to_java_which_refers_to_changed_groovy_and_fails_in_compiler() throws IOException {
    PsiFile used = myFixture.addFileToProject("Used.groovy", "class Used { }");
    myFixture.addFileToProject("Java.java", """
      abstract class Java {
        Object getProp() { return null; }
        abstract void foo(Used used);
      }
    """);
    VirtualFile main = myFixture.addFileToProject("Main.groovy", """
      class Main {
        def foo(Java j) {
          return j.prop
        }
      }
    """).getVirtualFile();

    assertEmpty("Expected zero compilation errors", make());

    touch(used.getVirtualFile());
    touch(main);

    List<CompilerMessage> messages = make();
    if (isRebuildExpectedAfterChangesInGroovyWhichUseJava()){
      assertEquals(chunkRebuildMessage("Groovy compiler"), ContainerUtil.map(messages, CompilerMessage::getMessage));
    } else {
      assertEmpty(messages);
    }
  }

  public void testMakeInDependentModuleAfterChunkRebuild() throws IOException {
    PsiFile used = myFixture.addFileToProject("Used.groovy", "class Used { }");
    PsiFile java = myFixture.addFileToProject("Java.java", "class Java { void foo(Used used) {} }");
    VirtualFile main = myFixture.addFileToProject("Main.groovy", "class Main extends Java {  }").getVirtualFile();

    addGroovyLibrary(addDependentModule());

    PsiFile dep = myFixture.addFileToProject("dependent/Dep.java", "class Dep { }");

    assertEmpty("Expected zero compilation errors", make());

    setFileText(used, "class Used { String prop }");
    touch(main);
    setFileText(dep, "class Dep { String prop = new Used().getProp(); }");

    if (isRebuildExpectedAfterChangesInGroovyWhichUseJava()) {
      assertEquals(chunkRebuildMessage("Groovy stub generator"), ContainerUtil.map(make(), CompilerMessage::getMessage));
    } else {
      assertEmpty("Expected zero compilation errors", make());
    }
  }

  public void test_extend_package_private_class_from_another_module() {
    addGroovyLibrary(addDependentModule());

    myFixture.addClass("package foo; class Foo {}");
    myFixture.addFileToProject("dependent/foo/Bar.java", "package foo; class Bar extends Foo {}");
    myFixture.addFileToProject("dependent/foo/Goo.groovy", "package foo; class Goo extends Bar {}");

    assertEmpty("Expected zero compilation errors", make());
  }

  public void test_do_not_recompile_unrelated_files_after_breaking_compilation() throws IOException {
    PsiFile fooFile = myFixture.addFileToProject("Foo.groovy", "class Foo {}");
    myFixture.addFileToProject("Bar.groovy", "class Bar {}");
    assertEmpty("Expected zero compilation errors", make());

    File barCompiled = findClassFile("Bar");
    long barStamp = barCompiled.lastModified();

    setFileText(fooFile, "class Foo ext { }");
    GroovyCompilerTestCase.shouldFail(make());
    setFileText(fooFile, "interface Foo extends Runnable { }");
    assertEmpty("Expected zero compilation errors", make());

    assertEquals(barStamp, barCompiled.lastModified());
  }

  public void test_module_cycle() throws ExecutionException {
    final Module dep = addDependentModule();
    ModuleRootModificationUtil.addDependency(getModule(), dep);
    addGroovyLibrary(dep);

    myFixture.addFileToProject("Foo.groovy", "class Foo extends Bar { static void main(String[] args) { println \"Hello from Foo\" } }");
    myFixture.addFileToProject("FooX.java", "class FooX extends Bar { }");
    myFixture.addFileToProject("FooY.groovy", "class FooY extends BarX { }");
    myFixture.addFileToProject("dependent/Bar.groovy", "class Bar { Foo f; static void main(String[] args) { println 'Hello from Bar' } }");
    myFixture.addFileToProject("dependent/BarX.java", "class BarX { Foo f; }");
    myFixture.addFileToProject("dependent/BarY.groovy", "class BarY extends FooX { }");

    Runnable checkClassFiles = () -> {
      assertNotNull(findClassFile("Foo", getModule()));
      assertNotNull(findClassFile("FooX", getModule()));
      assertNotNull(findClassFile("Bar", dep));
      assertNotNull(findClassFile("BarX", dep));

      assertNull(findClassFile("Bar", getModule()));
      assertNull(findClassFile("BarX", getModule()));
      assertNull(findClassFile("Foo", dep));
      assertNull(findClassFile("FooX", dep));
    };

    assertEmpty(make());
    checkClassFiles.run();

    assertEmpty(make());
    checkClassFiles.run();

    assertOutput("Foo", "Hello from Foo", getModule());
    assertOutput("Bar", "Hello from Bar", dep);

    checkClassFiles.run();
  }

public void testCompileTimeConstants() throws ExecutionException {
    myFixture.addFileToProject("Gr.groovy", """
      class Gr {
        public static final String HELLO = "Hello"
        public static final int MAGIC = 239
        public static final Boolean BOOL = true
        public static final boolean bool = true
      }
    """);
    myFixture.addFileToProject("Main.java", """
      public class Main {
        public static void main(String[] args) {
          System.out.println(Gr.HELLO + ", " + Gr.BOOL + Gr.bool + Gr.MAGIC);
        }
      }
    """);
    make();
    assertOutput("Main", "Hello, truetrue239");
  }

  public void test_reporting_rebuild_errors_caused_by_missing_files_excluded_from_compilation() {
    PsiFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo {}");
    myFixture.addFileToProject("Bar.groovy", "class Bar extends Foo {}");

    make();

    excludeFromCompilation(foo);

    GroovyCompilerTestCase.shouldFail(rebuild());
  }

  public void test_compile_groovy_excluded_from_stub_generation() {
    PsiFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo {}");
    myFixture.addFileToProject("Bar.groovy", "class Bar extends Foo {}");

    excludeFromCompilation(GroovyCompilerConfiguration.getInstance(getProject()).getExcludeFromStubGeneration(), foo);

    assertEmpty("Expected zero compilation errors", make());
  }

  private void excludeFromCompilation(PsiFile foo) {
    excludeFromCompilation(CompilerConfiguration.getInstance(getProject()).getExcludedEntriesConfiguration(), foo);
  }

  private void excludeFromCompilation(ExcludesConfiguration configuration, PsiFile foo) {
    configuration.addExcludeEntryDescription(new ExcludeEntryDescription(foo.getVirtualFile(), false, true, myFixture.getTestRootDisposable()));
  }

  public void test_make_stub_level_error_and_correct_it() throws IOException {
    PsiFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo { }");
    myFixture.addFileToProject("Bar.java", "class Bar extends Foo {}");

    assertEmpty("Expected zero compilation errors", make());

    setFileText(foo, "class Foo implements Runnabl {}");

    String compilerTempRoot = BuildManager.getInstance().getProjectSystemDirectory(getProject()).getAbsolutePath();
    VfsRootAccess.allowRootAccess(getTestRootDisposable(),
                                  compilerTempRoot);//because compilation error points to file under 'groovyStubs' directory
    GroovyCompilerTestCase.shouldFail(make());

    setFileText(foo, "class Foo {}");

    assertEmpty("Expected zero compilation errors", make());
  }

  public void test_reporting_module_compile_errors_caused_by_missing_files_excluded_from_compilation() {
    PsiFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo {}");
    myFixture.addFileToProject("Bar.groovy", "class Bar extends Foo {}");

    make();

    excludeFromCompilation(foo);

    GroovyCompilerTestCase.shouldFail(compileModule(getModule()));
  }

  public void test_stubs_generated_while_processing_groovy_class_file_dependencies() throws IOException {
    PsiFile foo = myFixture.addFileToProject("Foo.groovy", "class Foo { }");
    PsiFile bar = myFixture.addFileToProject("Bar.groovy", "class Bar extends Foo { }");
    PsiFile client = myFixture.addFileToProject("Client.groovy", "class Client { Bar bar = new Bar() }");
    PsiFile java =
      myFixture.addFileToProject("Java.java", "class Java extends Client { String getName(Bar bar) { return bar.toString();  } }");

    assertEmpty("Expected zero compilation errors", make());

    setFileText(bar, "class Bar { }");

