/*==============================================================================

  Program: 3D Slicer

  Copyright (c) Kitware Inc.

  See COPYRIGHT.txt
  or http://www.slicer.org/copyright/copyright.txt for details.

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

  This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc.
  and was partially funded by NIH grant 3P41RR013218-12S1

==============================================================================*/

// Qt includes
#include <QDir>
#include <QStringList>
#include <QTextStream>
#include <QTime>

// CTK includes
#include <ctkCoreTestingMacros.h>
#include <ctkUtils.h>

// Slicer includes
#include <qSlicerUtils.h>

// Slicer includes
#include "vtkSlicerConfigure.h"
#include "vtkSlicerVersionConfigure.h"

// STD includes
#include <cstdlib>
#include <iostream>
#include <stdexcept>

namespace
{
//-----------------------------------------------------------------------------
bool createFile(int line, const QDir& dir, const QString& relativePath, const QString& fileName)
{
  QDir newDir(dir);
  newDir.mkpath(relativePath);
  newDir.cd(relativePath);
  QString filePath = QFileInfo(newDir, fileName).filePath();
  QFile file(filePath);
  file.open(QIODevice::Text | QIODevice::WriteOnly);
  QTextStream out(&file);
  out << "Generated by qSlicerUtilsTest1" << ctk::endl;
  file.close();

  if (!QFile::exists(filePath))
  {
    std::cerr << "Line " << line << " - Failed to create file" << qPrintable(filePath) << std::endl;
    return false;
  }

  return true;
}

//-----------------------------------------------------------------------------
bool isPluginInstalledTest(int line, bool expectedResult, const QString& path, const QString& applicationHomeDir)
{
  bool res = qSlicerUtils::isPluginInstalled(path, applicationHomeDir);
  if (res != expectedResult)
  {
    //    std::cerr << "Line " << line << " - Problem with isPluginInstalled()\n"
    //              << "\tpath: " << qPrintable(path) << "\n"
    //              << "\tapplicationHomeDir: " << qPrintable(applicationHomeDir) << std::endl;
    QString msg("Line %1 - Problem with isPluginInstalled()\n\tpath: %2\n\tapplicationHomeDir: %3");
    throw std::runtime_error(qPrintable(msg.arg(line).arg(path).arg(applicationHomeDir)));
  }
  return res;
}

//-----------------------------------------------------------------------------
bool isPluginBuiltInTest(int line, bool expectedResult, const QString& path, const QString& applicationHomeDir)
{
  bool res = qSlicerUtils::isPluginBuiltIn(path, applicationHomeDir, QString::fromUtf8(Slicer_REVISION));
  if (res != expectedResult)
  {
    QString msg("Line %1 - Problem with isPluginBuiltIn(). Expected %2 for:\n\tpath: %3\n\tapplicationHomeDir: %4");
    throw std::runtime_error(qPrintable(msg.arg(line).arg(expectedResult).arg(path).arg(applicationHomeDir)));
  }
  return res;
}

//-----------------------------------------------------------------------------
int isExecutableNameTest()
{
  QStringList executableNames;
  executableNames << "Threshold.bat" << "Threshold.com"
                  << "Threshold.sh" << "Threshold.csh"
                  << "Threshold.tcsh" << "Threshold.pl"
                  << "Threshold.py" << "Threshold.tcl"
                  << "Threshold.m" << "Threshold.exe";

  for (const QString& executableName : executableNames)
  {
    CHECK_BOOL(qSlicerUtils::isExecutableName(executableName), true);
  }

  QStringList notExecutableNames;
  notExecutableNames << "Threshold.ini" << "Threshold.txt" << "Threshold";
  for (const QString& notExecutableName : notExecutableNames)
  {
    CHECK_BOOL(qSlicerUtils::isExecutableName(notExecutableName), false);
  }
  return EXIT_SUCCESS;
}

//-----------------------------------------------------------------------------
int isCLILoadableModuleTest()
{
  QStringList validFilePaths;
  QStringList invalidFilePaths;

  validFilePaths << "ThresholdLib.dll"
                 << "ThresholdLib.DLL"
                 << "libThresholdLib.dylib"
                 << "libThresholdLib.so";
  for (const QString& filePath : validFilePaths)
  {
    CHECK_BOOL(qSlicerUtils::isCLILoadableModule(filePath), true);
  }
  invalidFilePaths << "libThresholdLib.dll"
                   << "ThresholdLib.dylib"
                   << "ThresholdLib.so"
                   << "libThresholdLib.xyz"
                   << "libLib.so"
                   << "Lib.so"
                   << "Lib.dll"
                   << "libThresholdlib.so"
                   << "Thresholdlib.so";
  for (const QString& filePath : invalidFilePaths)
  {
    CHECK_BOOL(qSlicerUtils::isCLILoadableModule(filePath), false);
  }
  return EXIT_SUCCESS;
}

//-----------------------------------------------------------------------------
int isLoadableModuleTest()
{
  QStringList validFilePaths;
  QStringList invalidFilePaths;

  validFilePaths << "qSlicerThresholdModule.dll"
                 << "qSlicerThresholdModule.DLL"
                 << "libqSlicerThresholdModule.dylib"
                 << "libqSlicerThresholdModule.so";
  for (const QString& filePath : validFilePaths)
  {
    CHECK_BOOL(qSlicerUtils::isLoadableModule(filePath), true);
  }

  invalidFilePaths << "libqSlicerThresholdModule.dll"
                   << "qSlicerThresholdModule.dylib"
                   << "qSlicerThresholdModule.so"
                   << "libqSlicerThresholdModule.xyz"
                   << "qSlicerModule.dll"
                   << "libQSlicerThresholdmodule.so"
                   << "QSlicerThresholdmodule.so";
  for (const QString& filePath : invalidFilePaths)
  {
    CHECK_BOOL(qSlicerUtils::isLoadableModule(filePath), false);
  }

  return EXIT_SUCCESS;
}

//-----------------------------------------------------------------------------
int executableExtensionTest()
{
#ifdef _WIN32
  QString expectedExecutableExtension = ".exe";
#else
  QString expectedExecutableExtension = "";
#endif
  QString executableExtension = qSlicerUtils::executableExtension();
  CHECK_QSTRING(executableExtension, expectedExecutableExtension);

  //-----------------------------------------------------------------------------
  // Test extractModuleNameFromLibraryName()
  //-----------------------------------------------------------------------------
  QStringList libraryNames;
  libraryNames << "VRLib.dll"
               << "VR.dll"
               << "libVR.so"
               << "libVR.so.2.3"
               << "libVR.dylib"
               << "qSlicerVRModule.so"
               << "qSlicerVR.dylib";

  QString expectedModuleName = "VR";

  for (const QString& libraryName : libraryNames)
  {
    QString moduleName = qSlicerUtils::extractModuleNameFromLibraryName(libraryName);
    CHECK_QSTRING(moduleName, expectedModuleName);
  }
  return EXIT_SUCCESS;
}

//-----------------------------------------------------------------------------
int extractModuleNameFromClassNameTest()
{
  CHECK_QSTRING(qSlicerUtils::extractModuleNameFromClassName("qSlicerThresholdModule"), "Threshold");
  CHECK_QSTRING(qSlicerUtils::extractModuleNameFromClassName("qSlicerThresholdModuleModule"), "ThresholdModule");
  CHECK_QSTRING(qSlicerUtils::extractModuleNameFromClassName("qSlicerModuleThresholdModule"), "ModuleThreshold");
  return EXIT_SUCCESS;
}

//-----------------------------------------------------------------------------
int isPluginInstalledBuiltinTest()
{
  QStringList directoriesToRemove;

  //
  // Case 1: Application and plugins are located inside the application build tree
  //

  // 'tmp1' is considered to be the application and plugins build tree

  QDir tmp1 = QDir::temp();
  QString temporaryDirName = QString("qSlicerUtilsTest1-tmp1.%1").arg(QTime::currentTime().toString("hhmmsszzz"));

  tmp1.mkdir(temporaryDirName);
  tmp1.cd(temporaryDirName);

  QString debug("Debug");
  QString release("Release");
  QString relWithDebInfo("RelWithDebInfo");
  QString minSizeRel("MinSizeRel");
  QString foo("foo");
  QString fooDebug("foo/Debug");
  QString fooRelease("foo/Release");
  QString fooBar("foo/bar");
  QString fooBarDebug("foo/bar/Debug");
  QString fooBarRelease("foo/bar/Release");

  createFile(__LINE__, tmp1, ".", "CMakeCache.txt");

  QStringList relativePaths;
  relativePaths = QStringList() << debug << release << relWithDebInfo << minSizeRel << foo << fooDebug << fooRelease << fooBar << fooBarDebug << fooBarRelease;
  for (const QString& relativePath : relativePaths)
  {
    createFile(__LINE__, tmp1, relativePath, "plugin.txt");
  }

  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + debug + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + release + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + relWithDebInfo + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + minSizeRel + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + foo + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooDebug + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooRelease + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBar + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBarDebug + "/plugin.txt", tmp1.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBarRelease + "/plugin.txt", tmp1.path());

  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + debug + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + release + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + relWithDebInfo + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + minSizeRel + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + foo + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + fooDebug + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + fooRelease + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + fooBar + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + fooBarDebug + "/plugin.txt", tmp1.path());
  isPluginBuiltInTest(__LINE__, true, tmp1.path() + "/" + fooBarRelease + "/plugin.txt", tmp1.path());

  directoriesToRemove << tmp1.path();

  //
  // Case 2: Application and plugins are in two different build trees
  //

  // 'tmp1' is considered to be the plugins build tree
  // 'tmp2' is considered to be the application build tree

  temporaryDirName = QString("qSlicerUtilsTest1-tmp2.%1").arg(QTime::currentTime().toString("hhmmsszzz"));

  QDir tmp2 = QDir::temp();
  tmp2.mkdir(temporaryDirName);
  tmp2.cd(temporaryDirName);

  createFile(__LINE__, tmp2, ".", "CMakeCache.txt");

  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + debug + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + release + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + relWithDebInfo + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + minSizeRel + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + foo + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooDebug + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooRelease + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBar + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBarDebug + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBarRelease + "/plugin.txt", tmp2.path());

  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + debug + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + release + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + relWithDebInfo + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + minSizeRel + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + foo + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooDebug + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooRelease + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooBar + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooBarDebug + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooBarRelease + "/plugin.txt", tmp2.path());

  directoriesToRemove << tmp2.path();

  //
  // Case 3: Application is in its own build tree, plugins are installed in a different location
  //

  // 'tmp2' is considered to be the application build tree
  // 'tmp3' is considered to be the plugins installed tree

  temporaryDirName = QString("qSlicerUtilsTest1-tmp3.%1").arg(QTime::currentTime().toString("hhmmsszzz"));
  QDir tmp3 = QDir::temp();
  tmp3.mkdir(temporaryDirName);
  tmp3.cd(temporaryDirName);

  relativePaths = QStringList() << foo << fooBar;
  for (const QString& relativePath : relativePaths)
  {
    createFile(__LINE__, tmp1, relativePath, "plugin.txt");
  }

  isPluginInstalledTest(__LINE__, true, tmp3.path() + "/" + foo + "/plugin.txt", tmp2.path());
  isPluginInstalledTest(__LINE__, true, tmp3.path() + "/" + fooBar + "/plugin.txt", tmp2.path());

  isPluginBuiltInTest(__LINE__, false, tmp3.path() + "/" + foo + "/plugin.txt", tmp2.path());
  isPluginBuiltInTest(__LINE__, false, tmp3.path() + "/" + fooBar + "/plugin.txt", tmp2.path());

  directoriesToRemove << tmp3.path();

  //
  // Case 4: Application is installed, plugins are in their own build tree
  //

  // 'tmp4' is considered to be the application installed tree
  // 'tmp1' is considered to be the plugins build tree

  temporaryDirName = QString("qSlicerUtilsTest1-tmp4.%1").arg(QTime::currentTime().toString("hhmmsszzz"));
  QDir tmp4 = QDir::temp();
  tmp4.mkdir(temporaryDirName);
  tmp4.cd(temporaryDirName);

  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + debug + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + release + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + relWithDebInfo + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + minSizeRel + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + foo + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooDebug + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooRelease + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBar + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBarDebug + "/plugin.txt", tmp4.path());
  isPluginInstalledTest(__LINE__, false, tmp1.path() + "/" + fooBarRelease + "/plugin.txt", tmp4.path());

  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + debug + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + release + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + relWithDebInfo + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + minSizeRel + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + foo + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooDebug + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooRelease + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooBar + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooBarDebug + "/plugin.txt", tmp4.path());
  isPluginBuiltInTest(__LINE__, false, tmp1.path() + "/" + fooBarRelease + "/plugin.txt", tmp4.path());

  directoriesToRemove << tmp4.path();

