#include <test/jtx.h>

#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h>

namespace xrpl {
namespace test {

class LoanBroker_test : public beast::unit_test::suite
{
    // Ensure that all the features needed for Lending Protocol are included,
    // even if they are set to unsupported.
    FeatureBitset const all{
        jtx::testable_amendments() | featureMPTokensV1 | featureSingleAssetVault |
        featureLendingProtocol};

    void
    testDisabled()
    {
        testcase("Disabled");
        // Lending Protocol depends on Single Asset Vault (SAV). Test
        // combinations of the two amendments.
        // Single Asset Vault depends on MPTokensV1, but don't test every combo
        // of that.
        using namespace jtx;
        auto failAll = [this](FeatureBitset features, bool goodVault = false) {
            Env env(*this, features);

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

            // Try to create a vault
            PrettyAsset const asset{xrpIssue(), 1'000'000};
            Vault vault{env};
            auto const [tx, keylet] = vault.create({.owner = alice, .asset = asset});
            env(tx, ter(goodVault ? ter(tesSUCCESS) : ter(temDISABLED)));
            env.close();
            BEAST_EXPECT(static_cast<bool>(env.le(keylet)) == goodVault);

            using namespace loanBroker;
            // Can't create a loan broker regardless of whether the vault exists
            env(set(alice, keylet.key), ter(temDISABLED));
            auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
            // Other LoanBroker transactions are disabled, too.
            // 1. LoanBrokerCoverDeposit
            env(coverDeposit(alice, brokerKeylet.key, asset(1000)), ter(temDISABLED));
            // 2. LoanBrokerCoverWithdraw
            env(coverWithdraw(alice, brokerKeylet.key, asset(1000)), ter(temDISABLED));
            // 3. LoanBrokerCoverClawback
            env(coverClawback(alice), ter(temDISABLED));
            env(coverClawback(alice), loanBrokerID(brokerKeylet.key), ter(temDISABLED));
            env(coverClawback(alice), amount(asset(0)), ter(temDISABLED));
            env(coverClawback(alice),
                loanBrokerID(brokerKeylet.key),
                amount(asset(1000)),
                ter(temDISABLED));
            // 4. LoanBrokerDelete
            env(del(alice, brokerKeylet.key), ter(temDISABLED));
        };
        failAll(all - featureMPTokensV1);
        failAll(all - featureSingleAssetVault - featureLendingProtocol);
        failAll(all - featureSingleAssetVault);
        failAll(all - featureLendingProtocol, true);
    }

    struct VaultInfo
    {
        jtx::PrettyAsset asset;
        uint256 vaultID;
        jtx::Account pseudoAccount;
        VaultInfo(jtx::PrettyAsset const& asset_, uint256 const& vaultID_, AccountID const& pseudo)
            : asset(asset_), vaultID(vaultID_), pseudoAccount("vault", pseudo)
        {
        }
    };

