/*
 * Copyright 2018 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */

#include <buildboxcommon_runner.h>

#include <buildboxcommon_argvec.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_remoteexecutionclient.h>
#include <buildboxcommon_systemutils.h>
#include <buildboxcommon_temporarydirectory.h>
#include <buildboxcommon_temporaryfile.h>

#include <fcntl.h>
#include <gtest/gtest.h>

using namespace buildboxcommon;

class TestRunner : public Runner {
  public:
    TestRunner()
    {
        this->d_actionResultPath = d_tempDirectory.strname() + "/actionresult";
        this->setOutputPath(this->d_actionResultPath);

        this->registerSignals();

        DigestGenerator::resetState();
        DigestGenerator::init();
    }

    ~TestRunner() override { this->resetSignals(); }

    TestRunner(const TestRunner &other) = delete;
    TestRunner(TestRunner &&other) = delete;
    TestRunner &operator=(const TestRunner &other) = delete;
    TestRunner &operator=(TestRunner &&other) = delete;

    int main(int argc, char *argv[])
    {
        DigestGenerator::resetState();
        return Runner::main(argc, argv);
    }

    ActionResult execute(const Command &, const Digest &,
                         const Platform &) override
    {
        return ActionResult();
    }
    // expose createOutputDirectories for testing
    void testCreateOutputDirectories(const Command &command,
                                     const std::string &workingDir) const
    {
        createOutputDirectories(command, workingDir);
    }

    static grpc::Status dummyUploadFunction(const std::string &stdout_file,
                                            const std::string &stderr_file,
                                            Digest *stdout_digest,
                                            Digest *stderr_digest)
    {
        // Return valid hashes so that tests can verify that this was invoked
        // with the expected data:
        *stdout_digest = DigestGenerator::hashFile(stdout_file);
        *stderr_digest = DigestGenerator::hashFile(stderr_file);
        return grpc::Status::OK;
    }

    void setStdOutFile(const std::string &path)
    {
        this->d_standardOutputsCaptureConfig.stdout_file_path = path;
    }

    void setStdErrFile(const std::string &path)
    {
        this->d_standardOutputsCaptureConfig.stderr_file_path = path;
    }

    void skipStandardOutputCapture()
    {
        this->d_standardOutputsCaptureConfig.skip_capture = true;
    }

    void setCollectExecutionStats(const bool value)
    {
        this->d_collect_execution_stats = value;
    }

    void setTimeout(std::chrono::seconds timeout)
    {
        this->d_timeout = timeout;
    }

    using Runner::executeAndStore;
    TemporaryDirectory d_tempDirectory;
    std::string d_actionResultPath;
};

TEST(RunnerTest, PrintingUsageDoesntCrash)
{
    TestRunner runner;
    auto argvec = Argvec({"buildbox-run"});
    EXPECT_NO_THROW(runner.main(argvec.argc(), argvec.argv()));
}

TEST(RunnerTest, Test) { TestRunner runner; }

TEST(RunnerTest, ValidateParametersOptionEmptyArgumentList)
{
    TestRunner runner;
    auto argvec = Argvec({"buildbox-run", "--validate-parameters"});
    ASSERT_NE(runner.main(argvec.argc(), argvec.argv()), 0);
}

TEST(RunnerTest, ValidateParametersOptionSuccess)
{
    TestRunner runner;
    auto argvec = Argvec(
        {"buildbox-run", "--remote=http://cas:50051",
         "--action=/path/to/action", "--action-result=/path/to/action-result",
         "--collect-execution-stats", "--output-bypass-local-cache",
         "--staging-mode=copy-or-link", "--validate-parameters"});
    ASSERT_EQ(runner.main(argvec.argc(), argvec.argv()), 0);
}

TEST(RunnerTest, ValidateParametersOptionMissingOption)
{
    TestRunner runner;
    // Not setting `--remote`:
    auto argvec = Argvec({"buildbox-run", "--action=/path/to/action",
                          "--action-result=/path/to/action-result",
                          "--validate-parameters"});
    ASSERT_NE(runner.main(argvec.argc(), argvec.argv()), 0);
}

