/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
#include "cmCTestScriptHandler.h"

#include <cstdlib>
#include <functional>
#include <map>
#include <sstream>
#include <utility>

#include <cm/memory>

#include "cmCTest.h"
#include "cmCTestBuildCommand.h"
#include "cmCTestConfigureCommand.h"
#include "cmCTestCoverageCommand.h"
#include "cmCTestEmptyBinaryDirectoryCommand.h"
#include "cmCTestMemCheckCommand.h"
#include "cmCTestReadCustomFilesCommand.h"
#include "cmCTestRunScriptCommand.h"
#include "cmCTestSleepCommand.h"
#include "cmCTestStartCommand.h"
#include "cmCTestSubmitCommand.h"
#include "cmCTestTestCommand.h"
#include "cmCTestUpdateCommand.h"
#include "cmCTestUploadCommand.h"
#include "cmDuration.h"
#include "cmGlobalGenerator.h"
#include "cmMakefile.h"
#include "cmState.h"
#include "cmStateDirectory.h"
#include "cmStateSnapshot.h"
#include "cmSystemTools.h"
#include "cmUVProcessChain.h"
#include "cmake.h"

cmCTestScriptHandler::cmCTestScriptHandler(cmCTest* ctest)
  : CTest(ctest)
{
}

cmCTestScriptHandler::~cmCTestScriptHandler() = default;

// just adds an argument to the vector
void cmCTestScriptHandler::AddConfigurationScript(std::string const& script,
                                                  bool pscope)
{
  this->ConfigurationScripts.emplace_back(script);
  this->ScriptProcessScope.push_back(pscope);
}

// the generic entry point for handling scripts, this routine will run all
// the scripts provides a -S arguments
int cmCTestScriptHandler::ProcessHandler()
{
  int res = 0;
  for (size_t i = 0; i < this->ConfigurationScripts.size(); ++i) {
    // for each script run it
    res |= this->RunConfigurationScript(this->ConfigurationScripts[i],
                                        this->ScriptProcessScope[i]);
  }
  return res;
}

void cmCTestScriptHandler::UpdateElapsedTime()
{
  if (this->Makefile) {
    // set the current elapsed time
    auto itime = cmDurationTo<unsigned int>(this->CTest->GetElapsedTime());
    auto timeString = std::to_string(itime);
    this->Makefile->AddDefinition("CTEST_ELAPSED_TIME", timeString);
  }
}

int cmCTestScriptHandler::ExecuteScript(std::string const& total_script_arg)
{
  // execute the script passing in the arguments to the script as well as the
  // arguments from this invocation of cmake
  std::vector<std::string> argv;
  argv.push_back(cmSystemTools::GetCTestCommand());
  argv.push_back("-SR");
  argv.push_back(total_script_arg);

  cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
             "Executable for CTest is: " << cmSystemTools::GetCTestCommand()
                                         << "\n");

  // now pass through all the other arguments
  std::vector<std::string>& initArgs =
    this->CTest->GetInitialCommandLineArguments();
  //*** need to make sure this does not have the current script ***
  for (size_t i = 1; i < initArgs.size(); ++i) {
    // in a nested subprocess, skip the parent's `-SR <path>` arguments.
    if (initArgs[i] == "-SR") {
      i++; // <path>
    } else {
      argv.push_back(initArgs[i]);
    }
  }

  // Now create process object
  cmUVProcessChainBuilder builder;
  builder.AddCommand(argv)
    .SetBuiltinStream(cmUVProcessChainBuilder::Stream_OUTPUT)
    .SetBuiltinStream(cmUVProcessChainBuilder::Stream_ERROR);
  auto process = builder.Start();

  std::vector<char> out;
  std::vector<char> err;
  std::string line;
  cmSystemTools::WaitForLineResult pipe;
  while ((pipe = cmSystemTools::WaitForLine(
            &process.GetLoop(), process.OutputStream(), process.ErrorStream(),
            line, out, err)) != cmSystemTools::WaitForLineResult::None) {
    cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
               "Output: " << line << "\n");
    if (pipe == cmSystemTools::WaitForLineResult::STDERR) {
      cmCTestLog(this->CTest, ERROR_MESSAGE, line << "\n");
    } else if (pipe == cmSystemTools::WaitForLineResult::STDOUT) {
      cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT, line << "\n");
    }
  }

  // Properly handle output of the build command
  process.Wait();
  auto const& status = process.GetStatus(0);
  auto result = status.GetException();
  int retVal = 0;
  bool failed = false;
  switch (result.first) {
    case cmUVProcessChain::ExceptionCode::None:
      retVal = static_cast<int>(status.ExitStatus);
      break;
    case cmUVProcessChain::ExceptionCode::Spawn:
      cmCTestLog(this->CTest, ERROR_MESSAGE,
                 "\tError executing ctest: " << result.second << std::endl);
      failed = true;
      break;
    default:
      retVal = status.TermSignal;
      cmCTestLog(this->CTest, ERROR_MESSAGE,
                 "\tThere was an exception: " << result.second << " " << retVal
                                              << std::endl);
      failed = true;
  }
  if (failed) {
    std::ostringstream message;
    message << "Error running command: [";
    message << static_cast<int>(result.first) << "] ";
    for (std::string const& arg : argv) {
      message << arg << " ";
    }
    cmCTestLog(this->CTest, ERROR_MESSAGE, message.str() << std::endl);
    return -1;
  }
  return retVal;
}

