package com.dlsc.workbenchfx;

import static com.dlsc.workbenchfx.Workbench.WorkbenchBuilder;
import static com.dlsc.workbenchfx.testing.MockFactory.createMockModule;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import com.dlsc.workbenchfx.model.WorkbenchDialog;
import com.dlsc.workbenchfx.model.WorkbenchModule;
import com.dlsc.workbenchfx.model.WorkbenchOverlay;
import com.dlsc.workbenchfx.testing.MockDialogControl;
import com.dlsc.workbenchfx.testing.MockNavigationDrawer;
import com.dlsc.workbenchfx.testing.MockPage;
import com.dlsc.workbenchfx.testing.MockTab;
import com.dlsc.workbenchfx.testing.MockTile;
import com.dlsc.workbenchfx.view.controls.GlassPane;
import com.dlsc.workbenchfx.view.controls.NavigationDrawer;
import com.dlsc.workbenchfx.view.controls.ToolbarItem;
import com.dlsc.workbenchfx.view.controls.dialog.DialogControl;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.testfx.api.FxRobot;
import org.testfx.framework.junit5.ApplicationTest;

/**
 * Tests for {@link Workbench}.
 */
@Tag("fast")
class WorkbenchTest extends ApplicationTest {

  private static final int SIZE = 3;

  private static final int FIRST_INDEX = 0;
  private static final int SECOND_INDEX = 1;
  private static final int LAST_INDEX = SIZE - 1;
  Workbench workbench;

  WorkbenchModule[] mockModules = new WorkbenchModule[SIZE];
  Node[] moduleNodes = new Node[SIZE];

  WorkbenchModule first;
  WorkbenchModule second;
  WorkbenchModule last;
  private ObservableMap<Region, WorkbenchOverlay> overlays;
  private ObservableList<Region> blockingOverlaysShown;
  private ObservableList<Region> overlaysShown;
  private Region overlay1;
  private Region overlay2;
  private Region overlay3;

  private MenuItem menuItem;
  private ObservableList<MenuItem> navigationDrawerItems;

  private FxRobot robot;

  // ToolbarItem items
  private String toolbarItemText;
  private FontAwesomeIconView toolbarItemIconView;
  private MenuItem toolbarItemMenuItem;
  private ToolbarItem toolbarItemLeft;
  private ToolbarItem toolbarItemRight;

  private MockNavigationDrawer navigationDrawer;
  private MockDialogControl dialogControl;

  @Mock
  private WorkbenchDialog mockDialog;

  @Mock
  private Consumer<ButtonType> mockOnResult;

  private ObservableList<ButtonType> buttonTypes =
      FXCollections.observableArrayList(ButtonType.PREVIOUS, ButtonType.NEXT);

  private Pane drawer = new Pane();

  private BooleanProperty blocking;

  @Override
  public void start(Stage stage) {
    MockitoAnnotations.initMocks(this);

    robot = new FxRobot();

    for (int i = 0; i < moduleNodes.length; i++) {
      moduleNodes[i] = new Label("Module Content");
    }

    for (int i = 0; i < mockModules.length; i++) {
      mockModules[i] = createMockModule(
          moduleNodes[i], null, true, "Module " + i, workbench,
          FXCollections.observableArrayList(), FXCollections.observableArrayList()
      );
    }

    FontAwesomeIconView fontAwesomeIconView = new FontAwesomeIconView(FontAwesomeIcon.QUESTION);
    fontAwesomeIconView.getStyleClass().add("icon");
    menuItem = new MenuItem("Item 1.1", fontAwesomeIconView);

    // Initialization of items for ToolbarItem testing
    toolbarItemText = "ToolbarItem Text";
    toolbarItemIconView = new FontAwesomeIconView(FontAwesomeIcon.QUESTION);
    toolbarItemMenuItem = new MenuItem("Menu Item");

    toolbarItemLeft = new ToolbarItem(toolbarItemText, toolbarItemIconView, toolbarItemMenuItem);
    toolbarItemRight = new ToolbarItem(toolbarItemText, toolbarItemIconView, toolbarItemMenuItem);

    // Setup WorkbenchDialog Mock
    blocking = new SimpleBooleanProperty();
    when(mockDialog.getButtonTypes()).thenReturn(buttonTypes);
    when(mockDialog.getOnResult()).thenReturn(mockOnResult);
    when(mockDialog.blockingProperty()).thenReturn(blocking);

    navigationDrawer = new MockNavigationDrawer();
    dialogControl = new MockDialogControl();
    when(mockDialog.getDialogControl()).thenReturn(dialogControl);
    dialogControl.setDialog(mockDialog);

    workbench = Workbench.builder(
        mockModules[FIRST_INDEX],
        mockModules[SECOND_INDEX],
        mockModules[LAST_INDEX])
        .tabFactory(MockTab::new)
        .tileFactory(MockTile::new)
        .pageFactory(MockPage::new)
        .navigationDrawer(navigationDrawer)
        .navigationDrawerItems(menuItem)
        .toolbarLeft(toolbarItemLeft)
        .toolbarRight(toolbarItemRight)
        .build();

    first = mockModules[FIRST_INDEX];
    when(first.getWorkbench()).thenReturn(workbench);
    second = mockModules[SECOND_INDEX];
    when(second.getWorkbench()).thenReturn(workbench);
    last = mockModules[LAST_INDEX];
    when(last.getWorkbench()).thenReturn(workbench);

    overlays = workbench.getOverlays();
    blockingOverlaysShown = workbench.getBlockingOverlaysShown();
    overlaysShown = workbench.getNonBlockingOverlaysShown();
    overlay1 = new Label();
    overlay1.setVisible(false);
    overlay2 = new Label();
    overlay2.setVisible(false);
    overlay3 = new Label();
    overlay3.setVisible(false);

    navigationDrawerItems = workbench.getNavigationDrawerItems();

    Scene scene = new Scene(workbench, 100, 100);
    stage.setScene(scene);
    stage.show();
  }

  @Test
  void testCtor() {
    robot.interact(() -> {
      assertEquals(mockModules.length, workbench.getModules().size());
      for (int i = 0; i < mockModules.length; i++) {
        assertSame(mockModules[i], workbench.getModules().get(i));
      }

      assertEquals(0, workbench.getOpenModules().size());

      assertNull(workbench.activeModuleViewProperty().get());
    });
  }

