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

#include "cmCTest.h"
#include "cmCTestGenericHandler.h"
#include "cmMakefile.h"
#include "cmSystemTools.h"
#include "cmWorkingDirectory.h"
#include "cmake.h"

#include <cstring>
#include <sstream>
#include <stdlib.h>

class cmExecutionStatus;

cmCTestHandlerCommand::cmCTestHandlerCommand()
{
    const size_t INIT_SIZE = 100;
    size_t       cc;
    this->Arguments.reserve(INIT_SIZE);
    for(cc = 0; cc < INIT_SIZE; ++cc)
    {
        this->Arguments.push_back(nullptr);
    }
    this->Arguments[ct_RETURN_VALUE]        = "RETURN_VALUE";
    this->Arguments[ct_CAPTURE_CMAKE_ERROR] = "CAPTURE_CMAKE_ERROR";
    this->Arguments[ct_SOURCE]              = "SOURCE";
    this->Arguments[ct_BUILD]               = "BUILD";
    this->Arguments[ct_SUBMIT_INDEX]        = "SUBMIT_INDEX";
    this->Last                              = ct_LAST;
    this->AppendXML                         = false;
    this->Quiet                             = false;
}

namespace
{
// class to save and restore the error state for ctest_* commands
// if a ctest_* command has a CAPTURE_CMAKE_ERROR then put the error
// state into there and restore the system wide error to what
// it was before the command ran
class SaveRestoreErrorState
{
public:
    SaveRestoreErrorState()
    {
        this->InitialErrorState = cmSystemTools::GetErrorOccuredFlag();
        cmSystemTools::ResetErrorOccuredFlag();  // rest the error state
        this->CaptureCMakeErrorValue = false;
    }
    // if the function has a CAPTURE_CMAKE_ERROR then we should restore
    // the error state to what it was before the function was run
    // if not then let the error state be what it is
    void CaptureCMakeError() { this->CaptureCMakeErrorValue = true; }
    ~SaveRestoreErrorState()
    {
        // if we are not saving the return value then make sure
        // if it was in error it goes back to being in error
        // otherwise leave it be what it is
        if(!this->CaptureCMakeErrorValue)
        {
            if(this->InitialErrorState)
            {
                cmSystemTools::SetErrorOccured();
            }
            return;
        }
        // if we have saved the error in a return variable
        // then put things back exactly like they were
        bool currentState = cmSystemTools::GetErrorOccuredFlag();
        // if the state changed during this command we need
        // to handle it, if not then nothing needs to be done
        if(currentState != this->InitialErrorState)
        {
            // restore the initial error state
            if(this->InitialErrorState)
            {
                cmSystemTools::SetErrorOccured();
            } else
            {
                cmSystemTools::ResetErrorOccuredFlag();
            }
        }
    }

private:
    bool InitialErrorState;
    bool CaptureCMakeErrorValue;
};
}

