// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util.indexing;

import com.intellij.find.ngrams.TrigramIndex;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.ide.scratch.ScratchRootType;
import com.intellij.ide.scratch.ScratchesSearchScope;
import com.intellij.java.index.StringIndex;
import com.intellij.lang.LanguageParserDefinitions;
import com.intellij.lang.ParserDefinition;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.impl.UndoManagerImpl;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.diagnostic.DefaultLogger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.impl.CurrentEditorProvider;
import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl;
import com.intellij.openapi.fileTypes.ExactFileNameMatcher;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.fileTypes.PlainTextFileType;
import com.intellij.openapi.fileTypes.PlainTextLanguage;
import com.intellij.openapi.module.JavaModuleType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.impl.scopes.ModuleWithDependentsScope;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentIterator;
import com.intellij.openapi.roots.impl.FilePropertyPusher;
import com.intellij.openapi.roots.impl.JavaLanguageLevelPusher;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.impl.cache.impl.id.IdIndex;
import com.intellij.psi.impl.cache.impl.id.IdIndexEntry;
import com.intellij.psi.impl.java.JavaFunctionalExpressionIndex;
import com.intellij.psi.impl.java.stubs.index.JavaStubIndexKeys;
import com.intellij.psi.impl.search.JavaNullMethodArgumentIndex;
import com.intellij.psi.impl.source.*;
import com.intellij.psi.search.*;
import com.intellij.psi.search.impl.VirtualFileEnumeration;
import com.intellij.psi.stubs.*;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.testFramework.*;
import com.intellij.testFramework.builders.JavaModuleFixtureBuilder;
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase;
import com.intellij.util.*;
import com.intellij.util.indexing.dependencies.*;
import com.intellij.util.indexing.events.IndexedFilesListener;
import com.intellij.util.indexing.impl.IndexDebugProperties;
import com.intellij.util.indexing.impl.MapIndexStorage;
import com.intellij.util.indexing.impl.MapReduceIndex;
import com.intellij.util.indexing.impl.UpdatableValueContainer;
import com.intellij.util.indexing.impl.forward.IntForwardIndex;
import com.intellij.util.indexing.impl.storage.VfsAwareMapIndexStorage;
import com.intellij.util.indexing.impl.storage.VfsAwareMapReduceIndex;
import com.intellij.util.io.CaseInsensitiveEnumeratorStringDescriptor;
import com.intellij.util.io.EnumeratorStringDescriptor;
import com.intellij.util.io.PersistentMapImpl;
import com.intellij.util.ref.GCUtil;
import com.intellij.util.ref.GCWatcher;
import com.intellij.workspaceModel.ide.impl.WorkspaceEntityLifecycleSupporterUtils;
import com.siyeh.ig.JavaOverridingMethodUtil;
import kotlin.Unit;
import org.intellij.lang.annotations.Language;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.GroovyFileType;
import org.jetbrains.plugins.groovy.GroovyLanguage;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@SkipSlowTestLocally
public class IndexTest extends JavaCodeInsightFixtureTestCase {
  @Override
  protected void tuneFixture(JavaModuleFixtureBuilder moduleBuilder) throws Exception {
    if (getName().equals("test_indexed_state_for_file_without_content_requiring_indices")) {
      // should add file to test dire as soon as possible
      String otherRoot = myFixture.getTempDirPath() + "/otherRoot";
      assertTrue(new File(otherRoot).mkdirs());

      File exe = new File(otherRoot, "intellij.exe");
      assertTrue(exe.createNewFile());
      FileUtil.writeToFile(exe, new byte[]{1, 2, 3, 22});// convince IDEA it's binary
      moduleBuilder.addSourceContentRoot(otherRoot);
    }
  }

