// This file Copyright (C) 2013-2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.

#define LIBTRANSMISSION_VARIANT_MODULE

#include <cstdint> // int64_t
#include <locale>
#include <optional>
#include <string>
#include <string_view>

#include <gtest/gtest.h>

#include <libtransmission/crypto-utils.h>
#include <libtransmission/quark.h>
#include <libtransmission/utils.h>
#include <libtransmission/variant.h>

using namespace std::literals;

class JSONTest : public ::testing::TestWithParam<char const*>
{
protected:
    static void SetUpTestSuite()
    {
        tr_lib_init();
    }

    void SetUp() override
    {
        ::testing::TestWithParam<char const*>::SetUp();

        auto const* locale_str = GetParam();
        old_locale_ = tr_locale_set_global(locale_str);
        if (!old_locale_)
        {
            GTEST_SKIP();
        }
    }

    void TearDown() override
    {
        if (old_locale_)
        {
            tr_locale_set_global(*old_locale_);
        }

        ::testing::TestWithParam<char const*>::TearDown();
    }

private:
    std::optional<std::locale> old_locale_;
};

TEST_P(JSONTest, testElements)
{
    static auto constexpr In = std::string_view{
        "{ \"string\": \"hello world\","
        "  \"escaped\": \"bell \\b formfeed \\f linefeed \\n carriage return \\r tab \\t\","
        "  \"int\": 5, "
        "  \"float\": 6.5, "
        "  \"true\": true, "
        "  \"false\": false, "
        "  \"null\": null }"
    };

    // Same as In, just formatted differently
    static auto constexpr Out = std::string_view{
        // clang-format off
        "{"
            "\"escaped\":\"bell \\b formfeed \\f linefeed \\n carriage return \\r tab \\t\","
            "\"false\":false,"
            "\"float\":6.5,"
            "\"int\":5,"
            "\"null\":null,"
            "\"string\":\"hello world\","
            "\"true\":true"
        "}"
        // clang-format on
    };

    auto serde = tr_variant_serde::json().inplace().compact();
    auto var = serde.parse(In).value_or(tr_variant{});
    auto const* const map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);

    auto sv = map->value_if<std::string_view>(tr_quark_new("string"sv));
    ASSERT_TRUE(sv);
    EXPECT_EQ("hello world"sv, *sv);

    sv = map->value_if<std::string_view>(tr_quark_new("escaped"sv));
    ASSERT_TRUE(sv);
    EXPECT_EQ("bell \b formfeed \f linefeed \n carriage return \r tab \t"sv, *sv);

    auto i = map->value_if<int64_t>(tr_quark_new("int"sv));
    ASSERT_TRUE(i);
    EXPECT_EQ(5, *i);

    auto d = map->value_if<double>(tr_quark_new("float"sv));
    ASSERT_TRUE(d);
    EXPECT_EQ(65, int(*d * 10));

    auto b = map->value_if<bool>(tr_quark_new("true"sv));
    ASSERT_TRUE(b);
    EXPECT_TRUE(*b);

    b = map->value_if<bool>(tr_quark_new("false"sv));
    ASSERT_TRUE(b);
    EXPECT_FALSE(*b);

    auto n = map->value_if<std::nullptr_t>(tr_quark_new("null"sv));
    EXPECT_TRUE(n);

    EXPECT_EQ(serde.to_string(var), Out);
}

TEST_P(JSONTest, testUtf8)
{
    auto in = "{ \"key\": \"Letöltések\" }"sv;
    tr_quark const key = tr_quark_new("key"sv);

    auto serde = tr_variant_serde::json().inplace().compact();
    auto var = serde.parse(in).value_or(tr_variant{});
    auto* map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);
    auto sv = map->value_if<std::string_view>(key);
    ASSERT_TRUE(sv);
    EXPECT_EQ("Letöltések"sv, *sv);
    var.clear();

    in = R"({ "key": "\u005C" })"sv;
    var = serde.parse(in).value_or(tr_variant{});
    map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);
    sv = map->value_if<std::string_view>(key);
    ASSERT_TRUE(sv);
    EXPECT_EQ("\\"sv, *sv);
    var.clear();

    /**
     * 1. Feed it JSON-escaped nonascii to the JSON decoder.
     * 2. Confirm that the result is UTF-8.
     * 3. Feed the same UTF-8 back into the JSON encoder.
     * 4. Confirm that the result is UTF-8.
     * 5. Dogfood that result back into the parser.
     * 6. Confirm that the result is UTF-8.
     */
    in = R"({ "key": "Let\u00f6lt\u00e9sek" })"sv;
    var = serde.parse(in).value_or(tr_variant{});
    map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);
    sv = map->value_if<std::string_view>(key);
    ASSERT_TRUE(sv);
    EXPECT_EQ("Letöltések"sv, *sv);
    auto json = serde.to_string(var);
    var.clear();

    EXPECT_FALSE(std::empty(json));
    EXPECT_EQ(R"({"key":"Letöltések"})"sv, json);
    var = serde.parse(json).value_or(tr_variant{});
    map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);
    sv = map->value_if<std::string_view>(key);
    ASSERT_TRUE(sv);
    EXPECT_EQ("Letöltések"sv, *sv);

    // Test string known to be prone to locale issues
    // https://github.com/transmission/transmission/issues/5967
    var.clear();
    var = tr_variant::make_map(1U);
    map = var.get_if<tr_variant::Map>();
    map->try_emplace(key, "Дыскаграфія"sv);
    json = serde.to_string(var);
    EXPECT_EQ(R"({"key":"Дыскаграфія"})"sv, json);
    var = serde.parse(json).value_or(tr_variant{});
    map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);
    sv = map->value_if<std::string_view>(key);
    ASSERT_TRUE(sv);
    EXPECT_EQ("Дыскаграфія"sv, *sv);

    // Thinking emoji 🤔
    var.clear();
    var = tr_variant::make_map(1U);
    map = var.get_if<tr_variant::Map>();
    map->try_emplace(key, "\xf0\x9f\xa4\x94"sv);
    json = serde.to_string(var);
    EXPECT_EQ("{\"key\":\"\xf0\x9f\xa4\x94\"}"sv, json);
    var = serde.parse(json).value_or(tr_variant{});
    map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);
    sv = map->value_if<std::string_view>(key);
    ASSERT_TRUE(sv);
    EXPECT_EQ("\xf0\x9f\xa4\x94"sv, *sv);
}

