package com.jetbrains.lang.dart.ide.runner.util;
import com.google.common.annotations.VisibleForTesting;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.intellij.execution.Location;
import com.intellij.execution.PsiLocation;
import com.intellij.execution.testframework.sm.runner.SMTestLocator;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.lang.dart.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DartTestLocationProvider implements SMTestLocator, DumbAware {
private static final List<Location> NONE = Collections.emptyList();
private static final Gson GSON = new Gson();
public static final DartTestLocationProvider INSTANCE = new DartTestLocationProvider();
public static final Type STRING_LIST_TYPE = new TypeToken<List<String>>() {}.getType();
@NotNull
@Override
public List<Location> getLocation(@NotNull String protocol,
@NotNull String path,
@NotNull Project project,
@NotNull GlobalSearchScope scope) {
// see DartTestEventsConverter.addLocationHint()
// path is like /Users/x/projects/foo/test/foo_test.dart,35,12,["main tests","calculate_fail"]
int commaIdx1 = path.indexOf(',');
int commaIdx2 = path.indexOf(',', commaIdx1 + 1);
int commaIdx3 = path.indexOf(',', commaIdx2 + 1);
if (commaIdx3 < 0) return NONE;
final String filePath = path.substring(0, commaIdx1);
final int line = Integer.parseInt(path.substring(commaIdx1 + 1, commaIdx2));
final int column = Integer.parseInt(path.substring(commaIdx2 + 1, commaIdx3));
final String names = path.substring(commaIdx3 + 1);
final VirtualFile file = LocalFileSystem.getInstance().findFileByPath(filePath);
final PsiFile psiFile = file == null ? null : PsiManager.getInstance(project).findFile(file);
if (!(psiFile instanceof DartFile)) return NONE;
if (line >= 0 && column >= 0) {
final Location<PsiElement> location = getLocationByLineAndColumn(psiFile, line, column);
if (location != null) {
return Collections.singletonList(location);
}
}
final List<String> nodes = pathToNodes(names);
if (nodes.isEmpty()) {
return Collections.singletonList(new PsiLocation<PsiElement>(psiFile));
}
return getLocationByGroupAndTestNames(psiFile, nodes);
}
@Nullable
private static Location<PsiElement> getLocationByLineAndColumn(@NotNull final PsiFile file, final int line, final int column) {
final Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file);
if (document == null) return null;
final int offset = document.getLineStartOffset(line) + column;
final PsiElement element = file.findElementAt(offset);
final PsiElement parent1 = element == null ? null : element.getParent();
final PsiElement parent2 = parent1 instanceof DartId ? parent1.getParent() : null;
final PsiElement parent3 = parent2 instanceof DartReferenceExpression ? parent2.getParent() : null;
if (parent3 instanceof DartCallExpression) {
if (TestUtil.isTest((DartCallExpression)parent3) || TestUtil.isGroup((DartCallExpression)parent3)) {
return new PsiLocation<>(parent3);
}
}
return null;
}
private static List<String> pathToNodes(final String element) {
return GSON.fromJson(element, STRING_LIST_TYPE);
}
@VisibleForTesting
public List<Location> getLocationForTest(@NotNull final PsiFile psiFile, @NotNull final String testPath) {
return getLocationByGroupAndTestNames(psiFile, pathToNodes(testPath));
}
protected List<Location> getLocationByGroupAndTestNames(final PsiFile psiFile, final List<String> nodes) {
final List<Location> locations = new ArrayList<>();
if (psiFile instanceof DartFile && !nodes.isEmpty()) {
PsiElementProcessor<PsiElement> collector = new PsiElementProcessor<PsiElement>() {
@Override
public boolean execute(@NotNull final PsiElement element) {
if (element instanceof DartCallExpression) {
DartCallExpression expression = (DartCallExpression)element;
if (TestUtil.isTest(expression) || TestUtil.isGroup(expression)) {
if (nodes.get(nodes.size() - 1).equals(getTestLabel(expression))) {
boolean matches = true;
for (int i = nodes.size() - 2; i >= 0 && matches; --i) {
expression = getGroup(expression);
if (expression == null || !nodes.get(i).equals(getTestLabel(expression))) {
matches = false;
}
}
if (matches) {
locations.add(new PsiLocation<>(element));
return false;
}
}
}
}
return true;
}
@Nullable
private DartCallExpression getGroup(final DartCallExpression expression) {
return (DartCallExpression)PsiTreeUtil.findFirstParent(expression, true,
element -> element instanceof DartCallExpression &&
TestUtil.isGroup((DartCallExpression)element));
}
};
PsiTreeUtil.processElements(psiFile, collector);
}
return locations;
}
@Nullable
public static String getTestLabel(@NotNull final DartCallExpression testCallExpression) {
final DartArguments arguments = testCallExpression.getArguments();
final DartArgumentList argumentList = arguments == null ? null : arguments.getArgumentList();
final List<DartExpression> argExpressions = argumentList == null ? null : argumentList.getExpressionList();
return argExpressions != null && !argExpressions.isEmpty() && argExpressions.get(0) instanceof DartStringLiteralExpression
? StringUtil.unquoteString(argExpressions.get(0).getText())
: null;
}
}