// 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.

#include <array>
#include <cerrno>
#include <cstddef> // size_t
#include <string_view>
#include <vector>

#include <gtest/gtest.h>

#include <libtransmission/transmission.h>

#include <libtransmission/crypto-utils.h>
#include <libtransmission/error.h>
#include <libtransmission/file-utils.h>
#include <libtransmission/file.h>
#include <libtransmission/log.h>
#include <libtransmission/torrent-metainfo.h>
#include <libtransmission/torrent.h>
#include <libtransmission/tr-strbuf.h>

#include "test-fixtures.h"

using namespace std::literals;

namespace tr::test
{

using TorrentMetainfoTest = SessionTest;

TEST_F(TorrentMetainfoTest, magnetLink)
{
    // background info @ http://wiki.theory.org/BitTorrent_Magnet-URI_Webseeding
    auto constexpr MagnetLink =
        "magnet:?"
        "xt=urn:btih:14ffe5dd23188fd5cb53a1d47f1289db70abf31e"
        "&dn=ubuntu_12_04_1_desktop_32_bit"
        "&tr=http%3A%2F%2Ftracker.publicbt.com%2Fannounce"
        "&tr=udp%3A%2F%2Ftracker.publicbt.com%3A80"
        "&ws=http%3A%2F%2Ftransmissionbt.com"sv;

    auto metainfo = tr_torrent_metainfo{};
    EXPECT_TRUE(metainfo.parseMagnet(MagnetLink));
    EXPECT_EQ(0U, metainfo.file_count()); // because it's a magnet link
    EXPECT_EQ(2U, std::size(metainfo.announce_list()));
    EXPECT_EQ(MagnetLink, metainfo.magnet());
}

#define BEFORE_PATH \
    "d10:created by25:Transmission/2.82 (14160)13:creation datei1402280218e8:encoding5:UTF-84:infod5:filesld6:lengthi2e4:pathl"
#define AFTER_PATH \
    "eed6:lengthi2e4:pathl5:b.txteee4:name3:foo12:piece lengthi32768e6:pieces20:aaaaaaaaaaaaaaaaaaaa7:privatei0eee"

// FIXME: split these into parameterized tests?
TEST_F(TorrentMetainfoTest, bucket)
{
    struct LocalTest
    {
        std::string_view benc;
        bool expected_parse_result;
    };

    static auto constexpr Tests = std::array<LocalTest, 12U>{ {
        { .benc = BEFORE_PATH "5:a.txt" AFTER_PATH, .expected_parse_result = true },
        // allow empty components, but not =all= empty components, see bug #5517
        { .benc = BEFORE_PATH "0:5:a.txt" AFTER_PATH, .expected_parse_result = true },
        { .benc = BEFORE_PATH "0:0:" AFTER_PATH, .expected_parse_result = false },
        // allow path separators in a filename (replaced with '_')
        { .benc = BEFORE_PATH "7:a/a.txt" AFTER_PATH, .expected_parse_result = true },
        // allow "." components (skipped)
        { .benc = BEFORE_PATH "1:.5:a.txt" AFTER_PATH, .expected_parse_result = true },
        { .benc = BEFORE_PATH "5:a.txt1:." AFTER_PATH, .expected_parse_result = true },
        // allow ".." components (replaced with "__")
        { .benc = BEFORE_PATH "2:..5:a.txt" AFTER_PATH, .expected_parse_result = true },
        { .benc = BEFORE_PATH "5:a.txt2:.." AFTER_PATH, .expected_parse_result = true },
        // fail when coming across an invalid character
        { .benc = "dhe", .expected_parse_result = false },
        { .benc = "d10:longer than 10 characterse", .expected_parse_result = false },
        // fail when string is too short
        { .benc = "d10:short", .expected_parse_result = false },
        // fail on empty string
        { .benc = "", .expected_parse_result = false },
    } };

    tr_logSetLevel(TR_LOG_OFF);

    for (auto const& [benc, expected_parse_result] : Tests)
    {
        auto metainfo = tr_torrent_metainfo{};
        EXPECT_EQ(expected_parse_result, metainfo.parse_benc(benc));
    }
}

TEST_F(TorrentMetainfoTest, parseBencFuzzRegressions)
{
    static auto constexpr Tests = std::array<std::string_view, 1>{
        "ZC/veNSVW0Ss+KGfMqH4DQqtYXzgmVi5oBi0XlxviLytlwwjf7MLanOcnS73eSB/iye83hVyvSWg27tPl5oqWdNEZ0euMbo7E8FH/xgTvUEOnBVgvPno50CyI7c5F2QTw16avUB7dvGzx5xIjzJ2qkD2BsNtOoiZI3skC6XwSifsDfJUN8NxHFiwvWxmZRLq2eQlE2wGxAW5aLj6U1MHDzPZ83+2o81pRyMr11bHmWFcNorTGLeOpHBd9veduHpNOKNwOatoXeb57jZCy1Zmu9y/wCuUx6DP3I5FGQ/t3AYh7w028Z/zgIlvWat6QjqSPp7j1nEbl6SNZNl1doGmusl9hvRsbaCq9b1XHpTDtQSJ8Owj07fph0p0ZVu5kJpQBfOGsHLh6ALVrTepptIvcnNW9+nauE+NJa2z+9Yla7780sCdBsGYZZA6HUr0J9GXES7+uRPPBwAl2YB1qWhCsOCClixTiAlwrsBl1bJ/a4FV04aU5jXDEYrpJMzdSAEoypDWMsn3Fc5umLqJ1jtqPqykKY0HjPrCkVAMmvmacauBzIj5Eg/uw0xtZp+wXdLQv8qyuXgsJs7dExZbgTgfPY4niTBpftM6YFQrCx/IxiMshYp7tMolykoed/8gZMm6yyWizzml4BlvnvY3+J2eVKRvS7QToRKxN5eFP9l/pflrK+8cHbwVnjQ1pE3hTQACmNIQHRTY2QoOGwG+HTwo48akfbJnjJ3F0iN6miy7lvv5u0p1rpbM2On5FJ3G98OYnzGIxf8BomHvVp/3eX6QJZUMZKsUTpgbRqg0AJH9FjiERQ9v6B25Va+Q0yV8z5DmiA5AgyIwkIzlSBAl0PYsNaw+rH06a93yBhAfK6EPSArYLjMI6o/1kF4UxNyfE+F79xbdCAKRAX3iJ7DH1GncFoIQ1fZd/uZaF9tXjViQ7P/sHuKdZvfLpvJq88JV5Pcdsfdlle86QAF4weB+k/k8f/xgvxRNbbcAfjLvEHhDBzfEvHkgFrW19WvLHyAqjjUovpecIu3KeCqwyOr1dHViUVelxqc5BklyGQ+Asd6GnWPSzO5Hamj4rYrapgogEup5PKm1j2CgL2HH2tySWwjgtOWbooGhsdBnCeQOsapCxwc6ALtudG4Q9RBu6A6pLUfFE3rm1RuvNGoJNHiEQ4BAFiqLpYJd4lR7V2fI6EIKug0dB3SpHpUeNCQbG67IM+kVe0I+vP3cECGOGXo="sv,
    };

    for (auto const& test : Tests)
    {
        auto tm = tr_torrent_metainfo{};
        tm.parse_benc(tr_base64_decode(test));
    }
}

TEST_F(TorrentMetainfoTest, parseBencFuzz)
{
    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));
        // std::cerr << '[' << tr_base64_encode({ std::data(buf), std::size(buf) }) << ']' << std::endl;

        auto tm = tr_torrent_metainfo{};
        tm.parse_benc({ std::data(buf), std::size(buf) });
    }
}

