/*
 * Copyright 2019 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_systemutils.h>
#include <buildboxcommon_temporarydirectory.h>
#include <buildboxcommon_temporaryfile.h>
#include <filesystem>
#include <gtest/gtest.h>

#include <fstream>

#include <signal.h>
#include <string>
#include <sys/types.h>
#include <unistd.h>

using namespace buildboxcommon;

TEST(SystemUtilsTests, CommandNotFound)
{
    const std::vector<std::string> command = {"command-does-not-exist"};
    const int expectedError = system_utils_exitcode::COMMAND_NOT_FOUND;

    ASSERT_EQ(buildboxcommon::SystemUtils::executeCommand(command),
              expectedError);

    ASSERT_EQ(buildboxcommon::SystemUtils::executeCommandAndWait(command),
              expectedError);
}

TEST(SystemUtilsTests, CommandIsNotAnExecutable)
{
    TemporaryFile non_executable_file;

    const auto expectedError = system_utils_exitcode::COMMAND_CANNOT_EXECUTE;

    ASSERT_EQ(buildboxcommon::SystemUtils::executeCommand(
                  {non_executable_file.name()}),
              expectedError);

    ASSERT_EQ(buildboxcommon::SystemUtils::executeCommandAndWait(
                  {non_executable_file.name()}),
              expectedError);
}

TEST(SystemUtilsTests, WaitPidExitCode)
{
    // Creating a subprocess:
    const int pid = fork();
    ASSERT_NE(pid, -1);

    // The subprocess exits:
    if (pid == 0) {
        exit(42);
    }
    // And the parent gets its exit code:
    else {
        const int exit_status = SystemUtils::waitPid(pid);
        ASSERT_EQ(exit_status, 42);
    }
}

TEST(SystemUtilsTests, WaitPidSignalNumber)
{
    // Creating a subprocess:
    const int pid = fork();
    ASSERT_NE(pid, -1);

    const auto signal_number = SIGKILL;
    // The subprocess gets signaled:
    if (pid == 0) {
        auto _ = raise(signal_number);
        (void)_;
    }
    // And the parent gets an exit code that encodes the signal number as done
    // by Bash:
    else {
        const int exit_status = SystemUtils::waitPid(pid);
        ASSERT_EQ(exit_status, signal_number + 128);
    }
}

TEST(SystemUtilsTests, WaitPidThrowsOnError)
{
    const int invalid_pid = -1;
    ASSERT_THROW(SystemUtils::waitPid(invalid_pid), std::system_error);
}

TEST(SystemUtilsTests, WaitPidWithTimeoutExitBeforeTimeout)
{
    // Test case where process exits before timeout
    const int pid = fork();
    ASSERT_NE(pid, -1);

    if (pid == 0) { // Child process
        // Exit immediately with code 42
        exit(42);
    }
    else { // Parent process
        bool timedOut = false;
        const int exit_status = SystemUtils::waitPidOrSignalWithTimeout(
            pid, std::optional<std::chrono::seconds>{std::chrono::seconds(5)},
            &timedOut);

        // Process should exit with code 42 before timeout
        ASSERT_EQ(exit_status, 42);
        ASSERT_FALSE(timedOut);
    }
}

TEST(SystemUtilsTests, WaitPidWithTimeoutSignalBeforeTimeout)
{
    // Test case where process is terminated by a signal before timeout
    const int pid = fork();
    ASSERT_NE(pid, -1);

    if (pid == 0) { // Child process
        // Terminate self with signal
        auto _ = raise(SIGTERM);
        (void)_;
    }
    else { // Parent process
        bool timedOut = false;
        const int exit_status = SystemUtils::waitPidOrSignalWithTimeout(
            pid, std::optional<std::chrono::seconds>{std::chrono::seconds(5)},
            &timedOut);

        // Process should be terminated by SIGTERM signal
        constexpr int SIGNAL_OFFSET = 128;
        ASSERT_EQ(exit_status, SIGNAL_OFFSET + SIGTERM);
        ASSERT_FALSE(timedOut);
    }
}

TEST(SystemUtilsTests, WaitPidWithTimeoutTimesOut)
{
    // Test case where timeout occurs before process exits
    const int pid = fork();
    ASSERT_NE(pid, -1);

    if (pid == 0) { // Child process
        // Sleep longer than the timeout
        sleep(10);
        exit(0);
    }
    else { // Parent process
        bool timedOut = false;
        const int exit_status = SystemUtils::waitPidOrSignalWithTimeout(
            pid, std::optional<std::chrono::seconds>{std::chrono::seconds(1)},
            &timedOut);

        // Timeout should occur
        ASSERT_EQ(exit_status, -ETIMEDOUT);
        ASSERT_TRUE(timedOut);

        // Clean up the child process so it doesn't become a zombie
        kill(pid, SIGKILL);
        waitpid(pid, nullptr, 0);
    }
}

TEST(SystemUtilsTests, WaitPidWithNoTimeout)
{
    // Test case with NO_TIMEOUT (indefinite wait)
    const int pid = fork();
    ASSERT_NE(pid, -1);

    if (pid == 0) { // Child process
        // Exit after a short delay
        sleep(1);
        exit(84);
    }
    else { // Parent process
        bool timedOut = false;
        const int exit_status = SystemUtils::waitPidOrSignalWithTimeout(
            pid, std::nullopt, &timedOut);

        // Process should exit with code 84, no timeout
        ASSERT_EQ(exit_status, 84);
        ASSERT_FALSE(timedOut);
    }
}

class CommandLookupFixture : public ::testing::Test {
  private:
    std::string d_path = "";

  protected:
    CommandLookupFixture()
    {
        // Save the contents of $PATH:
        char *path_pointer = getenv("PATH");
        if (path_pointer == nullptr) {
            throw std::runtime_error("Could not read $PATH");
        }
        d_path = std::string(path_pointer);
    }

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

  public:
    ~CommandLookupFixture() override
    {
        // Restore $PATH:
        setenv("PATH", d_path.c_str(), true);
    }
};

TEST_F(CommandLookupFixture, NonExistentCommand)
{
    ASSERT_EQ(SystemUtils::getPathToCommand("command-does-not-exist"), "");
}

TEST_F(CommandLookupFixture, Command)
{
    ASSERT_NE(SystemUtils::getPathToCommand("echo"), "");
}

TEST_F(CommandLookupFixture, CustomCommand)
{
    TemporaryDirectory dir;
    const auto command_name = "test-executable";
    const auto path_to_command = std::string(dir.name()) + "/" + command_name;

    FileUtils::writeFileAtomically(path_to_command, "");
    FileUtils::makeExecutable(path_to_command.c_str());

    ASSERT_TRUE(FileUtils::isRegularFile(path_to_command.c_str()));
    ASSERT_TRUE(FileUtils::isExecutable(path_to_command.c_str()));

    ASSERT_EQ(setenv("PATH", dir.name(), true), 0);

    ASSERT_EQ(SystemUtils::getPathToCommand(command_name), path_to_command);
}

TEST_F(CommandLookupFixture, NonExecutableIgnored)
{
    TemporaryDirectory dir;
    const auto command_name = "non-executable";
    const auto path_to_command = std::string(dir.name()) + "/" + command_name;

    FileUtils::writeFileAtomically(path_to_command, "");

    ASSERT_TRUE(FileUtils::isRegularFile(path_to_command.c_str()));
    ASSERT_FALSE(FileUtils::isExecutable(path_to_command.c_str()));

    ASSERT_EQ(setenv("PATH", dir.name(), true), 0);

    ASSERT_EQ(SystemUtils::getPathToCommand(command_name), "");
}

TEST(SystemUtilsTests, ExecuteCommandAndWaitSuccess)
{
    const auto command_name = "true";
    const auto command_path = SystemUtils::getPathToCommand(command_name);
    // The command exists:
    ASSERT_FALSE(SystemUtils::getPathToCommand(command_name).empty());

    ASSERT_EQ(SystemUtils::executeCommandAndWait({command_path}), 0);
}

TEST(SystemUtilsTests, ExecuteCommandAndWaitError)
{
    const auto command_name = "false";
    const auto command_path = SystemUtils::getPathToCommand(command_name);
    // The command exists:
    ASSERT_FALSE(SystemUtils::getPathToCommand(command_name).empty());

    ASSERT_NE(SystemUtils::executeCommandAndWait({command_path}), 0);
}

TEST(SystemUtilsTests, ExecuteIgnoresPathEnvVar)
{
    const auto command_name = "echo";
    // The command exists:
    ASSERT_FALSE(SystemUtils::getPathToCommand(command_name).empty());

    // But `executeCommand(andWait)()`  will not find it:
    const int expectedError = system_utils_exitcode::COMMAND_NOT_FOUND;
    ASSERT_EQ(SystemUtils::executeCommand({command_name}), expectedError);
    ASSERT_EQ(SystemUtils::executeCommandAndWait({command_name}),
              expectedError);
}

TEST(SystemUtilsTests, RedirectStandardOutputs)
{
    TemporaryFile stdout_file, stderr_file;

    const auto pid = fork();
    ASSERT_NE(pid, -1);

    if (pid == 0) { // Child process
        ASSERT_NO_THROW(SystemUtils::redirectStandardOutputToFile(
            STDOUT_FILENO, stdout_file.strname()));

        ASSERT_NO_THROW(SystemUtils::redirectStandardOutputToFile(
            STDERR_FILENO, stderr_file.strname()));

        std::cout << "hello, stdout!";
        std::cerr << "hello, stderr!";
        exit(0);
    }
    else { // Parent
        SystemUtils::waitPid(pid);

        ASSERT_EQ(FileUtils::getFileContents(stdout_file.name()),
                  "hello, stdout!");
        ASSERT_EQ(FileUtils::getFileContents(stderr_file.name()),
                  "hello, stderr!");
    }
}

TEST(SystemUtilsTests, True)
{
    std::vector<std::string> command = {"true"};
    auto result = SystemUtils::executeCommandWithResult(command);
    EXPECT_EQ(result.d_exitCode, 0);
}

TEST(SystemUtilsTests, False)
{
    std::vector<std::string> command = {"false"};
    auto result = SystemUtils::executeCommandWithResult(command);
    EXPECT_NE(result.d_exitCode, 0);
}

TEST(SystemUtilsTests, CommandNotFoundWithResult)
{
    std::vector<std::string> command = {"this-command-does-not-exist-1234"};
    auto result = SystemUtils::executeCommandWithResult(command);
    EXPECT_EQ(result.d_exitCode, system_utils_exitcode::COMMAND_NOT_FOUND);
}

TEST(SystemUtilsTests, CommandIsNotAnExecutableWithResult)
{
    // Creating an empty file, which will fail when trying to execute it:
    buildboxcommon::TemporaryDirectory temp_dir;
    const std::string file_path = std::string(temp_dir.name()) + "/file.txt";
    std::ofstream file(file_path);
    file.close();

    std::vector<std::string> command = {file_path};
    auto result = SystemUtils::executeCommandWithResult(command);
    EXPECT_EQ(result.d_exitCode,
              system_utils_exitcode::COMMAND_CANNOT_EXECUTE);
}

TEST(SystemUtilsTests, OutputPipes)
{
    std::vector<std::string> command = {"echo", "hello", "world"};
    auto result = SystemUtils::executeCommandWithResult(command, true, true);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, "hello world\n");
    EXPECT_EQ(result.d_stdErr, "");
}

TEST(PlatformInfoTests, GetOSVersion)
{
    const auto &info = PlatformInfo::get();
    // assume an OS version is at least >= 0.1
    EXPECT_GE(info.majorVersion(), 0);
    EXPECT_GE(info.minorVersion(), 0);
    EXPECT_GT(info.majorVersion() + info.minorVersion(), 0);
}

TEST(SystemUtilsTests, ExecInAbsoluteDirectory)
{
    std::string workDir = std::filesystem::current_path();
    workDir += "/testSubDir";
    std::filesystem::create_directory(workDir);
    std::string expectedStdOut = workDir;
    expectedStdOut += "\n";

    std::vector<std::string> command = {"pwd"};
    auto result =
        SystemUtils::executeCommandWithResult(command, true, true, workDir);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, expectedStdOut);
    EXPECT_EQ(result.d_stdErr, "");
}

TEST(SystemUtilsTests, ExecInRelativeDirectory)
{
    const std::string workDir = "testSubDir";
    std::filesystem::create_directory(workDir);
    std::string expectedStdOut = std::filesystem::current_path();
    expectedStdOut += "/";
    expectedStdOut += workDir;
    expectedStdOut += "\n";

    std::vector<std::string> command = {"pwd"};
    auto result =
        SystemUtils::executeCommandWithResult(command, true, true, workDir);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, expectedStdOut);
    EXPECT_EQ(result.d_stdErr, "");
}

TEST(SystemUtilsTests, ExecInNonexistentDirectory)
{
    const std::string workDir =
        "/this/path/should/not/exist/nonexistentdirectory";

    std::vector<std::string> command = {"pwd"};
    auto result =
        SystemUtils::executeCommandWithResult(command, true, true, workDir);
    EXPECT_EQ(result.d_exitCode, system_utils_exitcode::COMMAND_NOT_FOUND);
    EXPECT_EQ(result.d_stdOut, "");
    EXPECT_EQ(result.d_stdErr, "");
}

TEST(SystemUtilsTests, ExecuteCommandWithInputBasic)
{
    // Test basic input/output with 'cat' command
    std::vector<std::string> command = {"cat"};
    const std::string input = "Hello, World!\n";
    auto result = SystemUtils::executeCommandWithResult(command, true, false,
                                                        std::nullopt, input);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, input);
}

TEST(SystemUtilsTests, ExecuteCommandWithInputMultiLine)
{
    // Test with multi-line input
    std::vector<std::string> command = {"cat"};
    const std::string input = "Line 1\nLine 2\nLine 3\n";
    auto result = SystemUtils::executeCommandWithResult(command, true, false,
                                                        std::nullopt, input);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, input);
}

TEST(SystemUtilsTests, ExecuteCommandWithInputEmpty)
{
    // Test with empty input
    std::vector<std::string> command = {"cat"};
    const std::string input = "";
    auto result = SystemUtils::executeCommandWithResult(command, true, false,
                                                        std::nullopt, input);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, "");
}

TEST(SystemUtilsTests, ExecuteCommandWithInputAndError)
{
    // Test command that writes to both stdout and stderr
    // Using 'sh -c' to run a shell command
    std::vector<std::string> command = {"sh", "-c", "cat && echo error >&2"};
    const std::string input = "test input\n";
    auto result = SystemUtils::executeCommandWithResult(command, true, true,
                                                        std::nullopt, input);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, "test input\n");
    EXPECT_EQ(result.d_stdErr, "error\n");
}

TEST(SystemUtilsTests, ExecuteCommandWithInputNonZeroExit)
{
    // Test command that exits with non-zero status
    std::vector<std::string> command = {"sh", "-c", "cat && exit 42"};
    const std::string input = "test\n";
    auto result = SystemUtils::executeCommandWithResult(command, true, false,
                                                        std::nullopt, input);
    EXPECT_EQ(result.d_exitCode, 42);
    EXPECT_EQ(result.d_stdOut, "test\n");
}

TEST(SystemUtilsTests, ExecuteCommandWithResultTimeout)
{
    // Test that a long-running process is killed when timeout is reached
    std::vector<std::string> command = {"sleep", "10"};
    auto result = SystemUtils::executeCommandWithResult(
        command, false, false, std::nullopt, std::nullopt,
        std::chrono::milliseconds(100));

    EXPECT_TRUE(result.d_timedOut);
    // Process should be killed by signal (SIGTERM=143 or SIGKILL=137)
    EXPECT_TRUE(result.d_exitCode == 143 || result.d_exitCode == 137);
}

TEST(SystemUtilsTests, ExecuteCommandWithResultNoTimeout)
{
    // Test that a process that completes before timeout works correctly
    std::vector<std::string> command = {"echo", "test"};
    auto result = SystemUtils::executeCommandWithResult(
        command, true, false, std::nullopt, std::nullopt,
        std::chrono::seconds(5));

    EXPECT_FALSE(result.d_timedOut);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, "test\n");
}

TEST(SystemUtilsTests, ExecuteCommandWithStdinAndTimeout)
{
    // Test combination of stdin input and timeout
    std::vector<std::string> command = {"cat"};
    const std::string input = "hello\n";
    auto result = SystemUtils::executeCommandWithResult(
        command, true, false, std::nullopt, input, std::chrono::seconds(1));

    EXPECT_FALSE(result.d_timedOut);
    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_EQ(result.d_stdOut, "hello\n");
}

TEST(SystemUtilsTests, ExecuteCommandWithStdinAndWorkingDir)
{
    // Test combination of stdin and working directory (previously unsupported)
    std::vector<std::string> command = {"sh", "-c", "cat && pwd"};
    const std::string input = "test\n";

    // Create a temporary directory for testing
    const std::string testDir = "/tmp";

    auto result = SystemUtils::executeCommandWithResult(command, true, false,
                                                        testDir, input);

    EXPECT_EQ(result.d_exitCode, 0);
    EXPECT_TRUE(result.d_stdOut.find("test\n") != std::string::npos);
    EXPECT_TRUE(result.d_stdOut.find("/tmp") != std::string::npos);
}

TEST(SystemUtilsTests, ExecuteCommandTimeout)
{
    // Test that a process exceeding it's timeout is
    // marked as timed out and has the appropriate exit code
    // indicating a SIGKILL
    std::vector<std::string> command = {"sleep", "5"};

    auto result = SystemUtils::executeCommandWithResult(
        command, false, false, std::nullopt, std::nullopt,
        std::chrono::milliseconds(200));

    EXPECT_TRUE(result.d_timedOut);
    EXPECT_TRUE(result.d_exitCode == 137);
}