    void
    lifecycle(
        char const* label,
        jtx::Env& env,
        jtx::Account const& issuer,
        jtx::Account const& alice,
        jtx::Account const& evan,
        jtx::Account const& bystander,
        VaultInfo const& vault,
        VaultInfo const& badVault,
        std::function<jtx::JTx(jtx::JTx const&)> modifyJTx,
        std::function<void(SLE::const_ref)> checkBroker,
        std::function<void(SLE::const_ref)> changeBroker,
        std::function<void(SLE::const_ref)> checkChangedBroker)
    {
        {
            auto const& asset = vault.asset.raw();
            std::string_view assetLabel;
            if (asset.native())
            {
                assetLabel = "XRP ";
            }
            else if (asset.holds<Issue>())
            {
                assetLabel = "IOU ";
            }
            else if (asset.holds<MPTIssue>())
            {
                assetLabel = "MPT ";
            }
            else
            {
                assetLabel = "Unknown ";
            }
            testcase << "Lifecycle: " << assetLabel << label;
        }

        using namespace jtx;
        using namespace loanBroker;

        // Bogus assets to use in test cases
        static PrettyAsset const badMptAsset = [&]() {
            MPTTester badMptt{env, evan, mptInitNoFund};
            badMptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
            env.close();
            return badMptt["BAD"];
        }();
        static PrettyAsset const badIouAsset = evan["BAD"];
        static Account const nonExistent{"NonExistent"};
        static PrettyAsset const ghostIouAsset = nonExistent["GST"];
        PrettyAsset const vaultPseudoIouAsset = vault.pseudoAccount["PSD"];

        auto const badKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
        env(set(alice, badVault.vaultID));
        env.close();
        auto const badBrokerPseudo = [&]() {
            if (auto const le = env.le(badKeylet); BEAST_EXPECT(le))
            {
                return Account{"Bad Broker pseudo-account", le->at(sfAccount)};
            }
            // Just to make the build work
            return vault.pseudoAccount;
        }();
        PrettyAsset const badBrokerPseudoIouAsset = badBrokerPseudo["WAT"];

        auto const keylet = keylet::loanbroker(alice.id(), env.seq(alice));
        {
            // Start with default values
            auto jtx = env.jt(set(alice, vault.vaultID));
            // Modify as desired
            if (modifyJTx)
                jtx = modifyJTx(jtx);
            // Successfully create a Loan Broker
            env(jtx);
        }

        env.close();
        if (auto broker = env.le(keylet); BEAST_EXPECT(broker))
        {
            // log << "Broker after create: " << to_string(broker->getJson())
            //     << std::endl;
            BEAST_EXPECT(broker->at(sfVaultID) == vault.vaultID);
            BEAST_EXPECT(broker->at(sfAccount) != alice.id());
            BEAST_EXPECT(broker->at(sfOwner) == alice.id());
            BEAST_EXPECT(broker->at(sfFlags) == 0);
            BEAST_EXPECT(broker->at(sfSequence) == env.seq(alice) - 1);
            BEAST_EXPECT(broker->at(sfOwnerCount) == 0);
            BEAST_EXPECT(broker->at(sfLoanSequence) == 1);
            BEAST_EXPECT(broker->at(sfDebtTotal) == 0);
            BEAST_EXPECT(broker->at(sfCoverAvailable) == 0);
            if (checkBroker)
                checkBroker(broker);

            // if (auto const vaultSLE = env.le(keylet::vault(vault.vaultID)))
            //{
            //     log << "Vault: " << to_string(vaultSLE->getJson()) <<
            //     std::endl;
            // }
            //  Load the pseudo-account
            Account const pseudoAccount{"Broker pseudo-account", broker->at(sfAccount)};

            auto const pseudoKeylet = keylet::account(pseudoAccount);
            if (auto const pseudo = env.le(pseudoKeylet); BEAST_EXPECT(pseudo))
            {
                // log << "Pseudo-account after create: "
                //     << to_string(pseudo->getJson()) << std::endl
                //     << std::endl;
                BEAST_EXPECT(
                    pseudo->at(sfFlags) == (lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth));
                BEAST_EXPECT(pseudo->at(sfSequence) == 0);
                BEAST_EXPECT(pseudo->at(sfBalance) == beast::zero);
                BEAST_EXPECT(pseudo->at(sfOwnerCount) == (vault.asset.raw().native() ? 0 : 1));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfAccountTxnID));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfRegularKey));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfEmailHash));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletLocator));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfWalletSize));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfMessageKey));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfTransferRate));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfDomain));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfTickSize));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfTicketCount));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfNFTokenMinter));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfMintedNFTokens));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfBurnedNFTokens));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfFirstNFTokenSequence));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfAMMID));
                BEAST_EXPECT(!pseudo->isFieldPresent(sfVaultID));
                BEAST_EXPECT(pseudo->at(sfLoanBrokerID) == keylet.key);
            }

            {
                // Get the AccountInfo RPC result for the broker pseudo-account
                std::string const pseudoStr = to_string(pseudoAccount.id());
                auto const accountInfo = env.rpc("account_info", pseudoStr);
                if (BEAST_EXPECT(accountInfo.isObject()))
                {
                    auto const& accountData = accountInfo[jss::result][jss::account_data];
                    if (BEAST_EXPECT(accountData.isObject()))
                    {
                        BEAST_EXPECT(accountData[jss::Account] == pseudoStr);
                        BEAST_EXPECT(accountData[sfLoanBrokerID] == to_string(keylet.key));
                    }
                    auto const& pseudoInfo = accountInfo[jss::result][jss::pseudo_account];
                    if (BEAST_EXPECT(pseudoInfo.isObject()))
                    {
                        BEAST_EXPECT(pseudoInfo[jss::type] == "LoanBroker");
                    }
                }
            }

            auto verifyCoverAmount =
                [&env, &vault, &pseudoAccount, &broker, &keylet, this](auto n) {
                    using namespace jtx;

                    if (BEAST_EXPECT(broker = env.le(keylet)))
                    {
                        auto const amount = vault.asset(n);
                        BEAST_EXPECT(broker->at(sfCoverAvailable) == amount.number());
                        env.require(balance(pseudoAccount, amount));
                    }
                };

            // Test Cover funding before allowing alterations
            env(coverDeposit(alice, uint256(0), vault.asset(10)), ter(temINVALID));
            env(coverDeposit(evan, keylet.key, vault.asset(10)), ter(tecNO_PERMISSION));
            env(coverDeposit(evan, keylet.key, vault.asset(0)), ter(temBAD_AMOUNT));
            env(coverDeposit(evan, keylet.key, vault.asset(-10)), ter(temBAD_AMOUNT));
            env(coverDeposit(alice, vault.vaultID, vault.asset(10)), ter(tecNO_ENTRY));

            verifyCoverAmount(0);

            // Test cover clawback failure cases BEFORE depositing any cover
            // Need one of brokerID or amount
            env(coverClawback(alice), ter(temINVALID));
            env(coverClawback(alice), loanBrokerID(uint256(0)), ter(temINVALID));
            env(coverClawback(alice), amount(XRP(1000)), ter(temBAD_AMOUNT));
            env(coverClawback(alice), amount(vault.asset(-10)), ter(temBAD_AMOUNT));
            // Clawbacks with an MPT need to specify the broker ID
            env(coverClawback(alice), amount(badMptAsset(1)), ter(temINVALID));
            env(coverClawback(evan), loanBrokerID(vault.vaultID), ter(tecNO_ENTRY));
            // Only the issuer can clawback
            env(coverClawback(alice), loanBrokerID(keylet.key), ter(tecNO_PERMISSION));
            if (vault.asset.raw().native())
            {
                // Can not clawback XRP under any circumstances
                env(coverClawback(issuer), loanBrokerID(keylet.key), ter(tecNO_PERMISSION));
            }
            else
            {
                if (vault.asset.raw().holds<Issue>())
                {
                    // Clawbacks without a loanBrokerID need to specify an IOU
                    // with the broker's pseudo-account as the issuer
                    env(coverClawback(alice), amount(ghostIouAsset(1)), ter(tecNO_ENTRY));
                    env(coverClawback(alice), amount(badIouAsset(1)), ter(tecOBJECT_NOT_FOUND));
                    // Pseudo-account is not for a broker
                    env(coverClawback(alice),
                        amount(vaultPseudoIouAsset(1)),
                        ter(tecOBJECT_NOT_FOUND));
                    // If we specify a pseudo-account as the IOU amount, it
                    // needs to match the loan broker
                    env(coverClawback(issuer),
                        loanBrokerID(keylet.key),
                        amount(badBrokerPseudoIouAsset(10)),
                        ter(tecWRONG_ASSET));
                    PrettyAsset const brokerWrongCurrencyAsset = pseudoAccount["WAT"];
                    env(coverClawback(issuer),
                        loanBrokerID(keylet.key),
                        amount(brokerWrongCurrencyAsset(10)),
                        ter(tecWRONG_ASSET));
                }
                else
                {
                    // Clawbacks with an MPT need to specify the broker ID, even
                    // if the asset is valid
                    BEAST_EXPECT(vault.asset.raw().holds<MPTIssue>());
                    env(coverClawback(alice), amount(vault.asset(10)), ter(temINVALID));
                }
                // Since no cover has been deposited, there's nothing to claw
                // back
                env(coverClawback(issuer),
                    loanBrokerID(keylet.key),
                    amount(vault.asset(10)),
                    ter(tecINSUFFICIENT_FUNDS));
            }
            env.close();

            // Fund the cover deposit
            env(coverDeposit(alice, keylet.key, vault.asset(10)));
            env.close();
            verifyCoverAmount(10);

            // Test withdrawal failure cases
            env(coverWithdraw(alice, uint256(0), vault.asset(10)), ter(temINVALID));
            env(coverWithdraw(evan, keylet.key, vault.asset(10)), ter(tecNO_PERMISSION));
            env(coverWithdraw(evan, keylet.key, vault.asset(0)), ter(temBAD_AMOUNT));
            env(coverWithdraw(evan, keylet.key, vault.asset(-10)), ter(temBAD_AMOUNT));
            env(coverWithdraw(alice, vault.vaultID, vault.asset(10)), ter(tecNO_ENTRY));
            env(coverWithdraw(alice, keylet.key, vault.asset(900)), ter(tecINSUFFICIENT_FUNDS));

            // Skip this test for XRP, because that can always be sent
            if (!vault.asset.raw().native())
            {
                TER const expected = vault.asset.raw().holds<MPTIssue>() ? tecNO_AUTH : tecNO_LINE;
                env(coverWithdraw(alice, keylet.key, vault.asset(1)),
                    destination(bystander),
                    ter(expected));
            }

            // Can not withdraw to the zero address
            env(coverWithdraw(alice, keylet.key, vault.asset(1)),
                destination(AccountID{}),
                ter(temMALFORMED));

            // Withdraw some of the cover amount
            env(coverWithdraw(alice, keylet.key, vault.asset(7)));
            env.close();
            verifyCoverAmount(3);

            // Add some more cover
            env(coverDeposit(alice, keylet.key, vault.asset(5)));
            env.close();
            verifyCoverAmount(8);

            // Withdraw some more. Send it to Evan. Very generous, considering
            // how much trouble he's been.
            env(coverWithdraw(alice, keylet.key, vault.asset(1)), destination(evan));
            env.close();
            verifyCoverAmount(7);

            // Withdraw some more. Send it to Evan. Very generous, considering
            // how much trouble he's been.
            env(coverWithdraw(alice, keylet.key, vault.asset(1)), destination(evan), dtag(3));
            env.close();
            verifyCoverAmount(6);

            if (!vault.asset.raw().native())
            {
                // Issuer claws back some of the cover
                env(coverClawback(issuer), loanBrokerID(keylet.key), amount(vault.asset(2)));
                env.close();
                verifyCoverAmount(4);

                // Deposit some back
                env(coverDeposit(alice, keylet.key, vault.asset(5)));
                env.close();
                verifyCoverAmount(9);

                // Issuer claws it all back in various different ways
                for (auto const& tx : {
                         // defer autofills until submission time
                         env.json(
                             coverClawback(issuer),
                             loanBrokerID(keylet.key),
                             fee(none),
                             seq(none),
                             sig(none)),
                         env.json(
                             coverClawback(issuer),
                             loanBrokerID(keylet.key),
                             amount(vault.asset(0)),
                             fee(none),
                             seq(none),
                             sig(none)),
                         env.json(
                             coverClawback(issuer),
                             loanBrokerID(keylet.key),
                             amount(vault.asset(6)),
                             fee(none),
                             seq(none),
                             sig(none)),
                         // amount will be truncated to what's available
                         env.json(
                             coverClawback(issuer),
                             loanBrokerID(keylet.key),
                             amount(vault.asset(100)),
                             fee(none),
                             seq(none),
                             sig(none)),
                     })
                {
                    // Issuer claws it all back
                    env(tx);
                    env.close();
                    verifyCoverAmount(0);

                    // Deposit some back
                    env(coverDeposit(alice, keylet.key, vault.asset(6)));
                    env.close();
                    verifyCoverAmount(6);
                }
            }

            // no-op
            env(set(alice, vault.vaultID), loanBrokerID(keylet.key));
            env.close();

            // Make modifications to the broker
            if (changeBroker)
                changeBroker(broker);

            env.close();

            // Check the results of modifications
            if (BEAST_EXPECT(broker = env.le(keylet)) && checkChangedBroker)
                checkChangedBroker(broker);

            // Verify that fields get removed when set to default values
            // Debt maximum: explicit 0
            // Data: explicit empty
            env(set(alice, vault.vaultID),
                loanBrokerID(broker->key()),
                debtMaximum(Number(0)),
                data(""));
            env.close();

            // Check the updated fields
            if (BEAST_EXPECT(broker = env.le(keylet)))
            {
                BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
                BEAST_EXPECT(!broker->isFieldPresent(sfData));
            }

            /////////////////////////////////////
            // try to delete the wrong broker object
            env(del(alice, vault.vaultID), ter(tecNO_ENTRY));
            // evan tries to delete the broker
            env(del(evan, keylet.key), ter(tecNO_PERMISSION));

            // Get the "bad" broker out of the way
            env(del(alice, badKeylet.key));
            env.close();

            // Note alice's balance of the asset and the broker account's cover
            // funds
            auto const aliceBalance = env.balance(alice, vault.asset);
            auto const coverFunds = env.balance(pseudoAccount, vault.asset);
            BEAST_EXPECT(coverFunds.number() == broker->at(sfCoverAvailable));
            BEAST_EXPECT(coverFunds != beast::zero);
            verifyCoverAmount(6);

            // delete the broker
            // log << "Broker before delete: " << to_string(broker->getJson())
            //    << std::endl;
            // if (auto const pseudo = env.le(pseudoKeylet);
            // BEAST_EXPECT(pseudo))
            //{
            //    log << "Pseudo-account before delete: "
            //        << to_string(pseudo->getJson()) << std::endl
            //        << std::endl;
            //}

            env(del(alice, keylet.key));
            env.close();
            {
                broker = env.le(keylet);
                BEAST_EXPECT(!broker);
                auto pseudo = env.le(pseudoKeylet);
                BEAST_EXPECT(!pseudo);
            }
            auto const expectedBalance = aliceBalance + coverFunds -
                (aliceBalance.value().native() ? STAmount(env.current()->fees().base.value())
                                               : vault.asset(0));
            env.require(balance(alice, expectedBalance));
            env.require(balance(pseudoAccount, vault.asset(none)));
        }
    }

    void
    testLifecycle()
    {
        testcase("Lifecycle");
        using namespace jtx;

        // Create 3 loan brokers: one for XRP, one for an IOU, and one for an
        // MPT. That'll require three corresponding SAVs.
        Env env(*this, all);

        Account issuer{"issuer"};
        // For simplicity, alice will be the sole actor for the vault & brokers.
        Account alice{"alice"};
        // Evan will attempt to be naughty
        Account evan{"evan"};
        // Bystander doesn't have anything to do with the SAV or Broker, or any
        // of the relevant tokens
        Account bystander{"bystander"};
        Vault vault{env};

        // Fund the accounts and trust lines with the same amount so that tests
        // can use the same values regardless of the asset.
        env.fund(XRP(100'000), issuer, noripple(alice, evan, bystander));
        env.close();

        env(fset(issuer, asfAllowTrustLineClawback));
        env.close();

        // Create assets
        PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
        PrettyAsset const iouAsset = issuer["IOU"];
        env(trust(alice, iouAsset(1'000'000)));
        env(trust(evan, iouAsset(1'000'000)));
        env.close();
        env(pay(issuer, evan, iouAsset(100'000)));
        env(pay(issuer, alice, iouAsset(100'000)));
        env.close();

        MPTTester mptt{env, issuer, mptInitNoFund};
        mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
        env.close();
        PrettyAsset const mptAsset = mptt["MPT"];
        mptt.authorize({.account = alice});
        mptt.authorize({.account = evan});
        env.close();
        env(pay(issuer, alice, mptAsset(100'000)));
        env(pay(issuer, evan, mptAsset(100'000)));
        env.close();

        std::array const assets{xrpAsset, iouAsset, mptAsset};

        // Create vaults
        std::vector<VaultInfo> vaults;
        for (auto const& asset : assets)
        {
            auto [tx, keylet] = vault.create({.owner = alice, .asset = asset});
            env(tx);
            env.close();
            if (auto const le = env.le(keylet); BEAST_EXPECT(env.le(keylet)))
            {
                vaults.emplace_back(asset, keylet.key, le->at(sfAccount));
            }

            env(vault.deposit({.depositor = alice, .id = keylet.key, .amount = asset(50)}));
            env.close();
        }
        VaultInfo const badVault = [&]() -> VaultInfo {
            auto [tx, keylet] = vault.create({.owner = alice, .asset = iouAsset});
            env(tx);
            env.close();
            if (auto const le = env.le(keylet); BEAST_EXPECT(env.le(keylet)))
            {
                return {iouAsset, keylet.key, le->at(sfAccount)};
            }
            // This should never happen
            return {iouAsset, keylet.key, evan.id()};
        }();

        auto const aliceOriginalCount = env.ownerCount(alice);

        // Create and update Loan Brokers
        for (auto const& vault : vaults)
        {
            {
                // Get the AccountInfo RPC result for the vault pseudo-account
                std::string const pseudoStr = to_string(vault.pseudoAccount.id());
                auto const accountInfo = env.rpc("account_info", pseudoStr);
                if (BEAST_EXPECT(accountInfo.isObject()))
                {
                    auto const& accountData = accountInfo[jss::result][jss::account_data];
                    if (BEAST_EXPECT(accountData.isObject()))
                    {
                        BEAST_EXPECT(accountData[jss::Account] == pseudoStr);
                        BEAST_EXPECT(accountData[sfVaultID] == to_string(vault.vaultID));
                    }
                    auto const& pseudoInfo = accountInfo[jss::result][jss::pseudo_account];
                    if (BEAST_EXPECT(pseudoInfo.isObject()))
                    {
                        BEAST_EXPECT(pseudoInfo[jss::type] == "Vault");
                    }
                }
            }

            using namespace loanBroker;
            using namespace xrpl::Lending;

            TenthBips32 const tenthBipsZero{0};

            auto badKeylet = keylet::vault(alice.id(), env.seq(alice));
            // Try some failure cases
            // not the vault owner
            env(set(evan, vault.vaultID), ter(tecNO_PERMISSION));
            // not a vault
            env(set(alice, badKeylet.key), ter(tecNO_ENTRY));
            // flags are checked first
            env(set(evan, vault.vaultID, ~tfUniversal), ter(temINVALID_FLAG));
            // field length validation
            // sfData: good length, bad account
            env(set(evan, vault.vaultID),
                data(std::string(maxDataPayloadLength, 'X')),
                ter(tecNO_PERMISSION));
            // sfData: too long
            env(set(evan, vault.vaultID),
                data(std::string(maxDataPayloadLength + 1, 'Y')),
                ter(temINVALID));
            // sfManagementFeeRate: good value, bad account
            env(set(evan, vault.vaultID),
                managementFeeRate(maxManagementFeeRate),
                ter(tecNO_PERMISSION));
            // sfManagementFeeRate: too big
            env(set(evan, vault.vaultID),
                managementFeeRate(maxManagementFeeRate + TenthBips16(10)),
                ter(temINVALID));
            // sfCoverRateMinimum and sfCoverRateLiquidation are linked
            // Cover: good value, bad account
            env(set(evan, vault.vaultID),
                coverRateMinimum(maxCoverRate),
                coverRateLiquidation(maxCoverRate),
                ter(tecNO_PERMISSION));
            // CoverMinimum: too big
            env(set(evan, vault.vaultID),
                coverRateMinimum(maxCoverRate + 1),
                coverRateLiquidation(maxCoverRate + 1),
                ter(temINVALID));
            // CoverLiquidation: too big
            env(set(evan, vault.vaultID),
                coverRateMinimum(maxCoverRate / 2),
                coverRateLiquidation(maxCoverRate + 1),
                ter(temINVALID));
            // Cover: zero min, non-zero liquidation - implicit and
            // explicit zero values.
            env(set(evan, vault.vaultID), coverRateLiquidation(maxCoverRate), ter(temINVALID));
            env(set(evan, vault.vaultID),
                coverRateMinimum(tenthBipsZero),
                coverRateLiquidation(maxCoverRate),
                ter(temINVALID));
            // Cover: non-zero min, zero liquidation - implicit and
            // explicit zero values.
            env(set(evan, vault.vaultID), coverRateMinimum(maxCoverRate), ter(temINVALID));
            env(set(evan, vault.vaultID),
                coverRateMinimum(maxCoverRate),
                coverRateLiquidation(tenthBipsZero),
                ter(temINVALID));
            // sfDebtMaximum: good value, bad account
            env(set(evan, vault.vaultID), debtMaximum(Number(0)), ter(tecNO_PERMISSION));
            // sfDebtMaximum: overflow
            env(set(evan, vault.vaultID), debtMaximum(Number(1, 100)), ter(temINVALID));
            // sfDebtMaximum: negative
            env(set(evan, vault.vaultID), debtMaximum(Number(-1)), ter(temINVALID));

            std::string testData;
            lifecycle(
                "default fields",
                env,
                issuer,
                alice,
                evan,
                bystander,
                vault,
                badVault,
                // No modifications
                {},
                [&](SLE::const_ref broker) {
                    // Extra checks
                    BEAST_EXPECT(!broker->isFieldPresent(sfManagementFeeRate));
                    BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateMinimum));
                    BEAST_EXPECT(!broker->isFieldPresent(sfCoverRateLiquidation));
                    BEAST_EXPECT(!broker->isFieldPresent(sfData));
                    BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
                    BEAST_EXPECT(broker->at(sfDebtMaximum) == 0);
                    BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0);
                    BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0);

                    BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount + 4);
                },
                [&](SLE::const_ref broker) {
                    // Modifications

                    // Update the fields
                    auto const nextKeylet = keylet::loanbroker(alice.id(), env.seq(alice));

                    // fields that can't be changed
                    // LoanBrokerID
                    env(set(alice, vault.vaultID), loanBrokerID(nextKeylet.key), ter(tecNO_ENTRY));
                    // VaultID
                    env(set(alice, nextKeylet.key), loanBrokerID(broker->key()), ter(tecNO_ENTRY));
                    // Owner
                    env(set(evan, vault.vaultID),
                        loanBrokerID(broker->key()),
                        ter(tecNO_PERMISSION));
                    // ManagementFeeRate
                    env(set(alice, vault.vaultID),
                        loanBrokerID(broker->key()),
                        managementFeeRate(maxManagementFeeRate),
                        ter(temINVALID));
                    // CoverRateMinimum
                    env(set(alice, vault.vaultID),
                        loanBrokerID(broker->key()),
                        coverRateMinimum(maxManagementFeeRate),
                        ter(temINVALID));
                    // CoverRateLiquidation
                    env(set(alice, vault.vaultID),
                        loanBrokerID(broker->key()),
                        coverRateLiquidation(maxManagementFeeRate),
                        ter(temINVALID));

                    // fields that can be changed
                    testData = "Test Data 1234";
                    // Bad data: too long
                    env(set(alice, vault.vaultID),
                        loanBrokerID(broker->key()),
                        data(std::string(maxDataPayloadLength + 1, 'W')),
                        ter(temINVALID));

                    // Bad debt maximum
                    env(set(alice, vault.vaultID),
                        loanBrokerID(broker->key()),
                        debtMaximum(Number(-175, -1)),
                        ter(temINVALID));
                    Number debtMax{175, -1};
                    if (vault.asset.integral())
                    {
                        env(set(alice, vault.vaultID),
                            loanBrokerID(broker->key()),
                            data(testData),
                            debtMaximum(debtMax),
                            ter(tecPRECISION_LOSS));
                        roundToAsset(vault.asset, debtMax);
                    }
                    // Data & Debt maximum
                    env(set(alice, vault.vaultID),
                        loanBrokerID(broker->key()),
                        data(testData),
                        debtMaximum(debtMax));
                },
                [&](SLE::const_ref broker) {
                    // Check the updated fields
                    BEAST_EXPECT(checkVL(broker->at(sfData), testData));
                    Number const expected = STAmount{vault.asset, Number(175, -1)};
                    auto const actual = broker->at(sfDebtMaximum);
                    BEAST_EXPECTS(
                        actual == expected,
                        "Expected: " + to_string(expected) + ", Actual: " + to_string(actual));
                });

            lifecycle(
                "non-default fields",
                env,
                issuer,
                alice,
                evan,
                bystander,
                vault,
                badVault,
                [&](jtx::JTx const& jv) {
                    testData = "spam spam spam spam";
                    // Finally, create another Loan Broker with none of the
                    // values at default
                    return env.jt(
                        jv,
                        data(testData),
                        managementFeeRate(TenthBips16(123)),
                        debtMaximum(Number(9)),
                        coverRateMinimum(TenthBips32(100)),
                        coverRateLiquidation(TenthBips32(200)));
                },
                [&](SLE::const_ref broker) {
                    // Extra checks
                    BEAST_EXPECT(broker->at(sfManagementFeeRate) == 123);
                    BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 100);
                    BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 200);
                    BEAST_EXPECT(broker->at(sfDebtMaximum) == Number(9));
                    BEAST_EXPECT(checkVL(broker->at(sfData), testData));
                },
                [&](SLE::const_ref broker) {
                    // Reset Data & Debt maximum to default values
                    env(set(alice, vault.vaultID),
                        loanBrokerID(broker->key()),
                        data(""),
                        debtMaximum(Number(0)));
                },
                [&](SLE::const_ref broker) {
                    // Check the updated fields
                    BEAST_EXPECT(!broker->isFieldPresent(sfData));
                    BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
                });
        }

        BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount);
    }

    enum LoanBrokerTest { CoverClawback, CoverDeposit, CoverWithdraw, Delete, Set };

    void
    testLoanBroker(
        std::function<jtx::PrettyAsset(jtx::Env&, jtx::Account const&, jtx::Account const&)>
            getAsset,
        LoanBrokerTest brokerTest)
    {
        using namespace jtx;
        using namespace loanBroker;
        Account const issuer{"issuer"};
        Account const alice{"alice"};
        Env env(*this);
        Vault vault{env};

        env.fund(XRP(100'000), issuer, alice);
        env.close();

        PrettyAsset const asset = [&]() {
            if (getAsset)
                return getAsset(env, issuer, alice);
            env(trust(alice, issuer["IOU"](1'000'000)));
            env.close();
            return PrettyAsset(issuer["IOU"]);
        }();

        env(pay(issuer, alice, asset(100'000)));
        env.close();

        auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
        env(tx);
        env.close();
        auto const le = env.le(vaultKeylet);
        VaultInfo vaultInfo = [&]() {
            if (BEAST_EXPECT(le))
                return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
            return VaultInfo{asset, {}, {}};
        }();
        if (vaultInfo.vaultID == uint256{})
            return;

        env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
        env.close();

        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
        env(set(alice, vaultInfo.vaultID));
        env.close();

        auto broker = env.le(brokerKeylet);
        if (!BEAST_EXPECT(broker))
            return;

        auto testZeroBrokerID = [&](auto&& getTxJv) {
            auto jv = getTxJv();
            // empty broker ID
            jv[sfLoanBrokerID] = "";
            env(jv, ter(temINVALID));
            // zero broker ID
            jv[sfLoanBrokerID] = to_string(uint256{});
            // needs a flag to distinguish the parsed STTx from the prior
            // test
            env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID));
        };
        auto testZeroVaultID = [&](auto&& getTxJv) {
            auto jv = getTxJv();
            // empty broker ID
            jv[sfVaultID] = "";
            env(jv, ter(temINVALID));
            // zero broker ID
            jv[sfVaultID] = to_string(uint256{});
            // needs a flag to distinguish the parsed STTx from the prior
            // test
            env(jv, txflags(tfFullyCanonicalSig), ter(temINVALID));
        };

        if (brokerTest == CoverDeposit)
        {
            // preflight: temINVALID (empty/zero broker id)
            testZeroBrokerID([&]() { return coverDeposit(alice, brokerKeylet.key, asset(10)); });

            // preclaim: tecWRONG_ASSET
            env(coverDeposit(alice, brokerKeylet.key, issuer["BAD"](10)), ter(tecWRONG_ASSET));

            // preclaim: tecINSUFFICIENT_FUNDS
            env(pay(alice, issuer, asset(100'000 - 50)));
            env.close();
            env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)),
                ter(tecINSUFFICIENT_FUNDS));

            // preclaim: tecFROZEN
            env(fset(issuer, asfGlobalFreeze));
            env.close();
            env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), ter(tecFROZEN));
        }
        else
        {
            // Fund the cover deposit
            env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)));
        }
        env.close();

        if (brokerTest == CoverWithdraw)
        {
            // preflight: temINVALID (empty/zero broker id)
            testZeroBrokerID([&]() { return coverWithdraw(alice, brokerKeylet.key, asset(10)); });

            // preclaim: tecWRONG_ASSET
            env(coverWithdraw(alice, brokerKeylet.key, issuer["BAD"](10)), ter(tecWRONG_ASSET));

            // preclaim: tecNO_DST
            Account const bogus{"bogus"};
            env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
                destination(bogus),
                ter(tecNO_DST));

            // preclaim: tecDST_TAG_NEEDED
            Account const dest{"dest"};
            env.fund(XRP(1'000), dest);
            env(fset(dest, asfRequireDest));
            env.close();
            env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
                destination(dest),
                ter(tecDST_TAG_NEEDED));

            // preclaim: tecNO_PERMISSION
            env(fclear(dest, asfRequireDest));
            env(fset(dest, asfDepositAuth));
            env.close();
            env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
                destination(dest),
                ter(tecNO_PERMISSION));

            // preclaim: tecFROZEN
            env(trust(dest, asset(1'000)));
            env(fclear(dest, asfDepositAuth));
            env(fset(issuer, asfGlobalFreeze));
            env.close();
            env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
                destination(dest),
                ter(tecFROZEN));

            // preclaim:: tecFROZEN (deep frozen)
            env(fclear(issuer, asfGlobalFreeze));
            env(trust(issuer, asset(1'000), dest, tfSetFreeze | tfSetDeepFreeze));
            env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
                destination(dest),
                ter(tecFROZEN));

            // preclaim: tecPSEUDO_ACCOUNT
            env(coverWithdraw(alice, brokerKeylet.key, asset(10)),
                destination(vaultInfo.pseudoAccount),
                ter(tecPSEUDO_ACCOUNT));
        }

        if (brokerTest == CoverClawback)
        {
            // preflight: temINVALID (empty/zero broker id)
            testZeroBrokerID([&]() {
                return env.json(
                    coverClawback(alice),
                    loanBrokerID(brokerKeylet.key),
                    amount(vaultInfo.asset(2)));
            });

            if (asset.holds<Issue>())
            {
                // preclaim: AllowTrustLineClawback is not set
                env(coverClawback(issuer),
                    loanBrokerID(brokerKeylet.key),
                    amount(vaultInfo.asset(2)),
                    ter(tecNO_PERMISSION));

                // preclaim: NoFreeze is set
                env(fset(issuer, asfAllowTrustLineClawback | asfNoFreeze));
                env.close();
                env(coverClawback(issuer),
                    loanBrokerID(brokerKeylet.key),
                    amount(vaultInfo.asset(2)),
                    ter(tecNO_PERMISSION));
            }
            else
            {
                // preclaim: MPTCanClawback is not set or MPTCanLock is not set
                env(coverClawback(issuer),
                    loanBrokerID(brokerKeylet.key),
                    amount(vaultInfo.asset(2)),
                    ter(tecNO_PERMISSION));
            }
            env.close();
        }

        if (brokerTest == Delete)
        {
            Account const borrower{"borrower"};
            env.fund(XRP(1'000), borrower);
            env(loan::set(borrower, brokerKeylet.key, asset(50).value()),
                sig(sfCounterpartySignature, alice),
                fee(env.current()->fees().base * 2));

            // preflight: temINVALID (empty/zero broker id)
            testZeroBrokerID([&]() { return del(alice, brokerKeylet.key); });

            // preclaim: tecHAS_OBLIGATIONS
            env(del(alice, brokerKeylet.key), ter(tecHAS_OBLIGATIONS));

            // Repay and delete the loan
            auto const loanKeylet = keylet::loan(brokerKeylet.key, 1);
            env(loan::pay(borrower, loanKeylet.key, asset(50).value()));
            env(loan::del(alice, loanKeylet.key));

            env(trust(issuer, asset(0), alice, tfSetFreeze | tfSetDeepFreeze));
            // preclaim: tecFROZEN (deep frozen)
            env(del(alice, brokerKeylet.key), ter(tecFROZEN));
            env(trust(issuer, asset(0), alice, tfClearFreeze | tfClearDeepFreeze));

            // successful delete the loan broker object
            env(del(alice, brokerKeylet.key), ter(tesSUCCESS));
        }
        else
        {
            env(del(alice, brokerKeylet.key));
        }

        if (brokerTest == Set)
        {
            // preflight: temINVALID (empty/zero broker id)
            testZeroBrokerID([&]() {
                return env.json(set(alice, vaultInfo.vaultID), loanBrokerID(brokerKeylet.key));
            });
            // preflight: temINVALID (empty/zero vault id)
            testZeroVaultID([&]() {
                return env.json(set(alice, vaultInfo.vaultID), loanBrokerID(brokerKeylet.key));
            });

            if (asset.holds<Issue>())
            {
                env(fclear(issuer, asfDefaultRipple));
                env.close();
                // preclaim: DefaultRipple is not set
                env(set(alice, vaultInfo.vaultID), ter(terNO_RIPPLE));

                env(fset(issuer, asfDefaultRipple));
                env.close();
            }

            auto const amt =
                env.balance(alice) - env.current()->fees().accountReserve(env.ownerCount(alice));
            env(pay(alice, issuer, amt));

            // preclaim:: tecINSUFFICIENT_RESERVE
            env(set(alice, vaultInfo.vaultID), ter(tecINSUFFICIENT_RESERVE));
        }
    }

    void
    testInvalidLoanBrokerCoverClawback()
    {
        testcase("Invalid LoanBrokerCoverClawback");
        using namespace jtx;
        using namespace loanBroker;

        // preflight
        {
            Account const alice{"alice"};
            Account const issuer{"issuer"};
            auto const USD = alice["USD"];
            Env env(*this);
            env.fund(XRP(100'000), alice);
            env.close();

            auto jtx = env.jt(coverClawback(alice), amount(USD(100)));

            // holder == account
            env(jtx, ter(temINVALID));

            // holder == beast::zero
            STAmount bad(Issue{USD.currency, beast::zero}, 100);
            jtx.jv[sfAmount] = bad.getJson();
            jtx.stx = env.ust(jtx);
            Serializer s;
            jtx.stx->add(s);
            auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result];
            // fails in doSubmit() on STTx construction
            BEAST_EXPECT(jrr[jss::error] == "invalidTransaction");
            BEAST_EXPECT(jrr[jss::error_exception] == "invalid native account");
        }

        // preclaim

        // Issue:
        // AllowTrustLineClawback is not set or NoFreeze is set
        testLoanBroker({}, CoverClawback);

        // MPTIssue:
        // MPTCanClawback is not set
        testLoanBroker(
            [&](Env& env, Account const& issuer, Account const& alice) -> MPT {
                MPTTester mpt({.env = env, .issuer = issuer, .holders = {alice}});
                return mpt;
            },
            CoverClawback);
    }

    void
    testInvalidLoanBrokerCoverDeposit()
    {
        testcase("Invalid LoanBrokerCoverDeposit");
        using namespace jtx;

        // preclaim:
        // tecWRONG_ASSET, tecINSUFFICIENT_FUNDS, frozen asset
        testLoanBroker({}, CoverDeposit);
    }

    void
    testInvalidLoanBrokerCoverWithdraw()
    {
        testcase("Invalid LoanBrokerCoverWithdraw");
        using namespace jtx;

        /*
        preflight: illegal net
        isLegalNet() check is probably redundant. STAmount parsing
        should throw an exception on deserialize

        preclaim: tecWRONG_ASSET, tecNO_DST, tecDST_TAG_NEEDED,
            tecNO_PERMISSION, checkFrozen failure, checkDeepFrozenFailure,
            second+third tecINSUFFICIENT_FUNDS (can this happen)?
        doApply: tecPATH_DRY (can it happen, funds already checked?)
         */
        testLoanBroker({}, CoverWithdraw);
    }

    void
    testInvalidLoanBrokerDelete()
    {
        using namespace jtx;
        testcase("Invalid LoanBrokerDelete");
        /*
        preclaim: tecHAS_OBLIGATIONS
            doApply:
            accountSend failure, removeEmptyHolding failure,
            all tecHAS_OBLIGATIONS (can any of these happen?)
        */
        testLoanBroker({}, Delete);
    }

    void
    testInvalidLoanBrokerSet()
    {
        using namespace jtx;
        testcase("Invalid LoanBrokerSet");

        /*preclaim: canAddHolding failure (can it happen with MPT?
              can't create Vault if CanTransfer is not enabled.)
            doApply:
            first+second dirLink failure, createPseudoAccount failure,
            addEmptyHolding failure
            can any of these happen?
        */
        testLoanBroker({}, Set);
    }

    void
    testLoanBrokerCoverDepositNullVault()
    {
        // This test is lifted directly from
        // https://bugs.immunefi.com/dashboard/submission/57808
        using namespace jtx;
        Env env(*this);

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

        // Create a Vault owned by alice with an XRP asset
        PrettyAsset const asset{xrpIssue(), 1};
        Vault vault{env};
        auto const [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
        env(createTx);
        env.close();

        // Predict LoanBroker key using alice's current sequence BEFORE submit
        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));

        // Create LoanBroker pointing to the vault
        env(loanBroker::set(alice, vaultKeylet.key));
        env.close();

        // Build the CoverDeposit STTx directly
        STTx tx{ttLOAN_BROKER_COVER_DEPOSIT, [](STObject&) {}};
        tx.setAccountID(sfAccount, alice.id());
        tx.setFieldH256(sfLoanBrokerID, brokerKeylet.key);
        tx.setFieldAmount(sfAmount, asset(1));

        // Create a writable view cloned from the current ledger and remove the
        // vault SLE
        OpenView ov{*env.current()};
        test::StreamSink sink{beast::severities::kWarning};
        beast::Journal jlog{sink};
        ApplyContext ac{env.app(), ov, tx, tesSUCCESS, env.current()->fees().base, tapNONE, jlog};

        if (auto sleBroker = ac.view().peek(keylet::loanbroker(brokerKeylet.key)))
        {
            auto const vaultID = (*sleBroker)[sfVaultID];
            if (auto sleVault = ac.view().peek(keylet::vault(vaultID)))
            {
                ac.view().erase(sleVault);
            }
        }

        // Invoke preclaim against the mutated (ApplyView) view; triggers
        // nullptr deref
        PreclaimContext pctx{env.app(), ac.view(), tesSUCCESS, tx, tapNONE, jlog};
        (void)LoanBrokerCoverDeposit::preclaim(pctx);
    }

    void
    testRequireAuth()
    {
        testcase("Require Auth - Implicit Pseudo-account authorization");
        using namespace jtx;
        using namespace loanBroker;

        Account const issuer{"issuer"};
        Account const alice{"alice"};
        Env env(*this);
        Vault vault{env};

        env.fund(XRP(100'000), issuer, alice);
        env.close();

        auto asset = MPTTester({
            .env = env,
            .issuer = issuer,
            .holders = {alice},
            .flags = MPTDEXFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock,
            .authHolder = true,
        });

        env(pay(issuer, alice, asset(100'000)));
        env.close();

        // Alice is not authorized, can still create the vault
        asset.authorize({.account = issuer, .holder = alice, .flags = tfMPTUnauthorize});
        auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
        env(tx);
        env.close();

        auto const le = env.le(vaultKeylet);
        VaultInfo vaultInfo = [&]() {
            if (BEAST_EXPECT(le))
                return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
            return VaultInfo{asset, {}, {}};
        }();
        if (vaultInfo.vaultID == uint256{})
            return;

        // Can't unauthorize Vault pseudo-account
        asset.authorize(
            {.account = issuer,
             .holder = vaultInfo.pseudoAccount,
             .flags = tfMPTUnauthorize,
             .err = tecNO_PERMISSION});

        auto forUnauthAuth = [&](auto&& doTx) {
            for (auto const flag : {tfMPTUnauthorize, 0u})
            {
                asset.authorize({.account = issuer, .holder = alice, .flags = flag});
                env.close();
                doTx(flag == 0);
                env.close();
            }
        };

        // Can't deposit into Vault if the vault owner is not authorized
        forUnauthAuth([&](bool authorized) {
            auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
            env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(51)}),
                err);
        });

        // Can't withdraw from Vault if the vault owner is not authorized
        forUnauthAuth([&](bool authorized) {
            auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
            env(vault.withdraw({.depositor = alice, .id = vaultKeylet.key, .amount = asset(1)}),
                err);
        });

        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
        // Can create LoanBroker if the vault owner is not authorized
        forUnauthAuth([&](auto) { env(set(alice, vaultInfo.vaultID)); });

        auto const broker = env.le(brokerKeylet);
        if (!BEAST_EXPECT(broker))
            return;
        Account brokerPseudo("pseudo", broker->at(sfAccount));

        // Can't unauthorize LoanBroker pseudo-account
        asset.authorize(
            {.account = issuer,
             .holder = brokerPseudo,
             .flags = tfMPTUnauthorize,
             .err = tecNO_PERMISSION});

        // Can't cover deposit into Vault if the vault owner is not authorized
        forUnauthAuth([&](bool authorized) {
            auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
            env(coverDeposit(alice, brokerKeylet.key, vaultInfo.asset(10)), err);
        });

        // Can't cover withdraw from Vault if the vault owner is not authorized
        forUnauthAuth([&](bool authorized) {
            auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
            env(coverWithdraw(alice, brokerKeylet.key, vaultInfo.asset(5)), err);
        });

        // Issuer can always cover clawback. The holder authorization is n/a.
        forUnauthAuth([&](bool) {
            env(coverClawback(issuer), loanBrokerID(brokerKeylet.key), amount(vaultInfo.asset(1)));
        });
    }

    void
    testLoanBrokerSetDebtMaximum()
    {
        testcase("testLoanBrokerSetDebtMaximum");
        using namespace jtx;
        using namespace loanBroker;
        Account const issuer{"issuer"};
        Account const alice{"alice"};
        Env env(*this);
        Vault vault{env};

        env.fund(XRP(100'000), issuer, alice);
        env.close();

        PrettyAsset const asset = [&]() {
            MPTTester mptt{env, issuer, mptInitNoFund};
            mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
            env.close();
            PrettyAsset const mptAsset = mptt["MPT"];
            mptt.authorize({.account = alice});
            env.close();
            return mptAsset;
        }();

        env(pay(issuer, alice, asset(100'000)));
        env.close();

        auto [tx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
        env(tx);
        env.close();
        auto const le = env.le(vaultKeylet);
        VaultInfo vaultInfo = [&]() {
            if (BEAST_EXPECT(le))
                return VaultInfo{asset, vaultKeylet.key, le->at(sfAccount)};
            return VaultInfo{asset, {}, {}};
        }();
        if (vaultInfo.vaultID == uint256{})
            return;

        env(vault.deposit({.depositor = alice, .id = vaultKeylet.key, .amount = asset(50)}));
        env.close();

        auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
        env(set(alice, vaultInfo.vaultID));
        env.close();

        Account const borrower{"borrower"};
        env.fund(XRP(1'000), borrower);
        env(loan::set(borrower, brokerKeylet.key, asset(50).value()),
            sig(sfCounterpartySignature, alice),
            fee(env.current()->fees().base * 2));
        auto const broker = env.le(brokerKeylet);
        if (!BEAST_EXPECT(broker))
            return;

        BEAST_EXPECT(broker->at(sfDebtTotal) == 50);
        auto debtTotal = broker->at(sfDebtTotal);

        auto tx2 = set(alice, vaultInfo.vaultID);
        tx2[sfLoanBrokerID] = to_string(brokerKeylet.key);
        tx2[sfDebtMaximum] = debtTotal - 1;
        env(tx2, ter(tecLIMIT_EXCEEDED));

        tx2[sfDebtMaximum] = debtTotal + 1;
        env(tx2, ter(tesSUCCESS));

        tx2[sfDebtMaximum] = 0;
        env(tx2, ter(tesSUCCESS));

        tx2[sfDebtMaximum] = Json::Value::maxInt;
        env(tx2, ter(tesSUCCESS));

        {
            auto const dm = power(2, 64) - 1;
            BEAST_EXPECT(dm > maxMPTokenAmount);
            tx2[sfDebtMaximum] = dm;
            env(tx2, ter(temINVALID));
        }

        {
            auto const dm = power(2, 63) - 1;
            BEAST_EXPECTS(dm > maxMPTokenAmount, to_string(dm));
            tx2[sfDebtMaximum] = dm;
            env(tx2, ter(temINVALID));
        }

        {
            auto const dm = power(2, 63) - 3;
            BEAST_EXPECTS(dm == maxMPTokenAmount, to_string(dm));
            tx2[sfDebtMaximum] = dm;
            env(tx2, ter(tesSUCCESS));
        }

        {
            auto const dm = 2 * (power(2, 62) - 1) + 1;
            BEAST_EXPECTS(dm == maxMPTokenAmount, to_string(dm));
            tx2[sfDebtMaximum] = dm;
            env(tx2, ter(tesSUCCESS));
        }

        tx2[sfDebtMaximum] = Number{9223372036854775807, 0};
        env(tx2, ter(tesSUCCESS));
    }

    void
    testRIPD4323()
    {
        testcase << "RIPD-4323";
        using namespace jtx;
        Account const issuer("issuer");
        Account const holder("holder");
        Account const& broker = issuer;

        auto test = [&](auto&& getToken) {
            Env env(*this);

            env.fund(XRP(1'000), issuer, holder);
            env.close();

            auto const [token, deposit, err] = getToken(env);

            Vault vault(env);
            auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
            env(tx);
            env.close();

            env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = deposit}),
                ter(err));
            env.close();

            auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));

            env(loanBroker::set(broker, keylet.key));
            env.close();

            env(loanBroker::coverDeposit(broker, brokerKeylet.key, deposit), ter(err));
            env.close();
        };

        test([&](Env&) {
            // issuer can issue any amount
            auto const token = issuer["IOU"];
            return std::make_tuple(token, token(1'000), tesSUCCESS);
        });
        std::vector<std::tuple<
            std::uint64_t,                 // pay to holder
            std::optional<std::uint64_t>,  // max amount
            std::uint64_t,                 // deposit amount
            TER>>                          // expected error
            mptTests = {
                // issuer can issue up to 2'000 tokens
                {2'000, 4'000, 1'000, tesSUCCESS},
                // issuer can issue 500 tokens (250 VaultDeposit +
                // 250 LoanBrokerCoverDeposit)
                {2'000, 2'500, 250, tesSUCCESS},
                // issuer can issue 500 tokens (250 VaultDeposit +
                // 250 LoanBrokerCoverDeposit). MaximumAmount is default.
                {maxMPTokenAmount - 500, std::nullopt, 250, tesSUCCESS},
                // issuer can issue 500, and fails on depositing 1'000
                {2'000, 2'500, 1'000, tecINSUFFICIENT_FUNDS},
                // issuer has already issued MaximumAmount
                {2'000, 2'000, 1'000, tecINSUFFICIENT_FUNDS},
                // issuer has already issued MaximumAmount. MaximumAmount is
                // default.
                {maxMPTokenAmount, std::nullopt, 250, tecINSUFFICIENT_FUNDS},
            };
        for (auto const& [pay, max, deposit, err] : mptTests)
        {
            test([&](Env& env) -> std::tuple<MPT, PrettyAmount, TER> {
                MPT const token = MPTTester(
                    {.env = env,
                     .issuer = issuer,
                     .holders = {holder},
                     .pay = pay,
                     .flags = MPTDEXFlags,
                     .maxAmt = max});
                return std::make_tuple(token, token(deposit), err);
            });
        }
    }

    void
    testAMB06_VaultFreezeCheckMissing()
    {
        testcase << "RIPD-4466 - LoanBrokerSet disallows frozen vaults";
        using namespace jtx;
        Env env(*this);

        Account const issuer{"issuer"}, lender{"lender"}, borrower{"borrower"};
        env.fund(XRP(20'000), issuer, lender, borrower);
        auto const IOU = issuer["IOU"];

        Vault vault{env};
        auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = IOU.asset()});
        env(tx);
        env.close();

        // Get vault pseudo-account and FREEZE it
        auto const vaultSle = env.le(vaultKeylet);
        auto const vaultPseudo = vaultSle->at(sfAccount);
        auto const vaultPseudoAcct = Account("VaultPseudo", vaultPseudo);
        env(trust(issuer, vaultPseudoAcct["IOU"](0), tfSetFreeze));

        env(loanBroker::set(lender, vaultKeylet.key), ter(tecFROZEN));
    }

    void
    testRIPD4274IOU()
    {
        using namespace jtx;
        Account issuer("broker");
        Account broker("issuer");
        Account dest("destination");
        auto const token = issuer["IOU"];

        enum TrustState {
            RequireAuth,
            ZeroLimit,
            ReachedLimit,
            NearLimit,
            NoTrustLine,
        };

        auto test = [&](TrustState trustState) {
            Env env(*this);

            testcase << "RIPD-4274 IOU with state: " << static_cast<int>(trustState);

            auto setTrustLine = [&](Account const& acct, TrustState state) {
                switch (state)
                {
                    case RequireAuth:
                        env(trust(issuer, token(0), acct, tfSetfAuth));
                        break;
                    case ZeroLimit: {
                        auto jv = trust(acct, token(0));
                        // set QualityIn so that the trustline is not
                        // auto-deleted
                        jv[sfQualityIn] = 10'000'000;
                        env(jv);
                    }
                    break;
                    case ReachedLimit: {
                        env(trust(acct, token(1'000)));
                        env(pay(issuer, acct, token(1'000)));
                        env.close();
                    }
                    break;
                    case NearLimit: {
                        env(trust(acct, token(1'000)));
                        env(pay(issuer, acct, token(950)));
                        env.close();
                    }
                    break;
                    case NoTrustLine:
                        // don't create a trustline
                        break;
                    default:
                        BEAST_EXPECT(false);
                }
                env.close();
            };

            env.fund(XRP(1'000), issuer, broker, dest);
            env.close();

            if (trustState == RequireAuth)
            {
                env(fset(issuer, asfRequireAuth));
                env.close();

                setTrustLine(broker, RequireAuth);
            }

            setTrustLine(dest, trustState);

            env(trust(broker, token(2'000), 0));
            env(pay(issuer, broker, token(2'000)));
            env.close();

            Vault vault(env);
            auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
            env(tx);
            env.close();

            // Test Vault withdraw
            env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
            env.close();

            env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}),
                loanBroker::destination(dest),
                ter(std::ignore));
            BEAST_EXPECT(env.ter() == tecNO_LINE);
            env.close();

            env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));

            // Test LoanBroker withdraw
            auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));

            env(loanBroker::set(broker, keylet.key));
            env.close();

            env(loanBroker::coverDeposit(broker, brokerKeylet.key, token(1'000)));
            env.close();

            env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)),
                loanBroker::destination(dest),
                ter(std::ignore));
            BEAST_EXPECT(env.ter() == tecNO_LINE);
            env.close();

            // Clearing RequireAuth shouldn't change the result
            if (trustState == RequireAuth)
            {
                env(fclear(issuer, asfRequireAuth));
                env.close();

                env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)),
                    loanBroker::destination(dest),
                    ter(std::ignore));
                BEAST_EXPECT(env.ter() == tecNO_LINE);
                env.close();
            }
        };

        test(RequireAuth);
        test(ZeroLimit);
        test(ReachedLimit);
        test(NearLimit);
        test(NoTrustLine);
    }

    void
    testRIPD4274MPT()
    {
        using namespace jtx;
        Account issuer("broker");
        Account broker("issuer");
        Account dest("destination");

        enum MPTState {
            RequireAuth,
            ReachedMAX,
            NoMPT,
        };

        auto test = [&](MPTState MPTState) {
            Env env(*this);

            testcase << "RIPD-4274 MPT with state: " << static_cast<int>(MPTState);

            env.fund(XRP(1'000), issuer, broker, dest);
            env.close();

            auto const maybeToken = [&]() -> std::optional<MPT> {
                switch (MPTState)
                {
                    case RequireAuth: {
                        auto tester = MPTTester(
                            {.env = env,
                             .issuer = issuer,
                             .holders = {broker, dest},
                             .pay = 2'000,
                             .flags = MPTDEXFlags | tfMPTRequireAuth,
                             .authHolder = true,
                             .maxAmt = 5'000});
                        // unauthorize dest
                        tester.authorize(
                            {.account = issuer, .holder = dest, .flags = tfMPTUnauthorize});
                        return tester;
                    }
                    case ReachedMAX: {
                        auto tester = MPTTester(
                            {.env = env,
                             .issuer = issuer,
                             .holders = {broker, dest},
                             .pay = 2'000,
                             .flags = MPTDEXFlags,
                             .maxAmt = 4'000});
                        BEAST_EXPECT(env.balance(issuer, tester) == tester(-4'000));
                        return tester;
                    }
                    case NoMPT: {
                        return MPTTester(
                            {.env = env,
                             .issuer = issuer,
                             .holders = {broker},
                             .pay = 2'000,
                             .flags = MPTDEXFlags,
                             .maxAmt = 4'000});
                    }
                    default:
                        return std::nullopt;
                }
            }();
            if (!BEAST_EXPECT(maybeToken))
                return;

            auto const& token = *maybeToken;

            Vault vault(env);
            auto const [tx, keylet] = vault.create({.owner = broker, .asset = token.asset()});
            env(tx);
            env.close();

            // Test Vault withdraw
            env(vault.deposit({.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
            env.close();

            env(vault.withdraw({.depositor = broker, .id = keylet.key, .amount = token(1'000)}),
                loanBroker::destination(dest),
                ter(std::ignore));

            // Shouldn't fail if at MaximumAmount since no new tokens are issued
            TER const err = MPTState == ReachedMAX ? TER(tesSUCCESS) : tecNO_AUTH;
            BEAST_EXPECT(env.ter() == err);
            env.close();

            if (!isTesSuccess(err))
            {
                env(vault.withdraw(
                    {.depositor = broker, .id = keylet.key, .amount = token(1'000)}));
            }

            // Test LoanBroker withdraw
            auto const brokerKeylet = keylet::loanbroker(broker, env.seq(broker));

            env(loanBroker::set(broker, keylet.key));
            env.close();

            env(loanBroker::coverDeposit(broker, brokerKeylet.key, token(1'000)));
            env.close();

            env(loanBroker::coverWithdraw(broker, brokerKeylet.key, token(100)),
                loanBroker::destination(dest),
                ter(std::ignore));
            BEAST_EXPECT(env.ter() == err);
            env.close();
        };

        test(RequireAuth);
        test(ReachedMAX);
        test(NoMPT);
    }

    void
    testRIPD4274()
    {
        testRIPD4274IOU();
        testRIPD4274MPT();
    }

public:
    void
    run() override
    {
        testLoanBrokerSetDebtMaximum();
        testLoanBrokerCoverDepositNullVault();

        testDisabled();
        testLifecycle();
        testInvalidLoanBrokerCoverClawback();
        testInvalidLoanBrokerCoverDeposit();
        testInvalidLoanBrokerCoverWithdraw();
        testInvalidLoanBrokerDelete();
        testInvalidLoanBrokerSet();
        testRequireAuth();

        testRIPD4323();
        testAMB06_VaultFreezeCheckMissing();

        testRIPD4274();

        // TODO: Write clawback failure tests with an issuer / MPT that doesn't
        // have the right flags set.
    }
};

BEAST_DEFINE_TESTSUITE(LoanBroker, tx, xrpl);

}  // namespace test
}  // namespace xrpl