TEST_F(TorrentMetainfoTest, AndroidTorrent)
{
    auto const filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/Android-x86 8.1 r6 iso.torrent"sv };

    auto* ctor = tr_ctorNew(session_);
    auto error = tr_error{};
    EXPECT_TRUE(tr_ctorSetMetainfoFromFile(ctor, filename, &error));
    EXPECT_FALSE(error) << error;
    auto const* const metainfo = tr_ctorGetMetainfo(ctor);
    EXPECT_NE(nullptr, metainfo);
    EXPECT_EQ(336, metainfo->info_dict_offset());
    EXPECT_EQ(26583, metainfo->info_dict_size());
    tr_ctorFree(ctor);
}

TEST_F(TorrentMetainfoTest, ctorSaveContents)
{
    auto const sandbox = tr::test::Sandbox::createSandbox(::testing::TempDir(), "transmission-test-XXXXXX");
    auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/Android-x86 8.1 r6 iso.torrent"sv };
    auto const tgt_filename = tr_pathbuf{ sandbox, "save-contents-test.torrent" };

    // try saving without passing any metainfo.
    auto* ctor = tr_ctorNew(session_);
    auto error = tr_error{};
    EXPECT_FALSE(ctor->save(tgt_filename, &error));
    EXPECT_TRUE(error);
    EXPECT_EQ(EINVAL, error.code());
    error = {};

    // now try saving _with_ metainfo
    EXPECT_TRUE(tr_ctorSetMetainfoFromFile(ctor, src_filename, &error));
    EXPECT_FALSE(error) << error;
    EXPECT_TRUE(ctor->save(tgt_filename, &error));
    EXPECT_FALSE(error) << error;

    // the saved contents should match the source file's contents
    auto src_contents = std::vector<char>{};
    EXPECT_TRUE(tr_file_read(src_filename.sv(), src_contents, &error));
    auto tgt_contents = std::vector<char>{};
    EXPECT_TRUE(tr_file_read(tgt_filename.sv(), tgt_contents, &error));
    EXPECT_EQ(src_contents, tgt_contents);

    // cleanup
    EXPECT_TRUE(tr_sys_path_remove(tgt_filename, &error));
    EXPECT_FALSE(error) << error;
    tr_ctorFree(ctor);
}

