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

import com.intellij.execution.executors.DefaultRunExecutor;
import com.intellij.execution.process.ProcessOutputType;
import com.intellij.idea.TestFor;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListener;
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode;
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemProgressNotificationManager;
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
import com.intellij.openapi.externalSystem.util.task.TaskExecutionSpec;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.libraries.LibraryEx;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.testFramework.RunAll;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.gradle.frameworkSupport.buildscript.GradleBuildScriptBuilderUtil;
import org.jetbrains.plugins.gradle.service.resolve.VersionCatalogsLocator;
import org.jetbrains.plugins.gradle.settings.GradleSystemSettings;
import org.jetbrains.plugins.gradle.tooling.annotation.TargetJavaVersion;
import org.jetbrains.plugins.gradle.tooling.annotation.TargetVersions;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.BiPredicate;

import static com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil.*;
import static com.intellij.openapi.util.text.StringUtil.*;
import static com.intellij.util.containers.ContainerUtil.ar;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.jetbrains.plugins.gradle.service.project.GradleProjectResolverUtil.getSourceSetName;

/**
 * @author Vladislav.Soroka
 */
public class GradleDependenciesImportingTest extends GradleImportingTestCase {

  @Override
  public void importProject(@NonNls String config) throws IOException {
    boolean useConventions = isGradleOlderThan("8.2");
    config += "\n\n def useConventions = " + useConventions + "\n" + """
      allprojects {
        afterEvaluate {
          if((useConventions && convention.findPlugin(JavaPluginConvention) != null)
           || (!useConventions && extensions.findByType(JavaPluginExtension) != null)) {
            sourceSets.each { SourceSet sourceSet ->
              tasks.create(name: 'print'+ sourceSet.name.capitalize() +'CompileDependencies') {
                doLast { println sourceSet.compileClasspath.files.collect {it.name}.join(' ') }
              }
            }
          }
        }
      }
      """;
    super.importProject(config);
  }

  @Override
  public void tearDown() throws Exception {
    new RunAll(
      () -> GradleSystemSettings.getInstance().setDownloadSources(false),
      () -> super.tearDown()
    ).run();
  }

  protected void assertCompileClasspathOrdering(String moduleName) {
    assertCompileClasspathOrdering(moduleName, false);
  }

  protected void maybeAssertCompileClasspathOrderingWithEnabledClasspathPackaging(String moduleName) {
    assertCompileClasspathOrdering(moduleName, true);
  }

