/*
 * 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 <gmock/gmock.h>
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include <vector>

#include <buildboxcommon_commandline.h>
#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_connectionoptions_commandline.h>
#include <trexe_cmdlinespec.h>

using namespace trexe;
using namespace buildboxcommon;

TEST(TrexeCmdLineSpecTest, TestParseBasicOptions)
{
    trexe::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",
                          "--platform-properties=P1=V1,P2=V2",
                          "--environment=E1=V3,E2=V4",
                          "--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",
                          "--no-wait",
                          "--salt=1234",
                          "--output-node-properties=mtime",
                          "--output-node-properties=unix_mode",
                          "--result-metadata-file=/tmp/result.json",
                          "--action-result-json=/tmp/result.json",
                          "--use-localcas",
                          "--localcas-skip-upload",
                          "echo",
                          "hello",
                          "world"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);
    ASSERT_EQ("https://127.0.0.1:1234", commandLine.getString("remote"));

    ASSERT_EQ("/test/download/dir", commandLine.getString("d"));
    ASSERT_EQ("/path/to/working-dir", commandLine.getString("working-dir"));

    ASSERT_EQ("P1=V1,P2=V2", commandLine.getString("platform-properties"));
    ASSERT_EQ("E1=V3,E2=V4", commandLine.getString("environment"));

    ASSERT_EQ(1, commandLine.getInt("exec-timeout"));
    ASSERT_EQ(true, commandLine.getBool("skip-cache-lookup"));
    ASSERT_EQ(true, commandLine.getBool("do-not-cache"));
    ASSERT_FALSE(commandLine.exists("follow-symlinks"));
    ASSERT_TRUE(commandLine.exists("do-not-follow-symlinks"));
    ASSERT_TRUE(commandLine.exists("use-localcas"));
    ASSERT_TRUE(commandLine.exists("localcas-skip-upload"));

    ASSERT_EQ(1, commandLine.getInt("priority"));

    ASSERT_EQ("corr", commandLine.getString("correlated-invocations-id"));
    ASSERT_EQ("tid", commandLine.getString("tool-invocation-id"));
    ASSERT_EQ("tname", commandLine.getString("tool-name"));
    ASSERT_EQ("tver", commandLine.getString("tool-version"));
    ASSERT_EQ("1234", commandLine.getString("salt"));
    ASSERT_EQ("MD5", commandLine.getString("digest-function"));

    std::vector<std::string> nodeProperties =
        commandLine.getVS("output-node-properties");
    ASSERT_EQ(2, nodeProperties.size());

    std::vector<std::string> inputPaths = commandLine.getVS("input-path");
    ASSERT_EQ(2, inputPaths.size());
    ASSERT_NE(inputPaths.end(), find(inputPaths.begin(), inputPaths.end(),
                                     "/path/to/input-path-1"));
    ASSERT_NE(inputPaths.end(), find(inputPaths.begin(), inputPaths.end(),
                                     "/path/to/input-path-2"));

    std::vector<std::string> outputPaths = commandLine.getVS("output-path");
    ASSERT_EQ(2, outputPaths.size());
    ASSERT_NE(outputPaths.end(),
              find(outputPaths.begin(), outputPaths.end(), "/output/path-1"));
    ASSERT_NE(outputPaths.end(),
              find(outputPaths.begin(), outputPaths.end(), "/output/path-2"));

    ConnectionOptions connOptions;
    ASSERT_NO_THROW(ConnectionOptionsCommandLine::configureChannel(
                        commandLine, "", &connOptions););

    ASSERT_EQ(true, commandLine.exists("no-wait"));
    ASSERT_EQ(false, commandLine.exists("operation"));
    ASSERT_EQ("/tmp/result.json",
              commandLine.getString("result-metadata-file"));
    ASSERT_EQ("/tmp/result.json", commandLine.getString("action-result-json"));

    EXPECT_THAT(trexeSpec.d_command,
                testing::ElementsAre("echo", "hello", "world"));
}

TEST(TrexeCmdLineSpecTest, TestGetCompleted)
{
    trexe::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",
                          "--action-result-json=/tmp/result.json",
                          "--stdout-file=/tmp/stdout",
                          "--stderr-file=/tmp/stderr",
                          "echo",
                          "hello",
                          "world"};
    int argc = sizeof(argv) / sizeof(const char *);

    CommandLine commandLine(trexeSpec.d_spec);
    const bool success = commandLine.parse(argc, argv);
    ASSERT_EQ(true, success);
    ASSERT_EQ("https://127.0.0.1:1234", commandLine.getString("remote"));

    ASSERT_EQ("/test/download/dir", commandLine.getString("d"));

    ASSERT_EQ("corr", commandLine.getString("correlated-invocations-id"));
    ASSERT_EQ("tid", commandLine.getString("tool-invocation-id"));
    ASSERT_EQ("tname", commandLine.getString("tool-name"));
    ASSERT_EQ("tver", commandLine.getString("tool-version"));

    ConnectionOptions connOptions;
    ASSERT_NO_THROW(ConnectionOptionsCommandLine::configureChannel(
                        commandLine, "", &connOptions););

    ASSERT_EQ("opID", commandLine.getString("operation"));
    ASSERT_EQ("/tmp/result.json",
              commandLine.getString("result-metadata-file"));
    ASSERT_EQ("/tmp/result.json", commandLine.getString("action-result-json"));
    ASSERT_EQ("/tmp/stdout", commandLine.getString("stdout-file"));
    ASSERT_EQ("/tmp/stderr", commandLine.getString("stderr-file"));

    EXPECT_THAT(trexeSpec.d_command,
                testing::ElementsAre("echo", "hello", "world"));
}

TEST(TrexeCmdLineSpecTest, TestParseMissingOptionals)
{
    trexe::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",
                          "echo",
                          "hello",
                          "world"};
    int argc = sizeof(argv) / sizeof(const char *);

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

    ASSERT_EQ("/test/download/dir", commandLine.getString("d"));
    ASSERT_EQ("/path/to/working-dir", commandLine.getString("working-dir"));

    ASSERT_FALSE(commandLine.exists("platform-properties"));
    ASSERT_FALSE(commandLine.exists("environment"));

    ASSERT_FALSE(commandLine.exists("exec-timeout"));
    ASSERT_FALSE(commandLine.exists("skip-cache-lookup"));
    ASSERT_FALSE(commandLine.exists("do-not-cache"));
    ASSERT_FALSE(commandLine.exists("follow-symlinks"));
    ASSERT_FALSE(commandLine.exists("do-not-follow-symlinks"));

    ASSERT_FALSE(commandLine.exists("use-localcas"));
    ASSERT_FALSE(commandLine.exists("localcas-skip-upload"));

    ASSERT_EQ(false, commandLine.exists("priority"));

    ASSERT_EQ(false, commandLine.exists("correlated-invocations-id"));
    ASSERT_EQ(false, commandLine.exists("tool-invocation-id"));
    ASSERT_EQ(false, commandLine.exists("tool-name"));
    ASSERT_EQ(false, commandLine.exists("tool-version"));

    std::vector<std::string> inputPaths = commandLine.getVS("input-path");
    ASSERT_EQ(2, inputPaths.size());
    ASSERT_NE(inputPaths.end(), find(inputPaths.begin(), inputPaths.end(),
                                     "/path/to/input-path-1"));
    ASSERT_NE(inputPaths.end(), find(inputPaths.begin(), inputPaths.end(),
                                     "/path/to/input-path-2"));

    std::vector<std::string> outputPaths = commandLine.getVS("output-path");
    ASSERT_EQ(2, outputPaths.size());
    ASSERT_NE(outputPaths.end(),
              find(outputPaths.begin(), outputPaths.end(), "/output/path-1"));
    ASSERT_NE(outputPaths.end(),
              find(outputPaths.begin(), outputPaths.end(), "/output/path-2"));

    ConnectionOptions connOptions;
    ASSERT_NO_THROW(ConnectionOptionsCommandLine::configureChannel(
                        commandLine, "", &connOptions););

    ASSERT_EQ(false, commandLine.exists("no-wait"));
    ASSERT_EQ(false, commandLine.exists("operation"));
    ASSERT_EQ("/tmp/result.json",
              commandLine.getString("result-metadata-file"));

    EXPECT_THAT(trexeSpec.d_command,
                testing::ElementsAre("echo", "hello", "world"));
}

TEST(TrexeCmdLineSpecTest, TestInputRootDigest)
{
    trexe::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-root-digest=fakehashstring/12345",
                          "--output-path=/output/path-1",
                          "--output-path=/output/path-2",
                          "--result-metadata-file=/tmp/result.json",
                          "echo",
                          "hello",
                          "world"};
    int argc = sizeof(argv) / sizeof(const char *);

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

    ASSERT_EQ("/test/download/dir", commandLine.getString("d"));
    ASSERT_EQ("/path/to/working-dir", commandLine.getString("working-dir"));

    std::vector<std::string> inputPaths = commandLine.getVS("input-path");
    ASSERT_TRUE(inputPaths.empty());

    ASSERT_EQ("fakehashstring/12345",
              commandLine.getString("input-root-digest"));

    std::vector<std::string> outputPaths = commandLine.getVS("output-path");
    ASSERT_EQ(2, outputPaths.size());
    ASSERT_NE(outputPaths.end(),
              find(outputPaths.begin(), outputPaths.end(), "/output/path-1"));
    ASSERT_NE(outputPaths.end(),
              find(outputPaths.begin(), outputPaths.end(), "/output/path-2"));

    ConnectionOptions connOptions;
    ASSERT_NO_THROW(ConnectionOptionsCommandLine::configureChannel(
                        commandLine, "", &connOptions););

    ASSERT_EQ(false, commandLine.exists("no-wait"));
    ASSERT_EQ(false, commandLine.exists("operation"));
    ASSERT_EQ("/tmp/result.json",
              commandLine.getString("result-metadata-file"));

    EXPECT_THAT(trexeSpec.d_command,
                testing::ElementsAre("echo", "hello", "world"));
}

TEST(TrexeCmdLineSpecTest, TestMetricArguments)
{
    trexe::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",
                          "--metrics-mode=file:///path/to/cmd/line/file",
                          "--metrics-publish-interval=11",
                          "--metrics-prefix=custom-cmd-line-prefix",
                          "echo",
                          "hello"};
    int argc = sizeof(argv) / sizeof(const char *);

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

    ASSERT_EQ("file:///path/to/cmd/line/file",
              commandLine.getString("metrics-mode"));
    ASSERT_EQ(11, commandLine.getInt("metrics-publish-interval"));
    ASSERT_EQ("custom-cmd-line-prefix",
              commandLine.getString("metrics-prefix"));
}
