#include <test/jtx.h>
#include <test/jtx/envconfig.h>

#include <xrpld/app/misc/TxQ.h>
#include <xrpld/rpc/detail/Tuning.h>

#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/ResourceManager.h>
#include <xrpl/resource/detail/Entry.h>
#include <xrpl/resource/detail/Tuning.h>

#include <boost/algorithm/string/predicate.hpp>

namespace xrpl {

class NoRippleCheck_test : public beast::unit_test::suite
{
    void
    testBadInput()
    {
        testcase("Bad input to noripple_check");

        using namespace test::jtx;
        Env env{*this};

        auto const alice = Account{"alice"};
        env.fund(XRP(10000), alice);
        env.close();

        {  // missing account field
            auto const result = env.rpc("json", "noripple_check", "{}")[jss::result];
            BEAST_EXPECT(result[jss::error] == "invalidParams");
            BEAST_EXPECT(result[jss::error_message] == "Missing field 'account'.");
        }

        {  // missing role field
            Json::Value params;
            params[jss::account] = alice.human();
            auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
            BEAST_EXPECT(result[jss::error] == "invalidParams");
            BEAST_EXPECT(result[jss::error_message] == "Missing field 'role'.");
        }

        // test account non-string
        {
            auto testInvalidAccountParam = [&](auto const& param) {
                Json::Value params;
                params[jss::account] = param;
                params[jss::role] = "user";
                auto jrr = env.rpc("json", "noripple_check", to_string(params))[jss::result];
                BEAST_EXPECT(jrr[jss::error] == "invalidParams");
                BEAST_EXPECT(jrr[jss::error_message] == "Invalid field 'account'.");
            };

            testInvalidAccountParam(1);
            testInvalidAccountParam(1.1);
            testInvalidAccountParam(true);
            testInvalidAccountParam(Json::Value(Json::nullValue));
            testInvalidAccountParam(Json::Value(Json::objectValue));
            testInvalidAccountParam(Json::Value(Json::arrayValue));
        }

        {  // invalid role field
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::role] = "not_a_role";
            auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
            BEAST_EXPECT(result[jss::error] == "invalidParams");
            BEAST_EXPECT(result[jss::error_message] == "Invalid field 'role'.");
        }

        {  // invalid limit
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::role] = "user";
            params[jss::limit] = -1;
            auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
            BEAST_EXPECT(result[jss::error] == "invalidParams");
            BEAST_EXPECT(
                result[jss::error_message] == "Invalid field 'limit', not unsigned integer.");
        }

        {  // invalid ledger (hash)
            Json::Value params;
            params[jss::account] = alice.human();
            params[jss::role] = "user";
            params[jss::ledger_hash] = 1;
            auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
            BEAST_EXPECT(result[jss::error] == "invalidParams");
            BEAST_EXPECT(
                result[jss::error_message] == "Invalid field 'ledger_hash', not hex string.");
        }

        {  // account not found
            Json::Value params;
            params[jss::account] = Account{"nobody"}.human();
            params[jss::role] = "user";
            params[jss::ledger] = "current";
            auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
            BEAST_EXPECT(result[jss::error] == "actNotFound");
            BEAST_EXPECT(result[jss::error_message] == "Account not found.");
        }

        {  // passing an account private key will cause
           // parsing as a seed to fail
            Json::Value params;
            params[jss::account] = toBase58(TokenType::NodePrivate, alice.sk());
            params[jss::role] = "user";
            params[jss::ledger] = "current";
            auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
            BEAST_EXPECT(result[jss::error] == "actMalformed");
            BEAST_EXPECT(result[jss::error_message] == "Account malformed.");
        }

        {
            // ledger and ledger_hash are included
            Json::Value params;
            params[jss::account] = Account{"nobody"}.human();
            params[jss::role] = "user";
            params[jss::ledger] = "current";
            params[jss::ledger_hash] = "ABCDEF";
            auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
            BEAST_EXPECT(result[jss::error] == "invalidParams");
            BEAST_EXPECT(
                result[jss::error_message] ==
                "Exactly one of 'ledger', 'ledger_hash', or 'ledger_index' can "
                "be specified.");
        }