bool
cmCTestHandlerCommand::InitialPass(std::vector<std::string> const& args,
                                   cmExecutionStatus& /*unused*/)
{
    // save error state and restore it if needed
    SaveRestoreErrorState errorState;
    // Allocate space for argument values.
    this->Values.clear();
    this->Values.resize(this->Last, nullptr);

    // Process input arguments.
    this->ArgumentDoing = ArgumentDoingNone;
    // look at all arguments and do not short circuit on the first
    // bad one so that CAPTURE_CMAKE_ERROR can override setting the
    // global error state
    bool foundBadArgument = false;
    for(std::string const& arg : args)
    {
        // Check this argument.
        if(!this->CheckArgumentKeyword(arg) && !this->CheckArgumentValue(arg))
        {
            std::ostringstream e;
            e << "called with unknown argument \"" << arg << "\".";
            this->SetError(e.str());
            foundBadArgument = true;
        }
        // note bad argument
        if(this->ArgumentDoing == ArgumentDoingError)
        {
            foundBadArgument = true;
        }
    }
    bool capureCMakeError = (this->Values[ct_CAPTURE_CMAKE_ERROR] &&
                             *this->Values[ct_CAPTURE_CMAKE_ERROR]);
    // now that arguments are parsed check to see if there is a
    // CAPTURE_CMAKE_ERROR specified let the errorState object know.
    if(capureCMakeError)
    {
        errorState.CaptureCMakeError();
    }
    // if we found a bad argument then exit before running command
    if(foundBadArgument)
    {
        // store the cmake error
        if(capureCMakeError)
        {
            this->Makefile->AddDefinition(this->Values[ct_CAPTURE_CMAKE_ERROR],
                                          "-1");
            std::string const err = this->GetName() + " " + this->GetError();
            if(!cmSystemTools::FindLastString(err.c_str(), "unknown error."))
            {
                cmCTestLog(this->CTest, ERROR_MESSAGE,
                           err << " error from command\n");
            }
            // return success because failure is recorded in CAPTURE_CMAKE_ERROR
            return true;
        }
        // return failure because of bad argument
        return false;
    }

    // Set the config type of this ctest to the current value of the
    // CTEST_CONFIGURATION_TYPE script variable if it is defined.
    // The current script value trumps the -C argument on the command
    // line.
    const char* ctestConfigType =
        this->Makefile->GetDefinition("CTEST_CONFIGURATION_TYPE");
    if(ctestConfigType)
    {
        this->CTest->SetConfigType(ctestConfigType);
    }

    if(this->Values[ct_BUILD])
    {
        this->CTest->SetCTestConfiguration(
            "BuildDirectory",
            cmSystemTools::CollapseFullPath(this->Values[ct_BUILD]).c_str(),
            this->Quiet);
    } else
    {
        std::string const& bdir =
            this->Makefile->GetSafeDefinition("CTEST_BINARY_DIRECTORY");
        if(!bdir.empty())
        {
            this->CTest->SetCTestConfiguration(
                "BuildDirectory", cmSystemTools::CollapseFullPath(bdir).c_str(),
                this->Quiet);
        } else
        {
            cmCTestLog(this->CTest, ERROR_MESSAGE,
                       "CTEST_BINARY_DIRECTORY not set" << std::endl;);
        }
    }
    if(this->Values[ct_SOURCE])
    {
        cmCTestLog(this->CTest, DEBUG,
                   "Set source directory to: " << this->Values[ct_SOURCE]
                                               << std::endl);
        this->CTest->SetCTestConfiguration(
            "SourceDirectory",
            cmSystemTools::CollapseFullPath(this->Values[ct_SOURCE]).c_str(),
            this->Quiet);
    } else
    {
        this->CTest->SetCTestConfiguration(
            "SourceDirectory",
            cmSystemTools::CollapseFullPath(
                this->Makefile->GetSafeDefinition("CTEST_SOURCE_DIRECTORY"))
                .c_str(),
            this->Quiet);
    }

    if(const char* changeId = this->Makefile->GetDefinition("CTEST_CHANGE_ID"))
    {
        this->CTest->SetCTestConfiguration("ChangeId", changeId, this->Quiet);
    }

    cmCTestLog(this->CTest, DEBUG, "Initialize handler" << std::endl;);
    cmCTestGenericHandler* handler = this->InitializeHandler();
    if(!handler)
    {
        cmCTestLog(this->CTest, ERROR_MESSAGE,
                   "Cannot instantiate test handler " << this->GetName()
                                                      << std::endl);
        if(capureCMakeError)
        {
            this->Makefile->AddDefinition(this->Values[ct_CAPTURE_CMAKE_ERROR],
                                          "-1");
            const char* err = this->GetError();
            if(err && !cmSystemTools::FindLastString(err, "unknown error."))
            {
                cmCTestLog(this->CTest, ERROR_MESSAGE,
                           err << " error from command\n");
            }
            return true;
        }
        return false;
    }

    handler->SetAppendXML(this->AppendXML);

    handler->PopulateCustomVectors(this->Makefile);
    if(this->Values[ct_SUBMIT_INDEX])
    {
        if(!this->CTest->GetDropSiteCDash() &&
           this->CTest->GetDartVersion() <= 1)
        {
            cmCTestLog(this->CTest, ERROR_MESSAGE,
                       "Dart before version 2.0 does not support collecting "
                       "submissions."
                           << std::endl
                           << "Please upgrade the server to Dart 2 or higher, "
                              "or do not use "
                              "SUBMIT_INDEX."
                           << std::endl);
        } else
        {
            handler->SetSubmitIndex(atoi(this->Values[ct_SUBMIT_INDEX]));
        }
    }
    cmWorkingDirectory workdir(
        this->CTest->GetCTestConfiguration("BuildDirectory"));
    if(workdir.Failed())
    {
        this->SetError("failed to change directory to " +
                       this->CTest->GetCTestConfiguration("BuildDirectory") +
                       " : " + std::strerror(workdir.GetLastResult()));
        if(capureCMakeError)
        {
            this->Makefile->AddDefinition(this->Values[ct_CAPTURE_CMAKE_ERROR],
                                          "-1");
            cmCTestLog(this->CTest, ERROR_MESSAGE,
                       this->GetName() << " " << this->GetError() << "\n");
            // return success because failure is recorded in CAPTURE_CMAKE_ERROR
            return true;
        }
        return false;
    }

    int res = handler->ProcessHandler();
    if(this->Values[ct_RETURN_VALUE] && *this->Values[ct_RETURN_VALUE])
    {
        std::ostringstream str;
        str << res;
        this->Makefile->AddDefinition(this->Values[ct_RETURN_VALUE],
                                      str.str().c_str());
    }
    this->ProcessAdditionalValues(handler);
    // log the error message if there was an error
    if(capureCMakeError)
    {
        const char* returnString = "0";
        if(cmSystemTools::GetErrorOccuredFlag())
        {
            returnString    = "-1";
            const char* err = this->GetError();
            // print out the error if it is not "unknown error" which means
            // there was no message
            if(err && !cmSystemTools::FindLastString(err, "unknown error."))
            {
                cmCTestLog(this->CTest, ERROR_MESSAGE, err);
            }
        }
        // store the captured cmake error state 0 or -1
        this->Makefile->AddDefinition(this->Values[ct_CAPTURE_CMAKE_ERROR],
                                      returnString);
    }
    return true;
}

