/*
 * Copyright (c) 2018-2019 [Ribose Inc](https://www.ribose.com).
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 * 1.  Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *
 * 2.  Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "rnp_tests.h"
#include "support.h"
#include "time-utils.h"

#ifdef HAVE_SYS_WAIT_H
#include <sys/wait.h>
#else
#ifndef WIFEXITED
#define WIFEXITED(stat) (((*((int *) &(stat))) & 0xC0000000) == 0)
#endif

#ifndef WEXITSTATUS
#define WEXITSTATUS(stat) (*((int *) &(stat)))
#endif
#endif

#ifdef _WIN32
#include <windows.h>
#include "str-utils.h"
#endif

int rnp_main(int argc, char **argv);
int rnpkeys_main(int argc, char **argv);

static int
call_rnp(const char *cmd, ...)
{
    int     argc = 0;
    int     res;
    char ** argv = (char **) calloc(32, sizeof(char *));
    va_list args;

    if (!argv) {
        return -1;
    }

    va_start(args, cmd);
    while (cmd) {
        argv[argc++] = (char *) cmd;
        cmd = va_arg(args, char *);
    }
    va_end(args);
    /* reset state of getopt_long used in rnp */
    optind = 1;

    if (!strcmp(argv[0], "rnp")) {
        res = rnp_main(argc, argv);
    } else if (!strcmp(argv[0], "rnpkeys")) {
        res = rnpkeys_main(argc, argv);
    } else {
        res = -1;
    }
    free(argv);

    return res;
}

#define KEYS "data/keyrings"
#define GENKEYS "data/keyrings_genkey_tmp"
#define MKEYS "data/test_stream_key_merge/"
#define FILES "data/test_cli"
#define G10KEYS "data/test_stream_key_load/g10"

TEST_F(rnp_tests, test_cli_rnp_keyfile)
{
    int ret;

    /* sign with keyfile, using default key */
    ret = call_rnp("rnp",
                   "--keyfile",
                   MKEYS "key-sec.asc",
                   "--password",
                   "password",
                   "-s",
                   FILES "/hello.txt",
                   NULL);
    assert_int_equal(ret, 0);
    assert_true(rnp_file_exists(FILES "/hello.txt.pgp"));
    /* verify signed file */
    ret =
      call_rnp("rnp", "--keyfile", MKEYS "key-pub.asc", "-v", FILES "/hello.txt.pgp", NULL);
    assert_int_equal(ret, 0);
    assert_int_equal(rnp_unlink(FILES "/hello.txt.pgp"), 0);

    /* sign with keyfile, using user id */
    ret = call_rnp("rnp",
                   "-f",
                   MKEYS "key-sec.asc",
                   "-u",
                   "key-merge-uid-2",
                   "--password",
                   "password",
                   "--armor",
                   "-s",
                   FILES "/hello.txt",
                   NULL);
    assert_int_equal(ret, 0);
    assert_true(rnp_file_exists(FILES "/hello.txt.asc"));
    /* verify signed file */
    ret = call_rnp("rnp", "-f", MKEYS "key-pub.asc", "-v", FILES "/hello.txt.asc", NULL);
    assert_int_equal(ret, 0);
    /* verify with key without self-signature - should fail */
    ret =
      call_rnp("rnp", "-f", MKEYS "key-pub-just-key.pgp", "-v", FILES "/hello.txt.asc", NULL);
    assert_int_not_equal(ret, 0);
    assert_int_equal(rnp_unlink(FILES "/hello.txt.asc"), 0);

    /* encrypt with keyfile, using default key */
    ret = call_rnp("rnp", "--keyfile", MKEYS "key-pub.asc", "-e", FILES "/hello.txt", NULL);
    assert_int_equal(ret, 0);
    assert_true(rnp_file_exists(FILES "/hello.txt.pgp"));
    /* decrypt it with raw seckey, without userids and sigs */
    ret = call_rnp("rnp",
                   "--keyfile",
                   MKEYS "key-sec-no-uid-no-sigs.pgp",
                   "--password",
                   "password",
                   "-d",
                   FILES "/hello.txt.pgp",
                   "--output",
                   "-",
                   NULL);
    assert_int_equal(ret, 0);
    assert_int_equal(rnp_unlink(FILES "/hello.txt.pgp"), 0);

    /* try to encrypt with keyfile, using the signing subkey */
    ret = call_rnp("rnp",
                   "--keyfile",
                   MKEYS "key-pub.asc",
                   "-r",
                   "16CD16F267CCDD4F",
                   "--armor",
                   "-e",
                   FILES "/hello.txt",
                   NULL);
    assert_int_not_equal(ret, 0);
    assert_false(rnp_file_exists(FILES "/hello.txt.asc"));
    /* now encrypt with keyfile, using the encrypting subkey */
    ret = call_rnp("rnp",
                   "--keyfile",
                   MKEYS "key-pub.asc",
                   "-r",
                   "AF1114A47F5F5B28",
                   "--armor",
                   "-e",
                   FILES "/hello.txt",
                   NULL);
    assert_int_equal(ret, 0);
    assert_true(rnp_file_exists(FILES "/hello.txt.asc"));
    /* fail to decrypt it with pubkey */
    ret = call_rnp("rnp",
                   "--keyfile",
                   MKEYS "key-pub-subkey-1.pgp",
                   "--password",
                   "password",
                   "-d",
                   FILES "/hello.txt.asc",
                   "--output",
                   "-",
                   NULL);
    assert_int_not_equal(ret, 0);
    /* decrypt correctly with seckey + subkeys */
    ret = call_rnp("rnp",
                   "--keyfile",
                   MKEYS "key-sec.pgp",
                   "--password",
                   "password",
                   "-d",
                   FILES "/hello.txt.asc",
                   "--output",
                   "-",
                   NULL);
    assert_int_equal(ret, 0);
    assert_int_equal(rnp_unlink(FILES "/hello.txt.asc"), 0);
}

