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

import com.intellij.configurationStore.StoreUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.application.ex.PathManagerEx;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.ProjectJdkTable;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkModificator;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
import com.intellij.openapi.roots.impl.ModifiableModelCommitter;
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.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vcs.VcsConfiguration;
import com.intellij.openapi.vcs.changes.VcsIgnoreManager;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.openapi.vfs.newvfs.BulkFileListener;
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
import com.intellij.testFramework.JavaModuleTestCase;
import com.intellij.testFramework.VfsTestUtil;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.messages.SimpleMessageBusConnection;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.model.java.JavaResourceRootType;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Checks that proper {@link ModuleRootListener#rootsChanged} events are sent. Consider adding new tests to {@link LibraryRootsChangedTest}
 * or {@link ModuleRootsChangedTest} which use more convenient API.
 */
public class RootsChangedTest extends JavaModuleTestCase {
  private MyModuleRootListener myModuleRootListener;
  private boolean useWfiForPartialScanning;

  @Override
  protected void setUp() throws Exception {
    super.setUp();

    getOrCreateProjectBaseDir();
    MessageBusConnection connection = myProject.getMessageBus().connect(getTestRootDisposable());
    myModuleRootListener = new MyModuleRootListener(myProject);
    connection.subscribe(ModuleRootListener.TOPIC, myModuleRootListener);
    useWfiForPartialScanning = Registry.is("use.workspace.file.index.for.partial.scanning");
  }

  @Override
  protected void tearDown() throws Exception {
    myModuleRootListener = null;
    super.tearDown();
  }

  public void testEventsAfterFileModifications() {
    File root = new File(FileUtil.getTempDirectory());

    File dir1 = new File(root, "dir1");
    assertTrue(dir1.mkdirs());
    VirtualFile vDir1 = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(dir1);
    assertNotNull(vDir1);

    Module moduleA = createModule("a.iml");
    myModuleRootListener.reset();

    ModuleRootModificationUtil.addContentRoot(moduleA, vDir1.getPath());
    UIUtil.dispatchAllInvocationEvents();

    myModuleRootListener.assertEventsCount(1);
    assertSameElements(ModuleRootManager.getInstance(moduleA).getContentRoots(), vDir1);

    VfsTestUtil.deleteFile(vDir1);
    myModuleRootListener.assertEventsCount(1);
    assertEmpty(ModuleRootManager.getInstance(moduleA).getContentRoots());

    File dir2 = new File(root, "dir2");
    assertTrue(dir2.mkdirs());
    VirtualFile vDir2 = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(dir2);
    assertNotNull(vDir2);

    rename(vDir2, "dir1");
    myModuleRootListener.assertEventsCount(1);
    assertSameElements(ModuleRootManager.getInstance(moduleA).getContentRoots(), vDir2);

    // when the existing root is renamed, it remains a root
    rename(vDir2, "dir2");
    myModuleRootListener.assertEventsCount(1);
    assertSameElements(ModuleRootManager.getInstance(moduleA).getContentRoots(), vDir2);

    // and event if it is moved, it's still a root
    File subdir = new File(root, "subdir");
    assertTrue(subdir.mkdirs());
    VirtualFile vSubdir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(subdir);
    assertNotNull(vSubdir);

    move(vDir2, vSubdir);
    myModuleRootListener.assertEventsCount(1);
    assertSameElements(ModuleRootManager.getInstance(moduleA).getContentRoots(), vDir2);
  }

  public void testProjectLibraryChangeEvent() {
    final LibraryTable projectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject);
    verifyLibraryTableEditing(projectLibraryTable);
  }

  public void testGlobalLibraryChangeEvent() {
    final LibraryTable globalLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable();
    verifyLibraryTableEditing(globalLibraryTable);
  }

  public void testProjectLibraryEventsInUncommittedModel() {
    final LibraryTable projectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject);
    verifyLibraryTableEditingInUncommittedModel(projectLibraryTable);
  }

  public void testGlobalLibraryEventsInUncommittedModel() {
    final LibraryTable globalLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable();
    verifyLibraryTableEditingInUncommittedModel(globalLibraryTable);
  }

  public void testEditLibraryForModuleLoadFromXml() {
    ApplicationManager.getApplication().runWriteAction(() -> {
      Module a = loadModule(Paths.get(PathManagerEx.getHomePath(getClass())).resolve("java/java-tests/testData/moduleRootManager/rootsChanged/emptyModule/a.iml"));
      myModuleRootListener.assertEventsCount(1);

      final Sdk jdk = ProjectJdkTable.getInstance().createSdk("new-jdk", JavaSdk.getInstance());
      ProjectJdkTable.getInstance().addJdk(jdk, getTestRootDisposable());
      myModuleRootListener.assertNoEvents();

      ModuleRootModificationUtil.setModuleSdk(a, jdk);
      myModuleRootListener.assertEventsCount(1);

      SdkModificator sdkModificator = jdk.getSdkModificator();
      sdkModificator.addRoot(getTempDir().createVirtualDir(), OrderRootType.CLASSES);
      sdkModificator.commitChanges();
    });

    if (useWfiForPartialScanning) {
      myModuleRootListener.assertEventsCount(1);
    } else {
      myModuleRootListener.assertEventsCount(2);
    }
  }

  public void testModuleJdkEditing() {
    ApplicationManager.getApplication().runWriteAction(() -> {
      final Module moduleA = createModule("a.iml");
      final Module moduleB = createModule("b.iml");
      myModuleRootListener.assertEventsCount(2);

      final Sdk jdk = ProjectJdkTable.getInstance().createSdk("new-jdk", JavaSdk.getInstance());
      final Sdk unused = ProjectJdkTable.getInstance().createSdk("unused", JavaSdk.getInstance());
      ProjectJdkTable.getInstance().addJdk(jdk, getTestRootDisposable());
      ProjectJdkTable.getInstance().addJdk(unused, getTestRootDisposable());
      myModuleRootListener.assertNoEvents();

      final ModifiableRootModel rootModelA = ModuleRootManager.getInstance(moduleA).getModifiableModel();
      final ModifiableRootModel rootModelB = ModuleRootManager.getInstance(moduleB).getModifiableModel();
      rootModelA.setSdk(jdk);
      rootModelB.setSdk(jdk);
      ModifiableRootModel[] rootModels = {rootModelA, rootModelB};
      ModifiableModelCommitter.multiCommit(rootModels, ModuleManager.getInstance(rootModels[0].getProject()).getModifiableModel());
      myModuleRootListener.assertEventsCount(1);

      final SdkModificator sdkModificator = jdk.getSdkModificator();
      sdkModificator.addRoot(getTempDir().createVirtualDir(), OrderRootType.CLASSES);
      sdkModificator.commitChanges();
      if (useWfiForPartialScanning) {
        myModuleRootListener.assertEventsCount(1);
      } else {
        myModuleRootListener.assertEventsCount(2);
      }

      final SdkModificator sdkModificator2 = unused.getSdkModificator();
      sdkModificator2.addRoot(getTempDir().createVirtualDir(), OrderRootType.CLASSES);
      sdkModificator2.commitChanges();
      myModuleRootListener.assertNoEvents();
    });
  }

  public void testSetupUnknownJdk() {
    ApplicationManager.getApplication().runWriteAction(() -> {
      Module module = createModule("a.iml");
      myModuleRootListener.assertEventsCount(1);
      ModifiableRootModel model = ModuleRootManager.getInstance(module).getModifiableModel();
      model.setInvalidSdk("new-jdk", JavaSdk.getInstance().getName());
      model.commit();
      myModuleRootListener.assertEventsCount(1);

      Sdk unusedJdk = ProjectJdkTable.getInstance().createSdk("unused-jdk", JavaSdk.getInstance());
      ProjectJdkTable.getInstance().addJdk(unusedJdk, getTestRootDisposable());
      myModuleRootListener.assertNoEvents();
      
      Sdk jdk = ProjectJdkTable.getInstance().createSdk("new-jdk", JavaSdk.getInstance());
      ProjectJdkTable.getInstance().addJdk(jdk, getTestRootDisposable());
      if (useWfiForPartialScanning) {
        myModuleRootListener.assertEventsCount(1);
      } else {
        myModuleRootListener.assertEventsCount(2);
      }

      final SdkModificator sdkModificator = jdk.getSdkModificator();
      sdkModificator.addRoot(getTempDir().createVirtualDir(), OrderRootType.CLASSES);
      sdkModificator.commitChanges();
      if (useWfiForPartialScanning) {
        myModuleRootListener.assertEventsCount(1);
      } else {
        myModuleRootListener.assertEventsCount(2);
      }
    });
  }

  public void testInheritedJdkEditing() {
    ApplicationManager.getApplication().runWriteAction(() -> {
      final Module moduleA = createModule("a.iml");
      final Module moduleB = createModule("b.iml");
      myModuleRootListener.assertEventsCount(2);

      final Sdk jdk;
      final Sdk jdkBBB;
      jdk = ProjectJdkTable.getInstance().createSdk("AAA", JavaSdk.getInstance());
      ProjectJdkTable.getInstance().addJdk(jdk, getTestRootDisposable());
      myModuleRootListener.assertNoEvents();

      jdkBBB = ProjectJdkTable.getInstance().createSdk("BBB", JavaSdk.getInstance());
      ProjectJdkTable.getInstance().addJdk(jdk, getTestRootDisposable());
      myModuleRootListener.assertNoEvents();

      ProjectRootManager.getInstance(myProject).setProjectSdk(jdkBBB);

      // don't care if there are references from modules to project SDK or not: change in the "project sdk" always generates events
      myModuleRootListener.assertEventsCount(1);

      final ModifiableRootModel rootModelA = ModuleRootManager.getInstance(moduleA).getModifiableModel();
      final ModifiableRootModel rootModelB = ModuleRootManager.getInstance(moduleB).getModifiableModel();
      rootModelA.inheritSdk();
      rootModelB.inheritSdk();
      ModifiableRootModel[] rootModels = {rootModelA, rootModelB};
      ModifiableModelCommitter.multiCommit(rootModels, ModuleManager.getInstance(rootModels[0].getProject()).getModifiableModel());
      myModuleRootListener.assertEventsCount(1);

      ProjectRootManager.getInstance(myProject).setProjectSdk(jdk);
      myModuleRootListener.assertEventsCount(1);

      final SdkModificator sdkModificator = jdk.getSdkModificator();
      sdkModificator.addRoot(getTempDir().createVirtualDir(), OrderRootType.CLASSES);
      sdkModificator.commitChanges();
      if (useWfiForPartialScanning) {
        myModuleRootListener.assertEventsCount(1);
      } else {
        myModuleRootListener.assertEventsCount(2);
      }
    });
  }

  private void verifyLibraryTableEditing(final LibraryTable libraryTable) {
    final Module moduleA = createModule("a.iml");
    final Module moduleB = createModule("b.iml");
    myModuleRootListener.assertEventsCount(2);

    ApplicationManager.getApplication().runWriteAction(() -> {
      final Library libraryA = libraryTable.createLibrary("A");
      final Library.ModifiableModel libraryModifiableModel = libraryA.getModifiableModel();
      libraryModifiableModel.addRoot(someAbsoluteUrl("a"), OrderRootType.CLASSES);
      libraryModifiableModel.commit();
      myModuleRootListener.assertNoEvents();

      final ModifiableRootModel rootModelA = ModuleRootManager.getInstance(moduleA).getModifiableModel();
      final ModifiableRootModel rootModelB = ModuleRootManager.getInstance(moduleB).getModifiableModel();
      rootModelA.addLibraryEntry(libraryA);
      rootModelB.addLibraryEntry(libraryA);
      rootModelA.addInvalidLibrary("Q", libraryTable.getTableLevel());
      rootModelB.addInvalidLibrary("Q", libraryTable.getTableLevel());
      ModifiableRootModel[] rootModels = {rootModelA, rootModelB};
      ModifiableModelCommitter.multiCommit(rootModels, ModuleManager.getInstance(rootModels[0].getProject()).getModifiableModel());
      myModuleRootListener.assertEventsCount(1);

      Library.ModifiableModel libraryModifiableModel2 = libraryA.getModifiableModel();
      VirtualFile file = getTempDir().createVirtualDir();
      libraryModifiableModel2.addRoot(file.getUrl(), OrderRootType.CLASSES);
      libraryModifiableModel2.commit();
      myModuleRootListener.assertEventsCount(1);

      libraryTable.removeLibrary(libraryA);
      myModuleRootListener.assertEventsCount(1);

      final Library libraryQ = libraryTable.createLibrary("Q");
      myModuleRootListener.assertEventsCount(1);

      Library.ModifiableModel model = libraryQ.getModifiableModel();
      model.addRoot(getTempDir().createVirtualDir(), OrderRootType.CLASSES);
      model.commit();
      myModuleRootListener.assertEventsCount(1);

      libraryTable.removeLibrary(libraryQ);
      myModuleRootListener.assertEventsCount(1);
    });
  }

  @NotNull
  private static String someAbsoluteUrl(@NotNull String name) {
    return SystemInfo.isUnix ? "file:///" + name : "file://C:/"+name;
  }

  private void verifyLibraryTableEditingInUncommittedModel(LibraryTable libraryTable) {
    ApplicationManager.getApplication().runWriteAction(() -> {
      final Module moduleA = createModule("a.iml");
      final Module moduleB = createModule("b.iml");
      myModuleRootListener.assertEventsCount(2);

      final Library libraryA = libraryTable.createLibrary("A");
      final Library.ModifiableModel libraryModifiableModel = libraryA.getModifiableModel();
      libraryModifiableModel.addRoot(someAbsoluteUrl("a"), OrderRootType.CLASSES);
      libraryModifiableModel.commit();
      myModuleRootListener.assertNoEvents();

      final ModifiableRootModel rootModelA = ModuleRootManager.getInstance(moduleA).getModifiableModel();
      final ModifiableRootModel rootModelB = ModuleRootManager.getInstance(moduleB).getModifiableModel();
      rootModelA.addLibraryEntry(libraryA);
      rootModelB.addLibraryEntry(libraryA);
      final Library.ModifiableModel libraryModifiableModel2 = libraryA.getModifiableModel();
      libraryModifiableModel2.addRoot(someAbsoluteUrl("b"), OrderRootType.CLASSES);
      libraryModifiableModel2.commit();
      myModuleRootListener.assertNoEvents();

      libraryTable.removeLibrary(libraryA);
      myModuleRootListener.assertNoEvents(true);

      rootModelA.addInvalidLibrary("Q", libraryTable.getTableLevel());
      rootModelB.addInvalidLibrary("Q", libraryTable.getTableLevel());
      myModuleRootListener.assertNoEvents();

      final Library libraryQ = libraryTable.createLibrary("Q");
      myModuleRootListener.assertNoEvents(true);

      ModifiableRootModel[] rootModels = {rootModelA, rootModelB};
      ModifiableModelCommitter.multiCommit(rootModels, ModuleManager.getInstance(rootModels[0].getProject()).getModifiableModel());
      myModuleRootListener.assertEventsCount(1);

      libraryTable.removeLibrary(libraryQ);
      myModuleRootListener.assertEventsCount(1);
    });
  }

  static class MyModuleRootListener implements ModuleRootListener {
    private final Project myProject;
    private int beforeCount;
    private int afterCount;
    private long modificationCount;

    MyModuleRootListener(Project project) {
      myProject = project;
    }

    @Override
    public void beforeRootsChange(@NotNull ModuleRootEvent event) {
      beforeCount++;
    }

    @Override
    public void rootsChanged(@NotNull ModuleRootEvent event) {
      afterCount++;
    }

    void reset() {
      beforeCount = 0;
      afterCount = 0;
      modificationCount = ProjectRootManager.getInstance(myProject).getModificationCount();
    }

    void assertEventsCountAndIncrementModificationCount(int eventsCount,
                                                        boolean modificationCountMustBeIncremented,
                                                        boolean modificationCountMayBeIncremented) {
      final int beforeCount = this.beforeCount;
      final int afterCount = this.afterCount;
      assertEquals("beforeCount = " + beforeCount + ", afterCount = " + afterCount, beforeCount, afterCount);
      assertEquals(eventsCount, beforeCount);
      long currentModificationCount = ProjectRootManager.getInstance(myProject).getModificationCount();
      if (modificationCountMayBeIncremented) {
        assertTrue(currentModificationCount >= modificationCount);
      }
      else if (modificationCountMustBeIncremented) {
        assertTrue(currentModificationCount > modificationCount);
      }
      else {
        assertEquals(modificationCount, currentModificationCount);
      }
      reset();
    }

    void assertNoEvents(boolean modificationCountMayBeIncremented) {
      assertEventsCountAndIncrementModificationCount(0, false, modificationCountMayBeIncremented);
    }

    void assertEventsCount(int count) {
      assertEventsCountAndIncrementModificationCount(count, count != 0, false);
    }

    void assertNoEvents() {
      assertNoEvents(false);
    }
  }

  // create ".idea" - based project because it's 1) needed for testShelveChangesMustNotLeadToRootsChangedEvent and 2) is more common
  @Override
  protected boolean isCreateDirectoryBasedProject() {
    return true;
  }

  public void testShelveChangesMustNotLeadToRootsChangedEvent() {
    // create .idea
    StoreUtil.saveSettings(getProject());
    VirtualFile shelf = createChildDirectory(getProject().getProjectFile().getParent(), "shelf");
    VcsIgnoreManager vcsIgnoreManager = VcsIgnoreManager.getInstance(myProject);

    myModuleRootListener.reset();

    VirtualFile xxx = createChildData(shelf, "shelf1.dat");
    assertTrue(vcsIgnoreManager.isPotentiallyIgnoredFile(xxx));

    assertEquals(myModuleRootListener.modificationCount, ProjectRootManager.getInstance(myProject).getModificationCount());

    VirtualFile newShelf = createChildDirectory(getOrCreateProjectBaseDir().getParent(), "newShelf");
    VcsConfiguration vcs = VcsConfiguration.getInstance(myProject);
    vcs.USE_CUSTOM_SHELF_PATH = true;
    vcs.CUSTOM_SHELF_PATH = newShelf.getPath();
    myModuleRootListener.reset();

    VirtualFile xxx2 = createChildData(newShelf, "shelf1.dat");
    assertTrue(vcsIgnoreManager.isPotentiallyIgnoredFile(xxx2));
  }

  public void testCreationDeletionOfRootDirectoriesMustLeadToRootsChanged() {
    File root = new File(FileUtil.getTempDirectory());

    File dir1 = new File(root, "dir1");
    assertTrue(dir1.mkdirs());
    VirtualFile contentRoot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(dir1);
    assertNotNull(contentRoot);

    Module moduleA = createModule("a.iml");
    ModuleRootModificationUtil.addContentRoot(moduleA, contentRoot.getPath());
    String excludedUrl = contentRoot.getUrl() + "/excluded";
    String sourceUrl = contentRoot.getUrl() + "/src";
    String testSourceUrl = contentRoot.getUrl() + "/testSrc";
    String outputUrl = contentRoot.getUrl() + "/out";
    String testOutputUrl = contentRoot.getUrl() + "/testOut";
    String resourceUrl = contentRoot.getUrl() + "/res";
    String testResourceUrl = contentRoot.getUrl() + "/testRes";

    ModuleRootModificationUtil.updateModel(moduleA, model -> {
      ContentEntry entry = ContainerUtil.find(model.getContentEntries(), e-> contentRoot.equals(e.getFile()));

      entry.addExcludeFolder(excludedUrl);
      entry.addSourceFolder(sourceUrl, false);
      entry.addSourceFolder(testSourceUrl, true);
      entry.addSourceFolder(resourceUrl, JavaResourceRootType.RESOURCE);
      entry.addSourceFolder(testResourceUrl, JavaResourceRootType.TEST_RESOURCE);

      CompilerModuleExtension moduleExtension = model.getModuleExtension(CompilerModuleExtension.class);
      moduleExtension.inheritCompilerOutputPath(false);
      moduleExtension.setCompilerOutputPath(outputUrl);
      moduleExtension.setCompilerOutputPathForTests(testOutputUrl);
    });

    checkRootChangedOnDirCreationDeletion(contentRoot, excludedUrl, 1);
    checkRootChangedOnDirCreationDeletion(contentRoot, sourceUrl, 1);
    checkRootChangedOnDirCreationDeletion(contentRoot, testSourceUrl, 1);
    checkRootChangedOnDirCreationDeletion(contentRoot, resourceUrl, 1);
    checkRootChangedOnDirCreationDeletion(contentRoot, testResourceUrl, 1);
    checkRootChangedOnDirCreationDeletion(contentRoot, outputUrl, 1);
    checkRootChangedOnDirCreationDeletion(contentRoot, testOutputUrl, 1);
    checkRootChangedOnDirCreationDeletion(contentRoot, "xxx", 0);
  }

  private void checkRootChangedOnDirCreationDeletion(VirtualFile contentRoot, String dirUrl, int mustGenerateEvents) {
    myModuleRootListener.reset();
    UIUtil.dispatchAllInvocationEvents();

    VirtualFile dir = createChildDirectory(contentRoot, new File(dirUrl).getName());
    myModuleRootListener.assertEventsCount(mustGenerateEvents);

    myModuleRootListener.reset();
    UIUtil.dispatchAllInvocationEvents();
    delete(dir);
    myModuleRootListener.assertEventsCount(mustGenerateEvents);
  }

  public void testEmptyDirectoryCreatedAndSomeRogueFileListenerImmediatelyCallsGetChildrenPreventingFurtherCreationEventsForFilesThatHappenedToBeAlreadyThereByThatMoment() {
    File ioRoot = new File(FileUtil.getTempDirectory());

    VirtualFile vRoot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(ioRoot);
    vRoot.getChildren();

    Module module = createModule("a.iml");
    ModuleRootModificationUtil.addContentRoot(module, vRoot.getPath());
    ModuleRootModificationUtil.updateModel(module, model -> model.getContentEntries()[0].addExcludeFolder(vRoot.getUrl() + "/parent/excluded"));

    MessageBusConnection connect = ApplicationManager.getApplication().getMessageBus().connect(getTestRootDisposable());
    BulkFileListener rogueListenerWhichStupidlyGetChildrenRightAway = new BulkFileListener() {
      @Override
      public void after(@NotNull List<? extends @NotNull VFileEvent> events) {
        for (VFileEvent event : events) {
          VirtualFile file = event.getFile();
          if (event instanceof VFileCreateEvent && file != null) {
            file.getChildren();// aaaaaaaaah!
          }
        }
      }
    };
    connect.subscribe(VirtualFileManager.VFS_CHANGES, rogueListenerWhichStupidlyGetChildrenRightAway);

    myModuleRootListener.reset();

    File iParent = new File(ioRoot, "parent");
    assertTrue(iParent.mkdirs());
    vRoot.refresh(true, true);

    TimeoutUtil.sleep(1000); // hope that now async refresh has found "parent" and is waiting for EDT to fire events

    File ioExcluded = new File(iParent, "excluded");
    assertTrue(ioExcluded.mkdirs());
    UIUtil.dispatchAllInvocationEvents(); // now events are fired

    assertNotNull(LocalFileSystem.getInstance().refreshAndFindFileByIoFile(ioExcluded));

    myModuleRootListener.assertEventsCount(1);
  }

  public void testChangesInsideCompilerOutputDirectoryMustNotLeadToRootsChange() {
    File temp = new File(FileUtil.getTempDirectory());
    VirtualFile root = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(temp);
    VirtualFile content = VfsTestUtil.createDir(root, "content");

    Module module = createModule("a.iml");
    ModuleRootModificationUtil.addContentRoot(module, content.getPath());
    VirtualFile out = VfsTestUtil.createDir(root, "out");

    ModuleRootModificationUtil.updateModel(module, model -> {
      CompilerModuleExtension moduleExtension = model.getModuleExtension(CompilerModuleExtension.class);
      moduleExtension.inheritCompilerOutputPath(false);
      moduleExtension.setCompilerOutputPath(out);
      moduleExtension.setCompilerOutputPathForTests(out);
    });

    myModuleRootListener.reset();
    VirtualFile f1 = VfsTestUtil.createFile(out, "x/x.txt");
    VirtualFile f2 = VfsTestUtil.createFile(out, "x/x.class");
    myModuleRootListener.assertEventsCount(0);
    VfsTestUtil.deleteFile(f1);
    VfsTestUtil.deleteFile(f2);
    myModuleRootListener.assertEventsCount(0);
  }

  public void testBulkRootsChanging() {
    WriteAction.run(() -> {
      myModuleRootListener.reset();
      ProjectRootManagerEx.getInstanceEx(myProject).mergeRootsChangesDuring(() -> {
        for (int i = 0; i < 10; i++) {
          ModifiableRootModel model = ModuleRootManager.getInstance(myModule).getModifiableModel();
          try {
            File dir = createTempDir("src-" + i);
            VirtualFile vDir = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(dir);
            model.addContentEntry(vDir);
          }
          catch (IOException e) {
            throw new RuntimeException(e);
          }
          model.commit();

          assertEquals(1, myModuleRootListener.beforeCount);
          assertEquals(0, myModuleRootListener.afterCount);
        }
      });
      myModuleRootListener.assertEventsCount(1);
    });
  }

  public void testRootDirDeletionDoesntLeadToIndexing() throws IOException {
    File contentRoot = createTempDir("content-root");
    File excludedRoot = new File(contentRoot, "excluded-root");
    assertTrue(excludedRoot.mkdirs());
    VirtualFile excludedRootVFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(excludedRoot);

    ModuleRootModificationUtil.updateModel(myModule, model -> model.addContentEntry(excludedRootVFile.getParent()).addExcludeFolder(excludedRootVFile));

    AtomicInteger dumbModeCount = new AtomicInteger();
    SimpleMessageBusConnection connection = myProject.getMessageBus().simpleConnect();
    try {
      connection.subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
        @Override
        public void enteredDumbMode() {
          dumbModeCount.incrementAndGet();
        }
      });

      FileUtil.delete(excludedRoot);
      excludedRootVFile.refresh(false, true);
      assertFalse(excludedRootVFile.isValid());
      assertEquals(0, dumbModeCount.get());
    }
    finally {
      connection.disconnect();
    }
  }

  public void testRootsChangeEventAtCreatingAndMoveFolderUnderJarDir() throws IOException {
    String rootJarDirName = "jarDir";

    File temp = new File(FileUtil.getTempDirectory());
    VirtualFile root = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(temp);
    VirtualFile jarDir = VfsTestUtil.createDir(root, rootJarDirName);

    LibraryTable projectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject);
    LibraryTable.ModifiableModel projectLibraryTableModifiableModel = projectLibraryTable.getModifiableModel();
    Library projectLib = projectLibraryTableModifiableModel.createLibrary("lib");
    Library.ModifiableModel modifiableModel = projectLib.getModifiableModel();
    modifiableModel.addJarDirectory(jarDir, false);
    WriteAction.runAndWait(() -> {
      modifiableModel.commit();
      projectLibraryTableModifiableModel.commit();
    });

    myModuleRootListener.reset();
    // Creating simple file - first event
    VfsTestUtil.createFile(jarDir, "test.txt");
    // Creating folder - second event
    VirtualFile newVirtualDirectory = VfsTestUtil.createDir(jarDir, "foo");
    // Recursive folder creating - third event
    VfsTestUtil.createDir(newVirtualDirectory, "bar");
    myModuleRootListener.assertEventsCount(3);

    VirtualFile otherFolder = VfsTestUtil.createDir(root, "baz");
    myModuleRootListener.assertNoEvents();
    WriteAction.runAndWait(() -> otherFolder.move(this, newVirtualDirectory));
    myModuleRootListener.assertEventsCount(1);
  }

  public void testRootsChangeEventAtRenameCopyAndDeleteFileUnderJarDir() throws IOException {
    String rootJarDirName = "jarDir";

    File temp = new File(FileUtil.getTempDirectory());
    VirtualFile root = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(temp);
    VirtualFile testFileUnderRoot = VfsTestUtil.createFile(root, "test.jar");

    VirtualFile jarDir = VfsTestUtil.createDir(root, rootJarDirName);
    VirtualFile testFileUnderJarDir = VfsTestUtil.createFile(jarDir, "test.jar");

    LibraryTable projectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(myProject);
    LibraryTable.ModifiableModel projectLibraryTableModifiableModel = projectLibraryTable.getModifiableModel();
    Library projectLib = projectLibraryTableModifiableModel.createLibrary("lib");
    Library.ModifiableModel modifiableModel = projectLib.getModifiableModel();
    modifiableModel.addJarDirectory(jarDir, false);
    WriteAction.runAndWait(() -> {
      modifiableModel.commit();
      projectLibraryTableModifiableModel.commit();
    });

    myModuleRootListener.reset();
    WriteAction.runAndWait(() -> testFileUnderJarDir.rename(this, "test2.jar"));
    myModuleRootListener.assertEventsCount(1);
    WriteAction.runAndWait(() -> testFileUnderJarDir.delete(this));
    myModuleRootListener.assertEventsCount(1);

    WriteAction.runAndWait(() -> testFileUnderRoot.copy(this, jarDir, "test2.jar"));
    myModuleRootListener.assertEventsCount(1);
  }
}