  public void testUpdate() throws StorageException, IOException {
    StringIndex index = createIndex(getTestName(false), new EnumeratorStringDescriptor(), false);

    try {
      // build index
      index.update("com/ppp/a.java", "a b c d");
      index.update("com/ppp/b.java", "a b g h");
      index.update("com/ppp/c.java", "a z f");
      index.update("com/ppp/d.java", "a a u y z");
      index.update("com/ppp/e.java", "a n chj e c d");

      assertDataEquals(index.getFilesByWord("a"), "com/ppp/a.java", "com/ppp/b.java", "com/ppp/c.java", "com/ppp/d.java", "com/ppp/e.java");
      assertDataEquals(index.getFilesByWord("b"), "com/ppp/a.java", "com/ppp/b.java");
      assertDataEquals(index.getFilesByWord("c"), "com/ppp/a.java", "com/ppp/e.java");
      assertDataEquals(index.getFilesByWord("d"), "com/ppp/a.java", "com/ppp/e.java");
      assertDataEquals(index.getFilesByWord("g"), "com/ppp/b.java");
      assertDataEquals(index.getFilesByWord("h"), "com/ppp/b.java");
      assertDataEquals(index.getFilesByWord("z"), "com/ppp/c.java", "com/ppp/d.java");
      assertDataEquals(index.getFilesByWord("f"), "com/ppp/c.java");
      assertDataEquals(index.getFilesByWord("u"), "com/ppp/d.java");
      assertDataEquals(index.getFilesByWord("y"), "com/ppp/d.java");
      assertDataEquals(index.getFilesByWord("n"), "com/ppp/e.java");
      assertDataEquals(index.getFilesByWord("chj"), "com/ppp/e.java");
      assertDataEquals(index.getFilesByWord("e"), "com/ppp/e.java");

      // update index
      index.update("com/ppp/d.java", "a u y z");
      assertDataEquals(index.getFilesByWord("a"), "com/ppp/a.java", "com/ppp/b.java", "com/ppp/c.java", "com/ppp/d.java", "com/ppp/e.java");
      index.update("com/ppp/d.java", "u y z");
      assertDataEquals(index.getFilesByWord("a"), "com/ppp/a.java", "com/ppp/b.java", "com/ppp/c.java", "com/ppp/e.java");
      index.update("com/ppp/d.java", "a a a u y z");
      assertDataEquals(index.getFilesByWord("a"), "com/ppp/a.java", "com/ppp/b.java", "com/ppp/c.java", "com/ppp/d.java", "com/ppp/e.java");

      index.update("com/ppp/e.java", "a n chj e c d z");
      assertDataEquals(index.getFilesByWord("z"), "com/ppp/c.java", "com/ppp/d.java", "com/ppp/e.java");

      index.update("com/ppp/b.java", null);
      assertDataEquals(index.getFilesByWord("a"), "com/ppp/a.java", "com/ppp/c.java", "com/ppp/d.java", "com/ppp/e.java");
      assertDataEquals(index.getFilesByWord("b"), "com/ppp/a.java");
      assertDataEquals(index.getFilesByWord("g"));
      assertDataEquals(index.getFilesByWord("h"));
    }
    finally {
      index.dispose();
    }
  }

  public void testUpdateWithCustomEqualityPolicy() throws IOException, StorageException {
    StringIndex index = createIndex(getTestName(false), new CaseInsensitiveEnumeratorStringDescriptor(), false);
    try {
      index.update("a.java", "x");
      assertDataEquals(index.getFilesByWord("x"), "a.java");
      assertDataEquals(index.getFilesByWord("X"), "a.java");

      index.update("b.java", "y");
      assertDataEquals(index.getFilesByWord("y"), "b.java");
      index.update("c.java", "Y");
      assertDataEquals(index.getFilesByWord("y"), "b.java", "c.java");
    }
    finally {
      index.dispose();
    }
  }

