#include <test/jtx.h>

#include <xrpl/basics/StringUtilities.h>
#include <xrpl/protocol/jss.h>

namespace xrpl {

class LedgerData_test : public beast::unit_test::suite
{
public:
    // test helper
    static bool
    checkMarker(Json::Value const& val)
    {
        return val.isMember(jss::marker) && val[jss::marker].isString() &&
            !val[jss::marker].asString().empty();
    }

    void
    testCurrentLedgerToLimits(bool asAdmin)
    {
        using namespace test::jtx;
        Env env{*this, asAdmin ? envconfig() : envconfig(no_admin)};
        Account const gw{"gateway"};
        auto const USD = gw["USD"];
        env.fund(XRP(100000), gw);

        int const max_limit = 256;  // would be 2048 for binary requests, no
                                    // need to test that here

        for (auto i = 0; i < max_limit + 10; i++)
        {
            Account const bob{std::string("bob") + std::to_string(i)};
            env.fund(XRP(1000), bob);
        }
        // Note that calls to env.close() fail without admin permission.
        if (asAdmin)
            env.close();

        // with no limit specified, we get the max_limit if the total number of
        // accounts is greater than max, which it is here
        Json::Value jvParams;
        jvParams[jss::ledger_index] = "current";
        jvParams[jss::binary] = false;
        {
            auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            BEAST_EXPECT(
                jrr[jss::ledger_current_index].isIntegral() &&
                jrr[jss::ledger_current_index].asInt() > 0);
            BEAST_EXPECT(checkMarker(jrr));
            BEAST_EXPECT(checkArraySize(jrr[jss::state], max_limit));
        }

        // check limits values around the max_limit (+/- 1)
        for (auto delta = -1; delta <= 1; delta++)
        {
            jvParams[jss::limit] = max_limit + delta;
            auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            BEAST_EXPECT(checkArraySize(
                jrr[jss::state], (delta > 0 && !asAdmin) ? max_limit : max_limit + delta));
        }
    }

    void
    testCurrentLedgerBinary()
    {
        using namespace test::jtx;
        Env env{*this, envconfig(no_admin)};
        Account const gw{"gateway"};
        auto const USD = gw["USD"];
        env.fund(XRP(100000), gw);

        int const num_accounts = 10;

        for (auto i = 0; i < num_accounts; i++)
        {
            Account const bob{std::string("bob") + std::to_string(i)};
            env.fund(XRP(1000), bob);
        }

        // with no limit specified, we should get all of our fund entries
        // plus three more related to the gateway setup
        Json::Value jvParams;
        jvParams[jss::ledger_index] = "current";
        jvParams[jss::binary] = true;
        auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
        BEAST_EXPECT(
            jrr[jss::ledger_current_index].isIntegral() &&
            jrr[jss::ledger_current_index].asInt() > 0);
        BEAST_EXPECT(!jrr.isMember(jss::marker));
        BEAST_EXPECT(checkArraySize(jrr[jss::state], num_accounts + 4));
    }

    void
    testBadInput()
    {
        using namespace test::jtx;
        Env env{*this};
        Account const gw{"gateway"};
        auto const USD = gw["USD"];
        Account const bob{"bob"};

        env.fund(XRP(10000), gw, bob);
        env.trust(USD(1000), bob);

        {
            // bad limit
            Json::Value jvParams;
            jvParams[jss::limit] = "0";  // NOT an integer
            auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            BEAST_EXPECT(jrr[jss::error] == "invalidParams");
            BEAST_EXPECT(jrr[jss::status] == "error");
            BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'limit', not integer.");
        }

        {
            // invalid marker
            Json::Value jvParams;
            jvParams[jss::marker] = "NOT_A_MARKER";
            auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            BEAST_EXPECT(jrr[jss::error] == "invalidParams");
            BEAST_EXPECT(jrr[jss::status] == "error");
            BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'marker', not valid.");
        }

        {
            // invalid marker - not a string
            Json::Value jvParams;
            jvParams[jss::marker] = 1;
            auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            BEAST_EXPECT(jrr[jss::error] == "invalidParams");
            BEAST_EXPECT(jrr[jss::status] == "error");
            BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'marker', not valid.");
        }

        {
            // ask for a bad ledger index
            Json::Value jvParams;
            jvParams[jss::ledger_index] = 10u;
            auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            BEAST_EXPECT(jrr[jss::error] == "lgrNotFound");
            BEAST_EXPECT(jrr[jss::status] == "error");
            BEAST_EXPECT(jrr[jss::error_message] == "ledgerNotFound");
        }
    }

