#include <test/jtx/Env.h>
#include <test/jtx/JSONRPCClient.h>
#include <test/jtx/balance.h>
#include <test/jtx/fee.h>
#include <test/jtx/flags.h>
#include <test/jtx/pay.h>
#include <test/jtx/seq.h>
#include <test/jtx/sig.h>
#include <test/jtx/trust.h>
#include <test/jtx/utility.h>

#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/rpc/RPCCall.h>

#include <xrpl/basics/Slice.h>
#include <xrpl/basics/contract.h>
#include <xrpl/basics/scope.h>
#include <xrpl/core/NetworkIDService.h>
#include <xrpl/json/to_string.h>
#include <xrpl/net/HTTPClient.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TER.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/UintTypes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/server/NetworkOPs.h>

#include <memory>
#include <source_location>

namespace xrpl {
namespace test {
namespace jtx {

//------------------------------------------------------------------------------

Env::AppBundle::AppBundle(
    beast::unit_test::suite& suite,
    std::unique_ptr<Config> config,
    std::unique_ptr<Logs> logs,
    beast::severities::Severity thresh)
    : AppBundle()
{
    using namespace beast::severities;
    if (logs)
    {
        setDebugLogSink(logs->makeSink("Debug", kFatal));
    }
    else
    {
        logs = std::make_unique<SuiteLogs>(suite);
        // Use kFatal threshold to reduce noise from STObject.
        setDebugLogSink(std::make_unique<SuiteJournalSink>("Debug", kFatal, suite));
    }
    auto timeKeeper_ = std::make_unique<ManualTimeKeeper>();
    timeKeeper = timeKeeper_.get();
    // Hack so we don't have to call Config::setup
    HTTPClient::initializeSSLContext(
        config->SSL_VERIFY_DIR, config->SSL_VERIFY_FILE, config->SSL_VERIFY, debugLog());
    owned = make_Application(std::move(config), std::move(logs), std::move(timeKeeper_));
    app = owned.get();
    app->logs().threshold(thresh);
    if (!app->setup({}))
        Throw<std::runtime_error>("Env::AppBundle: setup failed");
    timeKeeper->set(app->getLedgerMaster().getClosedLedger()->header().closeTime);
    app->start(false /*don't start timers*/);
    thread = std::thread([&]() { app->run(); });

    client = makeJSONRPCClient(app->config());
}

Env::AppBundle::~AppBundle()
{
    client.reset();
    // Make sure all jobs finish, otherwise tests
    // might not get the coverage they expect.
    if (app != nullptr)
    {
        app->getJobQueue().rendezvous();
        app->signalStop("~AppBundle");
    }
    if (thread.joinable())
        thread.join();

    // Remove the debugLogSink before the suite goes out of scope.
    setDebugLogSink(nullptr);
}

//------------------------------------------------------------------------------

std::shared_ptr<ReadView const>
Env::closed()
{
    return app().getLedgerMaster().getClosedLedger();
}

bool
Env::close(NetClock::time_point closeTime, std::optional<std::chrono::milliseconds> consensusDelay)
{
    // Round up to next distinguishable value
    using namespace std::chrono_literals;
    bool res = true;
    closeTime += closed()->header().closeTimeResolution - 1s;
    timeKeeper().set(closeTime);
    // Go through the rpc interface unless we need to simulate
    // a specific consensus delay.
    if (consensusDelay)
    {
        app().getOPs().acceptLedger(consensusDelay);
    }
    else
    {
        auto resp = rpc("ledger_accept");
        if (resp["result"]["status"] != std::string("success"))
        {
            std::string reason = "internal error";
            if (resp.isMember("error_what"))
            {
                reason = resp["error_what"].asString();
            }
            else if (resp.isMember("error_message"))
            {
                reason = resp["error_message"].asString();
            }
            else if (resp.isMember("error"))
            {
                reason = resp["error"].asString();
            }

            JLOG(journal.error()) << "Env::close() failed: " << reason;
            res = false;
        }
    }
    timeKeeper().set(closed()->header().closeTime);
    return res;
}

void
Env::memoize(Account const& account)
{
    map_.emplace(account.id(), account);
}

Account const&
Env::lookup(AccountID const& id) const
{
    auto const iter = map_.find(id);
    if (iter == map_.end())
    {
        std::cout << "Unknown account: " << id << "\n";
        Throw<std::runtime_error>("Env::lookup:: unknown account ID");
    }
    return iter->second;
}

Account const&
Env::lookup(std::string const& base58ID) const
{
    auto const account = parseBase58<AccountID>(base58ID);
    if (!account)
        Throw<std::runtime_error>("Env::lookup: invalid account ID");
    return lookup(*account);
}

PrettyAmount
Env::balance(Account const& account) const
{
    auto const sle = le(account);
    if (!sle)
        return XRP(0);
    return {sle->getFieldAmount(sfBalance), ""};
}

PrettyAmount
Env::balance(Account const& account, Issue const& issue) const
{
    if (isXRP(issue.currency))
        return balance(account);
    auto const sle = le(keylet::line(account.id(), issue));
    if (!sle)
        return {STAmount(issue, 0), account.name()};
    auto amount = sle->getFieldAmount(sfBalance);
    amount.setIssuer(issue.account);
    if (account.id() > issue.account)
        amount.negate();
    return {amount, lookup(issue.account).name()};
}

PrettyAmount
Env::balance(Account const& account, MPTIssue const& mptIssue) const
{
    MPTID const id = mptIssue.getMptID();
    if (!id)
        return {STAmount(mptIssue, 0), account.name()};

    AccountID const issuer = mptIssue.getIssuer();
    if (account.id() == issuer)
    {
        // Issuer balance
        auto const sle = le(keylet::mptIssuance(id));
        if (!sle)
            return {STAmount(mptIssue, 0), account.name()};

        // Make it negative
        STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount), 0, true};
        return {amount, lookup(issuer).name()};
    }