static bool
test_cli_g10_key_sign(const char *userid)
{
    /* create signature */
    int ret = call_rnp("rnp",
                       "--homedir",
                       G10KEYS,
                       "--password",
                       "password",
                       "-u",
                       userid,
                       "-s",
                       FILES "/hello.txt",
                       NULL);
    if (ret) {
        rnp_unlink(FILES "/hello.txt.pgp");
        return false;
    }

    /* verify back */
    ret = call_rnp(
      "rnp", "--homedir", G10KEYS, "-v", FILES "/hello.txt.pgp", "--output", "-", NULL);
    rnp_unlink(FILES "/hello.txt.pgp");
    return !ret;
}

static bool
test_cli_g10_key_encrypt(const char *userid)
{
    /* encrypt */
    int ret =
      call_rnp("rnp", "--homedir", G10KEYS, "-r", userid, "-e", FILES "/hello.txt", NULL);
    if (ret) {
        rnp_unlink(FILES "/hello.txt.pgp");
        return false;
    }

    /* decrypt it back */
    ret = call_rnp("rnp",
                   "--homedir",
                   G10KEYS,
                   "--password",
                   "password",
                   "-d",
                   FILES "/hello.txt.pgp",
                   "--output",
                   "-",
                   NULL);
    rnp_unlink(FILES "/hello.txt.pgp");
    return !ret;
}