  private void assertCompileClasspathOrdering(String moduleName, boolean useCompileClasspathPackaging) {
    Module module = getModule(moduleName);
    String sourceSetName = getSourceSetName(module);
    assertNotNull("Can not find the sourceSet for the module", sourceSetName);

    ExternalSystemTaskExecutionSettings settings = new ExternalSystemTaskExecutionSettings();
    settings.setExternalProjectPath(getExternalProjectPath(module));
    String id = getExternalProjectId(module);
    String gradlePath = id.startsWith(":") ? trimEnd(trimEnd(id, sourceSetName), ":") : "";
    settings.setTaskNames(Collections.singletonList(gradlePath + ":print" + capitalize(sourceSetName) + "CompileDependencies"));
    settings.setExternalSystemIdString(GradleConstants.SYSTEM_ID.getId());
    settings.setScriptParameters("--quiet");

    if (useCompileClasspathPackaging) {
      // use jars instead of class folders for everything on the compile classpath
      // https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_classes_usage
      if (isGradleOlderThan("5.6.1")) {
        return;
      }
      settings.setVmOptions("-Dorg.gradle.java.compile-classpath-packaging=true");
    }
    ExternalSystemProgressNotificationManager notificationManager =
      ApplicationManager.getApplication().getService(ExternalSystemProgressNotificationManager.class);
    StringBuilder gradleClasspath = new StringBuilder();
    ExternalSystemTaskNotificationListener listener = new ExternalSystemTaskNotificationListener() {
      @Override
      public void onTaskOutput(@NotNull ExternalSystemTaskId id, @NotNull String text, @NotNull ProcessOutputType processOutputType) {
        if (!processOutputType.isStdout() || text.isBlank()) return;
        if (text.contains("Gradle Daemon")) return;
        gradleClasspath.append(text);
      }
    };
    notificationManager.addNotificationListener(listener);
    try {
      ExternalSystemUtil.runTask(settings, DefaultRunExecutor.EXECUTOR_ID, getMyProject(), GradleConstants.SYSTEM_ID, null,
                                 ProgressExecutionMode.NO_PROGRESS_SYNC);
    }
    finally {
      notificationManager.removeNotificationListener(listener);
    }

    List<String> ideClasspath = new ArrayList<>();
    ModuleRootManager.getInstance(module).orderEntries().withoutSdk().withoutModuleSourceEntries().compileOnly().productionOnly().forEach(
      entry -> {
        if (entry instanceof ModuleOrderEntry) {
          Module moduleDep = ((ModuleOrderEntry)entry).getModule();
          String sourceSetDepName = getSourceSetName(moduleDep);
          // for simplicity, only project dependency on 'default' configuration allowed here
          assert "main".equals(sourceSetDepName);

          String gradleProjectDepName = trimStart(trimEnd(getExternalProjectId(moduleDep), ":main"), ":");
          String version = getExternalProjectVersion(moduleDep);
          version = "unspecified".equals(version) ? "" : "-" + version;
          ideClasspath.add(gradleProjectDepName + version + ".jar");
        }
        else {
          ideClasspath.add(entry.getFiles(OrderRootType.CLASSES)[0].getName());
        }
        return true;
      });

    assertEquals(join(ideClasspath, " "), gradleClasspath.toString().trim());
  }

  @Test
  public void testModuleDependencies() throws IOException {
    createSettingsFile(including("project1", "project2"));
    createProjectSubFile("project1/build.gradle", script(it -> it.withJavaPlugin()
      .addImplementationDependency(it.project(":"))));
    createProjectSubFile("project2/build.gradle", script(it -> it.withJavaPlugin()
      .addImplementationDependency(it.project(":project1"))));
    importProject(script(it -> it.withJavaPlugin()));

    assertModules("project", "project.main", "project.test",
                  "project.project1", "project.project1.main", "project.project1.test",
                  "project.project2", "project.project2.main", "project.project2.test");

    assertModuleModuleDeps("project.main");
    assertModuleModuleDeps("project.test", "project.main");
    assertModuleModuleDeps("project.project1.main", "project.main");
    assertModuleModuleDeps("project.project1.test", "project.project1.main", "project.main");
    assertModuleModuleDeps("project.project2.main", "project.project1.main", "project.main");
    assertModuleModuleDeps("project.project2.test", "project.project2.main", "project.project1.main", "project.main");
  }