void
cmCTestHandlerCommand::ProcessAdditionalValues(cmCTestGenericHandler*)
{}

bool
cmCTestHandlerCommand::CheckArgumentKeyword(std::string const& arg)
{
    // Look for non-value arguments common to all commands.
    if(arg == "APPEND")
    {
        this->ArgumentDoing = ArgumentDoingNone;
        this->AppendXML     = true;
        return true;
    }
    if(arg == "QUIET")
    {
        this->ArgumentDoing = ArgumentDoingNone;
        this->Quiet         = true;
        return true;
    }

    // Check for a keyword in our argument/value table.
    for(unsigned int k = 0; k < this->Arguments.size(); ++k)
    {
        if(this->Arguments[k] && arg == this->Arguments[k])
        {
            this->ArgumentDoing = ArgumentDoingKeyword;
            this->ArgumentIndex = k;
            return true;
        }
    }
    return false;
}

bool
cmCTestHandlerCommand::CheckArgumentValue(std::string const& arg)
{
    if(this->ArgumentDoing == ArgumentDoingKeyword)
    {
        this->ArgumentDoing = ArgumentDoingNone;
        unsigned int k      = this->ArgumentIndex;
        if(this->Values[k])
        {
            std::ostringstream e;
            e << "Called with more than one value for " << this->Arguments[k];
            this->Makefile->IssueMessage(cmake::FATAL_ERROR, e.str());
            this->ArgumentDoing = ArgumentDoingError;
            return true;
        }
        this->Values[k] = arg.c_str();
        cmCTestLog(this->CTest, DEBUG,
                   "Set " << this->Arguments[k] << " to " << arg << "\n");
        return true;
    }
    return false;
}