TEST(RunnerTest, ValidateParametersOptionExtraOption)
{
    TestRunner runner;
    auto argvec = Argvec(
        {"buildbox-run", "--remote=http://cas:50051",
         "--action=/path/to/action", "--action-result=/path/to/action-result",
         "--validate-parameters", "--this-option-is-NOT-valid"});
    ASSERT_NE(runner.main(argvec.argc(), argvec.argv()), 0);
}

void assert_metadata_execution_timestamps_set(const ActionResult &result)
{
    // `ExecutedActionMetadata` execution timestamps are set:
    const auto empty_timestamp = google::protobuf::Timestamp();
    EXPECT_NE(result.execution_metadata().execution_start_timestamp(),
              empty_timestamp);
    EXPECT_NE(result.execution_metadata().execution_completed_timestamp(),
              empty_timestamp);

    // But the remaining timestamps aren't modified:
    EXPECT_EQ(result.execution_metadata().worker_start_timestamp(),
              empty_timestamp);
    EXPECT_EQ(result.execution_metadata().worker_completed_timestamp(),
              empty_timestamp);
    EXPECT_EQ(result.execution_metadata().worker_start_timestamp(),
              result.execution_metadata().worker_start_timestamp());
    EXPECT_EQ(result.execution_metadata().worker_completed_timestamp(),
              result.execution_metadata().worker_start_timestamp());
}

class TestOutputCaptureFixture : public ::testing::TestWithParam<bool> {

  protected:
    TestOutputCaptureFixture() {};

    TestRunner test_runner;
};

TEST_P(TestOutputCaptureFixture, ExecuteAndStoreHelloWorld)
{
    ActionResult result;

    std::unique_ptr<buildboxcommon::TemporaryFile> stdout_file;
    std::unique_ptr<buildboxcommon::TemporaryFile> stderr_file;
    const auto standard_outputs_redirected_to_custom_paths = GetParam();
    if (standard_outputs_redirected_to_custom_paths) {
        // We redirect the command's standard outputs to specific files.
        // Otherwise the runner will create and use its own temporay ones.
        stdout_file = std::make_unique<buildboxcommon::TemporaryFile>();
        stderr_file = std::make_unique<buildboxcommon::TemporaryFile>();

        test_runner.setStdOutFile(stdout_file->strname());
        test_runner.setStdErrFile(stderr_file->strname());
    }

    const auto path_to_echo = SystemUtils::getPathToCommand("echo");
    ASSERT_FALSE(path_to_echo.empty());
    test_runner.executeAndStore({path_to_echo, "hello", "world"},
                                TestRunner::dummyUploadFunction, &result);

    const auto expected_stdout = "hello world\n";
    EXPECT_EQ(result.stdout_digest(), DigestGenerator::hash(expected_stdout));
    EXPECT_TRUE(result.stdout_raw().empty()); // `Runner` does not inline.

    EXPECT_EQ(result.stderr_digest(), DigestGenerator::hash(""));

    EXPECT_EQ(result.exit_code(), 0);

    assert_metadata_execution_timestamps_set(result);

    ASSERT_EQ(result.execution_metadata().auxiliary_metadata_size(), 0);

    if (standard_outputs_redirected_to_custom_paths) {
        EXPECT_EQ(FileUtils::getFileContents(stdout_file->name()),
                  expected_stdout);
        EXPECT_EQ(FileUtils::getFileContents(stderr_file->name()), "");
    }
}
INSTANTIATE_TEST_SUITE_P(OutputRedirectionTest, TestOutputCaptureFixture,
                         ::testing::Values(false, true));

TEST(RunnerTest, TestEmptyOutputsNotUploaded)
{
    TestRunner runner;
    ActionResult result;

    const auto path_to_true = SystemUtils::getPathToCommand("true");
    ASSERT_FALSE(path_to_true.empty());

    runner.executeAndStore({path_to_true}, TestRunner::dummyUploadFunction,
                           &result);

    EXPECT_EQ(result.stdout_digest(), DigestGenerator::hash(""));
    EXPECT_EQ(result.stderr_digest(), DigestGenerator::hash(""));
    EXPECT_EQ(result.exit_code(), 0);

    assert_metadata_execution_timestamps_set(result);
}