    // Holder balance
    auto const sle = le(keylet::mptoken(id, account));
    if (!sle)
        return {STAmount(mptIssue, 0), account.name()};

    STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)};
    return {amount, lookup(issuer).name()};
}

PrettyAmount
// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
Env::balance(Account const& account, Asset const& asset) const
{
    return std::visit([&](auto const& issue) { return balance(account, issue); }, asset.value());
}

PrettyAmount
Env::limit(Account const& account, Issue const& issue) const
{
    auto const sle = le(keylet::line(account.id(), issue));
    if (!sle)
        return {STAmount(issue, 0), account.name()};
    auto const aHigh = account.id() > issue.account;
    if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit))
        return {(*sle)[aHigh ? sfLowLimit : sfHighLimit], account.name()};
    return {STAmount(issue, 0), account.name()};
}

std::uint32_t
Env::ownerCount(Account const& account) const
{
    auto const sle = le(account);
    if (!sle)
        Throw<std::runtime_error>("missing account root");
    return sle->getFieldU32(sfOwnerCount);
}

std::uint32_t
Env::seq(Account const& account) const
{
    auto const sle = le(account);
    if (!sle)
        Throw<std::runtime_error>("missing account root");
    return sle->getFieldU32(sfSequence);
}

std::shared_ptr<SLE const>
Env::le(Account const& account) const
{
    return le(keylet::account(account.id()));
}

std::shared_ptr<SLE const>
Env::le(Keylet const& k) const
{
    return current()->read(k);
}

void
Env::fund(bool setDefaultRipple, STAmount const& amount, Account const& account)
{
    memoize(account);
    if (setDefaultRipple)
    {
        // VFALCO NOTE Is the fee formula correct?
        apply(
            pay(master, account, amount + drops(current()->fees().base)),
            jtx::seq(jtx::autofill),
            fee(jtx::autofill),
            sig(jtx::autofill));
        apply(
            fset(account, asfDefaultRipple),
            jtx::seq(jtx::autofill),
            fee(jtx::autofill),
            sig(jtx::autofill));
        require(flags(account, asfDefaultRipple));
    }
    else
    {
        apply(
            pay(master, account, amount),
            jtx::seq(jtx::autofill),
            fee(jtx::autofill),
            sig(jtx::autofill));
        require(nflags(account, asfDefaultRipple));
    }
    require(jtx::balance(account, amount));
}

void
Env::trust(STAmount const& amount, Account const& account)
{
    auto const start = balance(account);
    apply(
        jtx::trust(account, amount),
        jtx::seq(jtx::autofill),
        fee(jtx::autofill),
        sig(jtx::autofill));
    apply(
        pay(master, account, drops(current()->fees().base)),
        jtx::seq(jtx::autofill),
        fee(jtx::autofill),
        sig(jtx::autofill));
    test.expect(balance(account) == start);
}

Env::ParsedResult
Env::parseResult(Json::Value const& jr)
{
    auto error = [](ParsedResult& parsed, Json::Value const& object) {
        // Use an error code that is not used anywhere in the transaction
        // engine to distinguish this case.
        parsed.ter = telENV_RPC_FAILED;
        // Extract information about the error
        if (!object.isObject())
            return;
        if (object.isMember(jss::error_code))
            parsed.rpcCode = safe_cast<error_code_i>(object[jss::error_code].asInt());
        if (object.isMember(jss::error_message))
            parsed.rpcMessage = object[jss::error_message].asString();
        if (object.isMember(jss::error))
            parsed.rpcError = object[jss::error].asString();
        if (object.isMember(jss::error_exception))
            parsed.rpcException = object[jss::error_exception].asString();
    };
    ParsedResult parsed;
    if (jr.isObject() && jr.isMember(jss::result))
    {
        auto const& result = jr[jss::result];
        if (result.isMember(jss::engine_result_code))
        {
            parsed.ter = TER::fromInt(result[jss::engine_result_code].asInt());
            parsed.rpcCode.emplace(rpcSUCCESS);
        }
        else
        {
            error(parsed, result);
        }
    }
    else
    {
        error(parsed, jr);
    }

    return parsed;
}