  @Test
  public void testDependencyScopeMerge() throws Exception {
    createSettingsFile(including("api", "impl"));

    importProject(script(it -> {
      it.allprojects(p -> {
          p.withJavaPlugin()
            .withMavenCentral();
        })
        .addImplementationDependency(it.project(":api"))
        .addTestImplementationDependency(it.project(":impl"))
        .addTestImplementationDependency("junit:junit:4.11")
        .addRuntimeOnlyDependency(it.project(":impl"));
    }));

    assertModules("project", "project.main", "project.test",
                  "project.api", "project.api.main", "project.api.test",
                  "project.impl", "project.impl.main", "project.impl.test");
    assertModuleModuleDepScope("project.test", "project.main", DependencyScope.COMPILE);
    assertModuleModuleDepScope("project.api.test", "project.api.main", DependencyScope.COMPILE);
    assertModuleModuleDepScope("project.impl.test", "project.impl.main", DependencyScope.COMPILE);

    assertModuleModuleDepScope("project.main", "project.api.main", DependencyScope.COMPILE);

    assertModuleModuleDepScope("project.main", "project.impl.main", DependencyScope.RUNTIME);
    assertModuleModuleDepScope("project.test", "project.impl.main", DependencyScope.COMPILE);

    assertModuleLibDepScope("project.test", "Gradle: org.hamcrest:hamcrest-core:1.3", DependencyScope.COMPILE);
    assertModuleLibDepScope("project.test", "Gradle: junit:junit:4.11", DependencyScope.COMPILE);

    assertCompileClasspathOrdering("project.main");

    importProjectUsingSingeModulePerGradleProject();
    assertModules("project", "project.api", "project.impl");

    assertModuleModuleDepScope("project", "project.impl", DependencyScope.RUNTIME, DependencyScope.TEST);

    assertModuleLibDepScope("project", "Gradle: org.hamcrest:hamcrest-core:1.3", DependencyScope.TEST);
    assertModuleLibDepScope("project", "Gradle: junit:junit:4.11", DependencyScope.TEST);
  }

  @Test
  @TargetVersions(BASE_GRADLE_VERSION)
  public void testSetExternalSourceForExistingLibrary() throws IOException {
    String libraryName = "Gradle: junit:junit:" + GradleBuildScriptBuilderUtil.getJunit4Version();
    WriteAction.runAndWait(() -> {
      LibraryTable.ModifiableModel model = LibraryTablesRegistrar.getInstance().getLibraryTable(getMyProject()).getModifiableModel();
      model.createLibrary(libraryName);
      model.commit();
    });

    importProject(createBuildScriptBuilder()
                    .withJavaPlugin()
                    .withJUnit4()
                    .generate());
    assertModules("project", "project.main", "project.test");
    Library library = assertSingleLibraryOrderEntry("project.test", libraryName).getLibrary();
    assertNotNull(library);
    assertNotNull(library.getExternalSource());
  }
  
  @Test
  @TargetVersions("<=6.9")
  public void testTransitiveNonTransitiveDependencyScopeMerge() throws Exception {
    createSettingsFile(including("project1", "project2"));

    importProject(
      createBuildScriptBuilder()
        .subprojects(it -> {
          it.withJavaPlugin()
            .withMavenCentral();
        })
        .project(":project1", it -> {
          it.addImplementationDependency("junit:junit:4.11");
        })
        .project(":project2", it -> {
          it.addPostfix("""
            dependencies.ext.strict = { projectPath ->
            dependencies.compile dependencies.project(path: projectPath, transitive: false)
            dependencies.runtime dependencies.project(path: projectPath, transitive: true)
            dependencies.testRuntime dependencies.project(path: projectPath, transitive: true)
          }
          
          dependencies {
            strict ':project1'
          }
          """);})
        .generate()
    );

    assertModules("project",
                  "project.project1", "project.project1.main", "project.project1.test",
                  "project.project2", "project.project2.main", "project.project2.test");

    assertModuleModuleDeps("project.project2.main", "project.project1.main");
    assertModuleModuleDepScope("project.project2.main", "project.project1.main", DependencyScope.COMPILE);
    assertModuleLibDepScope("project.project2.main", "Gradle: org.hamcrest:hamcrest-core:1.3", DependencyScope.RUNTIME);
    assertModuleLibDepScope("project.project2.main", "Gradle: junit:junit:4.11", DependencyScope.RUNTIME);

    importProjectUsingSingeModulePerGradleProject();
    assertModules("project", "project.project1", "project.project2");
    assertMergedModuleCompileModuleDepScope("project.project2", "project.project1");
    assertModuleLibDepScope("project.project2", "Gradle: org.hamcrest:hamcrest-core:1.3",
                            ar(DependencyScope.RUNTIME, DependencyScope.TEST));
    assertModuleLibDepScope("project.project2", "Gradle: junit:junit:4.11",
                            ar(DependencyScope.RUNTIME, DependencyScope.TEST));
  }