void cmCTestScriptHandler::CreateCMake()
{
  // create a cmake instance to read the configuration script
  this->CMake = cm::make_unique<cmake>(cmState::Role::CTest);
  this->CMake->GetCurrentSnapshot().SetDefaultDefinitions();
  this->CMake->AddCMakePaths();
  this->GlobalGenerator =
    cm::make_unique<cmGlobalGenerator>(this->CMake.get());

  cmStateSnapshot snapshot = this->CMake->GetCurrentSnapshot();
  std::string cwd = cmSystemTools::GetLogicalWorkingDirectory();
  snapshot.GetDirectory().SetCurrentSource(cwd);
  snapshot.GetDirectory().SetCurrentBinary(cwd);
  this->Makefile =
    cm::make_unique<cmMakefile>(this->GlobalGenerator.get(), snapshot);
  if (this->ParentMakefile) {
    this->Makefile->SetRecursionDepth(
      this->ParentMakefile->GetRecursionDepth());
  }

  this->CMake->SetProgressCallback(
    [this](std::string const& m, float /*unused*/) {
      if (!m.empty()) {
        cmCTestLog(this->CTest, HANDLER_OUTPUT, "-- " << m << std::endl);
      }
    });

  cmState* state = this->CMake->GetState();
  state->AddBuiltinCommand("ctest_build", cmCTestBuildCommand(this->CTest));
  state->AddBuiltinCommand("ctest_configure",
                           cmCTestConfigureCommand(this->CTest));
  state->AddBuiltinCommand("ctest_coverage",
                           cmCTestCoverageCommand(this->CTest));
  state->AddBuiltinCommand("ctest_empty_binary_directory",
                           cmCTestEmptyBinaryDirectoryCommand);
  state->AddBuiltinCommand("ctest_memcheck",
                           cmCTestMemCheckCommand(this->CTest));
  state->AddBuiltinCommand("ctest_read_custom_files",
                           cmCTestReadCustomFilesCommand(this->CTest));
  state->AddBuiltinCommand("ctest_run_script",
                           cmCTestRunScriptCommand(this->CTest));
  state->AddBuiltinCommand("ctest_sleep", cmCTestSleepCommand);
  state->AddBuiltinCommand("ctest_start", cmCTestStartCommand(this->CTest));
  state->AddBuiltinCommand("ctest_submit", cmCTestSubmitCommand(this->CTest));
  state->AddBuiltinCommand("ctest_test", cmCTestTestCommand(this->CTest));
  state->AddBuiltinCommand("ctest_update", cmCTestUpdateCommand(this->CTest));
  state->AddBuiltinCommand("ctest_upload", cmCTestUploadCommand(this->CTest));
}

