text: // Copyright 2000-2020 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 com.intellij.openapi.roots
  
  import com.intellij.openapi.application.runReadAction
  import com.intellij.openapi.application.runWriteActionAndWait
  import com.intellij.openapi.module.ModifiableModuleModel
  import com.intellij.openapi.module.Module
  import com.intellij.openapi.module.ModuleManager
  import com.intellij.openapi.roots.impl.ModifiableModelCommitter
  import com.intellij.openapi.roots.impl.RootConfigurationAccessor
  import com.intellij.testFramework.ApplicationRule
  import com.intellij.testFramework.rules.ProjectModelRule
  import org.assertj.core.api.Assertions.assertThat
  import org.jetbrains.annotations.NotNull
  import org.junit.ClassRule
  import org.junit.Rule
  import org.junit.Test
  
  class ModuleModelTest {
    companion object {
      @JvmField
      @ClassRule
      val appRule = ApplicationRule()
    }
  
    @Rule
    @JvmField
    val projectModel = ProjectModelRule()
  
    @Test
    fun `add remove module`() {
      val manager = projectModel.moduleManager
  
      val a = edit { model ->
        assertThat(model.isChanged).isFalse()
        assertThat(model.modules).isEmpty()
        val a = projectModel.createModule("a", model)
        assertThat(model.isChanged).isTrue()
        assertThat(model.modules).containsExactly(a)
        assertThat(model.getActualName(a)).isEqualTo("a")
        assertThat(model.getNewName(a)).isNull()
        assertThat(model.getModuleToBeRenamed("a")).isNull()
        assertThat(model.getModuleGroupPath(a)).isNull()
        assertThat(model.hasModuleGroups()).isFalse()
        a
      }
  
      assertThat(manager.modules).containsExactly(a)
      assertThat(getSortedModules()).containsExactly(a)
      assertThat(manager.findModuleByName("a")).isEqualTo(a)
      assertThat(manager.getModuleGroupPath(a)).isNull()
      assertThat(manager.hasModuleGroups()).isFalse()
  
  
      edit { model ->
        assertThat(model.modules).containsExactly(a)
        model.disposeModule(a)
        assertThat(model.isChanged).isTrue()
        assertThat(model.modules).isEmpty()
        assertThat(model.findModuleByName("a")).isNull()
      }
  
      assertThat(manager.modules).isEmpty()
      assertThat(getSortedModules()).isEmpty()
      assertThat(manager.findModuleByName("a")).isNull()
      assertThat(a.isDisposed).isTrue()
    }
  
    @Test
    fun `rename module`() {
      val manager = projectModel.moduleManager
  
      val module = edit { model ->
        val module = projectModel.createModule("a", model)
        model.renameModule(module, "b")
        assertThat(model.isChanged).isTrue()
        assertThat(model.modules).containsExactly(module)
        assertThat(model.getModuleToBeRenamed("a")).isNull()
        assertThat(model.getActualName(module)).isEqualTo("b")
        if (ProjectModelRule.isWorkspaceModelEnabled) {
          //in the old model newly added module doesn't get the new name until commit; it looks like a bug
          assertThat(model.findModuleByName("a")).isNull()
          assertThat(model.findModuleByName("b")).isEqualTo(module)
          assertThat(module.name).isEqualTo("b")
        }
        module
      }
  
      assertThat(manager.findModuleByName("b")).isEqualTo(module)
      assertThat(module.name).isEqualTo("b")
  
      edit { model ->
        model.renameModule(module, "c")
        assertThat(model.isChanged).isTrue()
        assertThat(model.modules).containsExactly(module)
        assertThat(model.findModuleByName("b")).isEqualTo(module)
        assertThat(model.findModuleByName("c")).isNull()
        assertThat(model.getModuleToBeRenamed("b")).isNull()
        assertThat(model.getNewName(module)).isEqualTo("c")
        assertThat(module.name).isEqualTo("b")
      }
  
      assertThat(manager.modules).containsExactly(module)
      assertThat(manager.findModuleByName("b")).isNull()
      assertThat(manager.findModuleByName("c")).isEqualTo(module)
      assertThat(module.name).isEqualTo("c")
    }
  
    @Test
    fun `remove model before committing`() {
      val a = edit { model ->
        val a = projectModel.createModule("a", model)
        model.disposeModule(a)
        assertThat(model.modules).isEmpty()
        assertThat(model.findModuleByName("a")).isNull()
        a
      }
      assertThat(projectModel.moduleManager.modules).isEmpty()
      assertThat(a.isDisposed).isTrue()
    }
  
    @Test
    fun `add module and dispose model`() {
      val a = projectModel.createModule("a")
      val model = createModifiableModuleModel()
      val b = projectModel.createModule("b", model)
      assertThat(model.modules).containsExactlyInAnyOrder(a, b)
      runWriteActionAndWait { model.dispose() }
      assertThat(b.isDisposed).isTrue()
      assertThat(a.isDisposed).isFalse()
      assertThat(projectModel.moduleManager.modules).containsExactly(a)
    }
  
    @Test
    fun `remove module and dispose model`() {
      val a = projectModel.createModule("a")
      val model = createModifiableModuleModel()
      model.disposeModule(a)
      assertThat(model.modules).isEmpty()
      runWriteActionAndWait { model.dispose() }
      assertThat(a.isDisposed).isFalse()
      assertThat(projectModel.moduleManager.modules).containsExactly(a)
    }
  
    @Test
    fun `commit root model before committing module model`() {
      val library = projectModel.addProjectLevelLibrary("lib")
      val moduleModel = createModifiableModuleModel()
      val module = projectModel.createModule("a", moduleModel)
      val model = createModifiableModel(module)
      val entry = model.addLibraryEntry(library)
      assertThat(entry.ownerModule).isEqualTo(module)
      val committed = commitModifiableRootModel(model)
      val libraryEntry = dropModuleSourceEntry(committed, 1).single() as LibraryOrderEntry
      assertThat(libraryEntry.ownerModule).isEqualTo(module)
      assertThat(libraryEntry.library).isEqualTo(library)
      runWriteActionAndWait { moduleModel.commit() }
      val libraryEntryForCommitted = dropModuleSourceEntry(ModuleRootManager.getInstance(module), 1).single() as LibraryOrderEntry
      assertThat(libraryEntryForCommitted.library).isEqualTo(library)
    }
  
    @Test
    fun `commit root model and dispose module`() {
      val library = projectModel.addProjectLevelLibrary("lib")
      val moduleModel = createModifiableModuleModel()
      val module = projectModel.createModule("a", moduleModel)
      val model = createModifiableModel(module)
      model.addLibraryEntry(library)
      commitModifiableRootModel(model)
      runWriteActionAndWait { moduleModel.dispose() }
      assertThat(projectModel.moduleManager.modules).isEmpty()
    }
  
    @Test
    fun `create two modules with dependency between them`() {
      val moduleModel = createModifiableModuleModel()
      val a = projectModel.createModule("a", moduleModel)
      val b = projectModel.createModule("b", moduleModel)
      assertThat(moduleModel.modules).containsExactlyInAnyOrder(a, b)
      val model = createModifiableModel(a)
      model.addModuleOrderEntry(b)
      runWriteActionAndWait { ModifiableModelCommitter.multiCommit(listOf(model), moduleModel) }
      val moduleManager = projectModel.moduleManager
      assertThat(moduleManager.modules).containsExactlyInAnyOrder(a, b)
      assertThat(getSortedModules()).containsExactly(b, a)
      runReadAction {
        assertThat(moduleManager.isModuleDependent(a, b)).isTrue()
        assertThat(moduleManager.isModuleDependent(b, a)).isFalse()
      }
      assertThat(moduleManager.findModuleByName("a")).isEqualTo(a)
      assertThat(ModuleRootManager.getInstance(a).dependencies.single()).isEqualTo(b)
    }
  
    @Test
    fun `commit multiple modules with module-level libraries`() {
      val moduleModel = createModifiableModuleModel()
      val a = projectModel.createModule("a", moduleModel)
      val b = projectModel.createModule("b", moduleModel)
      val modelA = createModifiableModel(a)
      val libraryAModel = modelA.moduleLibraryTable.createLibrary().modifiableModel
      val aRoot = projectModel.baseProjectDir.newVirtualDirectory("a-classes")
      libraryAModel.addRoot(aRoot, OrderRootType.CLASSES)
      libraryAModel.commit()
      val modelB = createModifiableModel(b)
      val libraryBModel = modelB.moduleLibraryTable.createLibrary().modifiableModel
      val bRoot = projectModel.baseProjectDir.newVirtualDirectory("b-classes")
      libraryBModel.addRoot(bRoot, OrderRootType.CLASSES)
      libraryBModel.commit()
      runWriteActionAndWait { ModifiableModelCommitter.multiCommit(listOf(modelA, modelB), moduleModel) }
      val moduleManager = projectModel.moduleManager
      assertThat(moduleManager.modules).containsExactlyInAnyOrder(a, b)
      assertThat((ModuleRootManager.getInstance(a).orderEntries[1] as LibraryOrderEntry).library!!.getFiles(OrderRootType.CLASSES)).containsExactly(aRoot)
      assertThat((ModuleRootManager.getInstance(b).orderEntries[1] as LibraryOrderEntry).library!!.getFiles(OrderRootType.CLASSES)).containsExactly(bRoot)
    }
  
    @Test
    fun `create two modules with circular dependency between them`() {
      val moduleModel = createModifiableModuleModel()
      val a = projectModel.createModule("a", moduleModel)
      val b = projectModel.createModule("b", moduleModel)
      val modelA = createModifiableModel(a)
      modelA.addModuleOrderEntry(b)
      val modelB = createModifiableModel(b)
      modelB.addModuleOrderEntry(a)
      runWriteActionAndWait { ModifiableModelCommitter.multiCommit(listOf(modelA, modelB), moduleModel) }
      val moduleManager = projectModel.moduleManager
      assertThat(moduleManager.modules).containsExactlyInAnyOrder(a, b)
      assertThat(getSortedModules()).containsExactlyInAnyOrder(a, b)
      assertThat(moduleManager.findModuleByName("a")).isEqualTo(a)
      assertThat(moduleManager.findModuleByName("b")).isEqualTo(b)
      runReadAction {
        assertThat(moduleManager.isModuleDependent(a, b)).isTrue()
        assertThat(moduleManager.isModuleDependent(b, a)).isTrue()
      }
      assertThat(ModuleRootManager.getInstance(a).dependencies.single()).isEqualTo(b)
      assertThat(ModuleRootManager.getInstance(b).dependencies.single()).isEqualTo(a)
    }
  
    @Test
    fun `rename module referenced from modifiable model`() {
      val a = projectModel.createModule("a")
      val b = projectModel.createModule("b")
      ModuleRootModificationUtil.addDependency(a, b)
      val moduleModel = createModifiableModuleModel()
      val modelA = createModifiableModel(a, ModifiableModuleModelAccessor(moduleModel))
      moduleModel.renameModule(b, "c")
      val moduleEntry = dropModuleSourceEntry(modelA, 1).single() as ModuleOrderEntry
      assertThat(moduleEntry.module).isEqualTo(b)
  
      runWriteActionAndWait { ModifiableModelCommitter.multiCommit(listOf(modelA), moduleModel) }
      val moduleManager = projectModel.moduleManager
      assertThat(moduleManager.modules).containsExactlyInAnyOrder(a, b)
      val committedEntry = dropModuleSourceEntry(ModuleRootManager.getInstance(a), 1).single() as ModuleOrderEntry
      assertThat(committedEntry.module).isEqualTo(b)
      assertThat(committedEntry.moduleName).isEqualTo("c")
    }
  
    @Test
    fun `rename newly created module referenced from modifiable model`() {
      val moduleModel = createModifiableModuleModel()
      val a = projectModel.createModule("a", moduleModel)
      val b = projectModel.createModule("b", moduleModel)
      val modelA = runReadAction { ModuleRootManagerEx.getInstanceEx(a).getModifiableModelForMultiCommit(ModifiableModuleModelAccessor(moduleModel)) }
      modelA.addModuleOrderEntry(b)
      moduleModel.renameModule(b, "c")
      val moduleEntry = dropModuleSourceEntry(modelA, 1).single() as ModuleOrderEntry
      assertThat(moduleEntry.module).isEqualTo(b)
  
      runWriteActionAndWait { ModifiableModelCommitter.multiCommit(listOf(modelA), moduleModel) }
      val moduleManager = projectModel.moduleManager
      assertThat(moduleManager.modules).containsExactlyInAnyOrder(a, b)
      val committedEntry = dropModuleSourceEntry(ModuleRootManager.getInstance(a), 1).single() as ModuleOrderEntry
      assertThat(committedEntry.module).isEqualTo(b)
      assertThat(committedEntry.moduleName).isEqualTo("c")
    }
  
    @Test
    fun `rename module after creating modifiable root model for it`() {
      val moduleModel = createModifiableModuleModel()
      val a = projectModel.createModule("a", moduleModel)
      val rootModel = runReadAction {
        ModuleRootManagerEx.getInstanceEx(a).getModifiableModelForMultiCommit(ModifiableModuleModelAccessor(moduleModel))
      }
      val root = projectModel.baseProjectDir.newVirtualDirectory("root")
      rootModel.addContentEntry(root)
      moduleModel.renameModule(a, "b")
      val root2 = projectModel.baseProjectDir.newVirtualDirectory("root2")
      rootModel.addContentEntry(root2)
      assertThat(rootModel.module).isEqualTo(a)
      assertThat(rootModel.contentRoots).containsExactly(root, root2)
  
      runWriteActionAndWait { ModifiableModelCommitter.multiCommit(listOf(rootModel), moduleModel) }
      val moduleManager = projectModel.moduleManager
      assertThat(moduleManager.modules).containsExactly(a)
      assertThat(ModuleRootManager.getInstance(a).contentRoots).containsExactly(root, root2)
    }
  
    @Test
    fun `twice module rename`() {
      val antModuleName = "ant"
      val mavenModuleName = "maven"
      val gradleModuleName = "gradle"
      val moduleManager = ModuleManager.getInstance(projectModel.project)
  
      val antModule = projectModel.createModule(antModuleName)
      var modules = moduleManager.modules
      assertThat(modules.size).isEqualTo(1)
      assertThat(modules[0].name).isEqualTo(antModuleName)
  
      runWriteActionAndWait {
        moduleManager.modifiableModel.let { model ->
          model.renameModule(antModule, mavenModuleName)
          model.renameModule(antModule, gradleModuleName)
          model.commit()
        }
      }
      assertThat(antModule.name).isEqualTo(gradleModuleName)
  
      modules = moduleManager.modules
      assertThat(modules.size).isEqualTo(1)
      assertThat(modules[0].name).isEqualTo(gradleModuleName)
    }
  
    @Test
    fun `set and remove module group`() {
      val module = projectModel.createModule()
      assertThat(projectModel.moduleManager.hasModuleGroups()).isFalse()
      edit { model ->
        assertThat(model.hasModuleGroups()).isFalse()
        model.setModuleGroupPath(module, arrayOf("foo", "bar"))
        assertThat(model.hasModuleGroups()).isTrue()
      }
      assertThat(projectModel.moduleManager.hasModuleGroups()).isTrue()
      assertThat(projectModel.moduleManager.getModuleGroupPath(module)).containsExactly("foo", "bar")
      edit { model ->
        assertThat(model.hasModuleGroups()).isTrue()
        model.setModuleGroupPath(module, null)
        assertThat(model.hasModuleGroups()).isFalse()
      }
      assertThat(projectModel.moduleManager.hasModuleGroups()).isFalse()
      assertThat(projectModel.moduleManager.getModuleGroupPath(module)).isNull()
    }
  
    @Test
    fun `set module group after changing module name`() {
      val module = projectModel.createModule("foo")
      edit { model ->
        model.renameModule(module, "bar")
        model.setModuleGroupPath(module, arrayOf("group"))
        assertThat(model.hasModuleGroups()).isTrue()
      }
      assertThat(projectModel.moduleManager.hasModuleGroups()).isTrue()
      assertThat(projectModel.moduleManager.getModuleGroupPath(module)).containsExactly("group")
    }
  
    @Test
    fun `remove module and add module with same name`() {
      val module = projectModel.createModule("foo")
      edit { model ->
        model.disposeModule(module)
        val updatedModule = projectModel.createModule("foo", model)
        assertThat(model.modules).containsExactly(updatedModule)
      }
      val updatedModule = projectModel.moduleManager.modules.single()
      assertThat(updatedModule.name).isEqualTo("foo")
      assertThat(module).isNotSameAs(updatedModule)
    }
  
    class ModifiableModuleModelAccessor(private val moduleModel: ModifiableModuleModel) : RootConfigurationAccessor() {
      override fun getModule(module: Module?, moduleName: String): Module? {
        return module ?: moduleModel.findModuleByName(moduleName)
      }
    }
  
    private fun createModifiableModuleModel(): @NotNull ModifiableModuleModel {
      //we need to get module manager outside of read action because it may lazily initialize the project requiring write action
      val moduleManager = projectModel.moduleManager
      return runReadAction { moduleManager.modifiableModel }
    }
  
    private fun getSortedModules() = runReadAction { projectModel.moduleManager.sortedModules }
  
    private fun <T> edit(action: (ModifiableModuleModel) -> T): T {
      val model = createModifiableModuleModel()
      checkConsistency(model)
      val result = action(model)
      checkConsistency(model)
      runWriteActionAndWait { model.commit() }
      return result
    }
  
    private fun checkConsistency(model: ModifiableModuleModel) {
      for (module in model.modules) {
        val newName = model.getNewName(module)
        if (newName != null) {
          assertThat(model.getActualName(module)).isEqualTo(newName)
          assertThat(model.getModuleToBeRenamed(newName)).isEqualTo(module)
        }
        else {
          assertThat(model.getActualName(module)).isEqualTo(module.name)
        }
        assertThat(model.findModuleByName(module.name)).isEqualTo(module)
      }
    }
  }