  @Test
  public void testProvidedDependencyScopeMerge() throws Exception {
    createSettingsFile(including("web", "user"));

    importProject(
      createBuildScriptBuilder()
        .subprojects(it -> {
          it
            .withMavenCentral()
            .withJavaPlugin()
            .addPostfix("configurations { provided }");
        })
        .project(":web", it -> { it.addDependency("provided", "junit:junit:4.11"); })
        .project(":user", it -> {
          it
            .applyPlugin("war")
            .addImplementationDependency(it.project(":web"))
            .addDependency("providedCompile", it.project(":web", "provided"));
        })
        .generate()
    );

    assertModules("project",
                  "project.web", "project.web.main", "project.web.test",
                  "project.user", "project.user.main", "project.user.test");

    assertModuleLibDeps("project.web");
    assertModuleLibDeps("project.web.main");
    assertModuleLibDeps("project.web.test");

    assertModuleModuleDeps("project.user.main", "project.web.main");
    assertModuleModuleDepScope("project.user.main", "project.web.main", DependencyScope.COMPILE);
    assertModuleLibDepScope("project.user.main", "Gradle: org.hamcrest:hamcrest-core:1.3", DependencyScope.PROVIDED);
    assertModuleLibDepScope("project.user.main", "Gradle: junit:junit:4.11", DependencyScope.PROVIDED);

    createProjectSubDirs("web", "user");
    assertCompileClasspathOrdering("project.user.main");
  }

  @Test
  public void testCustomSourceSetsDependencies() throws Exception {
    createSettingsFile(including("api", "impl"));

    importProject(script(it -> {
      it.allprojects( p -> {
        p.withJavaPlugin()
          .withMavenCentral();
        })
        .project("impl", p -> {
          p.addPrefix("sourceSets {",
                      "  myCustomSourceSet",
                      "  myAnotherSourceSet",
                      "}")
            .addImplementationDependency(it.code("sourceSets.main.output"), "myCustomSourceSet")
            .addImplementationDependency(it.project(":api"), "myCustomSourceSet")
            .addRuntimeOnlyDependency("junit:junit:4.11", "myCustomSourceSet");
        });
    }));

    assertModules("project", "project.main", "project.test",
                  "project.api", "project.api.main", "project.api.test",
                  "project.impl", "project.impl.main", "project.impl.test",
                  "project.impl.myCustomSourceSet", "project.impl.myAnotherSourceSet");

    assertModuleModuleDepScope("project.test", "project.main", DependencyScope.COMPILE);
    assertModuleModuleDepScope("project.api.test", "project.api.main", DependencyScope.COMPILE);
    assertModuleModuleDepScope("project.impl.test", "project.impl.main", DependencyScope.COMPILE);

    assertModuleModuleDepScope("project.impl.myCustomSourceSet", "project.impl.main", DependencyScope.COMPILE);
    assertModuleModuleDepScope("project.impl.myCustomSourceSet", "project.api.main", DependencyScope.COMPILE);
    assertModuleLibDepScope("project.impl.myCustomSourceSet", "Gradle: org.hamcrest:hamcrest-core:1.3", DependencyScope.RUNTIME);
    assertModuleLibDepScope("project.impl.myCustomSourceSet", "Gradle: junit:junit:4.11", DependencyScope.RUNTIME);
  }

