//------------------------------------------------------------------------------
/*
  This file is part of rippled: https://github.com/ripple/rippled
  Copyright (c) 2024 Ripple Labs Inc.

  Permission to use, copy, modify, and/or distribute this software for any
  purpose  with  or without fee is hereby granted, provided that the above
  copyright notice and this permission notice appear in all copies.

  THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
  MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  ANY  SPECIAL ,  DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
  ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#include <test/jtx.h>
#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>

namespace ripple {
namespace test {

class LPTokenTransfer_test : public jtx::AMMTest
{
    void
    testDirectStep(FeatureBitset features)
    {
        testcase("DirectStep");

        using namespace jtx;
        Env env{*this, features};
        fund(env, gw, {alice}, {USD(20'000), BTC(0.5)}, Fund::All);
        env.close();

        AMM ammAlice(env, alice, USD(20'000), BTC(0.5));
        BEAST_EXPECT(
            ammAlice.expectBalances(USD(20'000), BTC(0.5), IOUAmount{100, 0}));

        fund(env, gw, {carol}, {USD(4'000), BTC(1)}, Fund::Acct);
        ammAlice.deposit(carol, 10);
        BEAST_EXPECT(
            ammAlice.expectBalances(USD(22'000), BTC(0.55), IOUAmount{110, 0}));

        fund(env, gw, {bob}, {USD(4'000), BTC(1)}, Fund::Acct);
        ammAlice.deposit(bob, 10);
        BEAST_EXPECT(
            ammAlice.expectBalances(USD(24'000), BTC(0.60), IOUAmount{120, 0}));

        auto const lpIssue = ammAlice.lptIssue();
        env.trust(STAmount{lpIssue, 500}, alice);
        env.trust(STAmount{lpIssue, 500}, bob);
        env.trust(STAmount{lpIssue, 500}, carol);
        env.close();

        // gateway freezes carol's USD
        env(trust(gw, carol["USD"](0), tfSetFreeze));
        env.close();

        // bob can still send lptoken to carol even tho carol's USD is
        // frozen
        // Note: Deep freeze is not considered for LPToken transfer
        env(pay(bob, carol, STAmount{lpIssue, 5}));
        env.close();

        // cannot transfer to an amm account
        env(pay(carol, lpIssue.getIssuer(), STAmount{lpIssue, 5}),
            ter(tecNO_PERMISSION));
        env.close();

        // carol is frozen on USD and therefore can't send lptoken to bob
        env(pay(carol, bob, STAmount{lpIssue, 5}), ter(tecPATH_DRY));
    }

    void
    testBookStep(FeatureBitset features)
    {
        testcase("BookStep");

        using namespace jtx;
        Env env{*this, features};

        fund(
            env,
            gw,
            {alice, bob, carol},
            {USD(10'000), EUR(10'000)},
            Fund::All);
        AMM ammAlice(env, alice, USD(10'000), EUR(10'000));
        ammAlice.deposit(carol, 1'000);
        ammAlice.deposit(bob, 1'000);

        auto const lpIssue = ammAlice.lptIssue();

        // carols creates an offer to sell lptoken
        env(offer(carol, XRP(10), STAmount{lpIssue, 10}), txflags(tfPassive));
        env.close();
        BEAST_EXPECT(expectOffers(env, carol, 1));

        env.trust(STAmount{lpIssue, 1'000'000'000}, alice);
        env.trust(STAmount{lpIssue, 1'000'000'000}, bob);
        env.trust(STAmount{lpIssue, 1'000'000'000}, carol);
        env.close();

        // gateway freezes carol's USD
        env(trust(gw, carol["USD"](0), tfSetFreeze));
        env.close();

        // exercises alice's ability to consume carol's offer to sell lptoken
        // when carol's USD is frozen

        // alice fails to consume carol's offer since carol's USD is frozen
        env(pay(alice, bob, STAmount{lpIssue, 10}),
            txflags(tfPartialPayment),
            sendmax(XRP(10)),
            ter(tecPATH_DRY));
        env.close();
        BEAST_EXPECT(expectOffers(env, carol, 1));

        // gateway unfreezes carol's USD
        env(trust(gw, carol["USD"](1'000'000'000), tfClearFreeze));
        env.close();

        // alice successfully consumes carol's offer
        env(pay(alice, bob, STAmount{lpIssue, 10}),
            txflags(tfPartialPayment),
            sendmax(XRP(10)));
        env.close();
        BEAST_EXPECT(expectOffers(env, carol, 0));

        // make sure carol's USD is not frozen
        env(trust(gw, carol["USD"](1'000'000'000), tfClearFreeze));
        env.close();

        // ensure that carol's offer to buy lptoken can be consumed by alice
        // even when carol's USD is frozen
        {
            // carol creates an offer to buy lptoken
            env(offer(carol, STAmount{lpIssue, 10}, XRP(10)),
                txflags(tfPassive));
            env.close();
            BEAST_EXPECT(expectOffers(env, carol, 1));

            // gateway freezes carol's USD
            env(trust(gw, carol["USD"](0), tfSetFreeze));
            env.close();

            // alice successfully consumes carol's offer
            env(pay(alice, bob, XRP(10)),
                txflags(tfPartialPayment),
                sendmax(STAmount{lpIssue, 10}));
            env.close();
            BEAST_EXPECT(expectOffers(env, carol, 0));
        }
    }

    void
    testOfferCreation(FeatureBitset features)
    {
        testcase("Create offer");

        using namespace jtx;
        Env env{*this, features};

        fund(
            env,
            gw,
            {alice, bob, carol},
            {USD(10'000), EUR(10'000)},
            Fund::All);
        AMM ammAlice(env, alice, USD(10'000), EUR(10'000));
        ammAlice.deposit(carol, 1'000);
        ammAlice.deposit(bob, 1'000);

        auto const lpIssue = ammAlice.lptIssue();

        // gateway freezes carol's USD
        env(trust(gw, carol["USD"](0), tfSetFreeze));
        env.close();

        // exercises carol's ability to create a new offer to sell lptoken with
        // frozen USD

        // carol can't create an offer to sell lptoken when one of the assets is
        // frozen

        // carol can't create an offer to sell lptoken
        env(offer(carol, XRP(10), STAmount{lpIssue, 10}),
            txflags(tfPassive),
            ter(tecUNFUNDED_OFFER));
        env.close();
        BEAST_EXPECT(expectOffers(env, carol, 0));

        // gateway unfreezes carol's USD
        env(trust(gw, carol["USD"](1'000'000'000), tfClearFreeze));
        env.close();

        // carol can create an offer to sell lptoken after USD is unfrozen
        env(offer(carol, XRP(10), STAmount{lpIssue, 10}), txflags(tfPassive));
        env.close();
        BEAST_EXPECT(expectOffers(env, carol, 1));

        // gateway freezes carol's USD
        env(trust(gw, carol["USD"](0), tfSetFreeze));
        env.close();

        // carol can create offer to buy lptoken even if USD is frozen
        env(offer(carol, STAmount{lpIssue, 10}, XRP(5)), txflags(tfPassive));
        env.close();
        BEAST_EXPECT(expectOffers(env, carol, 2));
    }

    void
    testOfferCrossing(FeatureBitset features)
    {
        testcase("Offer crossing");

        using namespace jtx;
        Env env{*this, features};

        // Offer crossing with two AMM LPTokens.
        fund(env, gw, {alice, carol}, {USD(10'000)}, Fund::All);
        AMM ammAlice1(env, alice, XRP(10'000), USD(10'000));
        ammAlice1.deposit(carol, 10'000'000);

        fund(env, gw, {alice, carol}, {EUR(10'000)}, Fund::IOUOnly);
        AMM ammAlice2(env, alice, XRP(10'000), EUR(10'000));
        ammAlice2.deposit(carol, 10'000'000);
        auto const token1 = ammAlice1.lptIssue();
        auto const token2 = ammAlice2.lptIssue();

        // carol creates offer
        env(offer(carol, STAmount{token2, 100}, STAmount{token1, 100}));
        env.close();
        BEAST_EXPECT(expectOffers(env, carol, 1));

        // gateway freezes carol's USD, carol's token1 should be frozen as well
        env(trust(gw, carol["USD"](0), tfSetFreeze));
        env.close();

        // alice creates an offer which exhibits different behavior on offer
        // crossing depending on
        env(offer(alice, STAmount{token1, 100}, STAmount{token2, 100}));
        env.close();

        // exercises carol's offer's ability to cross with alice's offer when
        // carol's USD is frozen

        // alice's offer can no longer cross with carol's offer
        BEAST_EXPECT(
            expectLine(env, alice, STAmount{token1, 10'000'000}) &&
            expectLine(env, alice, STAmount{token2, 10'000'000}));
        BEAST_EXPECT(
            expectLine(env, carol, STAmount{token2, 10'000'000}) &&
            expectLine(env, carol, STAmount{token1, 10'000'000}));
        BEAST_EXPECT(
            expectOffers(env, alice, 1) && expectOffers(env, carol, 0));
    }

    void
    testCheck(FeatureBitset features)
    {
        testcase("Check");

        using namespace jtx;
        Env env{*this, features};

        fund(
            env,
            gw,
            {alice, bob, carol},
            {USD(10'000), EUR(10'000)},
            Fund::All);
        AMM ammAlice(env, alice, USD(10'000), EUR(10'000));
        ammAlice.deposit(carol, 1'000);
        ammAlice.deposit(bob, 1'000);

        auto const lpIssue = ammAlice.lptIssue();

        // gateway freezes carol's USD
        env(trust(gw, carol["USD"](0), tfSetFreeze));
        env.close();

        // carol can always create a check with lptoken that has frozen
        // token
        uint256 const carolChkId{keylet::check(carol, env.seq(carol)).key};
        env(check::create(carol, bob, STAmount{lpIssue, 10}));
        env.close();

        // bob fails to cash the check
        env(check::cash(bob, carolChkId, STAmount{lpIssue, 10}),
            ter(tecPATH_PARTIAL));

        env.close();

        // bob creates a check
        uint256 const bobChkId{keylet::check(bob, env.seq(bob)).key};
        env(check::create(bob, carol, STAmount{lpIssue, 10}));
        env.close();

        // carol cashes the bob's check. Even though carol is frozen, she can
        // still receive LPToken
        env(check::cash(carol, bobChkId, STAmount{lpIssue, 10}));
        env.close();
    }

    void
    testNFTOffers(FeatureBitset features)
    {
        testcase("NFT Offers");
        using namespace test::jtx;

        Env env{*this, features};

        // Setup AMM
        fund(
            env,
            gw,
            {alice, bob, carol},
            {USD(10'000), EUR(10'000)},
            Fund::All);
        AMM ammAlice(env, alice, USD(10'000), EUR(10'000));
        ammAlice.deposit(carol, 1'000);
        ammAlice.deposit(bob, 1'000);

        auto const lpIssue = ammAlice.lptIssue();

        // bob mints a nft
        uint256 const nftID{token::getNextID(env, bob, 0u, tfTransferable)};
        env(token::mint(bob, 0), txflags(tfTransferable));
        env.close();

        // bob creates a sell offer for lptoken
        uint256 const sellOfferIndex = keylet::nftoffer(bob, env.seq(bob)).key;
        env(token::createOffer(bob, nftID, STAmount{lpIssue, 10}),
            txflags(tfSellNFToken));
        env.close();

        // gateway freezes carol's USD
        env(trust(gw, carol["USD"](0), tfSetFreeze));
        env.close();

        // exercises one's ability to transfer NFT using lptoken when one of the
        // assets is frozen

        // Freezing USD will prevent buy/sell
        // offers with lptokens from being created/accepted

        // carol fails to accept bob's offer with lptoken because carol's
        // USD is frozen
        env(token::acceptSellOffer(carol, sellOfferIndex),
            ter(tecINSUFFICIENT_FUNDS));
        env.close();

        // gateway unfreezes carol's USD
        env(trust(gw, carol["USD"](1'000'000), tfClearFreeze));
        env.close();

        // carol can now accept the offer and own the nft
        env(token::acceptSellOffer(carol, sellOfferIndex));
        env.close();

        // gateway freezes bobs's USD
        env(trust(gw, bob["USD"](0), tfSetFreeze));
        env.close();

        // bob fails to create a buy offer with lptoken for carol's nft
        // since bob's USD is frozen
        env(token::createOffer(bob, nftID, STAmount{lpIssue, 10}),
            token::owner(carol),
            ter(tecUNFUNDED_OFFER));
        env.close();

        // gateway unfreezes bob's USD
        env(trust(gw, bob["USD"](1'000'000), tfClearFreeze));
        env.close();

        // bob can now create a buy offer
        env(token::createOffer(bob, nftID, STAmount{lpIssue, 10}),
            token::owner(carol));
        env.close();
    }

public:
    void
    run() override
    {
        FeatureBitset const all{jtx::supported_amendments()};

        testDirectStep(all);
        testBookStep(all);
        testOfferCreation(all);
        testOfferCrossing(all);
        testCheck(all);
        testNFTOffers(all);
    }
};

BEAST_DEFINE_TESTSUITE(LPTokenTransfer, app, ripple);
}  // namespace test
}  // namespace ripple
