#include <test/jtx.h>

#include <xrpld/app/misc/Transaction.h>

#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/jss.h>

namespace xrpl {

class Ticket_test : public beast::unit_test::suite
{
    /// @brief Validate metadata for a successful TicketCreate transaction.
    ///
    /// @param env current jtx env (tx and meta are extracted using it)
    void
    checkTicketCreateMeta(test::jtx::Env& env)
    {
        using namespace std::string_literals;

        Json::Value const& tx{env.tx()->getJson(JsonOptions::none)};
        {
            std::string const txType = tx[sfTransactionType.jsonName].asString();

            if (!BEAST_EXPECTS(
                    txType == jss::TicketCreate, "Unexpected TransactionType: "s + txType))
                return;
        }

        std::uint32_t const count = {tx[sfTicketCount.jsonName].asUInt()};
        if (!BEAST_EXPECTS(count >= 1, "Unexpected ticket count: "s + std::to_string(count)))
            return;

        std::uint32_t const txSeq = {tx[sfSequence.jsonName].asUInt()};
        std::string const account = tx[sfAccount.jsonName].asString();

        Json::Value const& metadata = env.meta()->getJson(JsonOptions::none);
        if (!BEAST_EXPECTS(
                metadata.isMember(sfTransactionResult.jsonName) &&
                    metadata[sfTransactionResult.jsonName].asString() == "tesSUCCESS",
                "Not metadata for successful TicketCreate."))
            return;

        BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName));
        BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].isArray());

        bool directoryChanged = false;
        std::uint32_t acctRootFinalSeq = {0};
        std::vector<std::uint32_t> ticketSeqs;
        ticketSeqs.reserve(count);
        for (Json::Value const& node : metadata[sfAffectedNodes.jsonName])
        {
            if (node.isMember(sfModifiedNode.jsonName))
            {
                Json::Value const& modified = node[sfModifiedNode.jsonName];
                std::string const entryType = modified[sfLedgerEntryType.jsonName].asString();
                if (entryType == jss::AccountRoot)
                {
                    auto const& previousFields = modified[sfPreviousFields.jsonName];
                    auto const& finalFields = modified[sfFinalFields.jsonName];
                    {
                        // Verify the account root Sequence did the right thing.
                        std::uint32_t const prevSeq = previousFields[sfSequence.jsonName].asUInt();

                        acctRootFinalSeq = finalFields[sfSequence.jsonName].asUInt();

                        if (txSeq == 0)
                        {
                            // Transaction used a TicketSequence.
                            BEAST_EXPECT(acctRootFinalSeq == prevSeq + count);
                        }
                        else
                        {
                            // Transaction used a (plain) Sequence.
                            BEAST_EXPECT(prevSeq == txSeq);
                            BEAST_EXPECT(acctRootFinalSeq == prevSeq + count + 1);
                        }
                    }

                    std::uint32_t const consumedTickets = {txSeq == 0u ? 1u : 0u};

                    // If...
                    //  1. The TicketCount is 1 and
                    //  2. A ticket was consumed by the ticket create, then
                    //  3. The final TicketCount did not change, so the
                    //     previous TicketCount is not reported.
                    // But, since the count did not change, we know it equals
                    // the final Ticket count.
                    bool const unreportedPrevTicketCount = {count == 1 && txSeq == 0};

                    // Verify the OwnerCount did the right thing
                    if (unreportedPrevTicketCount)
                    {
                        // The number of Tickets should not have changed, so
                        // the previous OwnerCount should not be reported.
                        BEAST_EXPECT(!previousFields.isMember(sfOwnerCount.jsonName));
                    }
                    else
                    {
                        // Verify the OwnerCount did the right thing.
                        std::uint32_t const prevCount = {
                            previousFields[sfOwnerCount.jsonName].asUInt()};

                        std::uint32_t const finalCount = {
                            finalFields[sfOwnerCount.jsonName].asUInt()};

                        BEAST_EXPECT(prevCount + count - consumedTickets == finalCount);
                    }

                    // Verify TicketCount metadata.
                    BEAST_EXPECT(finalFields.isMember(sfTicketCount.jsonName));

                    if (unreportedPrevTicketCount)
                    {
                        // The number of Tickets should not have changed, so
                        // the previous TicketCount should not be reported.
                        BEAST_EXPECT(!previousFields.isMember(sfTicketCount.jsonName));
                    }
                    else
                    {
                        // If the TicketCount was previously present it
                        // should have been greater than zero.
                        std::uint32_t const startCount = {
                            previousFields.isMember(sfTicketCount.jsonName)
                                ? previousFields[sfTicketCount.jsonName].asUInt()
                                : 0u};

                        BEAST_EXPECT(
                            (startCount == 0u) ^ previousFields.isMember(sfTicketCount.jsonName));

                        BEAST_EXPECT(
                            startCount + count - consumedTickets ==
                            finalFields[sfTicketCount.jsonName]);
                    }
                }
                else if (entryType == jss::DirectoryNode)
                {
                    directoryChanged = true;
                }
                else
                {
                    fail("Unexpected modified node: "s + entryType, __FILE__, __LINE__);
                }
            }
            else if (node.isMember(sfCreatedNode.jsonName))
            {
                Json::Value const& created = node[sfCreatedNode.jsonName];
                std::string const entryType = created[sfLedgerEntryType.jsonName].asString();
                if (entryType == jss::Ticket)
                {
                    auto const& newFields = created[sfNewFields.jsonName];

                    BEAST_EXPECT(newFields[sfAccount.jsonName].asString() == account);
                    ticketSeqs.push_back(newFields[sfTicketSequence.jsonName].asUInt());
                }
                else if (entryType == jss::DirectoryNode)
                {
                    directoryChanged = true;
                }
                else
                {
                    fail("Unexpected created node: "s + entryType, __FILE__, __LINE__);
                }
            }
            else if (node.isMember(sfDeletedNode.jsonName))
            {
                Json::Value const& deleted = node[sfDeletedNode.jsonName];
                std::string const entryType = deleted[sfLedgerEntryType.jsonName].asString();

                if (entryType == jss::Ticket)
                {
                    // Verify the transaction's Sequence == 0.
                    BEAST_EXPECT(txSeq == 0);

                    // Verify the account of the deleted ticket.
                    auto const& finalFields = deleted[sfFinalFields.jsonName];
                    BEAST_EXPECT(finalFields[sfAccount.jsonName].asString() == account);

                    // Verify the deleted ticket has the right TicketSequence.
                    BEAST_EXPECT(
                        finalFields[sfTicketSequence.jsonName].asUInt() ==
                        tx[sfTicketSequence.jsonName].asUInt());
                }
            }
            else
            {
                fail("Unexpected node type in TicketCreate metadata.", __FILE__, __LINE__);
            }
        }
        BEAST_EXPECT(directoryChanged);

        // Verify that all the expected Tickets were created.
        BEAST_EXPECT(ticketSeqs.size() == count);
        std::sort(ticketSeqs.begin(), ticketSeqs.end());
        BEAST_EXPECT(std::adjacent_find(ticketSeqs.begin(), ticketSeqs.end()) == ticketSeqs.end());
        BEAST_EXPECT(*ticketSeqs.rbegin() == acctRootFinalSeq - 1);
    }

    /// @brief Validate metadata for a ticket using transaction.
    ///
    /// The transaction may have been successful or failed with a tec.
    ///
    /// @param env current jtx env (tx and meta are extracted using it)
    void
    checkTicketConsumeMeta(test::jtx::Env& env)
    {
        Json::Value const& tx{env.tx()->getJson(JsonOptions::none)};

        // Verify that the transaction includes a TicketSequence.

        // Capture that TicketSequence.
        // Capture the Account from the transaction

        // Verify that metadata indicates a tec or a tesSUCCESS.

        // Walk affected nodes:
        //
        //   For each deleted node, see if it is a Ticket node.  If it is
        //   a Ticket Node being deleted, then assert that the...
        //
        //       Account == the transaction Account &&
        //       TicketSequence == the transaction TicketSequence
        //
        //   If a modified node is an AccountRoot, see if it is the transaction
        //   Account.  If it is then verify the TicketCount decreased by one.
        //   If the old TicketCount was 1, then the TicketCount field should be
        //   removed from the final fields of the AccountRoot.
        //
        // After looking at all nodes verify that exactly one Ticket node
        // was deleted.
        BEAST_EXPECT(tx[sfSequence.jsonName].asUInt() == 0);
        std::string const account{tx[sfAccount.jsonName].asString()};
        if (!BEAST_EXPECTS(
                tx.isMember(sfTicketSequence.jsonName),
                "Not metadata for a ticket consuming transaction."))
            return;

        std::uint32_t const ticketSeq{tx[sfTicketSequence.jsonName].asUInt()};

        Json::Value const& metadata{env.meta()->getJson(JsonOptions::none)};
        if (!BEAST_EXPECTS(
                metadata.isMember(sfTransactionResult.jsonName),
                "Metadata is missing TransactionResult."))
            return;

        {
            std::string const transactionResult{metadata[sfTransactionResult.jsonName].asString()};
            if (!BEAST_EXPECTS(
                    transactionResult == "tesSUCCESS" ||
                        transactionResult.compare(0, 3, "tec") == 0,
                    transactionResult + " neither tesSUCCESS nor tec"))
                return;
        }

        BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName));
        BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].isArray());

        bool acctRootFound{false};
        std::uint32_t acctRootSeq{0};
        int ticketsRemoved{0};
        for (Json::Value const& node : metadata[sfAffectedNodes.jsonName])
        {
            if (node.isMember(sfModifiedNode.jsonName))
            {
                Json::Value const& modified{node[sfModifiedNode.jsonName]};
                std::string const entryType = modified[sfLedgerEntryType.jsonName].asString();
                if (entryType == "AccountRoot" &&
                    modified[sfFinalFields.jsonName][sfAccount.jsonName].asString() == account)
                {
                    acctRootFound = true;

                    auto const& previousFields = modified[sfPreviousFields.jsonName];
                    auto const& finalFields = modified[sfFinalFields.jsonName];

                    acctRootSeq = finalFields[sfSequence.jsonName].asUInt();

                    // Check that the TicketCount was present and decremented
                    // by 1.  If it decremented to zero, then the field should
                    // be gone.
                    if (!BEAST_EXPECTS(
                            previousFields.isMember(sfTicketCount.jsonName),
                            "AccountRoot previous is missing TicketCount"))
                        return;

                    std::uint32_t const prevTicketCount =
                        previousFields[sfTicketCount.jsonName].asUInt();

                    BEAST_EXPECT(prevTicketCount > 0);
                    if (prevTicketCount == 1)
                    {
                        BEAST_EXPECT(!finalFields.isMember(sfTicketCount.jsonName));
                    }
                    else
                    {
                        BEAST_EXPECT(
                            finalFields.isMember(sfTicketCount.jsonName) &&
                            finalFields[sfTicketCount.jsonName].asUInt() == prevTicketCount - 1);
                    }
                }
            }
            else if (node.isMember(sfDeletedNode.jsonName))
            {
                Json::Value const& deleted{node[sfDeletedNode.jsonName]};
                std::string const entryType{deleted[sfLedgerEntryType.jsonName].asString()};

                if (entryType == jss::Ticket)
                {
                    // Verify the account of the deleted ticket.
                    BEAST_EXPECT(
                        deleted[sfFinalFields.jsonName][sfAccount.jsonName].asString() == account);

                    // Verify the deleted ticket has the right TicketSequence.
                    BEAST_EXPECT(
                        deleted[sfFinalFields.jsonName][sfTicketSequence.jsonName].asUInt() ==
                        ticketSeq);

                    ++ticketsRemoved;
                }
            }
        }
        BEAST_EXPECT(acctRootFound);
        BEAST_EXPECT(ticketsRemoved == 1);
        BEAST_EXPECT(ticketSeq < acctRootSeq);
    }

    void
    testTicketCreatePreflightFail()
    {
        testcase("Create Tickets that fail Preflight");

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

        Account const master{env.master};

        // Exercise boundaries on count.
        env(ticket::create(master, 0), ter(temINVALID_COUNT));
        env(ticket::create(master, 251), ter(temINVALID_COUNT));

        // Exercise fees.
        std::uint32_t const ticketSeq_A{env.seq(master) + 1};
        env(ticket::create(master, 1), fee(XRP(10)));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(master, 1), tickets(master, 1));

        env(ticket::create(master, 1), fee(XRP(-1)), ter(temBAD_FEE));

        // Exercise flags.
        std::uint32_t const ticketSeq_B{env.seq(master) + 1};
        env(ticket::create(master, 1), txflags(tfFullyCanonicalSig));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(master, 2), tickets(master, 2));

        env(ticket::create(master, 1), txflags(tfSell), ter(temINVALID_FLAG));
        env.close();
        env.require(owners(master, 2), tickets(master, 2));

        // We successfully created 1 ticket earlier.  Verify that we can
        // create 250 tickets in one shot.  We must consume one ticket first.
        env(noop(master), ticket::use(ticketSeq_A));
        checkTicketConsumeMeta(env);
        env.close();
        env.require(owners(master, 1), tickets(master, 1));

        env(ticket::create(master, 250), ticket::use(ticketSeq_B));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(master, 250), tickets(master, 250));
    }

    void
    testTicketCreatePreclaimFail()
    {
        testcase("Create Tickets that fail Preclaim");

        using namespace test::jtx;
        {
            // Create tickets on a non-existent account.
            Env env{*this};
            Account alice{"alice"};
            env.memoize(alice);

            env(ticket::create(alice, 1), json(jss::Sequence, 1), ter(terNO_ACCOUNT));
        }
        {
            // Exceed the threshold where tickets can no longer be
            // added to an account.
            Env env{*this};
            Account alice{"alice"};

            env.fund(XRP(100000), alice);

            std::uint32_t ticketSeq{env.seq(alice) + 1};
            env(ticket::create(alice, 250));
            checkTicketCreateMeta(env);
            env.close();
            env.require(owners(alice, 250), tickets(alice, 250));

            // Note that we can add one more ticket while consuming a ticket
            // because the final result is still 250 tickets.
            env(ticket::create(alice, 1), ticket::use(ticketSeq + 0));
            checkTicketCreateMeta(env);
            env.close();
            env.require(owners(alice, 250), tickets(alice, 250));

            // Adding one more ticket will exceed the threshold.
            env(ticket::create(alice, 2), ticket::use(ticketSeq + 1), ter(tecDIR_FULL));
            env.close();
            env.require(owners(alice, 249), tickets(alice, 249));

            // Now we can successfully add one more ticket.
            env(ticket::create(alice, 2), ticket::use(ticketSeq + 2));
            checkTicketCreateMeta(env);
            env.close();
            env.require(owners(alice, 250), tickets(alice, 250));

            // Since we're at 250, we can't add another ticket using a
            // sequence.
            env(ticket::create(alice, 1), ter(tecDIR_FULL));
            env.close();
            env.require(owners(alice, 250), tickets(alice, 250));
        }
        {
            // Explore exceeding the ticket threshold from another angle.
            Env env{*this};
            Account alice{"alice"};

            env.fund(XRP(100000), alice);
            env.close();

            std::uint32_t ticketSeq_AB{env.seq(alice) + 1};
            env(ticket::create(alice, 2));
            checkTicketCreateMeta(env);
            env.close();
            env.require(owners(alice, 2), tickets(alice, 2));

            // Adding 250 tickets (while consuming one) will exceed the
            // threshold.
            env(ticket::create(alice, 250), ticket::use(ticketSeq_AB + 0), ter(tecDIR_FULL));
            env.close();
            env.require(owners(alice, 1), tickets(alice, 1));

            // Adding 250 tickets (without consuming one) will exceed the
            // threshold.
            env(ticket::create(alice, 250), ter(tecDIR_FULL));
            env.close();
            env.require(owners(alice, 1), tickets(alice, 1));

            // Alice can now add 250 tickets while consuming one.
            env(ticket::create(alice, 250), ticket::use(ticketSeq_AB + 1));
            checkTicketCreateMeta(env);
            env.close();
            env.require(owners(alice, 250), tickets(alice, 250));
        }
    }

    void
    testTicketInsufficientReserve()
    {
        testcase("Create Ticket Insufficient Reserve");

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

        // Fund alice not quite enough to make the reserve for a Ticket.
        env.fund(env.current()->fees().accountReserve(1) - drops(1), alice);
        env.close();

        env(ticket::create(alice, 1), ter(tecINSUFFICIENT_RESERVE));
        env.close();
        env.require(owners(alice, 0), tickets(alice, 0));

        // Give alice enough to exactly meet the reserve for one Ticket.
        env(pay(env.master, alice, env.current()->fees().accountReserve(1) - env.balance(alice)));
        env.close();

        env(ticket::create(alice, 1));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(alice, 1), tickets(alice, 1));

        // Give alice not quite enough to make the reserve for a total of
        // 250 Tickets.
        env(
            pay(env.master,
                alice,
                env.current()->fees().accountReserve(250) - drops(1) - env.balance(alice)));
        env.close();

        // alice doesn't quite have the reserve for a total of 250
        // Tickets, so the transaction fails.
        env(ticket::create(alice, 249), ter(tecINSUFFICIENT_RESERVE));
        env.close();
        env.require(owners(alice, 1), tickets(alice, 1));

        // Give alice enough so she can make the reserve for all 250
        // Tickets.
        env(pay(env.master, alice, env.current()->fees().accountReserve(250) - env.balance(alice)));
        env.close();

        std::uint32_t const ticketSeq{env.seq(alice) + 1};
        env(ticket::create(alice, 249));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(alice, 250), tickets(alice, 250));
        BEAST_EXPECT(ticketSeq + 249 == env.seq(alice));
    }

    void
    testUsingTickets()
    {
        testcase("Using Tickets");

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

        env.fund(XRP(10000), alice);
        env.close();

        // Successfully create tickets (using a sequence)
        std::uint32_t const ticketSeq_AB{env.seq(alice) + 1};
        env(ticket::create(alice, 2));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(alice, 2), tickets(alice, 2));
        BEAST_EXPECT(ticketSeq_AB + 2 == env.seq(alice));

        // You can use a ticket to create one ticket ...
        std::uint32_t const ticketSeq_C{env.seq(alice)};
        env(ticket::create(alice, 1), ticket::use(ticketSeq_AB + 0));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(alice, 2), tickets(alice, 2));
        BEAST_EXPECT(ticketSeq_C + 1 == env.seq(alice));

        // ... you can use a ticket to create multiple tickets ...
        std::uint32_t const ticketSeq_DE{env.seq(alice)};
        env(ticket::create(alice, 2), ticket::use(ticketSeq_AB + 1));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(alice, 3), tickets(alice, 3));
        BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));

        // ... and you can use a ticket for other things.
        env(noop(alice), ticket::use(ticketSeq_DE + 0));
        checkTicketConsumeMeta(env);
        env.close();
        env.require(owners(alice, 2), tickets(alice, 2));
        BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));

        env(pay(alice, env.master, XRP(20)), ticket::use(ticketSeq_DE + 1));
        checkTicketConsumeMeta(env);
        env.close();
        env.require(owners(alice, 1), tickets(alice, 1));
        BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));

        env(trust(alice, env.master["USD"](20)), ticket::use(ticketSeq_C));
        checkTicketConsumeMeta(env);
        env.close();
        env.require(owners(alice, 1), tickets(alice, 0));
        BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice));

        // Attempt to use a ticket that has already been used.
        env(noop(alice), ticket::use(ticketSeq_C), ter(tefNO_TICKET));
        env.close();

        // Attempt to use a ticket from the future.
        std::uint32_t const ticketSeq_F{env.seq(alice) + 1};
        env(noop(alice), ticket::use(ticketSeq_F), ter(terPRE_TICKET));
        env.close();

        // Now create the ticket.  The retry will consume the new ticket.
        env(ticket::create(alice, 1));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(alice, 1), tickets(alice, 0));
        BEAST_EXPECT(ticketSeq_F + 1 == env.seq(alice));

        // Try a transaction that combines consuming a ticket with
        // AccountTxnID.
        std::uint32_t const ticketSeq_G{env.seq(alice) + 1};
        env(ticket::create(alice, 1));
        checkTicketCreateMeta(env);
        env.close();

        env(noop(alice),
            ticket::use(ticketSeq_G),
            json(R"({"AccountTxnID": "0"})"),
            ter(temINVALID));
        env.close();
        env.require(owners(alice, 2), tickets(alice, 1));
    }

    void
    testTransactionDatabaseWithTickets()
    {
        // The Transaction database keeps each transaction's sequence number
        // in an entry (called "FromSeq").  Until the introduction of tickets
        // each sequence stored for a given account would always be unique.
        // With the advent of tickets there could be lots of entries
        // with zero.
        //
        // We really don't expect those zeros to cause any problems since
        // there are no indexes that use "FromSeq".  But it still seems
        // prudent to exercise this a bit to see if tickets cause any obvious
        // harm.
        testcase("Transaction Database With Tickets");

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

        env.fund(XRP(10000), alice);
        env.close();

        // Lambda that returns the hash of the most recent transaction.
        auto getTxID = [&env, this]() -> uint256 {
            std::shared_ptr<STTx const> tx{env.tx()};
            if (!BEAST_EXPECTS(tx, "Transaction not found"))
                Throw<std::invalid_argument>("Invalid transaction ID");

            return tx->getTransactionID();
        };

        // A note about the metadata created by these transactions.
        //
        // We _could_ check the metadata on these transactions.  However
        // checking the metadata has the side effect of advancing the ledger.
        // So if we check the metadata we don't get to look at several
        // transactions in the same ledger.  Therefore a specific choice was
        // made to not check the metadata on these transactions.

        // Successfully create several tickets (using a sequence).
        std::uint32_t ticketSeq{env.seq(alice)};
        static constexpr std::uint32_t ticketCount{10};
        env(ticket::create(alice, ticketCount));
        uint256 const txHash_1{getTxID()};

        // Just for grins use the tickets in reverse from largest to smallest.
        ticketSeq += ticketCount;
        env(noop(alice), ticket::use(--ticketSeq));
        uint256 const txHash_2{getTxID()};

        env(pay(alice, env.master, XRP(200)), ticket::use(--ticketSeq));
        uint256 const txHash_3{getTxID()};

        env(deposit::auth(alice, env.master), ticket::use(--ticketSeq));
        uint256 const txHash_4{getTxID()};

        // Close the ledger so we look at transactions from a couple of
        // different ledgers.
        env.close();

        env(pay(alice, env.master, XRP(300)), ticket::use(--ticketSeq));
        uint256 const txHash_5{getTxID()};

        env(pay(alice, env.master, XRP(400)), ticket::use(--ticketSeq));
        uint256 const txHash_6{getTxID()};

        env(deposit::unauth(alice, env.master), ticket::use(--ticketSeq));
        uint256 const txHash_7{getTxID()};

        env(noop(alice), ticket::use(--ticketSeq));
        uint256 const txHash_8{getTxID()};

        env.close();

        // Checkout what's in the Transaction database.  We go straight
        // to the database.  Most of our interfaces cache transactions
        // in memory.  So if we use normal interfaces we would get the
        // transactions from memory rather than from the database.

        // Lambda to verify a transaction pulled from the Transaction database.
        auto checkTxFromDB = [&env, this](
                                 uint256 const& txID,
                                 std::uint32_t ledgerSeq,
                                 std::uint32_t txSeq,
                                 std::optional<std::uint32_t> ticketSeq,
                                 TxType txType) {
            error_code_i txErrCode{rpcSUCCESS};

            using TxPair = std::pair<std::shared_ptr<Transaction>, std::shared_ptr<TxMeta>>;
            std::variant<TxPair, TxSearched> maybeTx =
                Transaction::load(txID, env.app(), txErrCode);

            BEAST_EXPECT(txErrCode == rpcSUCCESS);
            if (auto txPtr = std::get_if<TxPair>(&maybeTx))
            {
                std::shared_ptr<Transaction>& tx = txPtr->first;
                BEAST_EXPECT(tx->getLedger() == ledgerSeq);
                std::shared_ptr<STTx const> const& sttx = tx->getSTransaction();
                BEAST_EXPECT((*sttx)[sfSequence] == txSeq);
                if (ticketSeq)
                    BEAST_EXPECT((*sttx)[sfTicketSequence] == *ticketSeq);
                BEAST_EXPECT((*sttx)[sfTransactionType] == txType);
            }
            else
            {
                fail("Expected transaction was not found");
            }
        };

        //   txID ledgerSeq txSeq ticketSeq          txType
        checkTxFromDB(txHash_1, 4, 4, {}, ttTICKET_CREATE);
        checkTxFromDB(txHash_2, 4, 0, 13, ttACCOUNT_SET);
        checkTxFromDB(txHash_3, 4, 0, 12, ttPAYMENT);
        checkTxFromDB(txHash_4, 4, 0, 11, ttDEPOSIT_PREAUTH);

        checkTxFromDB(txHash_5, 5, 0, 10, ttPAYMENT);
        checkTxFromDB(txHash_6, 5, 0, 9, ttPAYMENT);
        checkTxFromDB(txHash_7, 5, 0, 8, ttDEPOSIT_PREAUTH);
        checkTxFromDB(txHash_8, 5, 0, 7, ttACCOUNT_SET);
    }

    void
    testSignWithTicketSequence()
    {
        // The sign and the submit RPC commands automatically fill in the
        // Sequence field of a transaction if none is provided.  If a
        // TicketSequence is provided in the transaction, then the
        // auto-filled Sequence should be zero.
        testcase("Sign with TicketSequence");

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

        env.fund(XRP(10000), alice);
        env.close();

        // Successfully create tickets (using a sequence)
        std::uint32_t const ticketSeq = env.seq(alice) + 1;
        env(ticket::create(alice, 2));
        checkTicketCreateMeta(env);
        env.close();
        env.require(owners(alice, 2), tickets(alice, 2));
        BEAST_EXPECT(ticketSeq + 2 == env.seq(alice));

        {
            // Test that the "sign" RPC command fills in a "Sequence": 0 field
            // if none is provided.

            // Create a noop transaction using a TicketSequence but don't fill
            // in the Sequence field.
            Json::Value tx = Json::objectValue;
            tx[jss::tx_json] = noop(alice);
            tx[jss::tx_json][sfTicketSequence.jsonName] = ticketSeq;
            tx[jss::secret] = toBase58(generateSeed("alice"));

            // Verify that there is no "Sequence" field.
            BEAST_EXPECT(!tx[jss::tx_json].isMember(sfSequence.jsonName));

            // Call the "sign" RPC command and see the "Sequence": 0 field
            // filled in.
            Json::Value jr = env.rpc("json", "sign", to_string(tx));

            // Verify that "sign" inserted a "Sequence": 0 field.
            if (BEAST_EXPECT(jr[jss::result][jss::tx_json].isMember(sfSequence.jsonName)))
            {
                BEAST_EXPECT(jr[jss::result][jss::tx_json][sfSequence.jsonName] == 0);
            }

            // "sign" should not have consumed any of alice's tickets.
            env.close();
            env.require(owners(alice, 2), tickets(alice, 2));

            // "submit" the signed blob and see one of alice's tickets consumed.
            env.rpc("submit", jr[jss::result][jss::tx_blob].asString());
            env.close();
            env.require(owners(alice, 1), tickets(alice, 1));
        }
        {
            // Test that the "submit" RPC command fills in a "Sequence": 0
            // field if none is provided.

            // Create a noop transaction using a TicketSequence but don't fill
            // in the Sequence field.
            Json::Value tx = Json::objectValue;
            tx[jss::tx_json] = noop(alice);
            tx[jss::tx_json][sfTicketSequence.jsonName] = ticketSeq + 1;
            tx[jss::secret] = toBase58(generateSeed("alice"));

            // Verify that there is no "Sequence" field.
            BEAST_EXPECT(!tx[jss::tx_json].isMember(sfSequence.jsonName));

            // Call the "submit" RPC command and see the "Sequence": 0 field
            // filled in.
            Json::Value jr = env.rpc("json", "submit", to_string(tx));

            // Verify that "submit" inserted a "Sequence": 0 field.
            if (BEAST_EXPECT(jr[jss::result][jss::tx_json].isMember(sfSequence.jsonName)))
            {
                BEAST_EXPECT(jr[jss::result][jss::tx_json][sfSequence.jsonName] == 0);
            }

            // "submit" should have consumed the last of alice's tickets.
            env.close();
            env.require(owners(alice, 0), tickets(alice, 0));
        }
    }

    void
    testFixBothSeqAndTicket()
    {
        using namespace test::jtx;

        // It is an error if a transaction contains a non-zero Sequence field
        // and a TicketSequence field.  Verify that the error is detected.
        testcase("Fix both Seq and Ticket");

        Env env{*this, testable_amendments()};
        Account alice{"alice"};

        env.fund(XRP(10000), alice);
        env.close();

        // Create a ticket.
        std::uint32_t const ticketSeq = env.seq(alice) + 1;
        env(ticket::create(alice, 1));
        env.close();
        env.require(owners(alice, 1), tickets(alice, 1));
        BEAST_EXPECT(ticketSeq + 1 == env.seq(alice));

        // Create a transaction that includes both a ticket and a non-zero
        // sequence number.  The transaction fails with temSEQ_AND_TICKET.
        env(noop(alice), ticket::use(ticketSeq), seq(env.seq(alice)), ter(temSEQ_AND_TICKET));
        env.close();

        // Verify that the transaction failed by looking at alice's
        // sequence number and tickets.
        env.require(owners(alice, 1), tickets(alice, 1));
        BEAST_EXPECT(ticketSeq + 1 == env.seq(alice));
    }

public:
    void
    run() override
    {
        testTicketCreatePreflightFail();
        testTicketCreatePreclaimFail();
        testTicketInsufficientReserve();
        testUsingTickets();
        testTransactionDatabaseWithTickets();
        testSignWithTicketSequence();
        testFixBothSeqAndTicket();
    }
};

BEAST_DEFINE_TESTSUITE(Ticket, app, xrpl);

}  // namespace xrpl