// this sets up some variables for the script to use, creates the required
// cmake instance and generators, and then reads in the script
int cmCTestScriptHandler::ReadInScript(std::string const& total_script_arg)
{
  // Reset the error flag so that the script is read in no matter what
  cmSystemTools::ResetErrorOccurredFlag();

  // if the argument has a , in it then it needs to be broken into the fist
  // argument (which is the script) and the second argument which will be
  // passed into the scripts as S_ARG
  std::string script;
  std::string script_arg;
  std::string::size_type const comma_pos = total_script_arg.find(',');
  if (comma_pos != std::string::npos) {
    script = total_script_arg.substr(0, comma_pos);
    script_arg = total_script_arg.substr(comma_pos + 1);
  } else {
    script = total_script_arg;
  }
  // make sure the file exists
  if (!cmSystemTools::FileExists(script)) {
    cmSystemTools::Error("Cannot find file: " + script);
    return 1;
  }

  // read in the list file to fill the cache
  // create a cmake instance to read the configuration script
  this->CreateCMake();

  // set a variable with the path to the current script
  this->Makefile->AddDefinition("CTEST_SCRIPT_DIRECTORY",
                                cmSystemTools::GetFilenamePath(script));
  this->Makefile->AddDefinition("CTEST_SCRIPT_NAME",
                                cmSystemTools::GetFilenameName(script));
  this->Makefile->AddDefinition("CTEST_EXECUTABLE_NAME",
                                cmSystemTools::GetCTestCommand());
  this->Makefile->AddDefinition("CMAKE_EXECUTABLE_NAME",
                                cmSystemTools::GetCMakeCommand());
  this->UpdateElapsedTime();

  // set the CTEST_CONFIGURATION_TYPE variable to the current value of the
  // the -C argument on the command line.
  if (!this->CTest->GetConfigType().empty()) {
    this->Makefile->AddDefinition("CTEST_CONFIGURATION_TYPE",
                                  this->CTest->GetConfigType());
  }

  // add the script arg if defined
  if (!script_arg.empty()) {
    this->Makefile->AddDefinition("CTEST_SCRIPT_ARG", script_arg);
  }

  // set a callback function to update the elapsed time
  this->Makefile->OnExecuteCommand([this] { this->UpdateElapsedTime(); });

  /* Execute CTestScriptMode.cmake, which loads CMakeDetermineSystem and
  CMakeSystemSpecificInformation, so
  that variables like CMAKE_SYSTEM and also the search paths for libraries,
  header and executables are set correctly and can be used. Makes new-style
  ctest scripting easier. */
  std::string systemFile =
    this->Makefile->GetModulesFile("CTestScriptMode.cmake");
  if (!this->Makefile->ReadListFile(systemFile) ||
      cmSystemTools::GetErrorOccurredFlag()) {
    cmCTestLog(this->CTest, ERROR_MESSAGE,
               "Error in read:" << systemFile << "\n");
    return -1;
  }

  // Add definitions of variables passed in on the command line:
  std::map<std::string, std::string> const& defs =
    this->CTest->GetDefinitions();
  for (auto const& d : defs) {
    this->Makefile->AddDefinition(d.first, d.second);
  }

  int res = 0;

  // finally read in the script
  if (!this->Makefile->ReadListFile(script) ||
      cmSystemTools::GetErrorOccurredFlag()) {
    // Reset the error flag so that it can run more than
    // one script with an error when you use ctest_run_script.
    cmSystemTools::ResetErrorOccurredFlag();
    res = -1;
  }

  return this->CMake->HasScriptModeExitCode()
    ? this->CMake->GetScriptModeExitCode()
    : res;
}

// run a specific script
int cmCTestScriptHandler::RunConfigurationScript(
  std::string const& total_script_arg, bool pscope)
{
#ifndef CMAKE_BOOTSTRAP
  cmSystemTools::SaveRestoreEnvironment sre;
#endif

  int result;

  // read in the script
  if (pscope) {
    cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
               "Reading Script: " << total_script_arg << std::endl);
    result = this->ReadInScript(total_script_arg);
  } else {
    cmCTestLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
               "Executing Script: " << total_script_arg << std::endl);
    result = this->ExecuteScript(total_script_arg);
  }

  return result;
}

bool cmCTestScriptHandler::RunScript(cmCTest* ctest, cmMakefile* mf,
                                     std::string const& sname, bool InProcess,
                                     int* returnValue)
{
  auto sh = cm::make_unique<cmCTestScriptHandler>(ctest);
  sh->ParentMakefile = mf;
  sh->AddConfigurationScript(sname, InProcess);
  int res = sh->ProcessHandler();
  if (returnValue) {
    *returnValue = res;
  }
  return true;
}