  @Test
  public void testDependencyWithDifferentClassifiers() throws Exception {
    final VirtualFile depJar = createProjectJarSubFile("lib/dep/dep/1.0/dep-1.0.jar");
    final VirtualFile depTestsJar = createProjectJarSubFile("lib/dep/dep/1.0/dep-1.0-tests.jar");
    final VirtualFile depNonJar = createProjectSubFile("lib/dep/dep/1.0/dep-1.0.someExt");

    createProjectSubFile("lib/dep/dep/1.0/dep-1.0.pom", """
      <?xml version="1.0" encoding="UTF-8"?>
      <project
        xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
        <groupId>dep</groupId>
        <artifactId>dep</artifactId>
        <version>1.0</version>
      
      </project>
      """);
    importProject(
      createBuildScriptBuilder()
        .withJavaPlugin()
        .addRepository("maven { url = file('lib') }")
        .addImplementationDependency("dep:dep:1.0")
        .addTestImplementationDependency("dep:dep:1.0:tests")
        .addRuntimeOnlyDependency("dep:dep:1.0@someExt")
        .generate()
    );

    assertModules("project", "project.main", "project.test");

    assertModuleModuleDepScope("project.test", "project.main", DependencyScope.COMPILE);

    final String depName = "Gradle: dep:dep:1.0";
    assertModuleLibDep("project.main", depName, depJar.getUrl());
    assertModuleLibDepScope("project.main", depName, DependencyScope.COMPILE);
    assertModuleLibDep("project.test", depName, depJar.getUrl());
    assertModuleLibDepScope("project.test", depName, DependencyScope.COMPILE);

    final String depTestsName = "Gradle: dep:dep:tests:1.0";
    assertModuleLibDep("project.test", depTestsName, depTestsJar.getUrl());
    assertModuleLibDepScope("project.test", depTestsName, DependencyScope.COMPILE);

    final String depNonJarName = "Gradle: dep:dep:someExt:1.0";
    assertModuleLibDep("project.main", depNonJarName, depNonJar.getUrl());
    assertModuleLibDepScope("project.main", depNonJarName, DependencyScope.RUNTIME);
    assertModuleLibDep("project.test", depNonJarName, depNonJar.getUrl());
    assertModuleLibDepScope("project.test", depNonJarName, DependencyScope.RUNTIME);

    importProjectUsingSingeModulePerGradleProject();
    assertModules("project");

    assertModuleLibDep("project", depName, depJar.getUrl());
    assertMergedModuleCompileLibDepScope("project", depName);

    assertModuleLibDep("project", "Gradle: dep:dep:1.0:tests", depTestsJar.getUrl());
    assertModuleLibDepScope("project", "Gradle: dep:dep:1.0:tests", DependencyScope.TEST);

    assertModuleLibDep("project", "Gradle: dep:dep:1.0:someExt", depNonJar.getUrl());
    assertModuleLibDepScope("project", "Gradle: dep:dep:1.0:someExt", DependencyScope.RUNTIME, DependencyScope.TEST);
  }


  @Test
  public void testGlobalFileDepsImportedAsProjectLibraries() throws Exception {
    final VirtualFile depJar = createProjectJarSubFile("lib/dep.jar");
    final VirtualFile dep2Jar = createProjectJarSubFile("lib_other/dep.jar");
    createSettingsFile(including("p1", "p2"));

    importProjectUsingSingeModulePerGradleProject(
      createBuildScriptBuilder()
        .allprojects(p -> {
          p
            .withJavaPlugin()
            .addImplementationDependency(p.code("rootProject.files('lib/dep.jar', 'lib_other/dep.jar')"));
        })
        .generate());

    assertModules("project", "project.p1", "project.p2");
    Set<Library> libs = new HashSet<>();
    final List<LibraryOrderEntry> moduleLibDeps = getModuleLibDeps("project.p1", "Gradle: dep");
    moduleLibDeps.addAll(getModuleLibDeps("project.p1", "Gradle: dep_1"));
    moduleLibDeps.addAll(getModuleLibDeps("project.p2", "Gradle: dep"));
    moduleLibDeps.addAll(getModuleLibDeps("project.p2", "Gradle: dep_1"));
    for (LibraryOrderEntry libDep : moduleLibDeps) {
      libs.add(libDep.getLibrary());
      assertFalse("Dependency be project level: " + libDep, libDep.isModuleLevel());
    }

    assertProjectLibraries("Gradle: dep", "Gradle: dep_1");
    assertEquals("No duplicates of libraries are expected", 2, libs.size());
    assertContain(ContainerUtil.map(libs, l -> l.getUrls(OrderRootType.CLASSES)[0]), depJar.getUrl(), dep2Jar.getUrl());
  }