TEST(RunnerTest, CommandNotFound)
{
    TestRunner runner;
    ActionResult result;

    runner.executeAndStore({"command-does-not-exist"},
                           TestRunner::dummyUploadFunction, &result);
    EXPECT_EQ(result.exit_code(), 127); // "command not found" as in Bash

    google::rpc::Status status =
        buildboxcommon::ProtoUtils::readProtobufFromFile<google::rpc::Status>(
            runner.errorStatusCodeFilePath(runner.d_actionResultPath));
    EXPECT_EQ(status.code(), grpc::StatusCode::NOT_FOUND);

    assert_metadata_execution_timestamps_set(result);
}

TEST(RunnerTest, CommandIsNotAnExecutable)
{
    TestRunner runner;
    ActionResult result;

    TemporaryFile non_executable_file;
    runner.executeAndStore({non_executable_file.name()},
                           TestRunner::dummyUploadFunction, &result);
    EXPECT_EQ(result.exit_code(), 126); // Command invoked cannot execute
    google::rpc::Status status =
        buildboxcommon::ProtoUtils::readProtobufFromFile<google::rpc::Status>(
            runner.errorStatusCodeFilePath(runner.d_actionResultPath));
    EXPECT_EQ(status.code(), grpc::StatusCode::INVALID_ARGUMENT);

    assert_metadata_execution_timestamps_set(result);
}

TEST(RunnerTest, ExecuteAndStoreExitCode)
{
    TestRunner runner;
    ActionResult result;

    const auto path_to_sh = SystemUtils::getPathToCommand("sh");
    ASSERT_FALSE(path_to_sh.empty());

    runner.executeAndStore({path_to_sh, "-c", "exit 23"},
                           TestRunner::dummyUploadFunction, &result);

    EXPECT_EQ(result.exit_code(), 23);
}

TEST(RunnerTest, ExecuteAndStoreWithCustomWorkdir)
{
    TestRunner runner;
    ActionResult result;

    TemporaryDirectory td;

    const auto pathToTouch = SystemUtils::getPathToCommand("touch");
    ASSERT_FALSE(pathToTouch.empty());

    runner.executeAndStore({pathToTouch, "output"},
                           TestRunner::dummyUploadFunction, &result,
                           td.name());

    ASSERT_TRUE(FileUtils::isRegularFile(
        (std::string(td.name()) + "/output").c_str()));
}

TEST(RunnerTest, ExecuteAndStoreWithInvalidCustomWorkdir)
{
    TestRunner runner;
    ActionResult result;

    runner.executeAndStore({"echo", "Hello"}, TestRunner::dummyUploadFunction,
                           &result, "jlk2jlkjlk");
    // POSIX specifies exit code 127 for errors preventing execution of the
    // child process.
    EXPECT_EQ(result.exit_code(), 127);
    google::rpc::Status status =
        buildboxcommon::ProtoUtils::readProtobufFromFile<google::rpc::Status>(
            runner.errorStatusCodeFilePath(runner.d_actionResultPath));
    EXPECT_EQ(status.code(), grpc::StatusCode::INVALID_ARGUMENT);
}

TEST(RunnerTest, ExecuteAndCollectExecutionStats)
{
    TestRunner runner;
    runner.setCollectExecutionStats(true);

    const auto path_to_sh = SystemUtils::getPathToCommand("sh");
    ASSERT_FALSE(path_to_sh.empty());

    ActionResult result;
    runner.executeAndStore({path_to_sh, "-c", "exit 23"},
                           TestRunner::dummyUploadFunction, &result);

    EXPECT_EQ(result.exit_code(), 23);

    // `auxiliary_metadata` contains a `Digest` (pointing to the proto with
    // the actual metrics):
    ASSERT_EQ(result.execution_metadata().auxiliary_metadata_size(), 1);
    ASSERT_TRUE(
        result.execution_metadata().auxiliary_metadata(0).Is<Digest>());
}