    void
    testMarkerFollow()
    {
        using namespace test::jtx;
        Env env{*this, envconfig(no_admin)};
        Account const gw{"gateway"};
        auto const USD = gw["USD"];
        env.fund(XRP(100000), gw);

        int const num_accounts = 20;

        for (auto i = 0; i < num_accounts; i++)
        {
            Account const bob{std::string("bob") + std::to_string(i)};
            env.fund(XRP(1000), bob);
        }

        // with no limit specified, we should get all of our fund entries
        // plus three more related to the gateway setup
        Json::Value jvParams;
        jvParams[jss::ledger_index] = "current";
        jvParams[jss::binary] = false;
        auto jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
        auto const total_count = jrr[jss::state].size();

        // now make request with a limit and loop until we get all
        jvParams[jss::limit] = 5;
        jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
        BEAST_EXPECT(checkMarker(jrr));
        auto running_total = jrr[jss::state].size();
        while (jrr.isMember(jss::marker))
        {
            jvParams[jss::marker] = jrr[jss::marker];
            jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            running_total += jrr[jss::state].size();
        }
        BEAST_EXPECT(running_total == total_count);
    }

    void
    testLedgerHeader()
    {
        using namespace test::jtx;
        Env env{*this};
        env.fund(XRP(100000), "alice");
        env.close();

        // Ledger header should be present in the first query
        {
            // Closed ledger with non binary form
            Json::Value jvParams;
            jvParams[jss::ledger_index] = "closed";
            auto jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            if (BEAST_EXPECT(jrr.isMember(jss::ledger)))
            {
                BEAST_EXPECT(
                    jrr[jss::ledger][jss::ledger_hash] == to_string(env.closed()->header().hash));
            }
        }
        {
            // Closed ledger with binary form
            Json::Value jvParams;
            jvParams[jss::ledger_index] = "closed";
            jvParams[jss::binary] = true;
            auto jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            if (BEAST_EXPECT(jrr.isMember(jss::ledger)))
            {
                auto data = strUnHex(jrr[jss::ledger][jss::ledger_data].asString());
                if (BEAST_EXPECT(data); data.has_value())
                {
                    Serializer s(data->data(), data->size());
                    std::uint32_t seq = 0;
                    BEAST_EXPECT(s.getInteger<std::uint32_t>(seq, 0));
                    BEAST_EXPECT(seq == 3);
                }
            }
        }
        {
            // Current ledger with binary form
            Json::Value jvParams;
            jvParams[jss::binary] = true;
            auto jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            BEAST_EXPECT(jrr.isMember(jss::ledger));
            BEAST_EXPECT(!jrr[jss::ledger].isMember(jss::ledger_data));
        }
    }

    void
    testLedgerType()
    {
        // Put a bunch of different LedgerEntryTypes into a ledger
        using namespace test::jtx;

        // Make sure fixInnerObjTemplate2 doesn't break amendments.
        for (FeatureBitset const& features :
             {testable_amendments() - fixInnerObjTemplate2,
              testable_amendments() | fixInnerObjTemplate2})
        {
            using namespace std::chrono;
            Env env{*this, envconfig(validator, ""), features};

            Account const gw{"gateway"};
            auto const USD = gw["USD"];
            env.fund(XRP(100000), gw);

            auto makeRequest = [&env](Json::StaticString const& type) {
                Json::Value jvParams;
                jvParams[jss::ledger_index] = "current";
                jvParams[jss::type] = type;
                return env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
            };

            // Assert that state is an empty array.
            for (auto const& type :
                 {jss::amendments,
                  jss::check,
                  jss::directory,
                  jss::offer,
                  jss::signer_list,
                  jss::state,
                  jss::ticket,
                  jss::escrow,
                  jss::payment_channel,
                  jss::deposit_preauth})
            {
                auto const jrr = makeRequest(type);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 0));
            }

            int const num_accounts = 10;

            for (auto i = 0; i < num_accounts; i++)
            {
                Account const bob{std::string("bob") + std::to_string(i)};
                env.fund(XRP(1000), bob);
            }
            env(offer(Account{"bob0"}, USD(100), XRP(100)));
            env.trust(Account{"bob2"}["USD"](100), Account{"bob3"});

            auto majorities = getMajorityAmendments(*env.closed());
            for (int i = 0; i <= 256; ++i)
            {
                env.close();
                majorities = getMajorityAmendments(*env.closed());
                if (!majorities.empty())
                    break;
            }