TEST_F(TorrentMetainfoTest, magnetInfoHash)
{
    // compatibility with magnet torrents created by Transmission <= 3.0
    auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/gimp-2.10.32-1-arm64.dmg.torrent"sv };
    auto tm = tr_torrent_metainfo{};
    EXPECT_TRUE(tm.parse_torrent_file(src_filename));
}

TEST_F(TorrentMetainfoTest, HoffmanStyleWebseeds)
{
    auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/debian-11.2.0-amd64-DVD-1.iso.torrent"sv };
    auto tm = tr_torrent_metainfo{};
    EXPECT_TRUE(tm.parse_torrent_file(src_filename));
    EXPECT_EQ(size_t{ 2 }, tm.webseed_count());
    EXPECT_EQ(
        "https://cdimage.debian.org/cdimage/release/11.2.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-dvd/debian-11.2.0-amd64-DVD-1.iso"sv,
        tm.webseed(0));
    EXPECT_EQ(
        "https://cdimage.debian.org/cdimage/archive/11.2.0//srv/cdbuilder.debian.org/dst/deb-cd/weekly-builds/amd64/iso-dvd/debian-11.2.0-amd64-DVD-1.iso"sv,
        tm.webseed(1));
}

TEST_F(TorrentMetainfoTest, GetRightStyleWebseedList)
{
    auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/webseed-getright-list.torrent"sv };
    auto tm = tr_torrent_metainfo{};
    EXPECT_TRUE(tm.parse_torrent_file(src_filename));
    EXPECT_EQ(size_t{ 2 }, tm.webseed_count());
    EXPECT_EQ("http://www.webseed-one.com/"sv, tm.webseed(0));
    EXPECT_EQ("http://webseed-two.com/"sv, tm.webseed(1));
}

TEST_F(TorrentMetainfoTest, GetRightStyleWebseedString)
{
    auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, "/webseed-getright-string.torrent"sv };
    auto tm = tr_torrent_metainfo{};
    EXPECT_TRUE(tm.parse_torrent_file(src_filename));
    EXPECT_EQ(size_t{ 1 }, tm.webseed_count());
    EXPECT_EQ("http://www.webseed-one.com/"sv, tm.webseed(0));
}