TEST(RunnerTest, ExecuteAndStoreStderr)
{
    TestRunner runner;
    ActionResult result;

    const auto path_to_sh = SystemUtils::getPathToCommand("sh");
    ASSERT_FALSE(path_to_sh.empty());

    runner.executeAndStore({path_to_sh, "-c", "echo hello; echo world >&2"},
                           TestRunner::dummyUploadFunction, &result);

    const auto expected_stdout = "hello\n";
    const auto expected_stderr = "world\n";

    EXPECT_EQ(result.stdout_digest(), DigestGenerator::hash(expected_stdout));
    EXPECT_EQ(result.stderr_digest(), DigestGenerator::hash(expected_stderr));

    EXPECT_TRUE(result.stdout_raw().empty());
    EXPECT_TRUE(result.stderr_raw().empty());

    EXPECT_EQ(result.exit_code(), 0);
}

TEST(RunnerTest, ExecuteAndStoreWithoutStandardOutputCapture)
{
    const auto path_to_false = SystemUtils::getPathToCommand("false");
    ASSERT_FALSE(path_to_false.empty());

    TestRunner runner;
    runner.skipStandardOutputCapture();
    // Checking that this callback is never invoked:
    bool callback_invoked = false;
    const auto upload_callback = [&callback_invoked](const std::string &,
                                                     const std::string &,
                                                     Digest *, Digest *) {
        callback_invoked = true;
        return grpc::Status::OK;
    };

    ActionResult result;
    runner.executeAndStore({path_to_false}, upload_callback, &result);
    ASSERT_FALSE(callback_invoked);

    EXPECT_FALSE(result.has_stdout_digest());
    EXPECT_FALSE(result.has_stderr_digest());
    EXPECT_NE(result.exit_code(), 0);

    assert_metadata_execution_timestamps_set(result);
}

TEST(RunnerTest, ExecuteAndStoreTimeoutReached)
{
    TestRunner runner;
    ActionResult result;

    const auto path_to_sleep = SystemUtils::getPathToCommand("sleep");
    ASSERT_FALSE(path_to_sleep.empty());

    runner.setTimeout(std::chrono::seconds(1));
    runner.executeAndStore({path_to_sleep, "5"},
                           TestRunner::dummyUploadFunction, &result);

    google::rpc::Status status =
        buildboxcommon::ProtoUtils::readProtobufFromFile<google::rpc::Status>(
            runner.errorStatusCodeFilePath(runner.d_actionResultPath));
    EXPECT_EQ(status.code(), grpc::StatusCode::DEADLINE_EXCEEDED);
}

TEST(RunnerTest, ExecuteAndStoreTimeoutNotReached)
{
    TestRunner runner;
    ActionResult result;

    const auto path_to_sleep = SystemUtils::getPathToCommand("sleep");
    ASSERT_FALSE(path_to_sleep.empty());

    runner.setTimeout(std::chrono::seconds(5));
    runner.executeAndStore({path_to_sleep, "1"},
                           TestRunner::dummyUploadFunction, &result);

    EXPECT_EQ(result.exit_code(), 0);
}

TEST(RunnerTest, ExecuteAndStoreUnavailableOnSignal)
{
    TestRunner runner;
    ActionResult result;

    const auto path_to_sleep = SystemUtils::getPathToCommand("sleep");
    ASSERT_FALSE(path_to_sleep.empty());
    runner.handleSignal(SIGTERM);

    runner.executeAndStore({path_to_sleep, "1"},
                           TestRunner::dummyUploadFunction, &result);

    google::rpc::Status status =
        buildboxcommon::ProtoUtils::readProtobufFromFile<google::rpc::Status>(
            runner.errorStatusCodeFilePath(runner.d_actionResultPath));
    EXPECT_EQ(status.code(), grpc::StatusCode::UNAVAILABLE);
}