void
Env::submit(JTx const& jt, std::source_location const& loc)
{
    ParsedResult parsedResult;
    auto const jr = [&]() {
        if (jt.stx)
        {
            txid_ = jt.stx->getTransactionID();
            Serializer s;
            jt.stx->add(s);
            auto const jr = rpc("submit", strHex(s.slice()));

            parsedResult = parseResult(jr);
            test.expect(parsedResult.ter, "ter uninitialized!");
            ter_ = parsedResult.ter.value_or(telENV_RPC_FAILED);

            return jr;
        }

        // Parsing failed or the JTx is
        // otherwise missing the stx field.
        parsedResult.ter = ter_ = temMALFORMED;

        return Json::Value();
    }();
    postconditions(jt, parsedResult, jr, loc);
}

void
Env::sign_and_submit(JTx const& jt, Json::Value params, std::source_location const& loc)
{
    auto const account = lookup(jt.jv[jss::Account].asString());
    auto const& passphrase = account.name();

    Json::Value jr;
    if (params.isNull())
    {
        // Use the command line interface
        auto const jv = to_string(jt.jv);
        jr = rpc("submit", passphrase, jv);
    }
    else
    {
        // Use the provided parameters, and go straight
        // to the (RPC) client.
        assert(params.isObject());
        if (!params.isMember(jss::secret) && !params.isMember(jss::key_type) &&
            !params.isMember(jss::seed) && !params.isMember(jss::seed_hex) &&
            !params.isMember(jss::passphrase))
        {
            params[jss::secret] = passphrase;
        }
        params[jss::tx_json] = jt.jv;
        jr = client().invoke("submit", params);
    }

    if (!txid_.parseHex(jr[jss::result][jss::tx_json][jss::hash].asString()))
        txid_.zero();

    ParsedResult const parsedResult = parseResult(jr);
    test.expect(parsedResult.ter, "ter uninitialized!");
    ter_ = parsedResult.ter.value_or(telENV_RPC_FAILED);

    postconditions(jt, parsedResult, jr, loc);
}

void
Env::postconditions(
    JTx const& jt,
    ParsedResult const& parsed,
    Json::Value const& jr,
    std::source_location const& loc)
{
    auto const locStr = std::string("(") + loc.file_name() + ":" + to_string(loc.line()) + ")";
    bool bad = !test.expect(parsed.ter, "apply " + locStr + ": No ter result!");
    bad =
        (jt.ter && parsed.ter &&
         !test.expect(
             *parsed.ter == *jt.ter,
             "apply " + locStr + ": Got " + transToken(*parsed.ter) + " (" +
                 transHuman(*parsed.ter) + "); Expected " + transToken(*jt.ter) + " (" +
                 transHuman(*jt.ter) + ")"));
    using namespace std::string_literals;
    bad = (jt.rpcCode &&
           !test.expect(
               parsed.rpcCode == jt.rpcCode->first && parsed.rpcMessage == jt.rpcCode->second,
               "apply " + locStr + ": Got RPC result "s +
                   (parsed.rpcCode ? RPC::get_error_info(*parsed.rpcCode).token.c_str()
                                   : "NO RESULT") +
                   " (" + parsed.rpcMessage + "); Expected " +
                   RPC::get_error_info(jt.rpcCode->first).token.c_str() + " (" +
                   jt.rpcCode->second + ")")) ||
        bad;
    // If we have an rpcCode (just checked), then the rpcException check is
    // optional - the 'error' field may not be defined, but if it is, it must
    // match rpcError.
    bad = (jt.rpcException &&
           !test.expect(
               (jt.rpcCode && parsed.rpcError.empty()) ||
                   (parsed.rpcError == jt.rpcException->first &&
                    (!jt.rpcException->second || parsed.rpcException == *jt.rpcException->second)),
               "apply " + locStr + ": Got RPC result "s + parsed.rpcError + " (" +
                   parsed.rpcException + "); Expected " + jt.rpcException->first + " (" +
                   jt.rpcException->second.value_or("n/a") + ")")) ||
        bad;
    if (bad)
    {
        test.log << pretty(jt.jv) << std::endl;
        if (jr)
            test.log << pretty(jr) << std::endl;
        // Don't check postconditions if
        // we didn't get the expected result.
        return;
    }
    if (trace_ != 0)
    {
        if (trace_ > 0)
            --trace_;
        test.log << pretty(jt.jv) << std::endl;
    }
    for (auto const& f : jt.require)
        f(*this);
}