  @Test
  void testDefaultCtor() {
    robot.interact(() -> {
      Workbench defaultBench = new Workbench();
      defaultBench.getModules().addAll(first, second, last);
      assertEquals(mockModules.length, workbench.getModules().size());
      for (int i = 0; i < mockModules.length; i++) {
        assertSame(mockModules[i], workbench.getModules().get(i));
      }

      assertEquals(0, workbench.getOpenModules().size());

      assertNull(workbench.activeModuleViewProperty().get());

      // Tests if initNavigationDrawer() in the defaultCtor was called and this Workbench was set.
      NavigationDrawer defaultDrawer = defaultBench.getNavigationDrawer();
      assertNotNull(defaultDrawer.getWorkbench());
      assertSame(defaultBench, defaultDrawer.getWorkbench());
    });
  }

  @Test
  void testNavigationDrawerPropertyListener() {
    robot.interact(() -> {
      Workbench defaultBench = new Workbench();
      assertEquals(0, defaultBench.getNavigationDrawerItems().size());

      // Tests if listener triggers when setting a new NavigationDrawer
      MockNavigationDrawer mockNavigationDrawer = new MockNavigationDrawer();
      assertNull(mockNavigationDrawer.getWorkbench());
      defaultBench.setNavigationDrawer(mockNavigationDrawer);
      assertNotNull(mockNavigationDrawer.getWorkbench());
      assertEquals(defaultBench, mockNavigationDrawer.getWorkbench());
      assertEquals(0, defaultBench.getNavigationDrawerItems().size());
    });
  }

  // asciidoctor Documentation - tag::openModule[]
  @Test
  void openModule() {
    robot.interact(() -> {
      // Open first
      workbench.openModule(first);
      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());
      InOrder inOrder = inOrder(first);
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Open last
      workbench.openModule(last);
      assertSame(last, workbench.getActiveModule());
      assertSame(moduleNodes[LAST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());
      inOrder = inOrder(first, last);
      inOrder.verify(first).deactivate();
      inOrder.verify(last).init(workbench);
      inOrder.verify(last).activate();
      // Open last again
      workbench.openModule(last);
      assertSame(last, workbench.getActiveModule());
      assertSame(moduleNodes[LAST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());
      verify(last, times(1)).init(workbench);
      verify(last, times(1)).activate();
      verify(last, never()).deactivate();
      // Open first (already initialized)
      workbench.openModule(first);
      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());
      verify(first, times(1)).init(workbench); // no additional init on first
      verify(last, times(1)).init(workbench); // no additional init on last
      inOrder = inOrder(first, last);
      inOrder.verify(last).deactivate();
      inOrder.verify(first).activate();
      verify(first, times(2)).activate();
      // Switch to home screen
      workbench.openAddModulePage();
      assertSame(null, workbench.getActiveModule());
      assertSame(null, workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());
      verify(first, times(1)).init(workbench); // no additional init on first
      verify(last, times(1)).init(workbench); // no additional init on last
      verify(first, times(2)).deactivate();
      // Open second
      workbench.openModule(second);
      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(3, workbench.getOpenModules().size());
      inOrder = inOrder(second);
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();

      ignoreModuleGetters(first, second, last);
      verifyNoMoreInteractions(first, second, last);
    });
  }

  @Test
  void openModuleInvalid() {
    /* Test if opening a module which has not been passed in the constructor of WorkbenchFxModel
    throws an exception */
    robot.interact(() -> {
      assertThrows(IllegalArgumentException.class,
          () -> workbench.openModule(mock(WorkbenchModule.class)));
    });
  }
  // asciidoctor Documentation - end::openModule[]