#ifdef Slicer_BUILD_EXTENSIONMANAGER_SUPPORT
  //
  // Case 5: Application is in its own build tree, plugins are installed in a sub-directory
  //         of the application build tree corresponding to the default extension install path.
  //
  //         This happens when the application is built with Slicer_STORE_SETTINGS_IN_APPLICATION_HOME_DIR
  //         set to ON.
  //
  //         In this case, the extension install directory is derived from the
  //         qSlicerCoreApplication::slicerRevisionUserSettingsFilePath() and the default extension install
  //         directory matches this pattern:
  //
  //           /path/to/Slicer-build/<organization_domain_or_name>/<Slicer_EXTENSIONS_DIRBASENAME>-<Slicer_REVISION>
  //

  // 'tmp5' is considered to be the application build tree (e.g "D:/D/P/S-0-build/Slicer-build")

  temporaryDirName = QString("qSlicerUtilsTest1-tmp5.%1").arg(QTime::currentTime().toString("hhmmsszzz"));
  QDir tmp5 = QDir::temp();
  tmp5.mkdir(temporaryDirName);
  tmp5.cd(temporaryDirName);

  createFile(__LINE__, tmp5, ".", "CMakeCache.txt");

  {
    QString extensionInstallDir = QString(Slicer_ORGANIZATION_DOMAIN "/%1-%2").arg(Slicer_EXTENSIONS_DIRBASENAME, Slicer_REVISION);
    isPluginInstalledTest(__LINE__, true, tmp5.path() + "/" + extensionInstallDir + "/" + foo + "/plugin.txt", tmp5.path());
  }
  {
    QString extensionInstallDir = QString(Slicer_ORGANIZATION_NAME "/%1-%2").arg(Slicer_EXTENSIONS_DIRBASENAME, Slicer_REVISION);
    isPluginInstalledTest(__LINE__, true, tmp5.path() + "/" + extensionInstallDir + "/" + foo + "/plugin.txt", tmp5.path());
  }

  directoriesToRemove << tmp5.path();
