/*
 * Copyright 2022 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 <algorithm>
#include <filesystem>
#include <gtest/gtest.h>
#include <set>

#include <trexe_cmdlinespec.h>
#include <trexe_executionoptions.h>

#include <buildboxcommon_commandline.h>
#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_merklize.h>
#include <buildboxcommon_protos.h>
#include <buildboxcommon_version.h>

using namespace trexe;
using namespace buildboxcommon;

TEST(TrexeExecutionOptionsTest, TestDefaultConstructor)
{
    ExecutionOptions execOptions;
    EXPECT_EQ(execOptions.d_metricPublishInterval, 15);
}

TEST(TrexeExecutionOptionsTest, TestParseFromCmdLine)
{
    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));

    const char *argv[] = {
        "./path/to/trexe",
        "--remote=https://127.0.0.1:1234",
        "--d=/test/download/dir",
        "--working-dir=/path/to/working-dir",
        "--input-path=/path/to/input-path-1",
        "--input-path=/path/to/input-path-2",
        "--input-path=/path/to/input-path-3:/remote/path-3",
        "--input-path=/path/to/input-path-3:/remote/path-3:rw",
        "--input-path=/path/to/input-path-4:/remote/path-4:unix_mode,mtime",
        "--output-path=/output/path-1",
        "--output-path=/output/path-2",
        "--platform-properties=P1=V1,P2=V2",
        "--environment=E1=V3,E2=V4",
        "--env=K1=V1",
        "--env=K2=V2,V2'",
        "--exec-timeout=1",
        "--skip-cache-lookup",
        "--do-not-cache",
        "--do-not-follow-symlinks",
        "--digest-function=MD5",
        "--priority=1",
        "--correlated-invocations-id=corr",
        "--tool-invocation-id=tid",
        "--tool-name=tname",
        "--tool-version=tver",
        "--locality-hint=locality",
        "--no-wait",
        "--result-metadata-file=/tmp/result.json",
        "--use-localcas",
        "--localcas-skip-upload",
        "--num-digest-threads=1",
        "--num-io-threads=2",
        "/usr/bin/echo"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);

    auto parsedOptions =
        ExecutionOptions::fromCommandLine({trexeSpec.d_command}, commandLine);
    ASSERT_EQ(false, parsedOptions.isBlocking());
    ASSERT_EQ(true, parsedOptions.d_skipCacheLookup);
    ASSERT_EQ("/test/download/dir", parsedOptions.d_downloadResultsPath);
    ASSERT_EQ(1, parsedOptions.d_argv.size());
    ASSERT_EQ("/usr/bin/echo", parsedOptions.d_argv[0]);
    ASSERT_EQ("/path/to/working-dir", parsedOptions.d_workingDir);

    ASSERT_EQ(DigestFunction_Value_MD5, parsedOptions.d_digestFunction);

    ASSERT_TRUE(parsedOptions.d_useLocalCas);
    ASSERT_TRUE(parsedOptions.d_localCasSkipUpload);

    EXPECT_EQ(1, parsedOptions.d_numDigestThreads);
    EXPECT_EQ(2, parsedOptions.d_numIOThreads);

    auto inputPaths = parsedOptions.d_inputPaths;
    ASSERT_EQ(5, inputPaths.size());
    ASSERT_NE(inputPaths.end(),
              find(inputPaths.begin(), inputPaths.end(),
                   InputPathOption{"/path/to/input-path-1"}));
    ASSERT_NE(inputPaths.end(),
              find(inputPaths.begin(), inputPaths.end(),
                   InputPathOption{"/path/to/input-path-2"}));
    ASSERT_NE(inputPaths.end(), find(inputPaths.begin(), inputPaths.end(),
                                     InputPathOption{"/path/to/input-path-3",
                                                     "/remote/path-3"}));
    ASSERT_NE(
        inputPaths.end(),
        find(inputPaths.begin(), inputPaths.end(),
             InputPathOption{"/path/to/input-path-3", "/remote/path-3",
                             InputPathOption::FilePermission::READ_WRITE}));
    ASSERT_NE(
        inputPaths.end(),
        find(inputPaths.begin(), inputPaths.end(),
             InputPathOption{"/path/to/input-path-4", "/remote/path-4",
                             InputPathOption::FilePermission::CAPTURE, true}));

    auto outputPaths = parsedOptions.d_outputPaths;
    ASSERT_EQ(2, outputPaths.size());
    ASSERT_TRUE(outputPaths.contains("/output/path-1"));
    ASSERT_TRUE(outputPaths.contains("/output/path-2"));

    std::set<std::pair<std::string, std::string>> v1 = {
        std::make_pair("P1", "V1"), std::make_pair("P2", "V2")};
    std::map<std::string, std::string> v2 = {
        {"E1", "V3"}, {"E2", "V4"}, {"K1", "V1"}, {"K2", "V2,V2'"}};
    ASSERT_EQ(v1, parsedOptions.d_platform);
    ASSERT_EQ(v2, parsedOptions.d_environment);

    ASSERT_EQ(1, parsedOptions.d_execTimeout);
    ASSERT_EQ(true, parsedOptions.d_doNotCache);

    ASSERT_EQ("corr",
              parsedOptions.d_metadata.at("correlated-invocations-id"));
    ASSERT_EQ("tid", parsedOptions.d_metadata.at("tool-invocation-id"));
    ASSERT_EQ(std::string("trexe") + std::string(":") + std::string("tname"),
              parsedOptions.d_metadata.at("tool-name"));
    ASSERT_EQ("locality", parsedOptions.d_schedulingMetadata.locality_hint());

    ASSERT_EQ(buildboxcommon::VERSION + std::string(":") + std::string("tver"),
              parsedOptions.d_metadata.at("tool-version"));

    ASSERT_EQ(1, *(parsedOptions.d_priority));
    ASSERT_EQ("/tmp/result.json", parsedOptions.d_resultMetadataFile);
    ASSERT_FALSE(parsedOptions.d_followSymlinks);

    EXPECT_EQ(parsedOptions.d_metricMode, "");
    EXPECT_EQ(parsedOptions.d_metricPublishInterval, 15);
    EXPECT_EQ(parsedOptions.d_metricPrefix, "trexe");
}

TEST(TrexeExecutionOptionsTest, TestParseFromCmdLineGetCompleted)
{
    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));

    const char *argv[] = {"./path/to/trexe",
                          "--remote=https://127.0.0.1:1234",
                          "--d=/test/download/dir",
                          "--correlated-invocations-id=corr",
                          "--tool-invocation-id=tid",
                          "--tool-name=tname",
                          "--tool-version=tver",
                          "--operation=opID",
                          "--result-metadata-file=/tmp/result.json"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);

    auto parsedOptions =
        ExecutionOptions::fromCommandLine({trexeSpec.d_command}, commandLine);

    ASSERT_EQ("corr",
              parsedOptions.d_metadata.at("correlated-invocations-id"));
    ASSERT_EQ("tid", parsedOptions.d_metadata.at("tool-invocation-id"));
    ASSERT_EQ(std::string("trexe") + std::string(":") + std::string("tname"),
              parsedOptions.d_metadata.at("tool-name"));

    ASSERT_EQ(buildboxcommon::VERSION + std::string(":") + std::string("tver"),
              parsedOptions.d_metadata.at("tool-version"));

    ASSERT_EQ("opID", parsedOptions.d_operation);
    ASSERT_FALSE(parsedOptions.isBlocking());
    ASSERT_EQ("/tmp/result.json", parsedOptions.d_resultMetadataFile);

    EXPECT_GE(parsedOptions.d_numDigestThreads, 0);
    EXPECT_GE(parsedOptions.d_numIOThreads, 0);
}

TEST(TrexeExecutionOptionsTest, TestParseFromCmdDownloadWait)
{
    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));

    const char *argv[] = {"./path/to/trexe", "--remote=https://127.0.0.1:1234",
                          "--d=/test/download/dir", "--operation=opID",
                          "--wait"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);

    auto parsedOptions =
        ExecutionOptions::fromCommandLine({trexeSpec.d_command}, commandLine);

    ASSERT_EQ("opID", parsedOptions.d_operation);
    ASSERT_TRUE(parsedOptions.isBlocking());

    EXPECT_GE(parsedOptions.d_numDigestThreads, 0);
    EXPECT_GE(parsedOptions.d_numIOThreads, 0);
}

TEST(TrexeExecutionOptionsTest, TestParseFromCmdLineNoOptionals)
{
    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));

    const char *argv[] = {"./path/to/trexe",
                          "--remote=https://127.0.0.1:1234",
                          "--d=/test/download/dir",
                          "--working-dir=/path/to/working-dir",
                          "--input-path=/path/to/input-path-1",
                          "--input-path=/path/to/input-path-2",
                          "--output-path=/output/path-1",
                          "--output-path=/output/path-2",
                          "--result-metadata-file=/tmp/result.json",
                          "/usr/bin/echo"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);

    auto parsedOptions =
        ExecutionOptions::fromCommandLine(trexeSpec.d_command, commandLine);
    ASSERT_EQ(true, parsedOptions.isBlocking());
    ASSERT_EQ(false, parsedOptions.d_skipCacheLookup);
    ASSERT_EQ("/test/download/dir", parsedOptions.d_downloadResultsPath);
    ASSERT_EQ(1, parsedOptions.d_argv.size());
    ASSERT_EQ("/usr/bin/echo", parsedOptions.d_argv[0]);
    ASSERT_EQ("/path/to/working-dir", parsedOptions.d_workingDir);

    ASSERT_FALSE(parsedOptions.d_useLocalCas);
    ASSERT_FALSE(parsedOptions.d_localCasSkipUpload);

    auto inputPaths = parsedOptions.d_inputPaths;
    ASSERT_EQ(2, inputPaths.size());
    ASSERT_NE(inputPaths.end(),
              find(inputPaths.begin(), inputPaths.end(),
                   InputPathOption{"/path/to/input-path-1"}));
    ASSERT_NE(inputPaths.end(),
              find(inputPaths.begin(), inputPaths.end(),
                   InputPathOption{"/path/to/input-path-2"}));

    auto outputPaths = parsedOptions.d_outputPaths;
    ASSERT_EQ(2, outputPaths.size());
    ASSERT_TRUE(outputPaths.contains("/output/path-1"));
    ASSERT_TRUE(outputPaths.contains("/output/path-2"));

    std::set<std::pair<std::string, std::string>> v1 = {};
    std::map<std::string, std::string> v2 = {};
    ASSERT_EQ(v1, parsedOptions.d_platform);
    ASSERT_EQ(v2, parsedOptions.d_environment);

    ASSERT_EQ(0, parsedOptions.d_execTimeout);
    ASSERT_EQ(false, parsedOptions.d_doNotCache);

    ASSERT_EQ(0, *parsedOptions.d_priority);

    ASSERT_EQ("", parsedOptions.d_metadata.at("correlated-invocations-id"));
    ASSERT_EQ("", parsedOptions.d_metadata.at("tool-invocation-id"));
    ASSERT_EQ("trexe", parsedOptions.d_metadata.at("tool-name"));

    ASSERT_EQ(buildboxcommon::VERSION,
              parsedOptions.d_metadata.at("tool-version"));
    ASSERT_EQ("/tmp/result.json", parsedOptions.d_resultMetadataFile);
    ASSERT_TRUE(parsedOptions.d_followSymlinks);
}

TEST(TrexeExecutionOptionsTest, TestParseSeparateOptionals)
{
    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));

    const char *argv[] = {"./path/to/trexe",
                          "--remote=https://127.0.0.1:1111",
                          "--client-cert",
                          "XYZ",
                          "--client-key",
                          "XYZ",
                          "--token-reload-interval=10",
                          "--cas-token-reload-interval=5",
                          "--ac-remote",
                          "https://127.0.0.1:4444",
                          "--ac-request-timeout=5",
                          "--exec-remote",
                          "https://127.0.0.1:8888",
                          "--exec-retry-limit=8",
                          "/usr/bin/echo"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);

    ConnectionOptions connectionOptions, connectionOptionsEX,
        connectionOptionsCAS, connectionOptionsAC;

    ConnectionOptionsCommandLine::configureChannel(commandLine, "",
                                                   &connectionOptions);

    connectionOptionsCAS = connectionOptions;
    connectionOptionsAC = connectionOptions;
    connectionOptionsEX = connectionOptions;

    ConnectionOptionsCommandLine::updateChannelOptions(commandLine, "cas-",
                                                       &connectionOptionsCAS);
    ConnectionOptionsCommandLine::updateChannelOptions(commandLine, "ac-",
                                                       &connectionOptionsAC);
    ConnectionOptionsCommandLine::updateChannelOptions(commandLine, "exec-",
                                                       &connectionOptionsEX);

    ASSERT_EQ("https://127.0.0.1:1111", connectionOptionsCAS.d_url);
    ASSERT_EQ("https://127.0.0.1:4444", connectionOptionsAC.d_url);
    ASSERT_EQ("https://127.0.0.1:8888", connectionOptionsEX.d_url);
    ASSERT_EQ("XYZ", connectionOptions.d_clientCertPath);
    ASSERT_EQ("XYZ", connectionOptionsEX.d_clientCertPath);
    ASSERT_EQ("XYZ", connectionOptionsCAS.d_clientCertPath);
    ASSERT_EQ("4", connectionOptions.d_retryLimit);
    ASSERT_EQ("8", connectionOptionsEX.d_retryLimit);
    ASSERT_EQ("5", connectionOptionsAC.d_requestTimeout);
    ASSERT_EQ("10", connectionOptionsAC.d_tokenReloadInterval);
    ASSERT_EQ("5", connectionOptionsCAS.d_tokenReloadInterval);
}

TEST(TrexeExecutionOptionsTest, TestCacheOnly)
{
    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));

    const char *argv[] = {"./path/to/trexe",
                          "--cache-only",
                          "--cas-remote=https://127.0.0.1:1234",
                          "--d=/test/download/dir",
                          "--working-dir=/path/to/working-dir",
                          "--input-path=/path/to/input-path-1",
                          "--input-path=/path/to/input-path-2",
                          "--output-path=/output/path-1",
                          "--output-path=/output/path-2",
                          "--result-metadata-file=/tmp/result.json",
                          "/usr/bin/echo"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);

    auto parsedOptions =
        ExecutionOptions::fromCommandLine(trexeSpec.d_command, commandLine);
    ASSERT_EQ(true, parsedOptions.isBlocking());
    ASSERT_EQ(true, parsedOptions.d_cacheOnly);
    ASSERT_EQ("/test/download/dir", parsedOptions.d_downloadResultsPath);
    ASSERT_EQ(1, parsedOptions.d_argv.size());
    ASSERT_EQ("/usr/bin/echo", parsedOptions.d_argv[0]);
    ASSERT_EQ("/path/to/working-dir", parsedOptions.d_workingDir);

    auto inputPaths = parsedOptions.d_inputPaths;
    ASSERT_EQ(2, inputPaths.size());
    ASSERT_NE(inputPaths.end(),
              find(inputPaths.begin(), inputPaths.end(),
                   InputPathOption{"/path/to/input-path-1"}));
    ASSERT_NE(inputPaths.end(),
              find(inputPaths.begin(), inputPaths.end(),
                   InputPathOption{"/path/to/input-path-2"}));

    auto outputPaths = parsedOptions.d_outputPaths;
    ASSERT_EQ(2, outputPaths.size());
    ASSERT_TRUE(outputPaths.contains("/output/path-1"));
    ASSERT_TRUE(outputPaths.contains("/output/path-2"));

    std::set<std::pair<std::string, std::string>> v1 = {};
    std::map<std::string, std::string> v2 = {};
    ASSERT_EQ(v1, parsedOptions.d_platform);
    ASSERT_EQ(v2, parsedOptions.d_environment);

    ASSERT_EQ("/tmp/result.json", parsedOptions.d_resultMetadataFile);
}

TEST(TrexeExecutionOptionsTest, TestTOMLEverything)
{
    // Use TOML to specify everything, which results in an invalid config,
    // but the validation will be performed at a later step.
    const std::string config = R"(
cancel = true
no-wait = true
cache-only = true
do-not-cache = true
skip-cache-lookup = true
follow-symlinks = false
stream-logs = true
use-localcas = true
localcas-skip-upload = true

download-path = "out"
command = ["clang++", "hello.cpp"]
working-dir = "src"
input-root-digest = "foo/123"
output-path = ["build", "out"]
output-node-property = ["foo"]
operation = "op"
exec-timeout = 42
result-metadata-file = "/tmp/out.json"
action-result-json = "/tmp/action.json"
stdout-file = "/tmp/stdout"
stderr-file = "/tmp/stderr"
buildbox-run = "buildbox-run-bubblewrap"
runner-args = ["a", "b"]
locality-hint = "locality"

num-digest-threads = 3
num-io-threads = 4

metrics-mode = "file:///path/to/toml/file"
metrics-publish-interval = 40
metrics-prefix = "custom-toml-trexe-prefix"

[connection]
remote = "https://remote:50051"
instance = "dev"
[connection.cas]
remote = "https://remote:50052"
instance = "cas"
[connection.exec]
retry-limit = 10
retry-on-code = ["UNKNOWN", "INTERNAL"]

[platform]
foo = ["bar", "baz"]
os = "linux"
[environment]
FOO = "1"
BAR = "0"

[[input-path]]
local = "deps"
remote = "remote/deps"
[[input-path]]
local = "src"
remote = "remote/src"
[[input-path]]
path = "test"
[[input-path]]
path = "ro-data"
file-permission = "ro"
[[input-path]]
local = "rw-workspace"
remote = "workspace"
file-permission = "rw"
[[input-path]]
local = "precise-dir"
remote = "remote/precise"
file-permission = "capture"
mtime = "capture"
[[input-path]]
local = "rotree"
remote = "remote/rotree"
file-permission = "capture"
mtime = "capture"
node-property = { SubtreeReadOnly = "true", foo = "bar" }
ignore = ["ignore/  ", "tmp"]
)";

    toml::table table = toml::parse(config);
    const ExecutionOptions eo = ExecutionOptions::newFromTOML(table);

    ASSERT_TRUE(eo.d_cancelMode);
    ASSERT_TRUE(eo.d_cacheOnly);
    ASSERT_FALSE(eo.isBlocking());
    ASSERT_TRUE(eo.d_skipCacheLookup);
    ASSERT_TRUE(eo.d_doNotCache);
    ASSERT_FALSE(eo.d_followSymlinks);
    ASSERT_TRUE(eo.d_streamLogs);
    ASSERT_TRUE(eo.d_useLocalCas);
    ASSERT_TRUE(eo.d_localCasSkipUpload);

    ASSERT_EQ(eo.d_downloadResultsPath, "out");
    std::vector<std::string> command{"clang++", "hello.cpp"};
    ASSERT_EQ(eo.d_argv, command);
    ASSERT_EQ(eo.d_workingDir, "src");
    ASSERT_EQ(eo.d_inputRootDigest, "foo/123");
    std::set<std::string> outputs{"build", "out"};
    ASSERT_EQ(eo.d_outputPaths, outputs);
    std::set<std::string> outputNodeProperties{"foo"};
    ASSERT_EQ(eo.d_outputNodeProperties, outputNodeProperties);
    ASSERT_EQ(eo.d_operation, "op");
    ASSERT_EQ(eo.d_execTimeout, 42);
    ASSERT_EQ(eo.d_resultMetadataFile, "/tmp/out.json");
    ASSERT_EQ(eo.d_actionResultJsonFile, "/tmp/action.json");
    ASSERT_EQ(eo.d_stdoutFile, "/tmp/stdout");
    ASSERT_EQ(eo.d_stderrFile, "/tmp/stderr");
    ASSERT_EQ(eo.d_runnerCommand, "buildbox-run-bubblewrap");
    std::vector<std::string> runnerArgs{"a", "b"};
    ASSERT_EQ(eo.d_extraRunArgs, runnerArgs);
    ASSERT_EQ(eo.d_schedulingMetadata.locality_hint(), "locality");
    EXPECT_EQ(eo.d_numDigestThreads, 3);
    EXPECT_EQ(eo.d_numIOThreads, 4);

    std::set<std::pair<std::string, std::string>> platform{
        {"foo", "bar"}, {"foo", "baz"}, {"os", "linux"}};
    EXPECT_EQ(eo.d_platform, platform);
    std::map<std::string, std::string> env{{"BAR", "0"}, {"FOO", "1"}};
    EXPECT_EQ(eo.d_environment, env);
    std::vector<InputPathOption> inputPaths{
        {"deps", "remote/deps"},
        {"src", "remote/src"},
        {"test"},
        {"ro-data", {}, InputPathOption::FilePermission::READ_ONLY},
        {"rw-workspace", "workspace",
         InputPathOption::FilePermission::READ_WRITE},
        {"precise-dir", "remote/precise",
         InputPathOption::FilePermission::CAPTURE, true},
        {"rotree", "remote/rotree", InputPathOption::FilePermission::CAPTURE,
         true,
         std::map<std::string, std::string>(
             {{"SubtreeReadOnly", "true"}, {"foo", "bar"}}),
         buildboxcommon::IgnoreMatcher::parseIgnorePatterns(
             std::vector<std::string>({"ignore/", "tmp"}))}};

    EXPECT_EQ(eo.d_inputPaths, inputPaths);

    EXPECT_EQ(eo.d_casConn.d_url, "https://remote:50052");
    EXPECT_EQ(eo.d_casConn.d_instanceName, "cas");
    EXPECT_EQ(eo.d_execConn.d_url, "https://remote:50051");
    EXPECT_EQ(eo.d_execConn.d_instanceName, "dev");
    EXPECT_EQ(eo.d_execConn.d_retryLimit, "10");
    EXPECT_EQ(
        eo.d_execConn.d_retryOnCodes,
        std::set({grpc::StatusCode::UNKNOWN, grpc::StatusCode::INTERNAL}));

    EXPECT_EQ(eo.d_metricMode, "file:///path/to/toml/file");
    EXPECT_EQ(eo.d_metricPublishInterval, 40);
    EXPECT_EQ(eo.d_metricPrefix, "custom-toml-trexe-prefix");
}

TEST(TrexeExecutionOptionsTest, TestInvalidPropertiesCmdline)
{
    // Test invalid capture properties via command line
    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));
    const char *argv[] = {
        "./path/to/trexe",
        "--cache-only",
        "--cas-remote=https://127.0.0.1:1234",
        "--d=/test/download/dir",
        "--working-dir=/path/to/working-dir",
        "--input-path=/path/to/input-path-1:/remote/input-path-1:special_mode",
        "--output-path=/output/path-2",
        "--result-metadata-file=/tmp/result.json",
        "/usr/bin/echo"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);

    EXPECT_THROW(
        ExecutionOptions::fromCommandLine(trexeSpec.d_command, commandLine),
        std::invalid_argument);
}

TEST(TrexeExecutionOptionsTest, InvalidMtimeAndPermissionValuesTOML)
{
    const std::string invalidFilePermission = R"(
[connection]
remote = "https://remote:50051"
instance = "dev"

[[input-path]]
local = "precise-dir"
remote = "remote/precise"
file-permission = "some-value"
mtime = "capture"
)";

    const std::string invalidMtimeValue = R"(
[connection]
remote = "https://remote:50051"
instance = "dev"

[[input-path]]
local = "precise-dir"
remote = "remote/precise"
file-permission = "capture"
mtime = "some-value"
)";

    toml::table table1 = toml::parse(invalidFilePermission);
    EXPECT_THROW(ExecutionOptions::newFromTOML(table1), std::invalid_argument);

    toml::table table2 = toml::parse(invalidMtimeValue);
    EXPECT_THROW(ExecutionOptions::newFromTOML(table2), std::invalid_argument);
}

TEST(TrexeExecutionOptionsTest, TOMLThenCLI)
{
    const std::string config = R"(
metrics-mode = "file:///path/to/toml/file"
metrics-publish-interval = 40
metrics-prefix = "custom-toml-trexe-prefix"

[connection]
remote = "https://remote:50051"
instance = "dev"
)";

    std::filesystem::path configPath =
        std::filesystem::temp_directory_path() / "config.toml";
    {
        std::ofstream configFile(configPath);
        configFile << config;
    }

    CmdLineSpec trexeSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""),
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"),
        buildboxcommon::ConnectionOptionsCommandLine("ActionCache", "ac-"),
        buildboxcommon::ConnectionOptionsCommandLine("Execution", "exec-"),
        buildboxcommon::ConnectionOptionsCommandLine("LogStream",
                                                     "logstream-"));

    const auto configFileOption =
        std::string("--config-file=") + configPath.native();
    const char *argv[] = {"trexe",
                          configFileOption.c_str(),
                          "--cas-instance=cas",
                          "--exec-instance=exec",
                          "--ac-remote=https://ac:60051",
                          "--metrics-mode=file:///path/to/cmd/line/file",
                          "--metrics-publish-interval=11",
                          "--metrics-prefix=custom-cmd-line-prefix",
                          "ls"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);

    const auto options =
        ExecutionOptions::fromCommandLine(trexeSpec.d_command, commandLine);
    options.validate();

    ASSERT_EQ(options.d_execConn.d_url, "https://remote:50051");
    ASSERT_EQ(options.d_execConn.d_instanceName, "exec");
    ASSERT_EQ(options.d_casConn.d_url, "https://remote:50051");
    ASSERT_EQ(options.d_casConn.d_instanceName, "cas");
    ASSERT_EQ(options.d_acConn.d_url, "https://ac:60051");
    ASSERT_EQ(options.d_acConn.d_instanceName, "dev");
    ASSERT_EQ(options.d_argv.size(), 1);

    ASSERT_EQ(options.d_metricMode, "file:///path/to/cmd/line/file");
    ASSERT_EQ(options.d_metricPublishInterval, 11);
    ASSERT_EQ(options.d_metricPrefix, "custom-cmd-line-prefix");
}