TEST_F(rnp_tests, test_cli_g10_operations)
{
    int ret;

    /* sign with default g10 key */
    ret = call_rnp(
      "rnp", "--homedir", G10KEYS, "--password", "password", "-s", FILES "/hello.txt", NULL);
    assert_int_equal(ret, 0);

    /* verify back */
    ret = call_rnp("rnp", "--homedir", G10KEYS, "-v", FILES "/hello.txt.pgp", NULL);
    assert_int_equal(ret, 0);
    assert_int_equal(rnp_unlink(FILES "/hello.txt.pgp"), 0);

    /* encrypt with default g10 key */
    ret = call_rnp("rnp", "--homedir", G10KEYS, "-e", FILES "/hello.txt", NULL);
    assert_int_equal(ret, 0);

    /* decrypt it back */
    ret = call_rnp("rnp",
                   "--homedir",
                   G10KEYS,
                   "--password",
                   "password",
                   "-d",
                   FILES "/hello.txt.pgp",
                   "--output",
                   "-",
                   NULL);
    assert_int_equal(ret, 0);
    assert_int_equal(rnp_unlink(FILES "/hello.txt.pgp"), 0);

    /* check dsa/eg key */
    assert_true(test_cli_g10_key_sign("c8a10a7d78273e10"));    // signing key
    assert_true(test_cli_g10_key_encrypt("c8a10a7d78273e10")); // will find subkey
    assert_false(test_cli_g10_key_sign("02a5715c3537717e"));   // fail - encrypting subkey
    assert_true(test_cli_g10_key_encrypt("02a5715c3537717e")); // success

    /* check rsa/rsa key, key is SC while subkey is E. Must succeed till year 2024 */
    assert_true(test_cli_g10_key_sign("2fb9179118898e8b"));
    assert_true(test_cli_g10_key_encrypt("2fb9179118898e8b"));
    assert_false(test_cli_g10_key_sign("6e2f73008f8b8d6e"));
    assert_true(test_cli_g10_key_encrypt("6e2f73008f8b8d6e"));

#ifdef CRYPTO_BACKEND_BOTAN
    /*  GnuPG extended key format requires AEAD support that is available for BOTAN backend
       only https://github.com/rnpgp/rnp/issues/1642 (???)
    */
    /* check new rsa/rsa key, key is SC while subkey is E. */
    assert_true(test_cli_g10_key_sign("bd860a52d1899c0f"));
    assert_true(test_cli_g10_key_encrypt("bd860a52d1899c0f"));
    assert_false(test_cli_g10_key_sign("8e08d46a37414996"));
    assert_true(test_cli_g10_key_encrypt("8e08d46a37414996"));
#endif

    /* check ed25519 key */
    assert_true(test_cli_g10_key_sign("cc786278981b0728"));
    assert_false(test_cli_g10_key_encrypt("cc786278981b0728"));

    /* check ed25519/x25519 key */
    assert_true(test_cli_g10_key_sign("941822a0fc1b30a5"));
    assert_true(test_cli_g10_key_encrypt("941822a0fc1b30a5"));
    assert_false(test_cli_g10_key_sign("c711187e594376af"));
    assert_true(test_cli_g10_key_encrypt("c711187e594376af"));

    /* check p256 key */
    assert_true(test_cli_g10_key_sign("23674f21b2441527"));
    assert_true(test_cli_g10_key_encrypt("23674f21b2441527"));
    assert_false(test_cli_g10_key_sign("37e285e9e9851491"));
    assert_true(test_cli_g10_key_encrypt("37e285e9e9851491"));

    /* check p384 key */
    assert_true(test_cli_g10_key_sign("242a3aa5ea85f44a"));
    assert_true(test_cli_g10_key_encrypt("242a3aa5ea85f44a"));
    assert_false(test_cli_g10_key_sign("e210e3d554a4fad9"));
    assert_true(test_cli_g10_key_encrypt("e210e3d554a4fad9"));

    /* check p521 key */
    assert_true(test_cli_g10_key_sign("2092ca8324263b6a"));
    assert_true(test_cli_g10_key_encrypt("2092ca8324263b6a"));
    assert_false(test_cli_g10_key_sign("9853df2f6d297442"));
    assert_true(test_cli_g10_key_encrypt("9853df2f6d297442"));

    /* check bp256 key */
    assert_true(test_cli_g10_key_sign("d0c8a3daf9e0634a") == brainpool_enabled());
    assert_true(test_cli_g10_key_encrypt("d0c8a3daf9e0634a") == brainpool_enabled());
    assert_false(test_cli_g10_key_sign("2edabb94d3055f76"));
    assert_true(test_cli_g10_key_encrypt("2edabb94d3055f76") == brainpool_enabled());

    /* check bp384 key */
    assert_true(test_cli_g10_key_sign("6cf2dce85599ada2") == brainpool_enabled());
    assert_true(test_cli_g10_key_encrypt("6cf2dce85599ada2") == brainpool_enabled());
    assert_false(test_cli_g10_key_sign("cff1bb6f16d28191"));
    assert_true(test_cli_g10_key_encrypt("cff1bb6f16d28191") == brainpool_enabled());

    /* check bp512 key */
    assert_true(test_cli_g10_key_sign("aa5c58d14f7b8f48") == brainpool_enabled());
    assert_true(test_cli_g10_key_encrypt("aa5c58d14f7b8f48") == brainpool_enabled());
    assert_false(test_cli_g10_key_sign("20cdaa1482ba79ce"));
    assert_true(test_cli_g10_key_encrypt("20cdaa1482ba79ce") == brainpool_enabled());

    /* check secp256k1 key */
    assert_true(test_cli_g10_key_sign("3ea5bb6f9692c1a0"));
    assert_true(test_cli_g10_key_encrypt("3ea5bb6f9692c1a0"));
    assert_false(test_cli_g10_key_sign("7635401f90d3e533"));
    assert_true(test_cli_g10_key_encrypt("7635401f90d3e533"));
}

