//------------------------------------------------------------------------------
/*
  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 <xrpld/app/tx/detail/PermissionedDomainSet.h>
#include <xrpld/ledger/ApplyViewImpl.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Issue.h>
#include <xrpl/protocol/jss.h>

#include <exception>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>

namespace ripple {
namespace test {

using namespace jtx;

static std::string
exceptionExpected(Env& env, Json::Value const& jv)
{
    try
    {
        env(jv, ter(temMALFORMED));
    }
    catch (std::exception const& ex)
    {
        return ex.what();
    }
    return {};
}

class PermissionedDomains_test : public beast::unit_test::suite
{
    FeatureBitset withoutFeature_{
        supported_amendments() - featurePermissionedDomains};
    FeatureBitset withFeature_{
        supported_amendments()  //
        | featurePermissionedDomains | featureCredentials};

    // Verify that each tx type can execute if the feature is enabled.
    void
    testEnabled()
    {
        testcase("Enabled");
        Account const alice("alice");
        Env env(*this, withFeature_);
        env.fund(XRP(1000), alice);
        pdomain::Credentials credentials{{alice, "first credential"}};
        env(pdomain::setTx(alice, credentials));
        BEAST_EXPECT(env.ownerCount(alice) == 1);
        auto objects = pdomain::getObjects(alice, env);
        BEAST_EXPECT(objects.size() == 1);
        // Test that account_objects is correct without passing it the type
        BEAST_EXPECT(objects == pdomain::getObjects(alice, env, false));
        auto const domain = objects.begin()->first;
        env(pdomain::deleteTx(alice, domain));
    }

    // Verify that PD cannot be created or updated if credentials are disabled
    void
    testCredentialsDisabled()
    {
        auto amendments = supported_amendments() | featureCredentials;
        amendments.set(featurePermissionedDomains);
        amendments.reset(featureCredentials);
        testcase("Credentials disabled");
        Account const alice("alice");
        Env env(*this, amendments);
        env.fund(XRP(1000), alice);
        pdomain::Credentials credentials{{alice, "first credential"}};
        env(pdomain::setTx(alice, credentials), ter(temDISABLED));
    }

    // Verify that each tx does not execute if feature is disabled
    void
    testDisabled()
    {
        testcase("Disabled");
        Account const alice("alice");
        Env env(*this, withoutFeature_);
        env.fund(XRP(1000), alice);
        pdomain::Credentials credentials{{alice, "first credential"}};
        env(pdomain::setTx(alice, credentials), ter(temDISABLED));
        env(pdomain::deleteTx(alice, uint256(75)), ter(temDISABLED));
    }

    // Verify that bad inputs fail for each of create new and update
    // behaviors of PermissionedDomainSet
    void
    testBadData(
        Account const& account,
        Env& env,
        std::optional<uint256> domain = std::nullopt)
    {
        Account const alice2("alice2");
        Account const alice3("alice3");
        Account const alice4("alice4");
        Account const alice5("alice5");
        Account const alice6("alice6");
        Account const alice7("alice7");
        Account const alice8("alice8");
        Account const alice9("alice9");
        Account const alice10("alice10");
        Account const alice11("alice11");
        Account const alice12("alice12");
        auto const setFee(drops(env.current()->fees().increment));

        // Test empty credentials.
        env(pdomain::setTx(account, pdomain::Credentials(), domain),
            ter(temARRAY_EMPTY));

        // Test 11 credentials.
        pdomain::Credentials const credentials11{
            {alice2, "credential1"},
            {alice3, "credential2"},
            {alice4, "credential3"},
            {alice5, "credential4"},
            {alice6, "credential5"},
            {alice7, "credential6"},
            {alice8, "credential7"},
            {alice9, "credential8"},
            {alice10, "credential9"},
            {alice11, "credential10"},
            {alice12, "credential11"}};
        BEAST_EXPECT(
            credentials11.size() ==
            maxPermissionedDomainCredentialsArraySize + 1);
        env(pdomain::setTx(account, credentials11, domain),
            ter(temARRAY_TOO_LARGE));

        // Test credentials including non-existent issuer.
        Account const nobody("nobody");
        pdomain::Credentials const credentialsNon{
            {alice2, "credential1"},
            {alice3, "credential2"},
            {alice4, "credential3"},
            {nobody, "credential4"},
            {alice5, "credential5"},
            {alice6, "credential6"},
            {alice7, "credential7"}};
        env(pdomain::setTx(account, credentialsNon, domain), ter(tecNO_ISSUER));

        // Test bad fee
        env(pdomain::setTx(account, credentials11, domain),
            fee(1, true),
            ter(temBAD_FEE));

        pdomain::Credentials const credentials4{
            {alice2, "credential1"},
            {alice3, "credential2"},
            {alice4, "credential3"},
            {alice5, "credential4"},
        };
        auto txJsonMutable = pdomain::setTx(account, credentials4, domain);
        auto const credentialOrig = txJsonMutable["AcceptedCredentials"][2u];

        // Remove Issuer from a credential and apply.
        txJsonMutable["AcceptedCredentials"][2u][jss::Credential].removeMember(
            jss::Issuer);
        BEAST_EXPECT(
            exceptionExpected(env, txJsonMutable).starts_with("invalidParams"));

        // Make an empty CredentialType.
        txJsonMutable["AcceptedCredentials"][2u] = credentialOrig;
        txJsonMutable["AcceptedCredentials"][2u][jss::Credential]
                     ["CredentialType"] = "";
        env(txJsonMutable, ter(temMALFORMED));

        // Make too long CredentialType.
        constexpr std::string_view longCredentialType =
            "Cred0123456789012345678901234567890123456789012345678901234567890";
        static_assert(longCredentialType.size() == maxCredentialTypeLength + 1);
        txJsonMutable["AcceptedCredentials"][2u] = credentialOrig;
        txJsonMutable["AcceptedCredentials"][2u][jss::Credential]
                     ["CredentialType"] = std::string(longCredentialType);
        BEAST_EXPECT(
            exceptionExpected(env, txJsonMutable).starts_with("invalidParams"));

        // Remove Credentialtype from a credential and apply.
        txJsonMutable["AcceptedCredentials"][2u][jss::Credential].removeMember(
            "CredentialType");
        BEAST_EXPECT(
            exceptionExpected(env, txJsonMutable).starts_with("invalidParams"));

        // Remove both
        txJsonMutable["AcceptedCredentials"][2u][jss::Credential].removeMember(
            jss::Issuer);
        BEAST_EXPECT(
            exceptionExpected(env, txJsonMutable).starts_with("invalidParams"));

        // Make 2 identical credentials. Duplicates are not supported by
        // permissioned domains, so transactions should return errors
        {
            pdomain::Credentials const credentialsDup{
                {alice7, "credential6"},
                {alice2, "credential1"},
                {alice3, "credential2"},
                {alice2, "credential1"},
                {alice5, "credential4"},
            };

            std::unordered_map<std::string, Account> human2Acc;
            for (auto const& c : credentialsDup)
                human2Acc.emplace(c.issuer.human(), c.issuer);

            auto const sorted = pdomain::sortCredentials(credentialsDup);
            BEAST_EXPECT(sorted.size() == 4);
            env(pdomain::setTx(account, credentialsDup, domain),
                ter(temMALFORMED));

            env.close();
            env(pdomain::setTx(account, sorted, domain));

            uint256 d;
            if (domain)
                d = *domain;
            else
                d = pdomain::getNewDomain(env.meta());
            env.close();
            auto objects = pdomain::getObjects(account, env);
            auto const fromObject =
                pdomain::credentialsFromJson(objects[d], human2Acc);
            auto const sortedCreds = pdomain::sortCredentials(credentialsDup);
            BEAST_EXPECT(fromObject == sortedCreds);
        }

        // Have equal issuers but different credentials and make sure they
        // sort correctly.
        {
            pdomain::Credentials const credentialsSame{
                {alice2, "credential3"},
                {alice3, "credential2"},
                {alice2, "credential9"},
                {alice5, "credential4"},
                {alice2, "credential6"},
            };
            std::unordered_map<std::string, Account> human2Acc;
            for (auto const& c : credentialsSame)
                human2Acc.emplace(c.issuer.human(), c.issuer);

            BEAST_EXPECT(
                credentialsSame != pdomain::sortCredentials(credentialsSame));
            env(pdomain::setTx(account, credentialsSame, domain));

            uint256 d;
            if (domain)
                d = *domain;
            else
                d = pdomain::getNewDomain(env.meta());
            env.close();
            auto objects = pdomain::getObjects(account, env);
            auto const fromObject =
                pdomain::credentialsFromJson(objects[d], human2Acc);
            auto const sortedCreds = pdomain::sortCredentials(credentialsSame);
            BEAST_EXPECT(fromObject == sortedCreds);
        }
    }

    // Test PermissionedDomainSet
    void
    testSet()
    {
        testcase("Set");
        Env env(*this, withFeature_);
        env.set_parse_failure_expected(true);

        const int accNum = 12;
        Account const alice[accNum] = {
            "alice",
            "alice2",
            "alice3",
            "alice4",
            "alice5",
            "alice6",
            "alice7",
            "alice8",
            "alice9",
            "alice10",
            "alice11",
            "alice12"};
        std::unordered_map<std::string, Account> human2Acc;
        for (auto const& c : alice)
            human2Acc.emplace(c.human(), c);

        for (int i = 0; i < accNum; ++i)
            env.fund(XRP(1000), alice[i]);

        // Create new from existing account with a single credential.
        pdomain::Credentials const credentials1{{alice[2], "credential1"}};
        {
            env(pdomain::setTx(alice[0], credentials1));
            BEAST_EXPECT(env.ownerCount(alice[0]) == 1);
            auto tx = env.tx()->getJson(JsonOptions::none);
            BEAST_EXPECT(tx[jss::TransactionType] == "PermissionedDomainSet");
            BEAST_EXPECT(tx["Account"] == alice[0].human());
            auto objects = pdomain::getObjects(alice[0], env);
            auto domain = objects.begin()->first;
            BEAST_EXPECT(domain.isNonZero());
            auto object = objects.begin()->second;
            BEAST_EXPECT(object["LedgerEntryType"] == "PermissionedDomain");
            BEAST_EXPECT(object["Owner"] == alice[0].human());
            BEAST_EXPECT(object["Sequence"] == tx["Sequence"]);
            BEAST_EXPECT(
                pdomain::credentialsFromJson(object, human2Acc) ==
                credentials1);
        }

        // Make longest possible CredentialType.
        {
            constexpr std::string_view longCredentialType =
                "Cred0123456789012345678901234567890123456789012345678901234567"
                "89";
            static_assert(longCredentialType.size() == maxCredentialTypeLength);
            pdomain::Credentials const longCredentials{
                {alice[1], std::string(longCredentialType)}};

            env(pdomain::setTx(alice[0], longCredentials));

            // One account can create multiple domains
            BEAST_EXPECT(env.ownerCount(alice[0]) == 2);

            auto tx = env.tx()->getJson(JsonOptions::none);
            BEAST_EXPECT(tx[jss::TransactionType] == "PermissionedDomainSet");
            BEAST_EXPECT(tx["Account"] == alice[0].human());

            bool findSeq = false;
            for (auto const& [domain, object] :
                 pdomain::getObjects(alice[0], env))
            {
                findSeq = object["Sequence"] == tx["Sequence"];
                if (findSeq)
                {
                    BEAST_EXPECT(domain.isNonZero());
                    BEAST_EXPECT(
                        object["LedgerEntryType"] == "PermissionedDomain");
                    BEAST_EXPECT(object["Owner"] == alice[0].human());
                    BEAST_EXPECT(
                        pdomain::credentialsFromJson(object, human2Acc) ==
                        longCredentials);
                    break;
                }
            }
            BEAST_EXPECT(findSeq);
        }

        // Create new from existing account with 10 credentials.
        // Last credential describe domain owner itself
        pdomain::Credentials const credentials10{
            {alice[2], "credential1"},
            {alice[3], "credential2"},
            {alice[4], "credential3"},
            {alice[5], "credential4"},
            {alice[6], "credential5"},
            {alice[7], "credential6"},
            {alice[8], "credential7"},
            {alice[9], "credential8"},
            {alice[10], "credential9"},
            {alice[0], "credential10"},
        };
        uint256 domain2;
        {
            BEAST_EXPECT(
                credentials10.size() ==
                maxPermissionedDomainCredentialsArraySize);
            BEAST_EXPECT(
                credentials10 != pdomain::sortCredentials(credentials10));
            env(pdomain::setTx(alice[0], credentials10));
            auto tx = env.tx()->getJson(JsonOptions::none);
            domain2 = pdomain::getNewDomain(env.meta());
            auto objects = pdomain::getObjects(alice[0], env);
            auto object = objects[domain2];
            BEAST_EXPECT(
                pdomain::credentialsFromJson(object, human2Acc) ==
                pdomain::sortCredentials(credentials10));
        }

        // Update with 1 credential.
        env(pdomain::setTx(alice[0], credentials1, domain2));
        BEAST_EXPECT(
            pdomain::credentialsFromJson(
                pdomain::getObjects(alice[0], env)[domain2], human2Acc) ==
            credentials1);

        // Update with 10 credentials.
        env(pdomain::setTx(alice[0], credentials10, domain2));
        env.close();
        BEAST_EXPECT(
            pdomain::credentialsFromJson(
                pdomain::getObjects(alice[0], env)[domain2], human2Acc) ==
            pdomain::sortCredentials(credentials10));

        // Update from the wrong owner.
        env(pdomain::setTx(alice[2], credentials1, domain2),
            ter(tecNO_PERMISSION));

        // Update a uint256(0) domain
        env(pdomain::setTx(alice[0], credentials1, uint256(0)),
            ter(temMALFORMED));

        // Update non-existent domain
        env(pdomain::setTx(alice[0], credentials1, uint256(75)),
            ter(tecNO_ENTRY));

        // Wrong flag
        env(pdomain::setTx(alice[0], credentials1),
            txflags(tfClawTwoAssets),
            ter(temINVALID_FLAG));

        // Test bad data when creating a domain.
        testBadData(alice[0], env);
        // Test bad data when updating a domain.
        testBadData(alice[0], env, domain2);

        // Try to delete the account with domains.
        auto const acctDelFee(drops(env.current()->fees().increment));
        constexpr std::size_t deleteDelta = 255;
        {
            // Close enough ledgers to make it potentially deletable if empty.
            std::size_t ownerSeq = env.seq(alice[0]);
            while (deleteDelta + ownerSeq > env.current()->seq())
                env.close();
            env(acctdelete(alice[0], alice[2]),
                fee(acctDelFee),
                ter(tecHAS_OBLIGATIONS));
        }

        {
            // Delete the domains and then the owner account.
            for (auto const& objs : pdomain::getObjects(alice[0], env))
                env(pdomain::deleteTx(alice[0], objs.first));
            env.close();
            std::size_t ownerSeq = env.seq(alice[0]);
            while (deleteDelta + ownerSeq > env.current()->seq())
                env.close();
            env(acctdelete(alice[0], alice[2]), fee(acctDelFee));
        }
    }

    // Test PermissionedDomainDelete
    void
    testDelete()
    {
        testcase("Delete");
        Env env(*this, withFeature_);
        Account const alice("alice");

        env.fund(XRP(1000), alice);
        auto const setFee(drops(env.current()->fees().increment));

        pdomain::Credentials credentials{{alice, "first credential"}};
        env(pdomain::setTx(alice, credentials));
        env.close();

        auto objects = pdomain::getObjects(alice, env);
        BEAST_EXPECT(objects.size() == 1);
        auto const domain = objects.begin()->first;

        // Delete a domain that doesn't belong to the account.
        Account const bob("bob");
        env.fund(XRP(1000), bob);
        env(pdomain::deleteTx(bob, domain), ter(tecNO_PERMISSION));

        // Delete a non-existent domain.
        env(pdomain::deleteTx(alice, uint256(75)), ter(tecNO_ENTRY));

        // Test bad fee
        env(pdomain::deleteTx(alice, uint256(75)),
            ter(temBAD_FEE),
            fee(1, true));

        // Wrong flag
        env(pdomain::deleteTx(alice, domain),
            ter(temINVALID_FLAG),
            txflags(tfClawTwoAssets));

        // Delete a zero domain.
        env(pdomain::deleteTx(alice, uint256(0)), ter(temMALFORMED));

        // Make sure owner count reflects the existing domain.
        BEAST_EXPECT(env.ownerCount(alice) == 1);
        auto const objID = pdomain::getObjects(alice, env).begin()->first;
        BEAST_EXPECT(pdomain::objectExists(objID, env));

        // Delete domain that belongs to user.
        env(pdomain::deleteTx(alice, domain));
        auto const tx = env.tx()->getJson(JsonOptions::none);
        BEAST_EXPECT(tx[jss::TransactionType] == "PermissionedDomainDelete");

        // Make sure the owner count goes back to 0.
        BEAST_EXPECT(env.ownerCount(alice) == 0);

        // The object needs to be gone.
        BEAST_EXPECT(pdomain::getObjects(alice, env).empty());
        BEAST_EXPECT(!pdomain::objectExists(objID, env));
    }

    void
    testAccountReserve()
    {
        // Verify that the reserve behaves as expected for creating.
        testcase("Account Reserve");

        using namespace test::jtx;

        Env env(*this, withFeature_);
        Account const alice("alice");

        // Fund alice enough to exist, but not enough to meet
        // the reserve.
        auto const acctReserve = env.current()->fees().accountReserve(0);
        auto const incReserve = env.current()->fees().increment;
        env.fund(acctReserve, alice);
        env.close();
        BEAST_EXPECT(env.balance(alice) == acctReserve);
        BEAST_EXPECT(env.ownerCount(alice) == 0);

        // alice does not have enough XRP to cover the reserve.
        pdomain::Credentials credentials{{alice, "first credential"}};
        env(pdomain::setTx(alice, credentials), ter(tecINSUFFICIENT_RESERVE));
        BEAST_EXPECT(env.ownerCount(alice) == 0);
        BEAST_EXPECT(pdomain::getObjects(alice, env).size() == 0);
        env.close();

        auto const baseFee = env.current()->fees().base.drops();

        // Pay alice almost enough to make the reserve.
        env(pay(env.master, alice, incReserve + drops(2 * baseFee) - drops(1)));
        BEAST_EXPECT(
            env.balance(alice) ==
            acctReserve + incReserve + drops(baseFee) - drops(1));
        env.close();

        // alice still does not have enough XRP for the reserve.
        env(pdomain::setTx(alice, credentials), ter(tecINSUFFICIENT_RESERVE));
        env.close();
        BEAST_EXPECT(env.ownerCount(alice) == 0);

        // Pay alice enough to make the reserve.
        env(pay(env.master, alice, drops(baseFee) + drops(1)));
        env.close();

        // Now alice can create a PermissionedDomain.
        env(pdomain::setTx(alice, credentials));
        env.close();
        BEAST_EXPECT(env.ownerCount(alice) == 1);
    }

public:
    void
    run() override
    {
        testEnabled();
        testCredentialsDisabled();
        testDisabled();
        testSet();
        testDelete();
        testAccountReserve();
    }
};

BEAST_DEFINE_TESTSUITE(PermissionedDomains, app, ripple);

}  // namespace test
}  // namespace ripple