  @Test
  public void testLocalFileDepsImportedAsModuleLibraries() throws Exception {
    final VirtualFile depP1Jar = createProjectJarSubFile("p1/lib/dep.jar");
    final VirtualFile depP2Jar = createProjectJarSubFile("p2/lib/dep.jar");
    createSettingsFile(including("p1", "p2"));

    importProjectUsingSingeModulePerGradleProject(createBuildScriptBuilder()
                                                    .allprojects(p -> {
                                                      p
                                                        .withJavaPlugin()
                                                        .addImplementationDependency(p.code("files('lib/dep.jar')"));
                                                    })
                                                    .generate());

    assertModules("project", "project.p1", "project.p2");

    final List<LibraryOrderEntry> moduleLibDepsP1 = getModuleLibDeps("project.p1", "Gradle: dep");
    for (LibraryOrderEntry libDep : moduleLibDepsP1) {
      assertTrue("Dependency must be module level: " + libDep.toString(), libDep.isModuleLevel());
      assertEquals("Wrong library dependency", depP1Jar.getUrl(), libDep.getLibrary().getUrls(OrderRootType.CLASSES)[0]);
    }

    final List<LibraryOrderEntry> moduleLibDepsP2 = getModuleLibDeps("project.p2", "Gradle: dep");
    for (LibraryOrderEntry libDep : moduleLibDepsP2) {
      assertTrue("Dependency must be module level: " + libDep.toString(), libDep.isModuleLevel());
      assertEquals("Wrong library dependency", depP2Jar.getUrl(), libDep.getLibrary().getUrls(OrderRootType.CLASSES)[0]);
    }
  }

  @Test
  public void testProjectWithUnresolvedDependency() throws Exception {
    final VirtualFile depJar = createProjectJarSubFile("lib/dep/dep/1.0/dep-1.0.jar");
    createProjectSubFile("lib/dep/dep/1.0/dep-1.0.pom", """
      <?xml version="1.0" encoding="UTF-8"?>
      <project
        xmlns="http://maven.apache.org/POM/4.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
        <groupId>dep</groupId>
        <artifactId>dep</artifactId>
        <version>1.0</version>
      
      </project>
      """);
    importProject(
      createBuildScriptBuilder()
        .withJavaPlugin()
        .addRepository("maven { url = file('lib') }")
        .addImplementationDependency("dep:dep:1.0")
        .addImplementationDependency("some:unresolvable-lib:0.1")
        .generate()
    );

    assertModules("project", "project.main", "project.test");

    final String depName = "Gradle: dep:dep:1.0";
    assertModuleLibDep("project.main", depName, depJar.getUrl());
    assertModuleLibDepScope("project.main", depName, DependencyScope.COMPILE);
    assertModuleLibDepScope("project.main", "Gradle: some:unresolvable-lib:0.1", DependencyScope.COMPILE);

    List<LibraryOrderEntry> unresolvableDep = getModuleLibDeps("project.main", "Gradle: some:unresolvable-lib:0.1");
    assertEquals(1, unresolvableDep.size());
    LibraryOrderEntry unresolvableEntry = unresolvableDep.iterator().next();
    assertEquals(DependencyScope.COMPILE, unresolvableEntry.getScope());
    assertEmpty(unresolvableEntry.getRootUrls(OrderRootType.CLASSES));

    assertModuleLibDep("project.test", depName, depJar.getUrl());
    assertModuleLibDepScope("project.test", depName, DependencyScope.COMPILE);

    importProjectUsingSingeModulePerGradleProject();
    assertModules("project");

    assertModuleLibDep("project", depName, depJar.getUrl());
    assertMergedModuleCompileLibDepScope("project", depName);
    assertMergedModuleCompileLibDepScope("project", "Gradle: some:unresolvable-lib:0.1");

    unresolvableDep = getModuleLibDeps("project", "Gradle: some:unresolvable-lib:0.1");
    assertEquals(1, unresolvableDep.size());
    unresolvableEntry = unresolvableDep.iterator().next();
    assertTrue(unresolvableEntry.isModuleLevel());
    assertEquals(DependencyScope.COMPILE, unresolvableEntry.getScope());
    assertEmpty(unresolvableEntry.getRootUrls(OrderRootType.CLASSES));
  }