TEST_F(rnp_tests, test_cli_rnpkeys_unicode)
{
#ifdef _WIN32
    std::string  uid_acp = "\x80@a.com";
    std::wstring uid2_wide =
      L"\x03C9\x0410@b.com"; // some Greek and Cyrillic for CreateProcessW test
    std::string homedir_s = std::string(m_dir) + "/unicode";
    rnp_mkdir(homedir_s.c_str());
    std::string path_s = rnp::path::append(original_dir(), "../rnpkeys/rnpkeys.exe");
    std::string cmdline_s = path_s + " --numbits 2048 --homedir " + homedir_s +
                            " --password password --userid " + uid_acp + " --generate-key";
    UINT         acp = GetACP();
    STARTUPINFOA si;
    ZeroMemory(&si, sizeof si);
    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof pi);
    BOOL res = CreateProcessA(NULL, // (LPSTR) path_s.c_str(), // Module name
                              (LPSTR) cmdline_s.c_str(), // Command line
                              NULL,                      // Process handle not inheritable
                              NULL,                      // Thread handle not inheritable
                              FALSE,                     // Handle inheritance
                              0,                         // Creation flags
                              NULL,                      // Use parent's environment block
                              NULL,                      // Use parent's starting directory
                              &si,                       // Pointer to STARTUPINFO structure
                              &pi); // Pointer to PROCESS_INFORMATION structure
    assert_true(res);
    assert_true(WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_OBJECT_0);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

    std::wstring homedir_ws = wstr_from_utf8(homedir_s);
    std::wstring path_ws = wstr_from_utf8(path_s);
    std::wstring cmdline_ws = path_ws + L" --numbits 2048 --homedir " + homedir_ws +
                              L" --password password --userid " + uid2_wide +
                              L" --generate-key";
    STARTUPINFOW siw;
    ZeroMemory(&siw, sizeof siw);
    ZeroMemory(&pi, sizeof pi);
    res = CreateProcessW(NULL,
                         (LPWSTR) cmdline_ws.c_str(), // Command line
                         NULL,                        // Process handle not inheritable
                         NULL,                        // Thread handle not inheritable
                         FALSE,                       // Handle inheritance
                         0,                           // Creation flags
                         NULL,                        // Use parent's environment block
                         NULL,                        // Use parent's starting directory
                         &siw,                        // Pointer to STARTUPINFO structure
                         &pi); // Pointer to PROCESS_INFORMATION structure
    assert_true(res);
    assert_true(WaitForSingleObject(pi.hProcess, INFINITE) == WAIT_OBJECT_0);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    // Load the keyring and check what was actually written
    rnp_ffi_t ffi;
    assert_rnp_success(rnp_ffi_create(&ffi, "GPG", "GPG"));
    rnp_input_t input = NULL;
    assert_rnp_success(rnp_input_from_path(&input, "unicode/pubring.gpg"));
    assert_rnp_success(rnp_load_keys(ffi, "GPG", input, RNP_LOAD_SAVE_PUBLIC_KEYS));
    rnp_input_destroy(input);

    // convert from ACP to wide char via Windows native mechanism
    int convertResult = MultiByteToWideChar(acp, 0, uid_acp.c_str(), uid_acp.size(), NULL, 0);
    assert_true(convertResult > 0);
    std::wstring uid_wide;
    uid_wide.resize(convertResult);
    convertResult = MultiByteToWideChar(
      acp, 0, uid_acp.c_str(), uid_acp.size(), &uid_wide[0], (int) uid_wide.size());
    assert_true(convertResult > 0);

    // we expect to find UID in UTF-8
    std::string      uid_utf8 = wstr_to_utf8(uid_wide);
    rnp_key_handle_t key = NULL;
    assert_rnp_success(rnp_locate_key(ffi, "userid", uid_utf8.c_str(), &key));
    assert_non_null(key);

    size_t uids = 0;
    assert_rnp_success(rnp_key_get_uid_count(key, &uids));
    assert_int_equal(uids, 1);

    rnp_uid_handle_t uid = NULL;
    assert_rnp_success(rnp_key_get_uid_handle_at(key, 0, &uid));
    assert_non_null(uid);

    size_t size = 0;
    char * data = NULL;
    assert_rnp_success(rnp_uid_get_data(uid, (void **) &data, &size));
    std::string uid_read(data, data + size);
    assert_int_equal(0, uid_read.compare(uid_utf8));
    rnp_buffer_destroy(data);
    rnp_uid_handle_destroy(uid);
    rnp_key_handle_destroy(key);

    uid_utf8 = wstr_to_utf8(uid2_wide);
    key = NULL;
    assert_rnp_success(rnp_locate_key(ffi, "userid", uid_utf8.c_str(), &key));
    assert_non_null(key);

    uids = 0;
    assert_rnp_success(rnp_key_get_uid_count(key, &uids));
    assert_int_equal(uids, 1);

    uid = NULL;
    assert_rnp_success(rnp_key_get_uid_handle_at(key, 0, &uid));
    assert_non_null(uid);

    size = 0;
    data = NULL;
    assert_rnp_success(rnp_uid_get_data(uid, (void **) &data, &size));
    std::string uid2_read(data, data + size);
    assert_int_equal(0, uid2_read.compare(uid_utf8));
    rnp_buffer_destroy(data);
    rnp_uid_handle_destroy(uid);
    rnp_key_handle_destroy(key);
    rnp_ffi_destroy(ffi);
