/*
 * Copyright 2020 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_connectionoptions.h>
#include <buildboxcommon_connectionoptions_commandline.h>

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

using namespace buildboxcommon;
using namespace testing;

// clang-format off
const char *argvTest[] = {
    "/some/path/to/some_program.tsk",
    "--cas-remote=http://127.0.0.1:50011",
    "--cas-instance=dev",
    "--cas-server-cert=my-server-cert",
    "--cas-client-key=my-client-key",
    "--cas-client-cert=my-client-cert",
    "--cas-access-token=my-access-token",
    "--cas-googleapi-auth=true",
    "--cas-retry-limit=10",
    "--cas-retry-delay=500",
    "--cas-retry-on-code=UNKNOWN",
    "--cas-retry-on-code=INTERNAL",
    "--cas-request-timeout=123",
    "--cas-keepalive-time=456",
    "--cas-min-throughput=1M",
    "--cas-load-balancing-policy=round_robin"
};

const char *argvTestDefaults[] = {
    "/some/path/to/some_program.tsk",
    "--cas-remote=http://127.0.0.1:50011"
};

const char *argvTestRequired[] = {
    "/some/path/to/some_program.tsk"
};

const char *argvTestConfig[] = {
    "/some/path/to/some_program.tsk",
    "--cas-remote=http://127.0.0.1:50011",
    "--cas-client-cert=my-client-cert",
    "--cas-client-key=my-client-key",
    "--cas-token-reload-interval=10"
};

const char *argvTestUpdate[] = {
    "/some/path/to/some_program.tsk",
    "--cas-remote=http://127.0.0.1:50021",
    "--cas-client-key=my-updated-client-key",
    "--cas-client-cert=my-updated-client-cert",
};

const char *argvTestOverride[] = {
    "/some/path/to/some_program.tsk",
    "--remote=http://common",
    "--instance=common",
    "--server-cert=commoncert",
    "--client-key=commonkey",
    "--client-cert=commoncert",
    "--access-token=commontoken",
    "--token-reload-interval=1",
    "--googleapi-auth=true",
    "--retry-limit=1",
    "--retry-delay=1",
    "--retry-on-code=UNKNOWN",
    "--request-timeout=1",
    "--min-throughput=1",
    "--keepalive-time=1",
    "--load-balancing-policy=grpclb",
    "--cas-remote=http://override",
    "--cas-instance=override",
    "--cas-server-cert=overridecert",
    "--cas-client-key=overridekey",
    "--cas-client-cert=overrideclientcert",
    "--cas-access-token=overridetoken",
    "--cas-token-reload-interval=0",
    "--cas-googleapi-auth=false",
    "--cas-retry-limit=0",
    "--cas-retry-delay=0",
    "--cas-retry-on-code=NOT_FOUND",
    "--cas-retry-on-code=INTERNAL",
    "--cas-request-timeout=0",
    "--cas-min-throughput=0",
    "--cas-keepalive-time=0",
    "--cas-load-balancing-policy=round_robin",
};

const char *argvTestNoOverride[] = {
    "/some/path/to/some_program.tsk",
    "--remote=http://common",
    "--instance=common",
    "--server-cert=commoncert",
    "--client-key=commonkey",
    "--client-cert=commonclientcert",
    "--access-token=commontoken",
    "--token-reload-interval=1",
    "--googleapi-auth=true",
    "--retry-limit=1",
    "--retry-delay=1",
    "--retry-on-code=UNKNOWN",
    "--request-timeout=1",
    "--min-throughput=1",
    "--keepalive-time=1",
    "--load-balancing-policy=grpclb",
};
// clang-format on

TEST(ConnectionOptionsCommandLineTest, Test)
{
    ConnectionOptionsCommandLine spec("CAS", "cas-");
    CommandLine commandLine(spec.spec());

    EXPECT_TRUE(
        commandLine.parse(sizeof(argvTest) / sizeof(const char *), argvTest));

    EXPECT_FALSE(ConnectionOptionsCommandLine::configureChannel(
        commandLine, "cas-", nullptr));

    ConnectionOptions channel;
    EXPECT_TRUE(ConnectionOptionsCommandLine::configureChannel(
        commandLine, "cas-", &channel));

    EXPECT_EQ("http://127.0.0.1:50011", channel.d_url);
    EXPECT_EQ("dev", channel.d_instanceName);
    EXPECT_EQ("my-server-cert", channel.d_serverCertPath);
    EXPECT_EQ("my-client-key", channel.d_clientKeyPath);
    EXPECT_EQ("my-client-cert", channel.d_clientCertPath);
    EXPECT_EQ("my-access-token", channel.d_accessTokenPath);
    EXPECT_TRUE(channel.d_useGoogleApiAuth);
    EXPECT_EQ("10", channel.d_retryLimit);
    EXPECT_EQ("500", channel.d_retryDelay);
    EXPECT_EQ("123", channel.d_requestTimeout);
    EXPECT_EQ("456", channel.d_keepaliveTime);
    EXPECT_EQ(1000000, channel.d_minThroughput);
    EXPECT_EQ("round_robin", channel.d_loadBalancingPolicy);
    EXPECT_EQ(std::set<grpc::StatusCode>(
                  {grpc::StatusCode::UNKNOWN, grpc::StatusCode::INTERNAL}),
              channel.d_retryOnCodes);
}

TEST(ConnectionOptionsCommandLineTest, TestDefaults)
{
    ConnectionOptionsCommandLine spec("CAS", "cas-");
    CommandLine commandLine(spec.spec());

    EXPECT_TRUE(commandLine.parse(
        sizeof(argvTestDefaults) / sizeof(const char *), argvTestDefaults));

    ConnectionOptions channel;
    EXPECT_TRUE(ConnectionOptionsCommandLine::configureChannel(
        commandLine, "cas-", &channel));

    EXPECT_EQ("http://127.0.0.1:50011", channel.d_url);

    // test default values
    EXPECT_EQ("", channel.d_instanceName);
    EXPECT_FALSE(channel.d_useGoogleApiAuth);
    EXPECT_EQ("4", channel.d_retryLimit);
    EXPECT_EQ("1000", channel.d_retryDelay);
    EXPECT_EQ("0", channel.d_requestTimeout);
    EXPECT_EQ("0", channel.d_keepaliveTime);
    EXPECT_EQ(0, channel.d_minThroughput);

    // untouched
    EXPECT_EQ(channel.d_serverCertPath, "");
    EXPECT_EQ(channel.d_clientKeyPath, "");
    EXPECT_EQ(channel.d_clientCertPath, "");
    EXPECT_EQ(channel.d_accessTokenPath, "");
}

TEST(ConnectionOptionsCommandLineTest, TestUpdate)
{
    ConnectionOptionsCommandLine spec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    CommandLine commandLine(spec.spec());

    EXPECT_TRUE(commandLine.parse(
        sizeof(argvTestConfig) / sizeof(const char *), argvTestConfig));

    ConnectionOptions channel;
    ConnectionOptionsCommandLine::configureChannel(commandLine, "cas-",
                                                   &channel);

    EXPECT_EQ("http://127.0.0.1:50011", channel.d_url);
    EXPECT_EQ("my-client-cert", channel.d_clientCertPath);
    EXPECT_EQ("10", channel.d_tokenReloadInterval);

    ConnectionOptionsCommandLine specUpdate(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    CommandLine commandLineUpdate(specUpdate.spec());

    commandLineUpdate.parse(sizeof(argvTestUpdate) / sizeof(const char *),
                            argvTestUpdate);
    ConnectionOptionsCommandLine::updateChannelOptions(commandLineUpdate,
                                                       "cas-", &channel);

    // Two updated values and one unchanged
    EXPECT_EQ("http://127.0.0.1:50021", channel.d_url);
    EXPECT_EQ("my-updated-client-cert", channel.d_clientCertPath);
    EXPECT_EQ("10", channel.d_tokenReloadInterval);
}

TEST(ConnectionOptionsCommandLineTest, TestOverride)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine commonSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""));
    spec.insert(spec.end(), commonSpec.spec().cbegin(),
                commonSpec.spec().cend());

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    EXPECT_TRUE(commandLine.parse(
        sizeof(argvTestOverride) / sizeof(const char *), argvTestOverride));

    ConnectionOptions channel;
    ConnectionOptionsCommandLine::configureChannel(commandLine, "", &channel);
    ConnectionOptionsCommandLine::updateChannelOptions(commandLine, "cas-",
                                                       &channel);

    EXPECT_EQ("http://override", channel.d_url);
    EXPECT_EQ("override", channel.d_instanceName);
    EXPECT_EQ("overridecert", channel.d_serverCertPath);
    EXPECT_EQ("overridekey", channel.d_clientKeyPath);
    EXPECT_EQ("overrideclientcert", channel.d_clientCertPath);
    EXPECT_EQ("overridetoken", channel.d_accessTokenPath);
    EXPECT_EQ("0", channel.d_tokenReloadInterval);
    EXPECT_EQ(false, channel.d_useGoogleApiAuth);
    EXPECT_EQ("0", channel.d_retryLimit);
    EXPECT_EQ("0", channel.d_retryDelay);
    EXPECT_EQ("0", channel.d_requestTimeout);
    EXPECT_EQ(0, channel.d_minThroughput);
    EXPECT_EQ("0", channel.d_keepaliveTime);
    EXPECT_EQ("round_robin", channel.d_loadBalancingPolicy);
}

TEST(ConnectionOptionsCommandLineTest, TestNoOverride)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine commonSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""));
    spec.insert(spec.end(), commonSpec.spec().cbegin(),
                commonSpec.spec().cend());

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    EXPECT_TRUE(
        commandLine.parse(sizeof(argvTestNoOverride) / sizeof(const char *),
                          argvTestNoOverride));

    ConnectionOptions channel;
    ConnectionOptionsCommandLine::configureChannel(commandLine, "", &channel);
    ConnectionOptionsCommandLine::updateChannelOptions(commandLine, "cas-",
                                                       &channel);

    EXPECT_EQ("http://common", channel.d_url);
    EXPECT_EQ("common", channel.d_instanceName);
    EXPECT_EQ("commoncert", channel.d_serverCertPath);
    EXPECT_EQ("commonkey", channel.d_clientKeyPath);
    EXPECT_EQ("commonclientcert", channel.d_clientCertPath);
    EXPECT_EQ("commontoken", channel.d_accessTokenPath);
    EXPECT_EQ("1", channel.d_tokenReloadInterval);
    EXPECT_EQ(true, channel.d_useGoogleApiAuth);
    EXPECT_EQ("1", channel.d_retryLimit);
    EXPECT_EQ("1", channel.d_retryDelay);
    EXPECT_EQ("1", channel.d_requestTimeout);
    EXPECT_EQ(1, channel.d_minThroughput);
    EXPECT_EQ("1", channel.d_keepaliveTime);
    EXPECT_EQ("grpclb", channel.d_loadBalancingPolicy);
}

TEST(ConnectionOptionsCommandLineTest, TestClientCertWithoutKey)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine commonSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""));
    spec.insert(spec.end(), commonSpec.spec().cbegin(),
                commonSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--client-cert=commonclientcert",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "", &channel));
}

TEST(ConnectionOptionsCommandLineTest, TestClientKeyWithoutCert)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine commonSpec(
        buildboxcommon::ConnectionOptionsCommandLine("", ""));
    spec.insert(spec.end(), commonSpec.spec().cbegin(),
                commonSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--client-key=commonclientcert",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "", &channel));
}

TEST(ConnectionOptionsCommandLineTest, TestCASClientKeyWithoutCert)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--cas-client-key=commonclientcert",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "cas-", &channel));
}

TEST(ConnectionOptionsCommandLineTest, TestCASClientCertWithoutKey)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--cas-client-cert=commonclientcert",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "cas-", &channel));
}

TEST(ConnectionOptionsCommandLineTest, TestCASServerCertWithoutRemote)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--cas-server-cert=cert",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "cas-", &channel));
}

TEST(ConnectionOptionsCommandLineTest, TestCASClientCertKeyWithoutRemote)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--cas-client-cert=cert",
        "--cas-client-key=key",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "cas-", &channel));
}

TEST(ConnectionOptionsCommandLineTest, TestCASAccessTokenWithoutRemote)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--cas-access-token=token",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "cas-", &channel));
}

TEST(ConnectionOptionsCommandLineTest, TestCASGoogleAPIAuthWithoutRemote)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--cas-googleapi-auth=cert",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "cas-", &channel));
}

TEST(ConnectionOptionsCommandLineTest, TestCredentialHelper)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--cas-remote=grpcs://cas.example.com",
        "--cas-credential-helper=/usr/bin/helper",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_TRUE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "cas-", &channel));

    EXPECT_EQ("grpcs://cas.example.com", channel.d_url);
    EXPECT_EQ("/usr/bin/helper", channel.d_credentialHelperPath);
}

TEST(ConnectionOptionsCommandLineTest, TestCredentialHelperWithoutRemote)
{
    std::vector<CommandLineTypes::ArgumentSpec> spec;

    ConnectionOptionsCommandLine casSpec(
        buildboxcommon::ConnectionOptionsCommandLine("CAS", "cas-"));
    spec.insert(spec.end(), casSpec.spec().cbegin(), casSpec.spec().cend());

    CommandLine commandLine(spec);

    const char *argv[] = {
        "/some/path/to/some_program.tsk",
        "--cas-credential-helper=/usr/bin/helper",
    };
    EXPECT_TRUE(commandLine.parse(sizeof(argv) / sizeof(const char *), argv));
    ConnectionOptions channel;
    EXPECT_FALSE(ConnectionOptionsCommandLine::updateChannelOptions(
        commandLine, "cas-", &channel));
}
