// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.gradle.importing;

import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Segment;
import com.intellij.openapi.util.Trinity;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.PsiSearchHelper;
import com.intellij.psi.search.SearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariableDeclaration;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrOpenBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.junit.Test;
import org.junit.runners.Parameterized;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import static com.intellij.openapi.util.Pair.pair;
import static com.intellij.testFramework.EdtTestUtil.runInEdtAndGet;

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

  /**
   * It's sufficient to run the test against one gradle version
   */
  @SuppressWarnings("MethodOverridesStaticMethodOfSuperclass")
  @Parameterized.Parameters(name = "with Gradle-{0}")
  public static Collection<Object[]> data() {
    return Arrays.asList(new Object[][]{{BASE_GRADLE_VERSION}});
  }

  @Test
  public void testBuildSrcClassesUsages() throws Exception {
    createProjectSubFile("settings.gradle", "rootProject.name = 'multiproject'\n" +
                                            "include ':app'");

    createProjectSubFile("buildSrc/src/main/groovy/testBuildSrcClassesUsages/BuildSrcClass.groovy", "package testBuildSrcClassesUsages;\n" +
                                                                                                    "public class BuildSrcClass {" +
                                                                                                    "   public String sayHello() { 'Hello!' }" +
                                                                                                    "}");
    createProjectSubFile("app/build.gradle", "def foo = new testBuildSrcClassesUsages.BuildSrcClass().sayHello()");

    importProject();
    assertModules("multiproject", "multiproject.app",
                  "multiproject.buildSrc", "multiproject.buildSrc.main", "multiproject.buildSrc.test");

    Module buildSrcModule = getModule("multiproject.buildSrc.main");
    assertNotNull(buildSrcModule);
    assertUsages("testBuildSrcClassesUsages.BuildSrcClass", GlobalSearchScope.moduleScope(buildSrcModule), 1);
    assertUsages("testBuildSrcClassesUsages.BuildSrcClass", "sayHello", GlobalSearchScope.moduleScope(buildSrcModule), 1);
  }

  @Test
  public void testMultiModuleBuildSrcClassesUsages() throws Exception {
    createProjectSubFile("settings.gradle", "rootProject.name = 'multiproject'\n" +
                                            "include ':app'");
    // buildSrc module files
    createProjectSubFile("buildSrc/settings.gradle", "include 'buildSrcSubProject'");
    createProjectSubFile("buildSrc/build.gradle", """
      allprojects {
          apply plugin: 'groovy'
          dependencies {
              implementation gradleApi()
              implementation localGroovy()
          }
          repositories {
              mavenCentral()
          }
      
          if (it != rootProject) {
              rootProject.dependencies {
                  runtimeOnly project(path)
              }
          }
      }
      """);
    createProjectSubFile("buildSrc/src/main/groovy/testMultiModuleBuildSrcClassesUsages/BuildSrcClass.groovy",
                         "package testMultiModuleBuildSrcClassesUsages;\n" +
                         "public class BuildSrcClass {}");
    createProjectSubFile("buildSrc/buildSrcSubProject/src/main/java/testMultiModuleBuildSrcClassesUsages/BuildSrcAdditionalClass.java",
                         "package testMultiModuleBuildSrcClassesUsages;\n" +
                         "public class BuildSrcAdditionalClass {}");

    createProjectSubFile("build.gradle", "def foo = new testMultiModuleBuildSrcClassesUsages.BuildSrcClass()");
    createProjectSubFile("app/build.gradle", "def foo1 = new testMultiModuleBuildSrcClassesUsages.BuildSrcClass()\n" +
                                             "def foo2 = new testMultiModuleBuildSrcClassesUsages.BuildSrcAdditionalClass()");

    importProject();
    assertModules("multiproject", "multiproject.app",
                  "multiproject.buildSrc", "multiproject.buildSrc.main", "multiproject.buildSrc.test",
                  "multiproject.buildSrc.buildSrcSubProject", "multiproject.buildSrc.buildSrcSubProject.main", "multiproject.buildSrc.buildSrcSubProject.test");

    assertUsages(pair("testMultiModuleBuildSrcClassesUsages.BuildSrcClass", 2),
                 pair("testMultiModuleBuildSrcClassesUsages.BuildSrcAdditionalClass", 1));

    importProjectUsingSingeModulePerGradleProject();
    assertModules("multiproject", "multiproject.app",
                  "multiproject.buildSrc",
                  "multiproject.buildSrc.buildSrcSubProject");

    assertUsages(pair("testMultiModuleBuildSrcClassesUsages.BuildSrcClass", 2),
                 pair("testMultiModuleBuildSrcClassesUsages.BuildSrcAdditionalClass", 1));
  }

  @Test
  public void testIncludedBuildSrcClassesUsages_nonQN() throws Exception {
    createProjectWithIncludedBuildAndBuildSrcModules("testIncludedBuildSrcClassesUsages_nonQN");
    // check for non-qualified module names
    getCurrentExternalProjectSettings().setUseQualifiedModuleNames(false);

    importProject();
    assertModules("multiproject", "app",
                  "multiproject_multiproject-buildSrc", "multiproject_multiproject-buildSrc_main", "multiproject_multiproject-buildSrc_test",
                  "gradle-plugin", "gradle-plugin_test", "gradle-plugin_main",
                  "gradle-plugin_gradle-plugin-buildSrc", "gradle-plugin_gradle-plugin-buildSrc_main", "gradle-plugin_gradle-plugin-buildSrc_test");

    assertUsages("testIncludedBuildSrcClassesUsages_nonQN.BuildSrcClass", 2);
    assertUsages("testIncludedBuildSrcClassesUsages_nonQN.IncludedBuildSrcClass", 1);
    assertUsages("testIncludedBuildSrcClassesUsages_nonQN.IncludedBuildClass", 2);
  }

  @Test
  public void testBuildSrcLocalPropertyUsages() throws Exception {
    createProjectSubFile("settings.gradle", "rootProject.name = 'multiproject'\n" + "include ':app'");

    createProjectSubFile("buildSrc/src/main/groovy/testBuildSrcClassesUsages/BuildSrcClass.groovy", """
      package testBuildSrcClassesUsages;
      public class BuildSrcClass {\
         public String foo() { \
            def a = 0
            println a\
         }\
      }""");
    createProjectSubFile("app/build.gradle", "def srcClass = new testBuildSrcClassesUsages.BuildSrcClass()");

    importProject();
    assertModules("multiproject", "multiproject.app", "multiproject.buildSrc", "multiproject.buildSrc.main", "multiproject.buildSrc.test");

    Module buildSrcModule = getModule("multiproject.buildSrc.main");
    assertNotNull(buildSrcModule);

    ReadAction.run(() -> {
      PsiClass clazz = JavaPsiFacade.getInstance(getMyProject())
        .findClass("testBuildSrcClassesUsages.BuildSrcClass", GlobalSearchScope.moduleScope(buildSrcModule));

      PsiMethod[] methods = clazz.findMethodsByName("foo", false);
      assertSize(1, methods);
      assertTrue(methods[0] instanceof GrMethod);
      @Nullable GrOpenBlock body = ((GrMethod)methods[0]).getBlock();
      assertNotNull(body);
      GrStatement[] statements = body.getStatements();
      assertSize(2, statements);
      assertInstanceOf(statements[0], GrVariableDeclaration.class);
      GrVariable[] variables = ((GrVariableDeclaration)statements[0]).getVariables();
      assertSize(1, variables);
      PsiElement localProperty = variables[0];
      SearchScope useScope = PsiSearchHelper.getInstance(localProperty.getProject()).getUseScope(localProperty);
      assertInstanceOf(useScope, LocalSearchScope.class);
      assertNotNull(ContainerUtil.find(((LocalSearchScope)useScope).getScope(), e -> PsiTreeUtil.isAncestor(e, statements[1], false)));
    });
  }

  @Test
  public void testIncludedBuildSrcClassesUsages_merged_nonQN() throws Exception {
    createProjectWithIncludedBuildAndBuildSrcModules("testIncludedBuildSrcClassesUsages_merged_nonQN");
    // check for non-qualified module names
    getCurrentExternalProjectSettings().setUseQualifiedModuleNames(false);

    importProjectUsingSingeModulePerGradleProject();
    assertModules("multiproject", "app",
                  "multiproject_multiproject-buildSrc",
                  "gradle-plugin",
                  "gradle-plugin_gradle-plugin-buildSrc");
    assertUsages(pair("testIncludedBuildSrcClassesUsages_merged_nonQN.BuildSrcClass", 2),
                 pair("testIncludedBuildSrcClassesUsages_merged_nonQN.IncludedBuildSrcClass", 1));
    assertUsages("testIncludedBuildSrcClassesUsages_merged_nonQN.IncludedBuildClass", 2);
  }

  @Test
  public void testIncludedBuildSrcClassesUsages_qualified_names() throws Exception {
    createProjectWithIncludedBuildAndBuildSrcModules("testIncludedBuildSrcClassesUsages_qualified_names");
    importProject();
    assertModules("multiproject", "multiproject.app",
                  "multiproject.buildSrc", "multiproject.buildSrc.main", "multiproject.buildSrc.test",
                  "gradle-plugin", "gradle-plugin.test", "gradle-plugin.main",
                  "gradle-plugin.buildSrc", "gradle-plugin.buildSrc.main", "gradle-plugin.buildSrc.test");
    assertUsages(pair("testIncludedBuildSrcClassesUsages_qualified_names.BuildSrcClass", 2),
                 pair("testIncludedBuildSrcClassesUsages_qualified_names.IncludedBuildSrcClass", 1));
    assertUsages("testIncludedBuildSrcClassesUsages_qualified_names.IncludedBuildClass", 2);
  }

  @Test
  public void testIncludedBuildSrcClassesUsages_merged_qualified_names() throws Exception {
    createProjectWithIncludedBuildAndBuildSrcModules("testIncludedBuildSrcClassesUsages_merged_qualified_names");
    // check for qualified module names
    getCurrentExternalProjectSettings().setUseQualifiedModuleNames(true);
    importProjectUsingSingeModulePerGradleProject();
    assertModules("multiproject", "multiproject.app",
                  "multiproject.buildSrc",
                  "gradle-plugin",
                  "gradle-plugin.buildSrc");
    assertUsages(pair("testIncludedBuildSrcClassesUsages_merged_qualified_names.BuildSrcClass", 2),
                 pair("testIncludedBuildSrcClassesUsages_merged_qualified_names.IncludedBuildSrcClass", 1));
    assertUsages("testIncludedBuildSrcClassesUsages_merged_qualified_names.IncludedBuildClass", 2);
  }

  private void createProjectWithIncludedBuildAndBuildSrcModules(@NotNull String classPackage) throws IOException {
    createProjectSubFile("settings.gradle", """
      rootProject.name = 'multiproject'
      include ':app'
      includeBuild 'gradle-plugin'""");
    createProjectSubFile("buildSrc/src/main/groovy/" + classPackage + "/BuildSrcClass.groovy", "package " + classPackage + ";\n" +
                                                                                               "public class BuildSrcClass {}");

    createProjectSubFile("build.gradle", "buildscript {\n" +
                                         "    dependencies {\n" +
                                         "        classpath 'my.included:gradle-plugin:0'\n" +
                                         "    }\n" +
                                         "}\n" +
                                         "def foo1 = new " + classPackage + ".BuildSrcClass()\n" +
                                         "def foo2 = new " + classPackage + ".IncludedBuildClass()");
    createProjectSubFile("app/build.gradle", "def foo1 = new " + classPackage + ".BuildSrcClass()\n" +
                                             "def foo2 = new " + classPackage + ".IncludedBuildClass()");

    // included build
    createProjectSubFile("gradle-plugin/settings.gradle", "");
    createProjectSubFile("gradle-plugin/build.gradle", "group 'my.included'\n" +
                                                       "apply plugin: 'java'\n" +
                                                       "def foo = new " + classPackage + ".IncludedBuildSrcClass()");
    createProjectSubFile("gradle-plugin/buildSrc/src/main/groovy/" + classPackage + "/IncludedBuildSrcClass.groovy",
                         "package " + classPackage + ";\n" +
                         "public class IncludedBuildSrcClass {}");
    createProjectSubFile("gradle-plugin/src/main/java/" + classPackage + "/IncludedBuildClass.java",
                         "package " + classPackage + ";\n" +
                         "public class IncludedBuildClass {}");
  }

  private void assertUsages(String fqn, GlobalSearchScope scope, int count) throws Exception {
    assertUsages(fqn, null, scope, count);
  }

  private void assertUsages(@NotNull String fqn, @Nullable String methodName, GlobalSearchScope scope, int count) throws Exception {
    PsiClass[] psiClasses = runInEdtAndGet(() -> JavaPsiFacade.getInstance(getMyProject()).findClasses(fqn, scope));
    assertEquals(1, psiClasses.length);
    PsiClass aClass = psiClasses[0];
    if (methodName != null) {
      PsiMethod[] methods = runInEdtAndGet(() -> aClass.findMethodsByName(methodName, false));
      List<UsageInfo> actualUsages = new ArrayList<>();
      for (PsiMethod method : methods) {
        actualUsages.addAll(findUsages(method));
      }
      assertUsagesCount(count, actualUsages);
    }
    else {
      assertUsagesCount(count, aClass);
    }
  }

  private void assertUsages(String fqn, int count) throws Exception {
    assertUsages(fqn, GlobalSearchScope.projectScope(getMyProject()), count);
  }

  @SafeVarargs
  private void assertUsages(Trinity<String, GlobalSearchScope, Integer>... classUsageCount) throws Exception {
    for (Trinity<String, GlobalSearchScope, Integer> trinity : classUsageCount) {
      assertUsages(trinity.first, trinity.second, trinity.third);
    }
  }

  @SafeVarargs
  private void assertUsages(Pair<String, Integer>... classUsageCount) throws Exception {
    for (Pair<String, Integer> pair : classUsageCount) {
      assertUsages(Trinity.create(pair.first, GlobalSearchScope.projectScope(getMyProject()), pair.second));
    }
  }

  private static void assertUsagesCount(int expectedUsagesCount, PsiElement element) throws Exception {
    assertUsagesCount(expectedUsagesCount, findUsages(element));
  }

  private static void assertUsagesCount(int expectedUsagesCount, Collection<UsageInfo> usages) throws Exception {
    String message = "Found usges: " + runInEdtAndGet(() -> {
      StringBuilder buf = new StringBuilder();
      for (UsageInfo usage : usages) {
        buf.append(usage).append(", from ").append(usage.getVirtualFile().getPath());
        Segment navigationRange = usage.getNavigationRange();
        if (navigationRange != null) {
          buf.append(": ").append(usage.getNavigationRange().getStartOffset())
            .append(",").append(usage.getNavigationRange().getEndOffset());
        }
        buf.append(" \n");
      }

      return buf.toString();
    });
    assertEquals(message, expectedUsagesCount, usages.size());
  }
}