#endif
}

TEST_F(rnp_tests, test_cli_rnp)
{
    int ret;
    assert_int_equal(0, call_rnp("rnp", "--version", NULL));

    /* sign with default key */
    ret = call_rnp("rnp",
                   "--homedir",
                   KEYS "/1",
                   "--password",
                   "password",
                   "--sign",
                   FILES "/hello.txt",
                   NULL);
    assert_int_equal(ret, 0);

    /* encrypt with default key */
    ret = call_rnp(
      "rnp", "--homedir", KEYS "/1", "--encrypt", FILES "/hello.txt", "--overwrite", NULL);
    assert_int_equal(ret, 0);

    /* sign and verify back with g10 key */
    ret = call_rnp("rnp",
                   "--homedir",
                   KEYS "/3",
                   "-u",
                   "4BE147BB22DF1E60",
                   "--password",
                   "password",
                   "--sign",
                   FILES "/hello.txt",
                   "--overwrite",
                   NULL);
    assert_int_equal(ret, 0);
    ret = call_rnp("rnp", "--homedir", KEYS "/3", "--verify", FILES "/hello.txt.pgp", NULL);
    assert_int_equal(ret, 0);

    /* encrypt and decrypt back with g10 key */
    ret = call_rnp("rnp",
                   "--homedir",
                   KEYS "/3",
                   "-r",
                   "4BE147BB22DF1E60",
                   "--encrypt",
                   FILES "/hello.txt",
                   "--overwrite",
                   NULL);
    assert_int_equal(ret, 0);
    ret = call_rnp("rnp",
                   "--homedir",
                   KEYS "/3",
                   "--password",
                   "password",
                   "--decrypt",
                   FILES "/hello.txt.pgp",
                   "--output",
                   "-",
                   NULL);
    assert_int_equal(ret, 0);
}