  @Test
  public void testSourceSetOutputDirsAsArtifactDependencies() throws Exception {
    createSettingsFile("""
                         rootProject.name = 'server'
                         """ + including("api", "modules:X", "modules:Y"));
    importProject(
      "configure(subprojects - project(':modules')) {\n" +
      "    group = 'server'\n" +
      "    version = '1.0-SNAPSHOT'\n" +
      "    apply plugin: 'java'\n" +
      (isGradleAtLeast("8.2")
       ? "  java { sourceCompatibility = 1.8 }\n"
       : "    sourceCompatibility = 1.8\n") +
      "}\n" +
      "\n" +
      "project(':api') {\n" +
      "    sourceSets {\n" +
      "        webapp\n" +
      "    }\n" +
      "    configurations {\n" +
      "        webappConf {\n" +
      "            afterEvaluate {\n" +
      "                sourceSets.webapp.output.each {\n" +
      "                    outgoing.artifact(it) {\n" +
      "                        builtBy(sourceSets.webapp.output)\n" +
      "                    }\n" +
      "                }\n" +
      "            }\n" +
      "        }\n" +
      "    }\n" +
      "}\n" +
      "\n" +
      "def webProjects = [project(':modules:X'), project(':modules:Y')]\n" +
      "configure(webProjects) {\n" +
      "    dependencies {\n" +
      "        implementation project(path: ':api', configuration: 'webappConf')\n" +
      "    }\n" +
      "}"
    );

    assertModules("server", "server.modules",
                  "server.modules.X", "server.modules.X.main", "server.modules.X.test",
                  "server.modules.Y", "server.modules.Y.main", "server.modules.Y.test",
                  "server.api", "server.api.main", "server.api.test", "server.api.webapp");

    assertModuleModuleDeps("server.modules.X.main", "server.api.webapp");
    assertModuleModuleDepScope("server.modules.X.main", "server.api.webapp", DependencyScope.COMPILE);

    assertModuleModuleDeps("server.modules.Y.main", "server.api.webapp");
    assertModuleModuleDepScope("server.modules.Y.main", "server.api.webapp", DependencyScope.COMPILE);
  }

  @Test
  public void testSourceSetOutputDirsAsRuntimeDependencies() throws Exception {
    importProject(
      "apply plugin: 'java'\n" +
      "sourceSets.main.output.dir file(\"$buildDir/generated-resources/main\")"
    );

    assertModules("project", "project.main", "project.test");
    final String path = path("build/generated-resources/main");
    final String depName = PathUtil.toPresentableUrl(path);
    String root = "file://" + path;
    assertModuleLibDep("project.main", depName, root);
    assertModuleLibDepScope("project.main", depName, DependencyScope.RUNTIME);

    String[] excludedRoots = new String[]{root};
    assertLibraryExcludedRoots("project.main", depName, excludedRoots);
    assertLibraryExcludedRoots("project.test", depName, excludedRoots);

    VirtualFile depJar = createProjectJarSubFile("lib/dep.jar");
    TestGradleBuildScriptBuilder builder = createBuildScriptBuilder();
    importProject(
      builder.withJavaPlugin()
        .withMavenCentral()
        .addPrefix("sourceSets.main.output.dir file(\"$buildDir/generated-resources/main\")")
        .addRuntimeOnlyDependency("junit:junit:4.11")
        .addRuntimeOnlyDependency(builder.code("files('lib/dep.jar')"))
        .generate()
    );

    assertLibraryExcludedRoots("project.main", depName, excludedRoots);
    assertLibraryExcludedRoots("project.test", depName, excludedRoots);
    assertLibraryExcludedRoots("project.main", convertToLibraryName(depJar), ArrayUtil.EMPTY_STRING_ARRAY);
    assertLibraryExcludedRoots("project.main", "Gradle: junit:junit:4.11", ArrayUtil.EMPTY_STRING_ARRAY);
  }