std::shared_ptr<STObject const>
Env::meta()
{
    if (current()->txCount() != 0)
    {
        // close the ledger if it has not already been closed
        // (metadata is not finalized until the ledger is closed)
        close();
    }
    auto const item = closed()->txRead(txid_);
    auto const result = item.second;
    if (result == nullptr)
    {
        test.log << "Env::meta: no metadata for txid: " << txid_ << std::endl;
        test.log << "This is probably because the transaction failed with a "
                    "non-tec error."
                 << std::endl;
        Throw<std::runtime_error>("Env::meta: no metadata for txid");
    }
    return result;
}

std::shared_ptr<STTx const>
Env::tx() const
{
    return current()->txRead(txid_).first;
}

void
Env::autofill_sig(JTx& jt)
{
    auto& jv = jt.jv;

    scope_success success([&]() {
        // Call all the post-signers after the main signers or autofill are done
        for (auto const& signer : jt.postSigners)
            signer(*this, jt);
    });

    // Call all the main signers
    if (!jt.mainSigners.empty())
    {
        for (auto const& signer : jt.mainSigners)
            signer(*this, jt);
        return;
    }

    // If the sig is still needed, get it here.
    if (!jt.fill_sig)
        return;
    auto const account = jv.isMember(sfDelegate.jsonName)
        ? lookup(jv[sfDelegate.jsonName].asString())
        : lookup(jv[jss::Account].asString());
    if (!app().checkSigs())
    {
        jv[jss::SigningPubKey] = strHex(account.pk().slice());
        // dummy sig otherwise STTx is invalid
        jv[jss::TxnSignature] = "00";
        return;
    }
    auto const ar = le(account);
    if (ar && ar->isFieldPresent(sfRegularKey))
    {
        jtx::sign(jv, lookup(ar->getAccountID(sfRegularKey)));
    }
    else
    {
        jtx::sign(jv, account);
    }
}

void
Env::autofill(JTx& jt)
{
    auto& jv = jt.jv;
    if (jt.fill_fee)
        jtx::fill_fee(jv, *current());
    if (jt.fill_seq)
        jtx::fill_seq(jv, *current());

    if (jt.fill_netid)
    {
        uint32_t networkID = app().getNetworkIDService().getNetworkID();
        if (!jv.isMember(jss::NetworkID) && networkID > 1024)
            jv[jss::NetworkID] = std::to_string(networkID);
    }

    // Must come last
    try
    {
        autofill_sig(jt);
    }
    catch (parse_error const&)
    {
        if (!parseFailureExpected_)
            test.log << "parse failed:\n" << pretty(jv) << std::endl;
        Rethrow();
    }
}

std::shared_ptr<STTx const>
Env::st(JTx const& jt)
{
    // The parse must succeed, since we
    // generated the JSON ourselves.
    std::optional<STObject> obj;
    try
    {
        obj = jtx::parse(jt.jv);
    }
    catch (jtx::parse_error const&)
    {
        test.log << "Exception: parse_error\n" << pretty(jt.jv) << std::endl;
        Rethrow();
    }

    try
    {
        return sterilize(STTx{std::move(*obj)});
    }
    catch (...)
    {
        return nullptr;
    }
}

std::shared_ptr<STTx const>
Env::ust(JTx const& jt)
{
    // The parse must succeed, since we
    // generated the JSON ourselves.
    std::optional<STObject> obj;
    try
    {
        obj = jtx::parse(jt.jv);
    }
    catch (jtx::parse_error const&)
    {
        test.log << "Exception: parse_error\n" << pretty(jt.jv) << std::endl;
        Rethrow();
    }

    try
    {
        return std::make_shared<STTx const>(std::move(*obj));
    }
    catch (...)
    {
        return nullptr;
    }
}

Json::Value
Env::do_rpc(
    unsigned apiVersion,
    std::vector<std::string> const& args,
    std::unordered_map<std::string, std::string> const& headers)
{
    auto response = rpcClient(args, app().config(), app().logs(), apiVersion, headers);

    for (unsigned ctr = 0; (ctr < retries_) and (response.first == rpcINTERNAL); ++ctr)
    {
        JLOG(journal.error()) << "Env::do_rpc error, retrying, attempt #" << ctr + 1 << " ...";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));

        response = rpcClient(args, app().config(), app().logs(), apiVersion, headers);
    }

    return response.second;
}

void
Env::enableFeature(uint256 const feature)
{
    // Env::close() must be called for feature
    // enable to take place.
    app().config().features.insert(feature);
}

void
Env::disableFeature(uint256 const feature)
{
    // Env::close() must be called for feature
    // enable to take place.
    app().config().features.erase(feature);
}

}  // namespace jtx
}  // namespace test
}  // namespace xrpl