        {
            // invalid ledger
            Json::Value params;
            params[jss::account] = Account{"nobody"}.human();
            params[jss::role] = "user";
            params[jss::ledger] = Json::objectValue;
            auto const result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
            BEAST_EXPECT(result[jss::error] == "invalidParams");
            BEAST_EXPECT(
                result[jss::error_message] == "Invalid field 'ledger', not string or number.");
        }
    }

    void
    testBasic(bool user, bool problems)
    {
        testcase << "Request noripple_check for " << (user ? "user" : "gateway") << " role, expect"
                 << (problems ? "" : " no") << " problems";

        using namespace test::jtx;
        Env env{*this};

        auto const gw = Account{"gw"};
        auto const alice = Account{"alice"};

        env.fund(XRP(10000), gw, alice);
        if ((user && problems) || (!user && !problems))
        {
            env(fset(alice, asfDefaultRipple));
            env(trust(alice, gw["USD"](100)));
        }
        else
        {
            env(fclear(alice, asfDefaultRipple));
            env(trust(alice, gw["USD"](100), gw, tfSetNoRipple));
        }
        env.close();

        Json::Value params;
        params[jss::account] = alice.human();
        params[jss::role] = (user ? "user" : "gateway");
        params[jss::ledger] = "current";
        auto result = env.rpc("json", "noripple_check", to_string(params))[jss::result];

        auto const pa = result["problems"];
        if (!BEAST_EXPECT(pa.isArray()))
            return;

        if (problems)
        {
            if (!BEAST_EXPECT(pa.size() == 2))
                return;

            if (user)
            {
                BEAST_EXPECT(boost::starts_with(pa[0u].asString(), "You appear to have set"));
                BEAST_EXPECT(boost::starts_with(pa[1u].asString(), "You should probably set"));
            }
            else
            {
                BEAST_EXPECT(boost::starts_with(pa[0u].asString(), "You should immediately set"));
                BEAST_EXPECT(boost::starts_with(pa[1u].asString(), "You should clear"));
            }
        }
        else
        {
            BEAST_EXPECT(pa.size() == 0);
        }

        // now make a second request asking for the relevant transactions this
        // time.
        params[jss::transactions] = true;
        result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
        if (!BEAST_EXPECT(result[jss::transactions].isArray()))
            return;

        auto const txs = result[jss::transactions];
        if (problems)
        {
            if (!BEAST_EXPECT(txs.size() == (user ? 1 : 2)))
                return;

            if (!user)
            {
                BEAST_EXPECT(txs[0u][jss::Account] == alice.human());
                BEAST_EXPECT(txs[0u][jss::TransactionType] == jss::AccountSet);
            }

            BEAST_EXPECT(result[jss::transactions][txs.size() - 1][jss::Account] == alice.human());
            BEAST_EXPECT(
                result[jss::transactions][txs.size() - 1][jss::TransactionType] == jss::TrustSet);
            BEAST_EXPECT(
                result[jss::transactions][txs.size() - 1][jss::LimitAmount] ==
                gw["USD"](100).value().getJson(JsonOptions::none));
        }
        else
        {
            BEAST_EXPECT(txs.size() == 0);
        }
    }

public:
    void
    run() override
    {
        testBadInput();
        for (auto user : {true, false})
        {
            for (auto problem : {true, false})
                testBasic(user, problem);
        }
    }
};

class NoRippleCheckLimits_test : public beast::unit_test::suite
{
    void
    testLimits(bool admin)
    {
        testcase << "Check limits in returned data, " << (admin ? "admin" : "non-admin");

        using namespace test::jtx;

        Env env{*this, admin ? envconfig() : envconfig(no_admin)};

        auto const alice = Account{"alice"};
        env.fund(XRP(100000), alice);
        env(fset(alice, asfDefaultRipple));
        env.close();

        auto checkBalance = [&env]() {
            // this is endpoint drop prevention. Non admin ports will drop
            // requests if they are coming too fast, so we manipulate the
            // resource manager here to reset the endpoint balance (for
            // localhost) if we get too close to the drop limit. It would
            // be better if we could add this functionality to Env somehow
            // or otherwise disable endpoint charging for certain test
            // cases.
            using namespace xrpl::Resource;
            using namespace std::chrono;
            using namespace beast::IP;
            auto c = env.app().getResourceManager().newInboundEndpoint(
                Endpoint::from_string(test::getEnvLocalhostAddr()));

            // if we go above the warning threshold, reset
            if (c.balance() > warningThreshold)
            {
                using ct = beast::abstract_clock<steady_clock>;
                c.entry().local_balance =
                    DecayingSample<decayWindowSeconds, ct>{steady_clock::now()};
            }
        };

        for (auto i = 0; i < xrpl::RPC::Tuning::noRippleCheck.rmax + 5; ++i)
        {
            if (!admin)
                checkBalance();

            auto& txq = env.app().getTxQ();
            auto const gw = Account{"gw" + std::to_string(i)};
            env.memoize(gw);
            auto const baseFee = env.current()->fees().base;
            env(pay(env.master, gw, XRP(1000)),
                seq(autofill),
                fee(toDrops(txq.getMetrics(*env.current()).openLedgerFeeLevel, baseFee) + 1),
                sig(autofill));
            env(fset(gw, asfDefaultRipple),
                seq(autofill),
                fee(toDrops(txq.getMetrics(*env.current()).openLedgerFeeLevel, baseFee) + 1),
                sig(autofill));
            env(trust(alice, gw["USD"](10)),
                fee(toDrops(txq.getMetrics(*env.current()).openLedgerFeeLevel, baseFee) + 1));
            env.close();
        }

        // default limit value
        Json::Value params;
        params[jss::account] = alice.human();
        params[jss::role] = "user";
        params[jss::ledger] = "current";
        auto result = env.rpc("json", "noripple_check", to_string(params))[jss::result];

        BEAST_EXPECT(result["problems"].size() == 301);

        // one below minimum
        params[jss::limit] = 9;
        result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
        BEAST_EXPECT(result["problems"].size() == (admin ? 10 : 11));

        // at minimum
        params[jss::limit] = 10;
        result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
        BEAST_EXPECT(result["problems"].size() == 11);

        // at max
        params[jss::limit] = 400;
        result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
        BEAST_EXPECT(result["problems"].size() == 401);

        // at max+1
        params[jss::limit] = 401;
        result = env.rpc("json", "noripple_check", to_string(params))[jss::result];
        BEAST_EXPECT(result["problems"].size() == (admin ? 402 : 401));
    }

public:
    void
    run() override
    {
        for (auto admin : {true, false})
            testLimits(admin);
    }
};

BEAST_DEFINE_TESTSUITE(NoRippleCheck, rpc, xrpl);

// These tests that deal with limit amounts are slow because of the
// offer/account setup, so making them manual -- the additional coverage
// provided by them is minimal

BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(NoRippleCheckLimits, rpc, xrpl, 1);

}  // namespace xrpl