  private void assertLibraryExcludedRoots(String moduleName, String depName, String ... roots) {
    List<LibraryOrderEntry> deps = getModuleLibDeps(moduleName, depName);
    assertThat(deps).hasSize(1);
    LibraryEx library = (LibraryEx)deps.get(0).getLibrary();

    assertThat(library.getUrls(OrderRootType.CLASSES)).hasSize(1);

    String[] excludedRootUrls = library.getExcludedRootUrls();
    assertThat(excludedRootUrls).containsExactly(roots);
  }

  @Test
  public void testSourceSetOutputDirsAsRuntimeDependenciesOfDependantModules() throws Exception {
    createSettingsFile(including("projectA", "projectB", "projectC"));
    importProject(
      createBuildScriptBuilder()
        .project(":projectA", it -> {
          it
            .withJavaPlugin()
            .addPostfix("sourceSets.main.output.dir file(\"$buildDir/generated-resources/main\")");
        })
        .project(":projectB", it -> {
          it
            .withJavaPlugin()
            .addImplementationDependency(it.project(":projectA"));
        })
        .project(":projectC", it -> {
          it
            .withJavaPlugin()
            .addRuntimeOnlyDependency(it.project(":projectB"));
        })
        .generate()
    );

    assertModules("project",
                  "project.projectA", "project.projectA.main", "project.projectA.test",
                  "project.projectB", "project.projectB.main", "project.projectB.test",
                  "project.projectC", "project.projectC.main", "project.projectC.test");

    assertModuleModuleDepScope("project.projectB.main", "project.projectA.main", DependencyScope.COMPILE);
    assertModuleModuleDepScope("project.projectC.main", "project.projectA.main", DependencyScope.RUNTIME);
    assertModuleModuleDepScope("project.projectC.main", "project.projectB.main", DependencyScope.RUNTIME);

    final String path = path("projectA/build/generated-resources/main");
    final String classesPath = "file://" + path;
    final String depName = PathUtil.toPresentableUrl(path);
    assertModuleLibDep("project.projectA.main", depName, classesPath);
    assertModuleLibDepScope("project.projectA.main", depName, DependencyScope.RUNTIME);
    assertModuleLibDep("project.projectB.main", depName, classesPath);
    assertModuleLibDepScope("project.projectB.main", depName, DependencyScope.RUNTIME);
    assertModuleLibDep("project.projectC.main", depName, classesPath);
    assertModuleLibDepScope("project.projectC.main", depName, DependencyScope.RUNTIME);
  }

  @Test
  public void testSourceSetOutputDirsAsDependenciesOfDependantModules() throws Exception {
    createSettingsFile(including("projectA", "projectB", "projectC"));
    importProject(
      """
        subprojects {\s
            apply plugin: "java"\s
        }
        project(':projectA') {
          sourceSets.main.output.dir file('generated/projectA')
        }
        project(':projectB') {
          sourceSets.main.output.dir file('generated/projectB')
          dependencies {
            implementation project(':projectA')
          }
        }
        project(':projectC') {
          dependencies {
            implementation project(':projectB')
          }
        }"""
    );

    assertModules("project",
                  "project.projectA", "project.projectA.main", "project.projectA.test",
                  "project.projectB", "project.projectB.main", "project.projectB.test",
                  "project.projectC", "project.projectC.main", "project.projectC.test");