TEST(RunnerTest, CreateOutputDirectoriesTest)
{
    TestRunner runner;
    const std::string cwd = SystemUtils::getCurrentWorkingDirectory();

    const std::vector<std::string> output_paths{
        "build_t/intermediate", "tmp_t/build",         "empty",    "",
        "intermediate_t/tmp.o", "artifacts_t/build.o", "empty.txt"};

    const std::vector<std::string> expected_directories{
        "build_t", "tmp_t", "intermediate_t", "artifacts_t"};

    for (const auto &dir : expected_directories) {
        std::string full_path = cwd + "/" + dir;
        // directories should not exist
        EXPECT_FALSE(FileUtils::isDirectory(full_path.c_str()));
    }

    // Use v2.1's `output_path` field instead of `output_{files, directories}`:
    Command command;
    for (const auto &path : output_paths) {
        *command.add_output_paths() = path;
    }

    runner.testCreateOutputDirectories(command, cwd);

    for (const auto &dir : expected_directories) {
        std::string full_path = cwd + "/" + dir;
        // directories should now exist
        EXPECT_TRUE(FileUtils::isDirectory(full_path.c_str()));
        // clean up directory now
        FileUtils::deleteDirectory(full_path.c_str());
    }
}

TEST(RunnerTest, ChmodDirectory)
{
    TemporaryDirectory dir;
    const std::string subdirectory_path = std::string(dir.name()) + "/subdir";

    mode_t perm = 0555;
    // create subdirectory with restrictive permissions.
    FileUtils::createDirectory(subdirectory_path.c_str(), perm);

    // check permissions of subdirectory
    struct stat sb = {};
    stat(subdirectory_path.c_str(), &sb);
    ASSERT_EQ((unsigned long)sb.st_mode & 0777, perm);

    // change permissions of directory
    perm = 0777;
    Runner::recursively_chmod_directories(dir.name(), perm);

    // check permissions of top level, and sub directories.
    stat(dir.name(), &sb);
    ASSERT_EQ((unsigned long)sb.st_mode & 0777, perm);

    stat(subdirectory_path.c_str(), &sb);
    ASSERT_EQ((unsigned long)sb.st_mode & 0777, perm);
}

TEST(RunnerTest, CustomStandardOutputDestinations)
{
    TestRunner runner;

    // Redirecting standard outputs to files:
    TemporaryFile stdout_file;
    runner.setStdOutFile(stdout_file.strname());
    TemporaryFile stderr_file;
    runner.setStdErrFile(stderr_file.strname());

    ActionResult result;

    const auto path_to_echo = SystemUtils::getPathToCommand("echo");
    ASSERT_FALSE(path_to_echo.empty());

    runner.executeAndStore({path_to_echo, "hello", "world"},
                           TestRunner::dummyUploadFunction, &result);

    const auto expected_stdout = "hello world\n";
    EXPECT_EQ(result.stdout_digest(), DigestGenerator::hash(expected_stdout));
    EXPECT_TRUE(result.stdout_raw().empty()); // `Runner` does not inline.
    ASSERT_EQ(FileUtils::getFileContents(stdout_file.name()), expected_stdout);

    EXPECT_EQ(result.stderr_digest(), DigestGenerator::hash(""));
    ASSERT_EQ(FileUtils::getFileContents(stderr_file.name()), "");

    EXPECT_EQ(result.exit_code(), 0);
}

TEST(RunnerTest, ErrorStatusFilePath)
{
    const std::string path = "/path/to/actionresult123";
    ASSERT_EQ(Runner::errorStatusCodeFilePath(path),
              "/path/to/actionresult123.error-status");

    ASSERT_EQ(Runner::errorStatusCodeFilePath(""), "");
}

TEST(RunnerTest, ReadStatusFile)
{
    TestRunner runner;
    google::rpc::Status status;
    status.set_code(grpc::INTERNAL);
    status.set_message("Error in test");
    buildboxcommon::TemporaryFile statusFile;
    status.SerializeToFileDescriptor(statusFile.fd());

    google::rpc::Status readStatus;
    Runner::readStatusFile(statusFile.strname(), &readStatus);

    EXPECT_TRUE(google::protobuf::util::MessageDifferencer::Equals(
        status, readStatus));
}