  // asciidoctor Documentation - tag::closeModule[]
  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleOne() {
    // open and close module
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.closeModule(first);

      assertSame(null, workbench.getActiveModule());
      assertSame(null, workbench.getActiveModuleView());
      assertEquals(0, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.closeModule(first)
      inOrder.verify(first).deactivate();
      inOrder.verify(first).destroy();

      ignoreModuleGetters(first);
      verifyNoMoreInteractions(first);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleLeft1() {
    robot.interact(() -> {
      // open two modules, close left module
      // right active
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(first);

      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());
      verify(second, never()).deactivate();

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(first)
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleLeft2() {
    robot.interact(() -> {
      // open two modules, close left module
      // left active
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.openModule(first);
      workbench.closeModule(first);

      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.openModule(first)
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // Call: workbench.closeModule(first)
      inOrder.verify(first).deactivate();
      inOrder.verify(first).destroy();
      inOrder.verify(second).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleRight1() {
    // open two modules, close right module
    // right active
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(second);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(second)
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy();
      inOrder.verify(first).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleRight2() {
    // open two modules, close right module
    // left active
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.openModule(first);
      workbench.closeModule(second);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.openModule(first)
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // Call: workbench.closeModule(second)
      inOrder.verify(second, never()).deactivate();
      inOrder.verify(second).destroy();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModuleMiddleActive() {
    // open three modules and close middle module
    // middle active
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.openModule(last);
      workbench.openModule(second);
      workbench.closeModule(second);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second, last);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.openModule(last)
      inOrder.verify(second).deactivate();
      inOrder.verify(last).init(workbench);
      inOrder.verify(last).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(last).deactivate();
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(second)
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy();
      inOrder.verify(first).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }
  // asciidoctor Documentation - end::closeModule[]

  // asciidoctor Documentation - tag::closeModuleInterrupt[]
  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModulePreventDestroyActive() {
    // open two modules, close second (active) module
    // destroy() on second module will return false, so the module shouldn't get closed
    when(second.destroy()).thenReturn(false);
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(second);

      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(second)
      // destroy second
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy();
      // notice destroy() was unsuccessful, keep focus on second
      inOrder.verify(second).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Precondition: openModule tests pass.
   */
  @Test
  void closeModulePreventDestroyInactive() {
    // open two modules, close first (inactive) module
    // destroy() on first module will return false, so the module shouldn't get closed
    when(first.destroy()).thenReturn(false);
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(first);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(second)
      // destroy second
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy();
      // notice destroy() was unsuccessful, switch focus to first
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Example of what happens in case of a closing dialog in the destroy() method of a module with
   * the user confirming the module should get closed. Precondition: openModule tests pass.
   */
  @Test
  void closeModuleDestroyInactiveDialogClose() {
    // open two modules, close first (inactive) module
    // destroy() on first module will return false, so the module shouldn't get closed
    when(first.destroy()).then(invocation -> {
      // dialog opens
      return false;
    });
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(first);
      // user confirms yes on dialog: WorkbenchModule#close()
      simulateModuleClose(first);

      assertSame(second, workbench.getActiveModule());
      assertSame(moduleNodes[SECOND_INDEX], workbench.getActiveModuleView());
      assertEquals(1, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(first)
      // attempt to destroy first
      inOrder.verify(first).destroy();
      // destroy() returns false, closeModule() opens first module
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // WorkbenchModule#close(), switch to second
      inOrder.verify(first).deactivate();
      inOrder.verify(second).activate();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  /**
   * Internal testing utility method.
   * Ignores calls to the getters of {@link WorkbenchModule}, which enables to safely call
   * {@link Mockito#verifyNoMoreInteractions} during lifecycle order verification tests without
   * having to make assumptions about how many times the getters have been called as well.
   */
  private void ignoreModuleGetters(WorkbenchModule... modules) {
    for (WorkbenchModule module : modules) {
      verify(module, atLeast(0)).getIcon();
      verify(module, atLeast(0)).getName();
      verify(module, atLeast(0)).getWorkbench();
      verify(module, atLeast(0)).getToolbarControlsLeft();
      verify(module, atLeast(0)).getToolbarControlsRight();
    }
  }

  /**
   * Internal testing method which simulates a call to {@link WorkbenchModule#close()}.
   */
  private void simulateModuleClose(WorkbenchModule module) {
    workbench.completeModuleCloseable(module);
  }

  /**
   * Example of what happens in case of a closing dialog in the destroy() method of a module with
   * the user confirming the module should NOT get closed. Precondition: openModule tests pass.
   */
  @Test
  void closeModulePreventDestroyInactiveDialogClose() {
    // open two modules, close first (inactive) module
    // destroy() on first module will return false, so the module shouldn't get closed
    when(first.destroy()).then(invocation -> {
      // dialog opens, user confirms NOT closing module
      return false;
    });

    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.closeModule(first);

      assertSame(first, workbench.getActiveModule());
      assertSame(moduleNodes[FIRST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.closeModule(first)
      // attempt to destroy first
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy();
      // destroy() returns false, switch open module to first
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // destroy() returns false, first stays the active module
      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);
    });
  }

  @Test
  void closeModuleInvalid() {
    robot.interact(() -> {
      // Test for null
      assertThrows(NullPointerException.class, () -> workbench.closeModule(null));
      // Test if closing a module not included in the modules at all throws an exception
      assertThrows(IllegalArgumentException.class,
          () -> workbench.closeModule(mock(WorkbenchModule.class)));
      // Test if closing a module not opened throws an exception
      assertThrows(IllegalArgumentException.class, () -> workbench.closeModule(mockModules[0]));
    });
  }

  @Test
  void closeInactiveModule() {
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);
      workbench.openModule(last);
      workbench.closeModule(second);

      assertSame(last, workbench.getActiveModule());
      assertSame(moduleNodes[LAST_INDEX], workbench.getActiveModuleView());
      assertEquals(2, workbench.getOpenModules().size());

      InOrder inOrder = inOrder(first, second, last);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();
      // Call: workbench.openModule(last)
      inOrder.verify(second).deactivate();
      inOrder.verify(last).init(workbench);
      inOrder.verify(last).activate();
      // Call: workbench.closeModule(second)
      inOrder.verify(second, never()).deactivate();
      inOrder.verify(second).destroy();
      inOrder.verify(last).getWorkbench();
      inOrder.verify(last).getName();
      inOrder.verify(last).getIcon();

      ignoreModuleGetters(first, second, last);
      verifyNoMoreInteractions(first, second, last);
    });
  }
  // asciidoctor Documentation - end::closeModuleInterrupt[]

  @Test
  void getOpenModules() {
    // Test if unmodifiable list is returned
    robot.interact(() -> {
      assertThrows(UnsupportedOperationException.class, () -> workbench.getOpenModules().remove(0));
    });
  }

  @Test
  void activeModuleViewProperty() {
    assertTrue(workbench.activeModuleViewProperty() instanceof ReadOnlyObjectProperty);
  }

  @Test
  void activeModuleProperty() {
    assertTrue(workbench.activeModuleProperty() instanceof ReadOnlyObjectProperty);
  }

  @Test
  void getAmountOfPages() {
    robot.interact(() -> {
      int modulesPerPage = 1;
      assertEquals(1, prepareWorkbench(1, modulesPerPage).getAmountOfPages());
      assertEquals(2, prepareWorkbench(2, modulesPerPage).getAmountOfPages());
      assertEquals(3, prepareWorkbench(3, modulesPerPage).getAmountOfPages());

      modulesPerPage = 2;
      assertEquals(1, prepareWorkbench(1, modulesPerPage).getAmountOfPages());
      assertEquals(1, prepareWorkbench(2, modulesPerPage).getAmountOfPages());
      assertEquals(2, prepareWorkbench(3, modulesPerPage).getAmountOfPages());
      assertEquals(2, prepareWorkbench(4, modulesPerPage).getAmountOfPages());
      assertEquals(3, prepareWorkbench(5, modulesPerPage).getAmountOfPages());

      modulesPerPage = 3;
      assertEquals(1, prepareWorkbench(1, modulesPerPage).getAmountOfPages());
      assertEquals(1, prepareWorkbench(2, modulesPerPage).getAmountOfPages());
      assertEquals(1, prepareWorkbench(3, modulesPerPage).getAmountOfPages());
      assertEquals(2, prepareWorkbench(4, modulesPerPage).getAmountOfPages());
      assertEquals(2, prepareWorkbench(5, modulesPerPage).getAmountOfPages());
      assertEquals(2, prepareWorkbench(6, modulesPerPage).getAmountOfPages());
      assertEquals(3, prepareWorkbench(7, modulesPerPage).getAmountOfPages());
    });
  }

  private Workbench prepareWorkbench(int moduleAmount, int modulesPerPage) {
    WorkbenchModule[] modules = new WorkbenchModule[moduleAmount];
    for (int i = 0; i < moduleAmount; i++) {
      modules[i] = mock(WorkbenchModule.class);
    }
    return Workbench.builder(modules).modulesPerPage(modulesPerPage).build();
  }

  @Test
  void builder() {
    WorkbenchBuilder builder = Workbench.builder();
    assertNotNull(builder);
  }

  @Test
  void getOverlays() {
    robot.interact(() -> {
      ObservableMap<Region, WorkbenchOverlay> overlays = workbench.getOverlays();
      // Test if unmodifiable map is returned
      assertThrows(UnsupportedOperationException.class,
          () -> overlays.put(new Label(), new WorkbenchOverlay(new Label(), null)));
    });
  }

  @Test
  void showOverlayBlocking() {
    robot.interact(() -> {
      assertEquals(0, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());

      workbench.showOverlay(overlay1, true);

      assertEquals(1, overlays.size());
      assertEquals(1, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
      assertTrue(overlay1.isVisible()); // overlay1 has been made visible
      GlassPane glassPane = overlays.get(overlay1).getGlassPane();
      assertFalse(glassPane.isHide());
      assertNull(glassPane.onMouseClickedProperty().get()); // no closing handler has been attached

      // test visibility binding to GlassPane
      overlay1.setVisible(false);
      assertTrue(glassPane.isHide());

      // test if calling showOverlay again, even though it's already showing, does anything
      workbench.showOverlay(overlay1, true);

      assertEquals(1, overlays.size());
      assertEquals(1, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible()); // overlay1 is still invisible
    });
  }

  @Test
  void showOverlayNonBlocking() {
    robot.interact(() -> {
      assertEquals(0, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());

      workbench.showOverlay(overlay1, false);

      assertEquals(1, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(1, overlaysShown.size());
      assertTrue(overlay1.isVisible()); // overlay1 has been made visible
      GlassPane glassPane = overlays.get(overlay1).getGlassPane();
      assertFalse(glassPane.isHide());
      assertNotNull(glassPane.onMouseClickedProperty().get()); // closing handler has been attached

      // test visibility binding to GlassPane
      overlay1.setVisible(false);
      assertTrue(glassPane.isHide());

      // test if calling showOverlay again, even though it's already showing, does anything
      workbench.showOverlay(overlay1, false);

      assertEquals(1, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(1, overlaysShown.size());
      assertFalse(overlay1.isVisible()); // overlay1 is still invisible
    });
  }

  @Test
  void showOverlayMultiple() {
    robot.interact(() -> {
      assertEquals(0, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());

      workbench.showOverlay(overlay1, false);
      workbench.showOverlay(overlay2, true);

      assertEquals(2, overlays.size());
      assertEquals(1, blockingOverlaysShown.size());
      assertEquals(1, overlaysShown.size());
      assertTrue(overlay1.isVisible()); // overlay1 has been made visible
      assertTrue(overlay2.isVisible()); // overlay2 has been made visible
      GlassPane glassPane1 = overlays.get(overlay1).getGlassPane();
      assertFalse(glassPane1.isHide());
      assertNotNull(glassPane1.onMouseClickedProperty().get()); // closing handler has been attached
      GlassPane glassPane2 = overlays.get(overlay2).getGlassPane();
      assertFalse(glassPane2.isHide());
      assertNull(glassPane2.onMouseClickedProperty().get()); // no closing handler has been attached

      // test visibility binding to GlassPane
      overlay1.setVisible(false);
      assertTrue(glassPane1.isHide());
      overlay2.setVisible(false);
      assertTrue(glassPane2.isHide());

      // test if calling showOverlay again, even though it's already showing, does anything
      workbench.showOverlay(overlay1, false);
      workbench.showOverlay(overlay2, true);

      assertEquals(2, overlays.size());
      assertEquals(1, blockingOverlaysShown.size());
      assertEquals(1, overlaysShown.size());
      assertFalse(overlay1.isVisible()); // overlay1 is still invisible
      assertFalse(overlay2.isVisible()); // overlay1 is still invisible
    });
  }

  /**
   * Precondition: showOverlay tests pass.
   */
  @Test
  void hideOverlayBlocking() {
    robot.interact(() -> {
      workbench.showOverlay(overlay1, true);
      boolean result = workbench.hideOverlay(overlay1);

      assertTrue(result);
      assertEquals(1, overlays.size()); // still loaded
      assertEquals(0, blockingOverlaysShown.size()); // none shown
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible()); // overlay1 is invisible
      GlassPane glassPane = overlays.get(overlay1).getGlassPane();
      assertTrue(glassPane.isHide());

      // test if calling hideOverlay again, even though it's already hidden, does anything
      result = workbench.hideOverlay(overlay1);

      assertFalse(result);
      assertEquals(1, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible()); // overlay1 is still invisible

      // test if calling showOverlay again, doesn't load the overlay1 into the map again
      result = workbench.showOverlay(overlay1, true);

      assertTrue(result);
      assertEquals(1, overlays.size());
      assertEquals(1, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
      assertTrue(overlay1.isVisible()); // overlay1 is visible again
    });
  }

  /**
   * Precondition: showOverlay tests pass.
   */
  @Test
  void hideOverlayNonBlocking() {
    robot.interact(() -> {
      workbench.showOverlay(overlay1, false);
      boolean result = workbench.hideOverlay(overlay1);

      assertTrue(result);
      assertEquals(1, overlays.size()); // still loaded
      assertEquals(0, blockingOverlaysShown.size()); // none shown
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible()); // overlay1 is invisible
      GlassPane glassPane = overlays.get(overlay1).getGlassPane();
      assertTrue(glassPane.isHide());

      // test if calling hideOverlay again, even though it's already hidden, does anything
      result = workbench.hideOverlay(overlay1);

      assertFalse(result);
      assertEquals(1, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible()); // overlay1 is still invisible

      // test if calling showOverlay again, doesn't load the overlay1 into the map again
      result = workbench.showOverlay(overlay1, false);

      assertTrue(result);
      assertEquals(1, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(1, overlaysShown.size());
      assertTrue(overlay1.isVisible()); // overlay1 is visible again
    });
  }

  @Test
  @DisplayName("Show non-blocking overlay and close by clicking on the GlassPane")
  void hideOverlayNonBlockingGlassPane() {
    robot.interact(() -> {
      workbench.showOverlay(overlay1, false);

      // hiding by GlassPane click
      simulateGlassPaneClick(overlay1);

      assertEquals(1, overlays.size()); // still loaded
      assertEquals(0, blockingOverlaysShown.size()); // none shown
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible()); // overlay1 is invisible
      GlassPane glassPane = overlays.get(overlay1).getGlassPane();
      assertTrue(glassPane.isHide());
    });
  }

  /**
   * Precondition: showOverlay tests pass.
   */
  @Test
  void clearOverlaysShowing() {
    robot.interact(() -> {
      workbench.showOverlay(overlay1, false);
      workbench.showOverlay(overlay2, true);
      workbench.showOverlay(overlay3, false);

      assertEquals(3, overlays.size());
      assertEquals(1, blockingOverlaysShown.size());
      assertEquals(2, overlaysShown.size());
      assertTrue(overlay1.isVisible());
      assertTrue(overlay2.isVisible());
      assertTrue(overlay3.isVisible());

      final GlassPane glassPane1 = overlays.get(overlay1).getGlassPane();
      final GlassPane glassPane2 = overlays.get(overlay2).getGlassPane();
      final GlassPane glassPane3 = overlays.get(overlay3).getGlassPane();

      workbench.clearOverlays();

      assertEquals(0, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible());
      assertFalse(overlay2.isVisible());
      assertFalse(overlay3.isVisible());
      // closing handler was removed for non-blocking
      assertNull(glassPane1.onMouseClickedProperty().get());
      assertNull(glassPane3.onMouseClickedProperty().get());
      // glass panes were unbound
      assertFalse(glassPane1.hideProperty().isBound());
      assertFalse(glassPane2.hideProperty().isBound());
      assertFalse(glassPane3.hideProperty().isBound());
    });
  }

  /**
   * Precondition: showOverlay tests pass.
   */
  @Test
  void clearOverlaysHiding() {
    robot.interact(() -> {
      workbench.showOverlay(overlay1, false);
      workbench.showOverlay(overlay2, true);
      workbench.showOverlay(overlay3, false);

      assertEquals(3, overlays.size());
      assertEquals(1, blockingOverlaysShown.size());
      assertEquals(2, overlaysShown.size());
      assertTrue(overlay1.isVisible());
      assertTrue(overlay2.isVisible());
      assertTrue(overlay3.isVisible());

      workbench.hideOverlay(overlay1);
      workbench.hideOverlay(overlay2);
      workbench.hideOverlay(overlay3);

      assertEquals(3, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible());
      assertFalse(overlay2.isVisible());
      assertFalse(overlay3.isVisible());

      final GlassPane glassPane1 = overlays.get(overlay1).getGlassPane();
      final GlassPane glassPane2 = overlays.get(overlay2).getGlassPane();
      final GlassPane glassPane3 = overlays.get(overlay3).getGlassPane();

      workbench.clearOverlays();

      assertEquals(0, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
      assertFalse(overlay1.isVisible());
      assertFalse(overlay2.isVisible());
      assertFalse(overlay3.isVisible());
      // closing handler was removed for non-blocking
      assertNull(glassPane1.onMouseClickedProperty().get());
      assertNull(glassPane3.onMouseClickedProperty().get());
      // glass panes were unbound
      assertFalse(glassPane1.hideProperty().isBound());
      assertFalse(glassPane2.hideProperty().isBound());
      assertFalse(glassPane3.hideProperty().isBound());
    });
  }

  @Test
  void showNavigationDrawer() {
    robot.interact(() -> {
      Node navigationDrawer = workbench.getNavigationDrawer();
      navigationDrawer.setVisible(false);

      workbench.showNavigationDrawer();

      assertEquals(1, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(1, overlaysShown.size());
      assertTrue(navigationDrawer.isVisible());
    });
  }

  // asciidoctor Documentation - tag::awaitility[]
  @Test
  void hideNavigationDrawer() {
    robot.interact(() -> {
      Node navigationDrawer = workbench.getNavigationDrawer();
      navigationDrawer.setVisible(false);

      workbench.showNavigationDrawer();
      workbench.hideNavigationDrawer();

      assertEquals(1, overlays.size());
      assertEquals(0, blockingOverlaysShown.size());
      assertEquals(0, overlaysShown.size());
    });

    // wait for closing animation to complete
    await().atMost(5, TimeUnit.SECONDS).until(() -> (!navigationDrawer.isVisible()));
  }
  // asciidoctor Documentation - end::awaitility[]

  @Test
  void getNavigationDrawerItems() {
    robot.interact(() -> {
      assertEquals(1, navigationDrawerItems.size());
      assertEquals(menuItem, navigationDrawerItems.get(0));
    });
  }

  /**
   * Precondition: getNavigationDrawerItems tests pass.
   */
  @Test
  void addNavigationDrawerItems() {
    robot.interact(() -> {
      workbench.getNavigationDrawerItems().add(menuItem);
      assertEquals(2, navigationDrawerItems.size());
      assertEquals(menuItem, navigationDrawerItems.get(1));
    });
  }

  /**
   * Precondition: getNavigationDrawerItems tests pass.
   */
  @Test
  void removeNavigationDrawerItems() {
    robot.interact(() -> {
      workbench.getNavigationDrawerItems().remove(menuItem);
      assertEquals(0, navigationDrawerItems.size());
    });
  }

  @Test
  void removeToolbarControlsLeftAndRight() {
    robot.interact(() -> {
      ToolbarItem d = new ToolbarItem(toolbarItemText, toolbarItemIconView, toolbarItemMenuItem);

      int initialSizeLeft = workbench.getToolbarControlsLeft().size();
      assertFalse(workbench.getToolbarControlsLeft().remove(d));
      assertSame(initialSizeLeft, workbench.getToolbarControlsLeft().size());

      int initialSizeRight = workbench.getToolbarControlsRight().size();
      assertFalse(workbench.getToolbarControlsRight().remove(d));
      assertSame(initialSizeRight, workbench.getToolbarControlsRight().size());

      assertTrue(workbench.getToolbarControlsLeft().remove(toolbarItemLeft));
      assertSame(initialSizeLeft - 1, workbench.getToolbarControlsLeft().size());
      assertTrue(workbench.getToolbarControlsRight().remove(toolbarItemRight));
      assertSame(initialSizeRight - 1, workbench.getToolbarControlsRight().size());
    });
  }

  @Test
  void addToolbarControlsLeftAndRight() {
    robot.interact(() -> {
      int initialSizeLeft = workbench.getToolbarControlsLeft().size();
      ToolbarItem d = new ToolbarItem(toolbarItemIconView, toolbarItemMenuItem);
      assertTrue(workbench.getToolbarControlsLeft().add(d));
      assertSame(initialSizeLeft + 1, workbench.getToolbarControlsLeft().size());

      int initialSizeRight = workbench.getToolbarControlsRight().size();
      d = new ToolbarItem(toolbarItemText, toolbarItemMenuItem);
      assertTrue(workbench.getToolbarControlsRight().add(d));
      assertSame(initialSizeRight + 1, workbench.getToolbarControlsRight().size());
    });
  }

  @Test
  void addModule() {
    robot.interact(() -> {
      ObservableList<WorkbenchModule> modules = workbench.getModules();
      int currentSize = modules.size();
      String mockModuleName = "Mock Module";
      WorkbenchModule mockModule = createMockModule(
          new Label(),null,true, mockModuleName, workbench,
          FXCollections.observableArrayList(), FXCollections.observableArrayList()
      );

      assertTrue(workbench.getModules().add(mockModule));

      assertSame(currentSize + 1, modules.size());
    });
  }

  @Test
  void removeModule() {
    robot.interact(() -> {
      ObservableList<WorkbenchModule> modules = workbench.getModules();
      int currentSize = modules.size();

      assertTrue(workbench.getModules().remove(mockModules[0]));

      assertSame(currentSize - 1, modules.size());

      // removing same module again should not remove it
      assertFalse(workbench.getModules().remove(mockModules[0]));

      assertSame(currentSize - 1, modules.size());
    });
  }

  // asciidoctor Documentation - tag::stageClosing[]
  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates all modules returning {@code true} when
   * {@link WorkbenchModule#destroy()} is being called on them during the cleanup.
   */
  @Test
  void closeStageSuccess() {
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);

      // simulate closing of the stage by pressing the X of the application
      closeStage();

      // all open modules should get closed before the application ends
      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();

      // Effects caused by "Workbench#setupCleanup" -> setOnCloseRequest
      // Implicit Call: workbench.closeModule(first)
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy();
      // Implicit Call: workbench.closeModule(second)
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy();

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);

      assertEquals(0, workbench.getOpenModules().size());
    });
  }

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates the first (inactive) module returning {@code false} and the second (active) module
   * returning {@code true}, when {@link WorkbenchModule#destroy()} is being called
   * on them during cleanup.
   */
  @Test
  void closeStageFailFirstModule() {
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);

      // make sure closing of the stage gets interrupted, if destroy returns false on a module
      when(first.destroy()).thenReturn(false);

      // simulate closing of the stage like when pressing the X of the application
      closeStage();

      // all open modules should get closed before the application ends
      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();

      // Effects caused by "Workbench#setupCleanup" -> setOnCloseRequest
      // Implicit Call: workbench.closeModule(first)
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy(); // returns false
      // Implicit Call: workbench.openModule(first) -> set focus on module that couldn't be closed
      inOrder.verify(second).deactivate();
      inOrder.verify(first).activate();
      // closing should be interrupted

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);

      assertEquals(2, workbench.getOpenModules().size());
    });
  }

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates the first (inactive) module returning {@code true} and the second (active) module
   * returning {@code false}, when {@link WorkbenchModule#destroy()} is being called on them during
   * cleanup.
   */
  @Test
  void closeStageFailSecondModule() {
    robot.interact(() -> {
      workbench.openModule(first);
      workbench.openModule(second);

      // make sure closing of the stage gets interrupted, if destroy returns false on a module
      when(second.destroy()).thenReturn(false);

      // simulate closing of the stage by pressing the X of the application
      closeStage();

      // all open modules should get closed before the application ends
      InOrder inOrder = inOrder(first, second);
      // Call: workbench.openModule(first)
      inOrder.verify(first).init(workbench);
      inOrder.verify(first).activate();
      // Call: workbench.openModule(second)
      inOrder.verify(first).deactivate();
      inOrder.verify(second).init(workbench);
      inOrder.verify(second).activate();

      // Effects caused by "Workbench#setupCleanup" -> setOnCloseRequest
      // Implicit Call: workbench.closeModule(first)
      inOrder.verify(first, never()).deactivate();
      inOrder.verify(first).destroy(); // returns true
      // Implicit Call: workbench.closeModule(second)
      inOrder.verify(second).deactivate();
      inOrder.verify(second).destroy(); // returns false
      // second should stay as the active module
      inOrder.verify(second).activate();
      // closing should be interrupted

      ignoreModuleGetters(first, second);
      verifyNoMoreInteractions(first, second);

      assertEquals(1, workbench.getOpenModules().size());
      assertEquals(second, workbench.getOpenModules().get(0));
    });
  }

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates a special case that caused in bug scenarios to have 2x {@code thenRun} set on
   * {@code moduleCloseable} in {@code stage.setOnCloseRequest}, which lead to 2 dialogs being open
   * instead of one, after the first module has been closed.
   */
  @Test
  void closeStageSpecial1() {
    robot.interact(() -> {
      // Given: 2 Modules open, destroy() on both opens a dialog and returns false.
      //        Pressing yes on the dialog calls WorkbenchModule#close(), pressing no leaves the
      //        module open.
      workbench.openModule(first);
      workbench.openModule(second);
      when(first.destroy()).then(invocationOnMock -> {
        workbench.showDialog(WorkbenchDialog.builder("1", "", WorkbenchDialog.Type.CONFIRMATION)
            .blocking(true).onResult(buttonType -> {
              if (ButtonType.YES.equals(buttonType)) {
                simulateModuleClose(first);
              }
            }).build());
        return false;
      });
      when(second.destroy()).then(invocationOnMock -> {
        workbench.showDialog(WorkbenchDialog.builder("2", "", WorkbenchDialog.Type.CONFIRMATION)
            .blocking(true).onResult(buttonType -> {
              if (ButtonType.YES.equals(buttonType)) {
                simulateModuleClose(second);
              }
            }).build());
        return false;
      });
      assertTrue(isStageOpen());

      // When: Close stage, press No, Close Stage, press yes.
      closeStage();
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      simulateDialogButtonClick(ButtonType.NO);
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      closeStage();
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      simulateDialogButtonClick(ButtonType.YES);
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(1, workbench.getOpenModules().size());

      // Then: Only second module is open and 1 dialog is open (closing of second module)
      assertEquals(second, workbench.getOpenModules().get(0));
      assertEquals("2", getShowingDialogControl().getDialog().getTitle());

      // When: Press yes
      simulateDialogButtonClick(ButtonType.YES);

      // Then: No modules and dialogs are open, stage is closed.
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(0, workbench.getOpenModules().size());
      assertFalse(isStageOpen());
    });
  }

  /**
   * Test for {@link Workbench#setupCleanup()}.
   * Simulates a special case that caused in bug scenarios for the stage closing process to go on,
   * even though a tab was closed and not the stage itself.
   */
  @Test
  void closeStageSpecial2() {
    robot.interact(() -> {
      // Given: 2 Modules open, destroy() on both opens a dialog and returns false.
      //        Pressing yes on the dialog calls WorkbenchModule#close(), pressing no leaves the
      //        module open.
      workbench.openModule(first);
      workbench.openModule(second);
      when(first.destroy()).then(invocationOnMock -> {
        workbench.showDialog(WorkbenchDialog.builder("1", "", WorkbenchDialog.Type.CONFIRMATION)
            .blocking(true).onResult(buttonType -> {
              if (ButtonType.YES.equals(buttonType)) {
                simulateModuleClose(first);
              }
            }).build());
        return false;
      });
      when(second.destroy()).then(invocationOnMock -> {
        workbench.showDialog(WorkbenchDialog.builder("2", "", WorkbenchDialog.Type.CONFIRMATION)
            .blocking(true).onResult(buttonType -> {
              if (ButtonType.YES.equals(buttonType)) {
                simulateModuleClose(second);
              }
            }).build());
        return false;
      });
      assertTrue(isStageOpen());

      // When: Close stage, press No, Close Tab, Press Yes
      closeStage();
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      simulateDialogButtonClick(ButtonType.NO);
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      workbench.closeModule(first); // simulate tab closing
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(2, workbench.getOpenModules().size());

      simulateDialogButtonClick(ButtonType.YES);
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(1, workbench.getOpenModules().size());

      // Then: Only second module is open and no dialogs are open (stage closing is interrupted)
      assertEquals(second, workbench.getOpenModules().get(0));
      assertTrue(isStageOpen());
    });
  }

  /**
   * Internal utility method for testing.
   * Determines whether the current stage is open or was closed.
   */
  private boolean isStageOpen() {
    return robot.listTargetWindows().size() == 1;
  }

  /**
   * Internal utility method for testing.
   * Simulates closing the stage, which fires a close request to test logic
   * inside of {@link Stage#setOnCloseRequest(EventHandler)}.
   * Using {@link FxRobot#closeCurrentWindow()} would be better, but it only works on Windows
   * because of its implementation, so this approach was chosen as a workaround.
   * @see <a href="https://github.com/TestFX/TestFX/issues/447">
   * closeCurrentWindow() doesn't work headless</a>
   */
  private void closeStage() {
    Stage stage = ((Stage) workbench.getScene().getWindow());
    stage.fireEvent(
        new WindowEvent(
            stage,
            WindowEvent.WINDOW_CLOSE_REQUEST
        )
    );
  }
  // asciidoctor Documentation - end::stageClosing[]

  @Test
  void initNavigationDrawer() {
    // verify no NPE is thrown by the listener when setting a null control
    workbench.setNavigationDrawer(null);
  }

  @Test
  @DisplayName("Show non-blocking dialog and close by clicking on the GlassPane")
  void showDialogNonBlockingCloseGlassPaneDefault() {
    robot.interact(() -> {
      assertDialogNotShown();

      WorkbenchDialog result = workbench.showDialog(mockDialog);

      assertDialogShown(result, false);
      verify(mockDialog, atLeastOnce()).getButtonTypes();
      verify(mockOnResult, never()).accept(any()); // no result yet

      // hiding by GlassPane click
      simulateGlassPaneClick(dialogControl);

      verify(mockOnResult).accept(ButtonType.CANCEL);
      assertDialogNotShown();
    });
  }

  @Test
  @DisplayName("Show non-blocking dialog and close by clicking on one of the dialog buttons")
  void showDialogNonBlockingCloseButton() {
    robot.interact(() -> {
      assertDialogNotShown();

      WorkbenchDialog result = workbench.showDialog(mockDialog);

      assertDialogShown(result, false);
      verify(mockDialog, atLeastOnce()).getButtonTypes();
      verify(mockOnResult, never()).accept(any()); // no result yet

      // hiding by button press
      ButtonType toPress = buttonTypes.get(0);
      simulateDialogButtonClick(dialogControl, toPress);

      verify(mockOnResult).accept(toPress);
      assertDialogNotShown();
    });
  }

  @Test
  @DisplayName("Show blocking dialog and try to close by clicking on the GlassPane")
  void showDialogBlockingCloseGlassPane() {
    robot.interact(() -> {
      when(mockDialog.isBlocking()).thenReturn(true);

      assertDialogNotShown();

      WorkbenchDialog result = workbench.showDialog(mockDialog);

      verify(mockDialog, atLeastOnce()).isBlocking(); // call showOverlay(...) inside showDialog()
      verify(mockDialog, atLeastOnce()).getButtonTypes();
      assertDialogShown(result, true);

      // try hiding by clicking on GlassPane
      simulateGlassPaneClick(dialogControl); // simulates a click on GlassPane

      verify(mockDialog, atLeast(0)).blockingProperty(); // ignore calls
      verify(mockDialog, never()).getOnResult();
      verify(mockOnResult, never()).accept(any());
      verifyNoMoreInteractions(mockDialog);
      verifyNoMoreInteractions(mockOnResult);
      // verify dialog hasn't been hidden
      assertDialogShown(result, true);
    });
  }

  @Test
  @DisplayName("Show blocking dialog and close by clicking on one of the dialog buttons")
  void showDialogBlockingCloseButton() {
    robot.interact(() -> {
      when(mockDialog.isBlocking()).thenReturn(true);

      assertDialogNotShown();

      WorkbenchDialog result = workbench.showDialog(mockDialog);

      assertDialogShown(result, true);
      verify(mockDialog, atLeastOnce()).isBlocking(); // call showOverlay(...) inside showDialog()
      verify(mockDialog, atLeastOnce()).getButtonTypes();
      verify(mockDialog, atLeastOnce()).getDialogControl();
      verify(mockDialog, never()).getOnResult();
      verify(mockOnResult, never()).accept(any());
      ObservableList<Button> buttons = dialogControl.getButtons();
      assertSame(buttonTypes.size(), buttons.size());

      // hiding by button press
      ButtonType toPress = buttonTypes.get(0);
      simulateDialogButtonClick(dialogControl, toPress);

      verify(mockDialog, atLeast(0)).blockingProperty(); // ignore calls
      verify(mockDialog).getOnResult();
      verify(mockDialog, atLeastOnce()).getDialogControl();
      verify(mockOnResult).accept(toPress);
      verifyNoMoreInteractions(mockDialog);
      verifyNoMoreInteractions(mockOnResult);
      assertDialogNotShown();
    });
  }

  private void assertDialogShown(WorkbenchDialog result, boolean blocking) {
    verify(result).getDialogControl();
    assertSame(mockDialog, result);
    assertSame(1, workbench.getOverlays().size());
    assertSame(dialogControl, workbench.getOverlays().keySet().stream().findAny().get());
    assertSame(workbench, dialogControl.getWorkbench());
    if (blocking) {
      assertSame(1, workbench.getBlockingOverlaysShown().size());
      assertSame(0, workbench.getNonBlockingOverlaysShown().size());
    } else {
      assertSame(0, workbench.getBlockingOverlaysShown().size());
      assertSame(1, workbench.getNonBlockingOverlaysShown().size());
    }
  }

  private void assertDialogNotShown() {
    assertSame(null, dialogControl.getWorkbench());
    assertSame(0, workbench.getBlockingOverlaysShown().size());
    assertSame(0, workbench.getNonBlockingOverlaysShown().size());
  }

  /**
   * Internal testing method that will simulate a click on a {@link GlassPane} of
   * an {@code overlayNode}.
   *
   * @param overlayNode of which the GlassPane should be clicked
   */
  private void simulateGlassPaneClick(Node overlayNode) {
    GlassPane glassPane = workbench.getOverlays().get(overlayNode).getGlassPane();
    glassPane.fireEvent(new MouseEvent(MouseEvent.MOUSE_CLICKED, 0, 0, 0, 0,
        MouseButton.PRIMARY, 1,
        false, false, false, false, true, false, false, false, false, false,
        null)
    );
  }

  /**
   * Internal testing method that will simulate a click on the {@link Button} of
   * {@link ButtonType} of the {@code dialog}.
   *
   * @param dialog of which the button is to be pressed
   * @param press the {@link ButtonType} of the {@link Button} that should be pressed
   */
  private void simulateDialogButtonClick(WorkbenchDialog dialog, ButtonType press) {
    Optional<Button> button = dialog.getButton(press);
    button.get().fire();
  }

  /**
   * Internal testing method that will simulate a click on the {@link Button} of
   * {@link ButtonType} of the {@code dialog}.
   *
   * @param dialog of which the button is to be pressed
   * @param press the {@link ButtonType} of the {@link Button} that should be pressed
   */
  private void simulateDialogButtonClick(DialogControl dialog, ButtonType press) {
    Optional<Button> button = dialog.getButton(press);
    button.get().fire();
  }

  /**
   * Internal testing method that will simulate a click on the {@link Button} of
   * {@link ButtonType} of the currently shown dialog, assuming only one overlay is shown, which
   * is a dialog.
   *
   * @param press the {@link ButtonType} of the {@link Button} that should be pressed
   */
  private void simulateDialogButtonClick(ButtonType press) {
    DialogControl showingDialogControl = getShowingDialogControl();
    simulateDialogButtonClick(showingDialogControl, press);
  }

  /**
   * Internal testing method which returns the currently shown DialogControl.
   */
  private DialogControl getShowingDialogControl() {
    if (workbench.getNonBlockingOverlaysShown().size() == 1) {
      return (DialogControl) workbench.getNonBlockingOverlaysShown().stream().findAny().get();
    } else if (workbench.getBlockingOverlaysShown().size() == 1) {
      return (DialogControl) workbench.getBlockingOverlaysShown().stream().findAny().get();
    }
    return null;
  }

  /**
   * Internal testing method which returns the currently shown overlay.
   */
  private Node getShowingOverlay() {
    if (workbench.getNonBlockingOverlaysShown().size() == 1) {
      return workbench.getNonBlockingOverlaysShown().stream().findAny().get();
    } else if (workbench.getBlockingOverlaysShown().size() == 1) {
      return workbench.getBlockingOverlaysShown().stream().findAny().get();
    }
    return null;
  }

  @Test
  void showDrawerInputValidation() {
    robot.interact(() -> {
      // null check
      assertThrows(NullPointerException.class, () -> workbench.showDrawer(drawer, null, 0));
      assertThrows(NullPointerException.class, () -> workbench.showDrawer(null, Side.LEFT, 0));

      // Percentage range
      assertThrows(IllegalArgumentException.class,
          () -> workbench.showDrawer(drawer, Side.LEFT, Integer.MIN_VALUE));
      assertThrows(IllegalArgumentException.class,
          () -> workbench.showDrawer(drawer, Side.LEFT, -2));
      workbench.showDrawer(drawer, Side.LEFT, -1); // valid
      workbench.showDrawer(drawer, Side.LEFT, 0); // valid
      workbench.showDrawer(drawer, Side.LEFT, 1); // valid
      workbench.showDrawer(drawer, Side.LEFT, 100); // valid
      assertThrows(IllegalArgumentException.class,
          () -> workbench.showDrawer(drawer, Side.LEFT, 101));
      assertThrows(IllegalArgumentException.class,
          () -> workbench.showDrawer(drawer, Side.LEFT, Integer.MAX_VALUE));
    });
  }

  @Test
  @DisplayName("Tests if only one drawer can be displayed at the same time")
  void showDrawerOnlyOne() {
    robot.interact(() -> {
      // given
      VBox drawer1 = spy(VBox.class);
      when(drawer1.getWidth()).thenReturn(100d);
      when(drawer1.getHeight()).thenReturn(100d);
      VBox drawer2 = new VBox();
      VBox drawer3 = new VBox();
      assertTrue(workbench.getBlockingOverlaysShown().isEmpty());
      assertTrue(workbench.getNonBlockingOverlaysShown().isEmpty());
      assertNull(workbench.getDrawerShown());
      assertNull(workbench.getDrawerSideShown());

      // when: showing two different drawers subsequently on the same side
      workbench.showDrawer(drawer1, Side.LEFT);
      workbench.showDrawer(drawer2, Side.LEFT);

      // then: only second one is showing
      assertTrue(workbench.getBlockingOverlaysShown().isEmpty());
      assertSame(1, workbench.getNonBlockingOverlaysShown().size());
      assertNotNull(workbench.getDrawerShown());
      assertSame(drawer2, getShowingOverlay());
      assertSame(drawer2, workbench.getDrawerShown());
      assertEquals(Side.LEFT, workbench.getDrawerSideShown());

      // when: showing drawer on a different side while another drawer is currently showing
      workbench.showDrawer(drawer3, Side.BOTTOM);

      // then: only new drawer is showing
      assertTrue(workbench.getBlockingOverlaysShown().isEmpty());
      assertSame(1, workbench.getNonBlockingOverlaysShown().size());
      assertNotNull(workbench.getDrawerShown());
      assertSame(drawer3, getShowingOverlay());
      assertSame(drawer3, workbench.getDrawerShown());
      assertEquals(Side.BOTTOM, workbench.getDrawerSideShown());
      assertSame(Pos.BOTTOM_LEFT, StackPane.getAlignment(drawer3)); // verify correct position

      // when: show first drawer again
      workbench.showDrawer(drawer1, Side.LEFT);

      // then: only drawer1 is showing
      assertTrue(workbench.getBlockingOverlaysShown().isEmpty());
      assertSame(1, workbench.getNonBlockingOverlaysShown().size());
      assertNotNull(workbench.getDrawerShown());
      assertSame(drawer1, getShowingOverlay());
      assertSame(drawer1, workbench.getDrawerShown());
      assertEquals(Side.LEFT, workbench.getDrawerSideShown());
    });
  }

  @Test
  void hideDrawerGlassPaneClick() {
    robot.interact(() -> {
      // given
      assertTrue(workbench.getBlockingOverlaysShown().isEmpty());
      assertTrue(workbench.getNonBlockingOverlaysShown().isEmpty());
      assertNull(workbench.getDrawerShown());
      workbench.showDrawer(drawer, Side.LEFT);

      // when:
      simulateGlassPaneClick(drawer);

      // then: drawer is hidden
      assertTrue(workbench.getBlockingOverlaysShown().isEmpty());
      assertTrue(workbench.getNonBlockingOverlaysShown().isEmpty());
      assertNull(workbench.getDrawerShown());
    });
  }

  @Test
  void hideDrawer() {
    robot.interact(() -> {
      // given
      assertTrue(workbench.getBlockingOverlaysShown().isEmpty());
      assertTrue(workbench.getNonBlockingOverlaysShown().isEmpty());
      assertNull(workbench.getDrawerShown());
      workbench.showDrawer(drawer, Side.LEFT);

      // when:
      workbench.hideDrawer();

      // then: drawer is hidden
      assertTrue(workbench.getBlockingOverlaysShown().isEmpty());
      assertTrue(workbench.getNonBlockingOverlaysShown().isEmpty());
      assertNull(workbench.getDrawerShown());
    });
  }


}