#endif

  // Clean temporary directories
  for (const QString& dir : directoriesToRemove)
  {
    ctk::removeDirRecursively(dir);
  }

  //
  // Case 6: Platform is macOS, application is installed
  //

#ifdef Slicer_BUILD_EXTENSIONMANAGER_SUPPORT
  QString macSlicerAppDir("/Applications/Slicer.app");
  // In this test we use the former method to get the extensions folder, as this is how
  // vtkSlicerApplicationLogic::IsPluginBuiltIn method determines the extension folder.
  QString macSlicerExtensionsPostfix = QString("/Contents/%1-%2/plugin.txt").arg(Slicer_EXTENSIONS_DIRBASENAME).arg(Slicer_REVISION);
  QString macSlicerAppExtensionsDir(macSlicerAppDir + macSlicerExtensionsPostfix);
  QString macRenamedSlicerAppDir("/Applications/Something.app");
  QString macRenamedSlicerAppExtensionsDir(macRenamedSlicerAppDir + macSlicerExtensionsPostfix);

  isPluginInstalledTest(__LINE__, true, macSlicerAppExtensionsDir, macSlicerAppDir);
  isPluginInstalledTest(__LINE__, true, macRenamedSlicerAppExtensionsDir, macRenamedSlicerAppDir);

  isPluginBuiltInTest(__LINE__, false, macSlicerAppExtensionsDir, macSlicerAppDir);
  isPluginBuiltInTest(__LINE__, false, macRenamedSlicerAppExtensionsDir, macRenamedSlicerAppDir);