            env(signers(Account{"bob0"}, 1, {{Account{"bob1"}, 1}, {Account{"bob2"}, 1}}));
            env(ticket::create(env.master, 1));

            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::EscrowCreate;
                jv[jss::Account] = Account{"bob5"}.human();
                jv[jss::Destination] = Account{"bob6"}.human();
                jv[jss::Amount] = XRP(50).value().getJson(JsonOptions::none);
                jv[sfFinishAfter.fieldName] =
                    NetClock::time_point{env.now() + 10s}.time_since_epoch().count();
                env(jv);
            }

            {
                Json::Value jv;
                jv[jss::TransactionType] = jss::PaymentChannelCreate;
                jv[jss::Account] = Account{"bob6"}.human();
                jv[jss::Destination] = Account{"bob7"}.human();
                jv[jss::Amount] = XRP(100).value().getJson(JsonOptions::none);
                jv[jss::SettleDelay] = NetClock::duration{10s}.count();
                jv[sfPublicKey.fieldName] = strHex(Account{"bob6"}.pk().slice());
                jv[sfCancelAfter.fieldName] =
                    NetClock::time_point{env.now() + 300s}.time_since_epoch().count();
                env(jv);
            }

            env(check::create("bob6", "bob7", XRP(100)));

            // bob9 DepositPreauths bob4 and bob8.
            env(deposit::auth(Account{"bob9"}, Account{"bob4"}));
            env(deposit::auth(Account{"bob9"}, Account{"bob8"}));
            env.close();

            // Now fetch each type

            {  // jvParams[jss::type] = "account";
                auto const jrr = makeRequest(jss::account);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 12));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::AccountRoot);
            }

            {  // jvParams[jss::type] = "amendments";
                auto const jrr = makeRequest(jss::amendments);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::Amendments);
            }

            {  // jvParams[jss::type] = "check";
                auto const jrr = makeRequest(jss::check);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::Check);
            }

            {  // jvParams[jss::type] = "directory";
                auto const jrr = makeRequest(jss::directory);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 9));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::DirectoryNode);
            }

            {  // jvParams[jss::type] = "fee";
                auto const jrr = makeRequest(jss::fee);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::FeeSettings);
            }

            {  // jvParams[jss::type] = "hashes";
                auto const jrr = makeRequest(jss::hashes);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 2));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::LedgerHashes);
            }

            {  // jvParams[jss::type] = "offer";
                auto const jrr = makeRequest(jss::offer);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::Offer);
            }

            {  // jvParams[jss::type] = "signer_list";
                auto const jrr = makeRequest(jss::signer_list);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::SignerList);
            }

            {  // jvParams[jss::type] = "state";
                auto const jrr = makeRequest(jss::state);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::RippleState);
            }

            {  // jvParams[jss::type] = "ticket";
                auto const jrr = makeRequest(jss::ticket);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::Ticket);
            }

            {  // jvParams[jss::type] = "escrow";
                auto const jrr = makeRequest(jss::escrow);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::Escrow);
            }

            {  // jvParams[jss::type] = "payment_channel";
                auto const jrr = makeRequest(jss::payment_channel);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 1));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::PayChannel);
            }

            {  // jvParams[jss::type] = "deposit_preauth";
                auto const jrr = makeRequest(jss::deposit_preauth);
                BEAST_EXPECT(checkArraySize(jrr[jss::state], 2));
                for (auto const& j : jrr[jss::state])
                    BEAST_EXPECT(j["LedgerEntryType"] == jss::DepositPreauth);
            }

            {  // jvParams[jss::type] = "misspelling";
                Json::Value jvParams;
                jvParams[jss::ledger_index] = "current";
                jvParams[jss::type] = "misspelling";
                auto const jrr = env.rpc("json", "ledger_data", to_string(jvParams))[jss::result];
                BEAST_EXPECT(jrr.isMember("error"));
                BEAST_EXPECT(jrr["error"] == "invalidParams");
                BEAST_EXPECT(jrr["error_message"] == "Invalid field 'type'.");
            }
        }
    }

    void
    run() override
    {
        testCurrentLedgerToLimits(true);
        testCurrentLedgerToLimits(false);
        testCurrentLedgerBinary();
        testBadInput();
        testMarkerFollow();
        testLedgerHeader();
        testLedgerType();
    }
};

BEAST_DEFINE_TESTSUITE_PRIO(LedgerData, rpc, xrpl, 1);

}  // namespace xrpl