  private static StringIndex createIndex(String testName, EnumeratorStringDescriptor keyDescriptor, boolean readOnly) throws IOException {
    File storageFile = FileUtil.createTempFile("index_test", "storage");
    File metaIndexFile = FileUtil.createTempFile("index_test_inputs", "storage");
    VfsAwareMapIndexStorage<String, String> indexStorage =
      new VfsAwareMapIndexStorage<>(storageFile.toPath(), keyDescriptor, new EnumeratorStringDescriptor(), 16 * 1024, readOnly);
    return new StringIndex(testName, indexStorage, metaIndexFile, !readOnly);
  }

  private static <T> void assertDataEquals(List<T> actual, T... expected) {
    assertSameElements(actual, expected);
  }

  public void testCollectedPsiWithChangedDocument() {
    VirtualFile vFile = myFixture.addClass("class Foo {}").getContainingFile().getVirtualFile();

    assertNotNull(findClass("Foo"));
    PsiFile psiFile = getPsiManager().findFile(vFile);
    assertNotNull(psiFile);

    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.deleteString(0, document.getTextLength()));
    assertNotNull(findClass("Foo"));

    PsiClass foo = findClass("Foo");
    assertNotNull(foo);
    assertTrue(foo.isValid());
    assertEquals("class Foo {}", foo.getText());
    //noinspection ConstantValue
    assertTrue(foo.isValid());

    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    assertNull(findClass("Foo"));
  }

  public void testCollectedPsiWithDocumentChangedCommittedAndChangedAgain() {
    VirtualFile vFile = myFixture.addClass("class Foo {}").getContainingFile().getVirtualFile();

    assertNotNull(findClass("Foo"));
    PsiFile psiFile = getPsiManager().findFile(vFile);
    assertNotNull(psiFile);

    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.deleteString(0, document.getTextLength()));
    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.insertString(0, " "));

    PsiClass foo = findClass("Foo");
    assertNull(foo);
  }

  private PsiClass findClass(String name) {
    return JavaPsiFacade.getInstance(getProject()).findClass(name, GlobalSearchScope.allScope(getProject()));
  }

  public void testSavedUncommittedDocument() {
    VirtualFile vFile = myFixture.addFileToProject("Foo.java", "").getVirtualFile();

    assertNull(findClass("Foo"));
    PsiFile psiFile = getPsiManager().findFile(vFile);
    assertNotNull(psiFile);

    long count = getPsiManager().getModificationTracker().getModificationCount();

    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.insertString(0, "class Foo {}"));
    FileDocumentManager.getInstance().saveDocument(document);

    assertEquals(count, getPsiManager().getModificationTracker().getModificationCount());
    assertNull(findClass("Foo"));

    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    assertNotNull(findClass("Foo"));
    assertNotNull(findClass("Foo").getText());
    // if Foo exists now, mod count should be different
    assertTrue(count != getPsiManager().getModificationTracker().getModificationCount());
  }

  public void testPersistentChangeAffectsDocument() throws IOException {
    PsiFile psiFile = myFixture.addFileToProject("Foo.java", "class Foo {}");
    VirtualFile vFile = psiFile.getVirtualFile();

    long stamp = FileBasedIndex.getInstance().getIndexModificationStamp(StubUpdatingIndex.INDEX_ID, getProject());

    WriteCommandAction.runWriteCommandAction(getProject(),
                                             (Computable<PsiElement>)() -> CodeStyleManager.getInstance(getProject()).reformat(psiFile));

    PostprocessReformattingAspect.getInstance(getProject()).doPostponedFormatting();
    WriteCommandAction.runWriteCommandAction(getProject(), () -> PsiManager.getInstance(getProject()).reloadFromDisk(psiFile));

    assertEquals(stamp, FileBasedIndex.getInstance().getIndexModificationStamp(StubUpdatingIndex.INDEX_ID,
                                                                               getProject()));

    FileContentUtilCore.reparseFiles(Collections.singletonList(vFile));

    FileViewProvider provider = PsiManager.getInstance(getProject()).findViewProvider(vFile);
    StubTree stubTree = ((PsiFileImpl)provider.getPsi(provider.getBaseLanguage())).getGreenStubTree();

    WriteAction.run(() -> VfsUtil.saveText(vFile, "class Bar {}"));

    assertNotNull(findClass("Foo"));
  }

  public void testPersistentChangeAffectsUnsavedDocument() throws IOException {
    PsiFile psiFile = myFixture.addFileToProject("Foo.java", "class Foo {}");
    VirtualFile vFile = psiFile.getVirtualFile();

    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.insertString(0, "class f {}"));
    WriteCommandAction.runWriteCommandAction(getProject(), () -> PsiManager.getInstance(getProject()).reloadFromDisk(psiFile));
    assertNotNull(findClass("Foo"));
    WriteAction.run(() -> VfsUtil.saveText(vFile, "class x {}"));
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.insertString(0, "class a {}"));
    GCUtil.tryGcSoftlyReachableObjects();
    assertNotNull(findClass("Foo"));
  }

  public void testSkipUnknownFileTypes() {
    VirtualFile vFile = myFixture.addFileToProject("Foo.test", "Foo").getVirtualFile();
    assertEquals(PlainTextFileType.INSTANCE, vFile.getFileType());
    PsiSearchHelper helper = PsiSearchHelper.getInstance(getProject());
    assertOneElement(helper.findFilesWithPlainTextWords("Foo"));

    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    //todo should file type be changed silently without events?
    //assertEquals(UnknownFileType.INSTANCE, vFile.getFileType());

    PsiFile file = PsiDocumentManager.getInstance(getProject()).getPsiFile(document);
    assertInstanceOf(file, PsiPlainTextFile.class);
    assertEquals("Foo", file.getText());

    assertOneElement(helper.findFilesWithPlainTextWords("Foo"));

    WriteCommandAction.runWriteCommandAction(getProject(), () -> {
      document.insertString(0, " ");
      assertEquals("Foo", file.getText());
      assertOneElement(helper.findFilesWithPlainTextWords("Foo"));

      FileDocumentManager.getInstance().saveDocument(document);
      assertEquals("Foo", file.getText());
      assertOneElement(helper.findFilesWithPlainTextWords("Foo"));

      PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
      assertEquals(" Foo", file.getText());
      assertOneElement(helper.findFilesWithPlainTextWords("Foo"));
    });
  }

  public void testUndoToFileContentForUnsavedCommittedDocument() {
    VirtualFile vFile = myFixture.addClass("class Foo {}").getContainingFile().getVirtualFile();
    ((VirtualFileSystemEntry)vFile).setModificationStamp(0);// as unchanged file

    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    assertNotNull(document);
    assertEquals(0, document.getModificationStamp());
    assertNotNull(findClass("Foo"));

    WriteCommandAction.runWriteCommandAction(getProject(), () -> {
      document.insertString(0, "import Bar;\n");
      PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
      assertNotNull(findClass("Foo"));
    });

    UndoManager undoManager = UndoManager.getInstance(getProject());
    FileEditor selectedEditor = FileEditorManager.getInstance(getProject()).openFile(vFile, false)[0];
    ((UndoManagerImpl)undoManager).setOverriddenEditorProvider(new CurrentEditorProvider() {
      @Override
      public FileEditor getCurrentEditor(@Nullable Project project) {
        return selectedEditor;
      }
    });

    assertTrue(undoManager.isUndoAvailable(selectedEditor));
    FileDocumentManager.getInstance().saveDocument(document);
    undoManager.undo(selectedEditor);

    assertNotNull(findClass("Foo"));
  }

  public void test_rename_unsaved_file() {
    PsiFile psiFile = myFixture.addFileToProject("Foo.java", "class Foo {}");
    GlobalSearchScope scope = GlobalSearchScope.allScope(getProject());

    assertEquals(0, FileDocumentManager.getInstance().getUnsavedDocuments().length);

    WriteCommandAction.runWriteCommandAction(getProject(),
                                             (Computable<PsiElement>)() -> ((PsiJavaFile)psiFile).getImportList()
                                               .add(getElementFactory().createImportStatementOnDemand("java.io")));

    GCUtil.tryGcSoftlyReachableObjects();

    assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("Foo", scope));

    assertNull(getPsiManager().getFileManagerEx().getCachedDirectory(psiFile.getVirtualFile().getParent()));
    WriteCommandAction.runWriteCommandAction(getProject(), () -> {
      assertEquals(psiFile.setName("Foo1.java"), psiFile);
    });

    assertTrue(FileDocumentManager.getInstance().getUnsavedDocuments().length > 0);
    assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("Foo", scope));
  }

  public void test_rename_dir_with_unsaved_file() throws IOException {
    PsiFile psiFile = myFixture.addFileToProject("foo/Foo.java", "package pkg; class Foo {}");
    GlobalSearchScope scope = GlobalSearchScope.allScope(getProject());

    assertEquals(0, FileDocumentManager.getInstance().getUnsavedDocuments().length);

    WriteCommandAction.runWriteCommandAction(getProject(), (Computable<PsiElement>)() ->
      ((PsiJavaFile)psiFile).getImportList().add(getElementFactory().createImportStatementOnDemand("java.io")));

    GCUtil.tryGcSoftlyReachableObjects();

    assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("pkg.Foo", scope));

    VirtualFile dir = psiFile.getVirtualFile().getParent();
    assertNull(getPsiManager().getFileManagerEx().getCachedDirectory(dir));
    WriteAction.run(() -> dir.rename(this, "bar"));

    assertTrue(FileDocumentManager.getInstance().getUnsavedDocuments().length > 0);
    assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("pkg.Foo", scope));
  }

  public void test_language_level_change() {
    PsiFile psiFile = myFixture.addFileToProject("Foo.java", "class Foo {}");
    GlobalSearchScope scope = GlobalSearchScope.allScope(getProject());

    WriteCommandAction.runWriteCommandAction(getProject(),
                                             (Computable<PsiElement>)() -> psiFile.add(getElementFactory().createEnum("SomeEnum")));

    WriteCommandAction.runWriteCommandAction(getProject(),
                                             (Computable<PsiElement>)() -> CodeStyleManager.getInstance(getProject()).reformat(psiFile));
    assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("Foo", scope));

    long stamp = FileBasedIndex.getInstance().getIndexModificationStamp(StubUpdatingIndex.INDEX_ID, getProject());

    IdeaTestUtil.setModuleLanguageLevel(myFixture.getModule(), LanguageLevel.JDK_1_3);

    assertNotNull(((PsiJavaFile)psiFile).getImportList().getNode());

    assertTrue(stamp != FileBasedIndex.getInstance().getIndexModificationStamp(StubUpdatingIndex.INDEX_ID, getProject()));
  }

  public void test_rename_file_with_indexed_associated_unsaved_document_don_t_lost_its_data() {
    LanguageLevel level = LanguageLevel.HIGHEST;
    IdeaTestUtil.setModuleLanguageLevel(myFixture.getModule(), level);
    PsiFile psiFile = myFixture.addFileToProject("Foo.java", "class Foo {}");

    VirtualFile file = psiFile.getVirtualFile();
    GlobalSearchScope scope = GlobalSearchScope.allScope(getProject());

    PostprocessReformattingAspect.getInstance(getProject()).doPostponedFormatting();
    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    FileViewProvider vp1 = PsiManager.getInstance(getProject()).findViewProvider(file);
    WriteCommandAction.runWriteCommandAction(getProject(), (Computable<PsiElement>)() -> {
      return CodeStyleManager.getInstance(getProject()).reformat(vp1.getPsi(vp1.getBaseLanguage()));
    });

    PostprocessReformattingAspect.getInstance(getProject()).doPostponedFormatting();
    FileContentUtilCore.reparseFiles(Collections.singletonList(file));

    FileViewProvider vp2 = PsiManager.getInstance(getProject()).findViewProvider(file);
    ((PsiFileImpl)vp2.getPsi(vp2.getBaseLanguage())).getGreenStubTree();

    PostprocessReformattingAspect.getInstance(getProject()).doPostponedFormatting();
    FileContentUtilCore.reparseFiles(Collections.singletonList(file));

    PostprocessReformattingAspect.getInstance(getProject()).doPostponedFormatting();
    IdeaTestUtil.setModuleLanguageLevel(myFixture.getModule(), level);

    assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("Foo", scope));
  }

  public void test_language_level_change2() {
    PsiFile psiFile = myFixture.addFileToProject("Foo.java", "class Foo {}");
    VirtualFile vFile = psiFile.getVirtualFile();
    GlobalSearchScope scope = GlobalSearchScope.allScope(getProject());

    WriteCommandAction.runWriteCommandAction(getProject(),
                                             (Computable<PsiElement>)() -> psiFile.add(getElementFactory().createEnum("SomeEnum")));

    WriteCommandAction.runWriteCommandAction(getProject(),
                                             (Computable<PsiElement>)() -> CodeStyleManager.getInstance(getProject()).reformat(psiFile));
    PostprocessReformattingAspect.getInstance(getProject()).doPostponedFormatting();

    assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("Foo", scope));

    IdeaTestUtil.setModuleLanguageLevel(myFixture.getModule(), LanguageLevel.JDK_1_3);
    assertNotNull(((PsiJavaFile)getPsiManager().findFile(vFile)).getImportList().getNode());

    GCUtil.tryGcSoftlyReachableObjects();
    assertNotNull(((PsiJavaFile)getPsiManager().findFile(vFile)).getImportList().getNode());
  }

  public void test_unknown_file_type_in_stubs() throws IOException {
    VirtualFile vFile = myFixture.addFileToProject("Foo.java", "").getVirtualFile();
    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.setText("class Foo {}"));
    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    assertNotNull(findClass("Foo"));

    WriteAction.run(() -> vFile.rename(null, "Foo1"));
    assertNull(findClass("Foo"));
  }

  public void test_uncommitted_saved_document_2() {
    PsiFile file = myFixture.addFileToProject("a.java", "class Foo {}");
    WriteCommandAction.runWriteCommandAction(getProject(), () -> {
      assertNotNull(findClass("Foo"));
      file.getViewProvider().getDocument().setText("");
      PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
      file.getViewProvider().getDocument().setText("class Foo {}");
      FileDocumentManager.getInstance().saveAllDocuments();
      assertNull(findClass("Foo"));
    });
  }

  public void test_plain_text_file_type_in_stubs() throws IOException {
    VirtualFile vFile = myFixture.addFileToProject("Foo.java", "class Bar {}").getVirtualFile();
    assertNotNull(findClass("Bar"));
    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.setText("class Foo {}"));
    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    assertNotNull(findClass("Foo"));
    assertNull(findClass("Bar"));

    WriteAction.run(() -> vFile.rename(null, "Foo1"));
    assertNull(findClass("Foo"));
    assertNull(findClass("Bar"));
  }

  public void test_changing_a_file_without_psi_makes_the_document_committed_and_updates_index() throws IOException {
    PsiFile psiFile = myFixture.addFileToProject("Foo.java", "class Foo {}");
    VirtualFile vFile = psiFile.getVirtualFile();
    GlobalSearchScope scope = GlobalSearchScope.allScope(getProject());

    WriteCommandAction.runWriteCommandAction(getProject(),
                                             () -> FileDocumentManager.getInstance().getDocument(vFile)
                                               .setText("import zoo.Zoo; class Foo1 {}"));
    assertTrue(PsiDocumentManager.getInstance(getProject()).getUncommittedDocuments().length > 0);

    FileDocumentManager.getInstance().saveAllDocuments();
    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();

    //Let's help GC, even in interpreter mode
    //noinspection UnusedAssignment
    psiFile = null;

    GCWatcher.tracking(getPsiManager().getFileManager().getCachedPsiFile(vFile))
      .ensureCollectedWithinTimeout(5_000, PlatformTestUtil::dispatchAllInvocationEventsInIdeEventQueue); // wait for document commit queue
    assertNull(getPsiManager().getFileManager().getCachedPsiFile(vFile));

    WriteAction.run(() -> VfsUtil.saveText(vFile, "class Foo3 {}"));

    assertEquals(0, PsiDocumentManager.getInstance(getProject()).getUncommittedDocuments().length);

    assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("Foo3", scope));
  }

  public void test_rename_file_invalidates_indices_in_right_order() throws IOException {
    GlobalSearchScope scope = GlobalSearchScope.allScope(getProject());

    for (int i = 0; i < 100; i++) {
      VirtualFile file =
        myFixture.addFileToProject("foo/Foo" + i + ".java", "package foo; class Foo" + i + " {}").getVirtualFile();
      assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("foo.Foo" + i, scope));
      String newName = "Bar" + i + ".java";
      WriteCommandAction.runWriteCommandAction(getProject(), (ThrowableComputable<?, IOException>)() -> {
        file.rename(this, newName);
        return null;
      });
      assertNotNull(JavaPsiFacade.getInstance(getProject()).findClass("foo.Foo" + i, scope));
    }
  }

  public void test_no_index_stamp_update_when_no_change() throws IOException {
    FileBasedIndex fileBasedIndex = FileBasedIndex.getInstance();
    Project project = getProject();

    VirtualFile vFile = myFixture.addClass("class Foo {}").getContainingFile().getVirtualFile();
    long stamp = fileBasedIndex.getIndexModificationStamp(IdIndex.NAME, project);
    //assertIsIndexed(vFile) FIXME-ank: not indexed, because indexing changes file's modCounter

    WriteAction.run(() -> VfsUtil.saveText(vFile, "Foo class"));
    IndexingRequestToken indexingRequest = project.getService(ProjectIndexingDependenciesService.class).getLatestIndexingRequestToken();
    assertFalse(IndexingFlag.isFileIndexed(vFile, indexingRequest.getFileIndexingStamp(vFile)));
    assertEquals(stamp, fileBasedIndex.getIndexModificationStamp(IdIndex.NAME, project));
    assertIsIndexed(vFile);

    WriteAction.run(() -> VfsUtil.saveText(vFile, "class Foo2 {}"));
    assertTrue(stamp != fileBasedIndex.getIndexModificationStamp(IdIndex.NAME, project));

    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    WriteCommandAction.runWriteCommandAction(project, () -> document.setText("Foo2 class"));
    stamp = fileBasedIndex.getIndexModificationStamp(IdIndex.NAME, project);
    WriteCommandAction.runWriteCommandAction(project, () -> document.setText("class Foo2"));
    assertEquals(stamp, fileBasedIndex.getIndexModificationStamp(IdIndex.NAME, project));

    WriteCommandAction.runWriteCommandAction(project, () -> document.setText("Foo3 class"));
    PsiDocumentManager.getInstance(project).commitAllDocuments();
    assertTrue(stamp != fileBasedIndex.getIndexModificationStamp(IdIndex.NAME, project));

    WriteCommandAction.runWriteCommandAction(project, () -> document.setText("class Foo { Runnable r = ( ) -> {}; }"));
    PsiDocumentManager.getInstance(project).commitAllDocuments();
    stamp = fileBasedIndex.getIndexModificationStamp(JavaFunctionalExpressionIndex.INDEX_ID, project);
    WriteCommandAction.runWriteCommandAction(project, () -> document.setText("class Foo { Runnable x = () -> { }; }"));
    PsiDocumentManager.getInstance(project).commitAllDocuments();
    assertEquals(stamp, fileBasedIndex.getIndexModificationStamp(JavaFunctionalExpressionIndex.INDEX_ID, project));
  }

  private void assertIsIndexed(VirtualFile vFile) {
    ScanningRequestToken indexingRequest = getProject().getService(ProjectIndexingDependenciesService.class).getReadOnlyTokenForTest();
    FileIndexingStamp indexingStamp = indexingRequest.getFileIndexingStamp(vFile);
    assertTrue(
      "File [" + vFile + "] must be indexed, but indexingStamp=" + indexingStamp,
      IndexingFlag.isFileIndexed(vFile, indexingStamp)
    );
    assertThat(IndexingFlag.isFileChanged(vFile, indexingStamp))
      .describedAs("File [" + vFile + "] must be hanged, but indexingStamp=" + indexingStamp)
      .isEqualTo(IsFileChangedResult.NO);
  }

  public void test_no_index_stamp_update_when_no_change_2() throws IOException {
    FileBasedIndex fileBasedIndex = FileBasedIndex.getInstance();
    @Language("JAVA") String text0 = """
                  class Main111 {
                      static void staticMethod(Object o) {
                        staticMethod(null);
                      }
                  }
      """;
    VirtualFile vFile = myFixture.configureByText(JavaFileType.INSTANCE, text0).getVirtualFile();
    long stamp = fileBasedIndex.getIndexModificationStamp(JavaNullMethodArgumentIndex.INDEX_ID, getProject());
    JavaNullMethodArgumentIndex.MethodCallData data = new JavaNullMethodArgumentIndex.MethodCallData("staticMethod", 0);
    Collection<VirtualFile> files = fileBasedIndex
      .getContainingFiles(JavaNullMethodArgumentIndex.INDEX_ID, data, GlobalSearchScope.projectScope(getProject()));
    assertEquals(1, files.size());
    assertEquals(files.iterator().next(), vFile);

    @Language("JAVA") String text = """
                  class Main {
                      static void staticMethod(Object o) {
                        staticMethod(null);
                      }
                  }
      """;
    WriteAction.run(() -> VfsUtil.saveText(vFile, text));
    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();

    assertEquals(stamp, fileBasedIndex.getIndexModificationStamp(JavaNullMethodArgumentIndex.INDEX_ID, getProject()));
    files = fileBasedIndex.getContainingFiles(JavaNullMethodArgumentIndex.INDEX_ID, data, GlobalSearchScope.projectScope(getProject()));
    assertEquals(1, files.size());
    assertEquals(files.iterator().next(), vFile);
  }

  public void test_snapshot_index_in_memory_state_after_commit_of_unsaved_document() {
    VirtualFile vFile = myFixture.addClass("class Foo {}").getContainingFile().getVirtualFile();
    IdIndexEntry classEntry = new IdIndexEntry("class", true);
    FileBasedIndex.ValueProcessor<Integer> findValueProcessor = (file, value) -> !file.equals(vFile);

    GlobalSearchScope projectScope = GlobalSearchScope.projectScope(getProject());
    boolean result = FileBasedIndex.getInstance().processValues(IdIndex.NAME, classEntry, null, findValueProcessor, projectScope);
    assertFalse(result);

    Document document = FileDocumentManager.getInstance().getDocument(vFile);
    WriteCommandAction.runWriteCommandAction(getProject(), () -> document.setText(""));
    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    result = FileBasedIndex.getInstance().processValues(IdIndex.NAME, classEntry, null, findValueProcessor, projectScope);
    assertTrue(result);
  }

  public void test_no_stub_index_stamp_update_when_no_change() throws IOException {
    VirtualFile vFile = myFixture.addClass("class Foo {}").getContainingFile().getVirtualFile();
    long stamp = ((StubIndexImpl)StubIndex.getInstance()).getIndexModificationStamp(JavaStubIndexKeys.CLASS_SHORT_NAMES, getProject());

    WriteAction.run(() -> VfsUtil.saveText(vFile, "class Foo { int foo; }"));
    assertEquals(stamp,
                 ((StubIndexImpl)StubIndex.getInstance()).getIndexModificationStamp(JavaStubIndexKeys.CLASS_SHORT_NAMES, getProject()));