TEST_P(JSONTest, test1)
{
    static auto constexpr Input =
        "{\n"
        "    \"headers\": {\n"
        "        \"type\": \"request\",\n"
        "        \"tag\": 666\n"
        "    },\n"
        "    \"body\": {\n"
        "        \"name\": \"torrent-info\",\n"
        "        \"arguments\": {\n"
        "            \"ids\": [ 7, 10 ]\n"
        "        }\n"
        "    }\n"
        "}\n"sv;

    auto serde = tr_variant_serde::json();
    auto var = serde.inplace().parse(Input).value_or(tr_variant{});
    auto* map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);

    auto* headers = map->find_if<tr_variant::Map>(tr_quark_new("headers"sv));
    ASSERT_NE(headers, nullptr);
    auto sv = headers->value_if<std::string_view>(tr_quark_new("type"sv));
    ASSERT_TRUE(sv);
    EXPECT_EQ("request"sv, *sv);
    auto i = headers->value_if<int64_t>(TR_KEY_tag);
    ASSERT_TRUE(i);
    EXPECT_EQ(666, *i);
    auto* body = map->find_if<tr_variant::Map>(tr_quark_new("body"sv));
    ASSERT_NE(body, nullptr);
    sv = body->value_if<std::string_view>(TR_KEY_name);
    ASSERT_TRUE(sv);
    EXPECT_EQ("torrent-info"sv, *sv);
    auto* args = body->find_if<tr_variant::Map>(tr_quark_new("arguments"sv));
    ASSERT_NE(args, nullptr);
    auto* ids = args->find_if<tr_variant::Vector>(TR_KEY_ids);
    ASSERT_NE(ids, nullptr);
    EXPECT_EQ(2U, std::size(*ids));
    i = (*ids)[0].value_if<int64_t>();
    ASSERT_TRUE(i);
    EXPECT_EQ(7, *i);
    i = (*ids)[1].value_if<int64_t>();
    ASSERT_TRUE(i);
    EXPECT_EQ(10, *i);
}

TEST_P(JSONTest, test2)
{
    static auto constexpr Input = " "sv;
    auto var = tr_variant_serde::json().inplace().parse(Input);
    EXPECT_FALSE(var.has_value());
}

TEST_P(JSONTest, test3)
{
    static auto constexpr Input =
        "{ \"error\": 2,"
        "  \"error_string\": \"torrent not registered with this tracker 6UHsVW'*C\","
        "  \"eta\": 262792,"
        "  \"id\": 25,"
        "  \"left_until_done\": 2275655680 }"sv;

    auto var = tr_variant_serde::json().inplace().parse(Input).value_or(tr_variant{});
    auto* map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);

    auto sv = map->value_if<std::string_view>(TR_KEY_error_string);
    ASSERT_TRUE(sv);
    EXPECT_EQ("torrent not registered with this tracker 6UHsVW'*C"sv, *sv);
}

TEST_P(JSONTest, unescape)
{
    static auto constexpr Input = R"({ "string-1": "\/usr\/lib" })"sv;

    auto var = tr_variant_serde::json().inplace().parse(Input).value_or(tr_variant{});
    auto* map = var.get_if<tr_variant::Map>();
    ASSERT_NE(map, nullptr);

    auto sv = map->value_if<std::string_view>(tr_quark_new("string-1"sv));
    ASSERT_TRUE(sv);
    EXPECT_EQ("/usr/lib"sv, *sv);
}

TEST_P(JSONTest, parseJsonFuzz)
{
    auto serde = tr_variant_serde::json().inplace();

    auto var = serde.parse({ nullptr, 0U });
    EXPECT_FALSE(var);

    auto buf = std::vector<char>{};
    for (size_t i = 0; i < 100000U; ++i)
    {
        buf.resize(tr_rand_int(1024U));
        tr_rand_buffer(std::data(buf), std::size(buf));

        (void)serde.parse({ std::data(buf), std::size(buf) });
    }
}

INSTANTIATE_TEST_SUITE_P( //
    JSON,
    JSONTest,
    ::testing::Values( //
        "C",
        "da_DK.UTF-8",
        "fr_FR.UTF-8",
        "ru_RU.UTF-8"));