#endif

  //-----------------------------------------------------------------------------
  // 'tmp' directory is common to 'pathWithoutIntDir' and 'pathEndsWith' tests
  //-----------------------------------------------------------------------------

  QString lib = "lib";
  QString libModule = lib + "/module";
  QString libModuleRelease = libModule + "/Release";

  QDir tmp = QDir::temp();
  temporaryDirName = QString("qSlicerUtilsTest1-pathWithoutIntDir.%1").arg(QTime::currentTime().toString("hhmmsszzz"));
  tmp.mkdir(temporaryDirName);
  tmp.cd(temporaryDirName);
  tmp.mkpath(libModuleRelease);

  //-----------------------------------------------------------------------------
  // Test pathWithoutIntDir()
  //-----------------------------------------------------------------------------

  QString inputPath = tmp.path() + "/" + libModule + "/Foo";
  QString subDirWithoutIntDir = "lib/module";
  QString expectedIntDir = "Foo";
  QString currentIntDir = "";
  QString expectedPath = tmp.path() + "/" + libModule;
  QString currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease;
  subDirWithoutIntDir = QString("lib%1module").arg(QDir::separator());
  expectedIntDir = "Release";
  currentIntDir = "";
  expectedPath = tmp.path() + "/" + libModule;
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease + "/";
  subDirWithoutIntDir = "lib/module";
  expectedIntDir = "Release";
  currentIntDir = "";
  expectedPath = tmp.path() + "/" + libModule;
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease;
  subDirWithoutIntDir = "module";
  expectedIntDir = "Release";
  currentIntDir = "";
  expectedPath = tmp.path() + "/" + libModule;
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease;
  subDirWithoutIntDir = "";
  expectedIntDir = "";
  currentIntDir = "";
  expectedPath = tmp.path() + "/" + libModuleRelease;
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease + "/foo.txt";
  subDirWithoutIntDir = "lib/module";
  expectedIntDir = "";
  currentIntDir = "";
  expectedPath = tmp.path() + "/" + libModuleRelease + "/foo.txt";
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease + "/foo.txt";
  subDirWithoutIntDir = libModuleRelease;
  expectedIntDir = "foo.txt";
  currentIntDir = "";
  expectedPath = tmp.path() + "/" + libModuleRelease;
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease;
  subDirWithoutIntDir = tmp.path() + "/" + libModule;
  expectedIntDir = "Release";
  currentIntDir = "";
  expectedPath = tmp.path() + "/" + libModule;
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/bin";
  subDirWithoutIntDir = libModule;
  expectedIntDir = "";
  currentIntDir = "";
  expectedPath = tmp.path() + "/bin";
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  QString fooAppContentsMacOSX("Foo.app/Contents/MacOSX");
  tmp.mkpath(fooAppContentsMacOSX);
  QString expectedFilePath = tmp.path() + "/" + fooAppContentsMacOSX;
  if (!QFile::exists(expectedFilePath))
  {
    std::cerr << "Line " << __LINE__ << " - Failed to create file" << qPrintable(expectedFilePath) << std::endl;
  }

  QString fooAppContentsBin("Foo.app/Contents/bin");
  tmp.mkpath(fooAppContentsBin);
  expectedFilePath = tmp.path() + "/" + fooAppContentsMacOSX;
  if (!QFile::exists(expectedFilePath))
  {
    std::cerr << "Line " << __LINE__ << " - Failed to create file" << qPrintable(expectedFilePath) << std::endl;
  }

  inputPath = tmp.path() + "/" + fooAppContentsMacOSX;
  subDirWithoutIntDir = "bin";
  expectedIntDir = "";
  currentIntDir = "";
  expectedPath = tmp.path() + "/" + fooAppContentsMacOSX;
  currentPath = qSlicerUtils::pathWithoutIntDir(inputPath, subDirWithoutIntDir, currentIntDir);
  if (currentPath != expectedPath || currentIntDir != expectedIntDir)
  {
    std::cerr << __LINE__ << " - Error in  pathWithoutIntDir()" << std::endl
              << "currentPath = " << qPrintable(currentPath) << std::endl
              << "expectedPath = " << qPrintable(expectedPath) << std::endl
              << "currentIntDir = " << qPrintable(currentIntDir) << std::endl
              << "expectedIntDir = " << qPrintable(expectedIntDir) << std::endl;
    return EXIT_FAILURE;
  }

  //-----------------------------------------------------------------------------
  // Test pathEndsWith()
  //-----------------------------------------------------------------------------

  inputPath = tmp.path() + "/" + libModuleRelease;
  QString relativePath = libModuleRelease;
  bool expected = true;
  bool current = qSlicerUtils::pathEndsWith(inputPath, relativePath);
  if (current != expected)
  {
    std::cerr << __LINE__ << " - Error in  pathEndsWith()" << std::endl << "current = " << current << std::endl << "expected = " << expected << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModule;
  relativePath = "module";
  expected = true;
  current = qSlicerUtils::pathEndsWith(inputPath, relativePath);
  if (current != expected)
  {
    std::cerr << __LINE__ << " - Error in  pathEndsWith()" << std::endl << "current = " << current << std::endl << "expected = " << expected << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease;
  relativePath = tmp.path() + "/" + libModuleRelease;
  expected = true;
  current = qSlicerUtils::pathEndsWith(inputPath, relativePath);
  if (current != expected)
  {
    std::cerr << __LINE__ << " - Error in  pathEndsWith()" << std::endl << "current = " << current << std::endl << "expected = " << expected << std::endl;
    return EXIT_FAILURE;
  }

  inputPath = tmp.path() + "/" + libModuleRelease;
  relativePath = tmp.path() + "/" + libModule;
  expected = false;
  current = qSlicerUtils::pathEndsWith(inputPath, relativePath);
  if (current != expected)
  {
    std::cerr << __LINE__ << " - Error in  pathEndsWith()" << std::endl << "current = " << current << std::endl << "expected = " << expected << std::endl;
    return EXIT_FAILURE;
  }

  //-----------------------------------------------------------------------------
  // Remove 'tmp' directory
  //-----------------------------------------------------------------------------
  ctk::removeDirRecursively(tmp.path());

  return EXIT_SUCCESS;
}

//-----------------------------------------------------------------------------
int setPermissionsRecursivelyTest()
{
  QDir tmp = QDir::temp();
  QString temporaryDirName = QString("qSlicerUtilsTest1-setPermissionsRecursively.%1").arg(QTime::currentTime().toString("hhmmsszzz"));
  tmp.mkdir(temporaryDirName);
  tmp.cd(temporaryDirName);

  QStringList relativeFilepaths;

  QString path1 = QLatin1String("any/foo/bar");
  QString path2 = QLatin1String("any/foo/bie");

  createFile(__LINE__, tmp, path1, "sol.txt");
  createFile(__LINE__, tmp, path1, "la.txt");
  createFile(__LINE__, tmp, path2, "si.txt");
  createFile(__LINE__, tmp, path2, "sol.txt");

  // Let's confirm that created file are readable
  relativeFilepaths = QStringList() //
                      << path1 + "/sol.txt" << path1 + "/la.txt" << path2 + "/si.txt" << path2 + "/sol.txt";
  for (const QString& relativeFilepath : relativeFilepaths)
  {
    CHECK_BOOL((QFile::permissions(tmp.filePath(relativeFilepath)) & QFile::ReadOwner) != 0, true);
  }

  // Make sure directories and files are read-only
  CHECK_BOOL(qSlicerUtils::setPermissionsRecursively(tmp.path(), QFile::ReadOwner, QFile::ReadOwner), true);

#ifndef Q_OS_WIN32
  // Exe permissions are not supported on windows:
  //  https://msdn.microsoft.com/en-us/library/1z319a54(v=vs.90).aspx
  //  https://qt.gitorious.org/qt/qt/blobs/092cd760d5fddf9640a310214fe01929f0fff3a8/src/corelib/io/qfsfileengine_win.cpp#line1781

  // Since directory are *NOT* executable, files should *NOT* be readable
  relativeFilepaths = QStringList() //
                      << path1 + "/sol.txt" << path1 + "/la.txt" << path2 + "/si.txt" << path2 + "/sol.txt";
  for (const QString& relativeFilepath : relativeFilepaths)
  {
    CHECK_BOOL((QFile::permissions(tmp.filePath(relativeFilepath)) & QFile::ReadOwner) != 0, false);
  }

  CHECK_BOOL(qSlicerUtils::setPermissionsRecursively(tmp.path(), QFile::ReadOwner | QFile::ExeOwner, QFile::ReadOwner), true);

  // Since directory are executable, files should be readable
  relativeFilepaths = QStringList() //
                      << path1 + "/sol.txt" << path1 + "/la.txt" << path2 + "/si.txt" << path2 + "/sol.txt";
  for (const QString& relativeFilepath : relativeFilepaths)
  {
    CHECK_BOOL((QFile::permissions(tmp.filePath(relativeFilepath)) & QFile::ReadOwner) != 0, true);
  }

  // Since directories and files are not writable, shouldn't be able to delete
  CHECK_BOOL(ctk::removeDirRecursively(tmp.path()), false);
#endif

  relativeFilepaths = QStringList() //
                      << path1 + "/sol.txt" << path1 + "/la.txt" << path2 + "/si.txt" << path2 + "/sol.txt";
  for (const QString& relativeFilepath : relativeFilepaths)
  {
    // Since files are read-only, they should *NOT* be writable
    CHECK_BOOL((QFile::permissions(tmp.filePath(relativeFilepath)) & QFile::WriteOwner) != 0, false);
  }

  CHECK_BOOL(qSlicerUtils::setPermissionsRecursively(tmp.path(), QFile::ReadOwner | QFile::ExeOwner | QFile::WriteOwner, QFile::ReadOwner | QFile::WriteOwner), true);

  // Make sure files are readable and writable
  relativeFilepaths = QStringList() //
                      << path1 + "/sol.txt" << path1 + "/la.txt" << path2 + "/si.txt" << path2 + "/sol.txt";
  for (const QString& relativeFilepath : relativeFilepaths)
  {
    CHECK_BOOL((QFile::permissions(tmp.filePath(relativeFilepath)) & QFile::WriteOwner) != 0, true);
  }

  // Should be possible to recursively delete
  CHECK_BOOL(ctk::removeDirRecursively(tmp.path()), true);

  return EXIT_SUCCESS;
}

//-----------------------------------------------------------------------------
int replaceWikiUrlVersionTest()
{
  //! [replaceWikiUrlVersion example1]
  CHECK_QSTRING(qSlicerUtils::replaceWikiUrlVersion("https://wiki.slicer.org/slicerWiki/index.php/Documentation/Nightly/Extensions/SlicerToKiwiExporter", "4.4"),
                "https://wiki.slicer.org/slicerWiki/index.php/Documentation/4.4/Extensions/SlicerToKiwiExporter");
  //! [replaceWikiUrlVersion example1]

  //! [replaceWikiUrlVersion example2]
  CHECK_QSTRING(qSlicerUtils::replaceWikiUrlVersion("https://wiki.slicer.org/slicerWiki/index.php/Documentation/Foo/Extensions/SlicerToKiwiExporter", "Bar"),
                "https://wiki.slicer.org/slicerWiki/index.php/Documentation/Bar/Extensions/SlicerToKiwiExporter");
  //! [replaceWikiUrlVersion example2]

  //! [replaceWikiUrlVersion example3]
  CHECK_QSTRING(qSlicerUtils::replaceWikiUrlVersion("https://wiki.slicer.org/slicerWiki/index.php/Documentation/Foo/Extensions/SlicerToKiwiExporter/Foo", "Bar"),
                "https://wiki.slicer.org/slicerWiki/index.php/Documentation/Bar/Extensions/SlicerToKiwiExporter/Foo");
  //! [replaceWikiUrlVersion example3]

  //! [replaceWikiUrlVersion example4]
  QString input = "Read documentation at "
                  "https://wiki.slicer.org/slicerWiki/index.php/Documentation/4.4/Extensions/SlicerToKiwiExporter."
                  "You will learn how to ...";
  QString expectedOutput = "Read documentation at "
                           "https://wiki.slicer.org/slicerWiki/index.php/Documentation/Nightly/Extensions/SlicerToKiwiExporter."
                           "You will learn how to ...";
  CHECK_QSTRING(qSlicerUtils::replaceWikiUrlVersion(input, "Nightly"), expectedOutput);
  //! [replaceWikiUrlVersion example4]

  return EXIT_SUCCESS;
}

//-----------------------------------------------------------------------------
int replaceDocumentationUrlVersionTest()
{

  QString hostname = "slicer.readthedocs.io";
  // Slicer ReadTheDocs -> replacements are done

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://slicer.readthedocs.io/en/latest/user_guide/get_help.html", hostname, "5.0"),
                "https://slicer.readthedocs.io/en/5.0/user_guide/get_help.html");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://slicer.readthedocs.io/en/stable/user_guide/get_help.html", hostname, "5.0"),
                "https://slicer.readthedocs.io/en/5.0/user_guide/get_help.html");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://slicer.readthedocs.io/en/4.11/user_guide/get_help.html", hostname, "5.0"),
                "https://slicer.readthedocs.io/en/5.0/user_guide/get_help.html");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://slicer.readthedocs.io/en/v4.11/user_guide/get_help.html", hostname, "v5.0"),
                "https://slicer.readthedocs.io/en/v5.0/user_guide/get_help.html");

  // GitHub -> no replacements are done

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://github.com/SlicerHeart/SlicerHeart#readme", hostname, "5.0"),
                "https://github.com/SlicerHeart/SlicerHeart#readme");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://github.com/SlicerHeart/SlicerHeart/tree/4.6#readme", hostname, "5.0"),
                "https://github.com/SlicerHeart/SlicerHeart/tree/4.6#readme");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://github.com/SlicerHeart/SlicerHeart/tree/v4.6#readme", hostname, "5.0"),
                "https://github.com/SlicerHeart/SlicerHeart/tree/v4.6#readme");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://github.com/SlicerHeart/SlicerHeart/blob/4.6/README.md", hostname, "5.0"),
                "https://github.com/SlicerHeart/SlicerHeart/blob/4.6/README.md");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://github.com/SlicerHeart/SlicerHeart/blob/main/README.md", hostname, "5.0"),
                "https://github.com/SlicerHeart/SlicerHeart/blob/main/README.md");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://github.com/SlicerHeart/SlicerHeart/blob/master/README.md", hostname, "5.0"),
                "https://github.com/SlicerHeart/SlicerHeart/blob/master/README.md");

  CHECK_QSTRING(qSlicerUtils::replaceDocumentationUrlVersion("https://github.com/SlicerHeart/SlicerHeart/blob/latest/data/1.0/README.md", hostname, "5.0"),
                "https://github.com/SlicerHeart/SlicerHeart/blob/latest/data/1.0/README.md");

  return EXIT_SUCCESS;
}

} // end of anonymous namespace

int qSlicerUtilsTest1(int, char*[])
{
  try
  {
    CHECK_EXIT_SUCCESS(isExecutableNameTest());
    CHECK_EXIT_SUCCESS(isCLILoadableModuleTest());
    CHECK_EXIT_SUCCESS(isLoadableModuleTest());
    CHECK_EXIT_SUCCESS(executableExtensionTest());
    CHECK_EXIT_SUCCESS(extractModuleNameFromClassNameTest());
    CHECK_EXIT_SUCCESS(isPluginInstalledBuiltinTest());
    CHECK_EXIT_SUCCESS(setPermissionsRecursivelyTest());
    CHECK_EXIT_SUCCESS(replaceWikiUrlVersionTest());
    CHECK_EXIT_SUCCESS(replaceDocumentationUrlVersionTest());
  }
  catch (const std::runtime_error& e)
  {
    std::cerr << e.what() << std::endl;
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
