// 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.kotlin.idea.navigation;

import com.google.common.collect.Ordering;
import com.intellij.codeInsight.navigation.GotoTargetHandler;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.UsefulTestCase;
import com.intellij.testFramework.fixtures.CodeInsightTestUtil;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.MultiMap;
import junit.framework.TestCase;
import kotlin.collections.ArraysKt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.asJava.LightClassUtilsKt;
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils;
import org.jetbrains.kotlin.test.util.ReferenceUtils;
import org.junit.Assert;

import java.util.*;
import java.util.stream.Collectors;

public final class NavigationTestUtils {
    private NavigationTestUtils() {
    }

    public static GotoTargetHandler.GotoData invokeGotoImplementations(Editor editor, PsiFile psiFile) {
        return CodeInsightTestUtil.gotoImplementation(editor, psiFile);
    }

    public static void assertGotoDataMatching(Editor editor, GotoTargetHandler.GotoData gotoData) {
        assertGotoDataMatching(editor, gotoData, false);
    }

    public static void assertGotoDataMatching(Editor editor, GotoTargetHandler.GotoData gotoData, boolean renderModule) {
        assertGotoDataMatching(editor, gotoData, renderModule, getExpectedReferences(editor.getDocument().getText(), "// REF:"));
    }

    public static void assertGotoDataMatching(
            Editor editor,
            GotoTargetHandler.GotoData gotoData,
            boolean renderModule,
            List<String> expectedReferences
    ) {
        String documentText = editor.getDocument().getText();

        if (gotoData != null) {
            List<PsiElement> targets;
            if (InTextDirectivesUtils.isDirectiveDefined(documentText, "// DISTINCT_REF")) {
                targets = ArraysKt.distinctBy(gotoData.targets, LightClassUtilsKt::getUnwrapped);
            } else {
                targets = Arrays.asList(gotoData.targets);
            }
            // Transform given reference result to strings
            List<String> psiElements = targets.stream().map(element -> {
                Assert.assertNotNull(element);
                return ReferenceUtils.renderAsGotoImplementation(element, renderModule);
            }).collect(Collectors.toList());

            // Compare
            UsefulTestCase.assertOrderedEquals(Ordering.natural().sortedCopy(psiElements), expectedReferences);
        } else {
            UsefulTestCase.assertOrderedEquals(Collections.emptyList(), expectedReferences);
        }
    }

    public static @NotNull List<String> getExpectedReferences(String documentText, String referencePrefix) {
        // Get expected references from the tested document
        List<String> expectedReferences = InTextDirectivesUtils.findListWithPrefixes(documentText, referencePrefix);
        for (int i = 0; i < expectedReferences.size(); i++) {
            String expectedText = expectedReferences.get(i);
            expectedText = expectedText.replace("\\n", "\n");
            if (!expectedText.startsWith("<")) {
                expectedText = PathUtil.toSystemDependentName(expectedText).replace("//", "/");
            }
            expectedReferences.set(i, expectedText);
        }

        expectedReferences.removeIf(String::isEmpty);
        Collections.sort(expectedReferences);
        return expectedReferences;
    }

    public static String getNavigateElementsText(Project project, Collection<? extends PsiElement> navigableElements) {
        MultiMap<VirtualFile, Pair<Integer, Integer>> filesToNumbersAndOffsets = new MultiMap<>();
        int refNumber = 1;
        for (PsiElement navigationElement : navigableElements) {
            Pair<Integer, Integer> numberAndOffset = new Pair<>(refNumber++, navigationElement.getTextOffset());
            filesToNumbersAndOffsets.putValue(navigationElement.getContainingFile().getVirtualFile(), numberAndOffset);
        }

        if (filesToNumbersAndOffsets.isEmpty()) {
            return "<no references>";
        }

        List<VirtualFile> files = new ArrayList<>(filesToNumbersAndOffsets.keySet());
        files.sort(Comparator.comparing(VirtualFile::getName));

        StringBuilder result = new StringBuilder();
        for (VirtualFile file : files) {
            List<Pair<Integer, Integer>> numbersAndOffsets = new ArrayList<>(filesToNumbersAndOffsets.get(file));

            numbersAndOffsets.sort(Collections.reverseOrder(
                    Comparator.comparingInt((Pair<Integer, Integer> t) -> t.second).thenComparingInt(t -> t.first)));

            Document document = FileDocumentManager.getInstance().getDocument(file);
            TestCase.assertNotNull(document);
            StringBuilder resultForFile = new StringBuilder(document.getText());
            for (Pair<Integer, Integer> numberOffset : numbersAndOffsets) {
                resultForFile.insert(numberOffset.second, String.format("<%d>", numberOffset.first));
            }

            int minLine = Integer.MAX_VALUE;
            int maxLine = Integer.MIN_VALUE;
            for (Pair<Integer, Integer> numberOffset : numbersAndOffsets) {
                int lineNumber = document.getLineNumber(numberOffset.second);
                minLine = Math.min(minLine, lineNumber);
                maxLine = Math.max(maxLine, lineNumber);
            }

            Document annotated = EditorFactory.getInstance().createDocument(resultForFile);
            String filePart = annotated.getText().substring(annotated.getLineStartOffset(minLine),
                                                            annotated.getLineEndOffset(maxLine));
            result.append(" ").append(file.getName()).append("\n");
            result.append(filePart).append("\n");
        }
        return result.toString();
    }
}