TEST_F(TorrentMetainfoTest, piecesKeyTest)
{
    static auto constexpr BadTorrents = std::array<std::pair<std::string_view, std::string_view>, 7U>{ {
        { "missing-pieces-key.torrent"sv, "missing v1 metadata"sv },
        { "bad-pieces-key.torrent"sv, "missing v1 metadata"sv },
        { "empty-pieces-key.torrent"sv, "missing v1 metadata"sv },
        { "too-few-pieces.torrent"sv, "'pieces' and torrent size mismatch: 7, 6"sv },
        { "too-many-pieces.torrent"sv, "'pieces' and torrent size mismatch: 5, 6"sv },
        { "dup-pieces-key.torrent"sv, "invalid duplicate 'pieces'"sv },
        { "invalid-pieces-length.torrent"sv,
          "invalid 'pieces' size: 119"sv }, // Test for https://github.com/transmission/transmission/issues/3591
    } };

    {
        auto tm = tr_torrent_metainfo{};
        static auto constexpr Path = LIBTRANSMISSION_TEST_ASSETS_DIR "/perfect-pieces.torrent"sv;
        EXPECT_TRUE(tm.parse_torrent_file(Path));
    }

    for (auto const& [name, errmsg] : BadTorrents)
    {
        auto error = tr_error{};
        auto tm = tr_torrent_metainfo{};
        auto const path = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, '/', name };
        EXPECT_FALSE(tm.parse_torrent_file(path, nullptr, &error));
        EXPECT_EQ(error.code(), EINVAL);
        EXPECT_EQ(error.message(), errmsg);
    }
}

TEST_F(TorrentMetainfoTest, pathKeyTest)
{
    static auto constexpr BadTorrents = std::array{ "dup-files.torrent"sv };

    for (auto const& name : BadTorrents)
    {
        auto tm = tr_torrent_metainfo{};
        auto const path = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, '/', name };
        EXPECT_FALSE(tm.parse_torrent_file(path));
    }
}

TEST_F(TorrentMetainfoTest, utf8Test)
{
// MacOS implementation uses non-deterministic conversion for illegal UTF-8
#if (defined(__APPLE__) && defined(__clang__))
    GTEST_SKIP();
#endif
    auto const src_filename = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR "/bad-utf8-path.torrent"sv };
    auto tm = tr_torrent_metainfo{};
    EXPECT_TRUE(tm.parse_torrent_file(src_filename));

    // good name: bad-utf8-path/πfile.😀😀😀
    EXPECT_EQ("bad-utf8-path/\u03C0file.\U0001F600\U0001F600\U0001F600", tm.file_subpath(0));
    // bad name, gets masked to: bad-utf8-path/file�.foo
    EXPECT_EQ("bad-utf8-path/file\uFFFD.foo", tm.file_subpath(1));
}

TEST_F(TorrentMetainfoTest, preservesInfoDictOrder)
{
    // These torrents' info dict are ordered differently, but they are identical otherwise.
    //
    // BEP-3 requires any dict to be sorted, but there are torrents where this is not
    // obeyed in the wild.
    //
    // These v1 hashes are calculated by hand with this command:
    // dd if=${TORRENT_FILE} skip=225 count=343 bs=1 status=none | sha1sum -b
    static auto constexpr Tests = std::array<std::pair<std::string_view, std::string_view>, 2U>{ {
        { "unordered-info-dict.torrent"sv, "e44a814ac5ed962f9181638d1136a6aface3f734"sv },
        { "perfect-pieces.torrent"sv, "efa7eb3dec5661698f353fde079418543a63e872"sv },
    } };

    for (auto const& [name, v1hash] : Tests)
    {
        auto tm = tr_torrent_metainfo{};
        auto const path = tr_pathbuf{ LIBTRANSMISSION_TEST_ASSETS_DIR, '/', name };
        EXPECT_TRUE(tm.parse_torrent_file(path)) << name;

        EXPECT_EQ(tm.info_hash_string(), v1hash);
    }
}

} // namespace tr::test