TEST_F(rnp_tests, test_cli_examples)
{
    auto examples_path = rnp::path::append(original_dir(), "../examples");
    /* key generation example */
    auto example_path = rnp::path::append(examples_path, "generate");
    assert_false(example_path.empty());
    assert_int_equal(system(example_path.c_str()), 0);

    /* encryption sample */
    example_path = rnp::path::append(examples_path, "encrypt");
    assert_false(example_path.empty());
    assert_int_equal(system(example_path.c_str()), 0);

    /* decryption sample */
    example_path = rnp::path::append(examples_path, "decrypt");
    assert_false(example_path.empty());
    assert_int_equal(system(example_path.c_str()), 0);

    /* signing sample */
    example_path = rnp::path::append(examples_path, "sign");
    assert_false(example_path.empty());
    assert_int_equal(system(example_path.c_str()), 0);

    /* verification sample */
    example_path = rnp::path::append(examples_path, "verify");
    assert_false(example_path.empty());
    assert_int_equal(system(example_path.c_str()), 0);
}

TEST_F(rnp_tests, test_cli_rnpkeys)
{
    int ret;
    assert_int_equal(0, call_rnp("rnpkeys", "--version", NULL));

    /* test keys listing */
    ret = call_rnp("rnpkeys", "--homedir", KEYS "/1", "--list-keys", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/1", "--list-keys", "--with-sigs", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/2", "--list-keys", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/2", "--list-keys", "--with-sigs", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/3", "--list-keys", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/3", "--list-keys", "--with-sigs", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/5", "--list-keys", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/5", "--list-keys", "--with-sigs", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", G10KEYS, "--list-keys", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", G10KEYS, "--list-keys", "--with-sigs", NULL);
    assert_int_equal(ret, 0);

    /* test single key listing command */
    ret = call_rnp("rnpkeys", "--homedir", KEYS "/1", "--list-keys", "2fcadf05ffa501bb", NULL);
    assert_int_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/1", "--list-keys", "00000000", NULL);
    assert_int_not_equal(ret, 0);

    ret = call_rnp("rnpkeys", "--homedir", KEYS "/1", "--list-keys", "zzzzzzzz", NULL);
    assert_int_not_equal(ret, 0);
}

// check both primary key and subkey for the given userid
static int
key_expiration_check(rnp_key_store_t *keystore,
                     const char *     userid,
                     uint32_t         expectedExpiration)
{
    int res = -1; // not found
    for (auto &key : keystore->keys) {
        pgp_key_t *pk;
        if (key.is_primary()) {
            pk = &key;
        } else {
            if (!key.has_primary_fp()) {
                return 0;
            }
            pk = rnp_key_store_get_key_by_fpr(keystore, key.primary_fp());
        }
        if (pk->uid_count() != 1) {
            return 0;
        }
        auto uid = pk->get_uid(0).str;
        if (uid != userid) {
            continue;
        }
        auto expiration = key.expiration();
        if (uid == "expiration_absolute@rnp" || uid == "expiration_beyond2038_absolute@rnp") {
            auto diff = expectedExpiration < expiration ? expiration - expectedExpiration :
                                                          expectedExpiration - expiration;
            // allow 10 minutes diff
            if (diff < 600) {
                res = 1;
            } else {
                return 0;
            }
        } else {
            if (expectedExpiration == expiration) {
                res = 1;
            } else {
                RNP_LOG(
                  "key_expiration_check error: userid=%s expectedExpiration=%u expiration=%u",
                  userid,
                  expectedExpiration,
                  expiration);
                return 0;
            }
        }
    }
    return res;
}

static int
key_generate(const char *homedir, const char *userid, const char *expiration)
{
    int ret = call_rnp("rnpkeys",
                       "--password",
                       "1234",
                       "--homedir",
                       homedir,
                       "--generate-key",
                       "--expiration",
                       expiration,
                       "--userid",
                       userid,
                       "--s2k-iterations",
                       "65536",
                       "--numbits",
                       "1024",
                       NULL);
    return ret;
}

TEST_F(rnp_tests, test_cli_rnpkeys_genkey)
{
    assert_false(RNP_MKDIR(GENKEYS, S_IRWXU));
    time_t   basetime = time(NULL);
    time_t   rawtime = basetime + 604800;
    time_t   y2k38time = INT32_MAX;
    uint32_t expected_diff_beyond2038_absolute;
    if (rnp_y2k38_warning(y2k38time)) {
        // we're on the system that doesn't support dates beyond y2k38
        auto diff_to_y2k38 = y2k38time - basetime;
        expected_diff_beyond2038_absolute = diff_to_y2k38;
    } else {
        struct tm tm2100;
        rnp_localtime(time(NULL), tm2100);
        tm2100.tm_hour = 0;
        tm2100.tm_min = 0;
        tm2100.tm_sec = 0;
        tm2100.tm_mday = 1;
        tm2100.tm_mon = 0;
        tm2100.tm_year = 200;
        /* line below is required to correctly handle DST changes */
        tm2100.tm_isdst = -1;
        expected_diff_beyond2038_absolute = mktime(&tm2100) - basetime;
    }
    struct tm timeinfo;
    rnp_localtime(rawtime, timeinfo);
    // clear hours, minutes and seconds
    timeinfo.tm_hour = 0;
    timeinfo.tm_min = 0;
    timeinfo.tm_sec = 0;
    rawtime = mktime(&timeinfo);
    auto exp =
      fmt("%d-%02d-%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday);

    // these should fail and not go to the keystore
    assert_int_not_equal(key_generate(GENKEYS, "expiration_negative@rnp", "-1"), 0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_unrecognized_1@rnp", "1z"), 0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_unrecognized_2@rnp", "now"), 0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_unrecognized_3@rnp", "00000-01-01"),
                         0);
    assert_int_not_equal(
      key_generate(GENKEYS, "expiration_integer_overflow@rnp", "1234567890123456789"), 0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_32bit_overflow@rnp", "4294967296"),
                         0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_overflow_day@rnp", "2037-02-29"),
                         0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_overflow_month@rnp", "2037-13-01"),
                         0);
    if (!rnp_y2k38_warning(y2k38time)) {
        assert_int_not_equal(
          key_generate(GENKEYS, "expiration_overflow_year@rnp", "2337-01-01"), 0);
    }
    assert_int_not_equal(key_generate(GENKEYS, "expiration_underflow_day@rnp", "2037-02-00"),
                         0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_underflow_month@rnp", "2037-00-01"),
                         0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_underflow_year@rnp", "1800-01-01"),
                         0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_overflow@rnp", "200y"), 0);
    assert_int_not_equal(key_generate(GENKEYS, "expiration_past@rnp", "2021-01-01"), 0);

    // these should pass and go to the keystore -- 17 primary keys and 17 subkeys
    assert_int_equal(key_generate(GENKEYS, "expiration_beyond2038_relative@rnp", "20y"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_beyond2038_absolute@rnp", "2100-01-01"),
                     0);
    assert_int_equal(key_generate(GENKEYS, "expiration_absolute@rnp", exp.c_str()), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_max_32bit@rnp", "4294967295"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_max_32bit_h@rnp", "1193046h"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_1sec@rnp", "1"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_1hour@rnp", "1h"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_1day@rnp", "1d"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_1week@rnp", "1w"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_1month@rnp", "1m"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_1year@rnp", "1y"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_2sec@rnp", "2"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_2hours@rnp", "2h"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_2days@rnp", "2d"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_2weeks@rnp", "2w"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_2months@rnp", "2m"), 0);
    assert_int_equal(key_generate(GENKEYS, "expiration_2years@rnp", "2y"), 0);

    auto         keystore = new rnp_key_store_t(PGP_KEY_STORE_GPG, "", global_ctx);
    pgp_source_t src = {};
    assert_rnp_success(init_file_src(&src, GENKEYS "/pubring.gpg"));
    assert_true(rnp_key_store_load_from_src(keystore, &src, NULL));
    assert_int_equal(rnp_key_store_get_key_count(keystore), 34);
    src_close(&src);
    assert_int_equal(key_expiration_check(keystore, "expiration_max_32bit@rnp", 4294967295),
                     1);
    assert_int_equal(key_expiration_check(keystore, "expiration_max_32bit_h@rnp", 4294965600),
                     1);
    assert_int_equal(key_expiration_check(keystore, "expiration_1sec@rnp", 1), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_1hour@rnp", 3600), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_1day@rnp", 86400), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_1week@rnp", 604800), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_1month@rnp", 2678400), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_1year@rnp", 31536000), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_2sec@rnp", 2), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_2hours@rnp", 7200), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_2days@rnp", 172800), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_2weeks@rnp", 1209600), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_2months@rnp", 5356800), 1);
    assert_int_equal(key_expiration_check(keystore, "expiration_2years@rnp", 63072000), 1);
    assert_int_equal(
      key_expiration_check(keystore, "expiration_absolute@rnp", rawtime - basetime), 1);
    assert_int_equal(key_expiration_check(keystore,
                                          "expiration_beyond2038_absolute@rnp",
                                          expected_diff_beyond2038_absolute),
                     1);
    assert_int_equal(
      key_expiration_check(keystore, "expiration_beyond2038_relative@rnp", 630720000), 1);

    delete keystore;
    delete_recursively(GENKEYS);
}

TEST_F(rnp_tests, test_cli_dump)
{
    auto dump_path = rnp::path::append(original_dir(), "../examples/dump");
    char cmd[512] = {0};
    int  chnum;
    int  status;
    /* call dump's help */
    chnum = snprintf(cmd, sizeof(cmd), "%s -h", dump_path.c_str());
    assert_true(chnum < (int) sizeof(cmd));
    status = system(cmd);
    assert_true(WIFEXITED(status));
    assert_int_equal(WEXITSTATUS(status), 1);
    /* run dump on some data */
    chnum = snprintf(cmd, sizeof(cmd), "%s \"%s\"", dump_path.c_str(), KEYS "/1/pubring.gpg");
    assert_true(chnum < (int) sizeof(cmd));
    status = system(cmd);
    assert_true(WIFEXITED(status));
    assert_int_equal(WEXITSTATUS(status), 0);
    /* run dump on some data with json output */
    chnum =
      snprintf(cmd, sizeof(cmd), "%s -j \"%s\"", dump_path.c_str(), KEYS "/1/pubring.gpg");
    assert_true(chnum < (int) sizeof(cmd));
    status = system(cmd);
    assert_true(WIFEXITED(status));
    assert_int_equal(WEXITSTATUS(status), 0);
    /* run dump on directory - must fail but not crash */
    chnum = snprintf(cmd, sizeof(cmd), "%s \"%s\"", dump_path.c_str(), KEYS "/1/");
    assert_true(chnum < (int) sizeof(cmd));
    status = system(cmd);
    assert_true(WIFEXITED(status));
    assert_int_not_equal(WEXITSTATUS(status), 0);
}

TEST_F(rnp_tests, test_cli_logname)
{
    char *      logname = getenv("LOGNAME");
    char *      user = getenv("USER");
    std::string testname(user ? user : "user");
    testname.append("-test-user");

    setenv("LOGNAME", testname.c_str(), 1);
    assert_string_equal(getenv_logname(), testname.c_str());
    if (user) {
        unsetenv("LOGNAME");
        assert_string_equal(getenv_logname(), user);
    }

    if (logname) {
        setenv("LOGNAME", logname, 1);
    } else {
        unsetenv("LOGNAME");
    }
}
