#include "fixture.h"

#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/storage/lib/idm/common.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/storage/lib/idm/role_tree.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/storage/lib/idm/idm_service.h>

#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/idm/project_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/idm/project_role_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/drive/firmware_updater/libs/db/include/idm/role_gateway.h>

#include <maps/libs/json/include/value.h>
#include <library/cpp/testing/gtest/gtest.h>

#include <set>

namespace maps::fw_updater::db::idm {
using introspection::operator==;
using introspection::operator!=;
} // namespace maps::fw_updater::db::idm

namespace maps::fw_updater::storage::idm {
using introspection::operator==;
using introspection::operator!=;
using introspection::operator<;
} // namespace maps::fw_updater::storage::idm

namespace maps::fw_updater::storage::idm::tests {

TEST(SlugPathTests, test_slug_conversion)
{
    std::vector<std::pair<std::string, SlugPath>> strAndPathPairs {
        {"", SlugPath{}},
        {"/role/president", SlugPath{{"role", "president"}}},
        {"/country/Russia/role/president",
            SlugPath{{"country", "Russia"}, {"role", "president"}}}
    };

    for (const auto& [str, path] : strAndPathPairs) {
        EXPECT_EQ(toString(path), str);
        const auto parsedPath = parseSlugPath(str);
        EXPECT_EQ(parsedPath, path);
    }

    std::string invalid = "/odd/tokens/number";
    EXPECT_THROW(parseSlugPath(invalid), BadParameter);
}

using Fixture = maps::fw_updater::storage::tests::Fixture;

/**
 * The tests below use the following hierarchy of roles:
 *
 *                 _root
 *            ______/  \_________
 *           /                   \
 *       project 2           project 1
 *        /   \                 /   \___________
 *       /     \               /                \
 *      /       \         subProject 1A     subProject 1B
 *     /         \          /      \              \
 * developer  manager   developer  manager     developer
 */
TEST_F(Fixture, test_role_tree_basic)
{
    auto txn = txnHandle();
    const auto roleTree = RoleTree::load(*txn);

    for (Id id : {2, 3, 4, 5}) {
        const auto& node = roleTree.getNode(id);
        auto project = db::idm::ProjectGateway{*txn}.loadById(id);
        EXPECT_EQ(node.project(), project);
    }

    {
        const auto& root = roleTree.root();
        EXPECT_EQ(root.id(), 1);
        Ids subprojectIds{2, 3};
        EXPECT_EQ(root.childIds(), subprojectIds);
        EXPECT_TRUE(root.leafChildIds().empty());
    }

    {
        const auto& projectOne = roleTree.getNode(2);
        Ids subprojectIds{4, 5};
        EXPECT_EQ(projectOne.childIds(), subprojectIds);
        EXPECT_TRUE(projectOne.leafChildIds().empty());
    }

    {
        const auto& projectTwo = roleTree.getNode(3);
        Ids projectRoleIds{10, 11};
        EXPECT_TRUE(projectTwo.childIds().empty());
        EXPECT_EQ(projectTwo.leafChildIds(), projectRoleIds);
    }

    {
        const auto& subProjectA = roleTree.getNode(4);
        Ids projectRoleIds{12, 13};
        EXPECT_TRUE(subProjectA.childIds().empty());
        EXPECT_EQ(subProjectA.leafChildIds(), projectRoleIds);
    }

    {
        const auto& subProjectB = roleTree.getNode(5);
        Ids projectRoleIds{14};
        EXPECT_TRUE(subProjectB.childIds().empty());
        EXPECT_EQ(subProjectB.leafChildIds(), projectRoleIds);
    }

    auto projectRoles = db::idm::ProjectRoleGateway{*txn}.load();
    for (const auto& projectRole : projectRoles) {
        Id id = projectRole.id();
        Id projectId = projectRole.projectId();
        Id roleId = projectRole.roleId();

        const auto& leafNode = roleTree.getLeafNode(id);
        EXPECT_EQ(leafNode.parentId(), projectId);
        auto role = db::idm::RoleGateway{*txn}.loadById(roleId);
        EXPECT_EQ(leafNode.role(), role);
    }
}

TEST_F(Fixture, test_slug_path_search)
{
    auto txn = txnHandle();
    const auto roleTree = RoleTree::load(*txn);

    std::vector<std::pair<Id, std::string>> roleTreeAndSlugPaths {
        {10, "/project/two/role/developer"},
        {11, "/project/two/role/manager"},
        {12, "/project/one/subproject/one-A/role/developer"},
        {13, "/project/one/subproject/one-A/role/manager"},
        {14, "/project/one/subproject/one-B/role/developer"}
    };

    for (const auto& [id, strPath] : roleTreeAndSlugPaths) {
        // Check id -> path
        auto path = roleTree.slugPathByLeafNodeId(id);
        EXPECT_EQ(toString(path), strPath);

        // Check path -> id
        auto optionalId = roleTree.leafNodeIdBySlugPath(parseSlugPath(strPath));
        ASSERT_TRUE(optionalId);
        EXPECT_EQ(*optionalId, id);
    }
}

TEST_F(Fixture, test_role_tree_json)
{
    auto txn = txnHandle();

    const auto roleTree = RoleTree::load(*txn);
    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        roleTree.toJson(builder);
    };
    auto rolesInfo = builder.str();

    EXPECT_EQ(json::Value::fromString(rolesInfo),
              json::Value::fromFile(SRC_("idm_roles_info.json")));
}

namespace {

const SlugPath ONE_A_DEVELOPER = parseSlugPath("project/one/subproject/one-A/role/developer");
const SlugPath ONE_A_MANAGER = parseSlugPath("project/one/subproject/one-A/role/manager");
const SlugPath ONE_B_DEVELOPER = parseSlugPath("project/one/subproject/one-B/role/developer");
const SlugPath TWO_DEVELOPER = parseSlugPath("project/two/role/developer");
const SlugPath TWO_MANAGER = parseSlugPath("project/two/role/manager");

const Login JOHN = "john";
const Login PAUL = "paul";
const Login GEORGE = "george";

} // namespace

TEST_F(Fixture, test_user_roles)
{
    {
        auto txn = txnHandle();
        auto slugPaths = getUserRoles(*txn, JOHN);
        EXPECT_TRUE(slugPaths.empty());
    }

    {
        // Add some role
        auto txn = txnHandle();
        addUserRole(*txn, JOHN, ONE_A_DEVELOPER);
        txn->commit();

        txn = txnHandle();
        auto slugPaths = getUserRoles(*txn, JOHN);
        ASSERT_EQ(slugPaths.size(), 1u);
        EXPECT_EQ(slugPaths[0], ONE_A_DEVELOPER);
    }

    {
        // Add some more roles
        auto txn = txnHandle();
        addUserRole(*txn, JOHN, ONE_A_MANAGER);
        addUserRole(*txn, JOHN, TWO_DEVELOPER);
        txn->commit();

        txn = txnHandle();
        auto slugPaths = getUserRoles(*txn, JOHN);
        std::set<SlugPath> received(slugPaths.begin(), slugPaths.end());
        std::set<SlugPath> expected{ONE_A_DEVELOPER, ONE_A_MANAGER, TWO_DEVELOPER};
        EXPECT_EQ(received, expected);
    }

    {
        // Remove role
        auto txn = txnHandle();
        removeUserRole(*txn, JOHN, ONE_A_MANAGER);
        txn->commit();

        txn = txnHandle();
        auto slugPaths = getUserRoles(*txn, JOHN);
        std::set<SlugPath> received(slugPaths.begin(), slugPaths.end());
        std::set<SlugPath> expected{ONE_A_DEVELOPER, TWO_DEVELOPER};
        EXPECT_EQ(received, expected);
    }

    {
        // Add already existing role
        auto txn = txnHandle();
        EXPECT_NO_THROW(addUserRole(*txn, JOHN, ONE_A_DEVELOPER));
        txn->commit();

        txn = txnHandle();
        EXPECT_EQ(getUserRoles(*txn, JOHN).size(), 2u);
    }

    {
        // Remove non-existing role
        auto txn = txnHandle();
        EXPECT_NO_THROW(removeUserRole(*txn, JOHN, ONE_A_MANAGER));
        txn->commit();

        txn = txnHandle();
        EXPECT_EQ(getUserRoles(*txn, JOHN).size(), 2u);
    }
}


TEST_F(Fixture, test_all_user_roles)
{
    auto txn = txnHandle();
    addUserRole(*txn, JOHN, ONE_A_DEVELOPER);
    addUserRole(*txn, JOHN, ONE_B_DEVELOPER);
    addUserRole(*txn, PAUL, ONE_A_DEVELOPER);
    addUserRole(*txn, PAUL, ONE_A_MANAGER);
    addUserRole(*txn, GEORGE, TWO_DEVELOPER);
    txn->commit();

    txn = txnHandle();
    auto allRoles = getAllRoles(*txn);
    ASSERT_EQ(allRoles.size(), 3u);

    std::set<SlugPath> receivedJohn(allRoles[JOHN].begin(), allRoles[JOHN].end());
    std::set<SlugPath> expectedJohn{ONE_A_DEVELOPER, ONE_B_DEVELOPER};
    EXPECT_EQ(receivedJohn, expectedJohn);

    std::set<SlugPath> receivedPaul(allRoles[PAUL].begin(), allRoles[PAUL].end());
    std::set<SlugPath> expectedPaul{ONE_A_DEVELOPER, ONE_A_MANAGER};
    EXPECT_EQ(receivedPaul, expectedPaul);

    ASSERT_EQ(allRoles[GEORGE].size(), 1u);
    EXPECT_EQ(allRoles[GEORGE][0], TWO_DEVELOPER);
}


TEST_F(Fixture, test_check_user_role)
{
    const Id PROJECT_ID_ONE_A = 4;
    const Id PROJECT_ID_ONE_B = 5;
    const std::string DEVELOPER = "developer";
    const std::string MANAGER = "manager";

    auto txn = txnHandle();
    EXPECT_FALSE(userHasRoleInProject(*txn, JOHN, DEVELOPER, PROJECT_ID_ONE_A));
    EXPECT_FALSE(userHasRoleInProject(*txn, JOHN, DEVELOPER, PROJECT_ID_ONE_B));

    addUserRole(*txn, JOHN, ONE_A_DEVELOPER);
    txn->commit();

    txn = txnHandle();
    EXPECT_TRUE(userHasRoleInProject(*txn, JOHN, DEVELOPER, PROJECT_ID_ONE_A));
    EXPECT_FALSE(userHasRoleInProject(*txn, JOHN, MANAGER, PROJECT_ID_ONE_A));
    EXPECT_FALSE(userHasRoleInProject(*txn, JOHN, DEVELOPER, PROJECT_ID_ONE_B));
    EXPECT_FALSE(userHasRoleInProject(*txn, PAUL, DEVELOPER, PROJECT_ID_ONE_A));

    EXPECT_FALSE(userHasRoleInProject(*txn, JOHN, "unknown", PROJECT_ID_ONE_A));

    removeUserRole(*txn, JOHN, ONE_A_MANAGER);
    txn->commit();

    EXPECT_TRUE(userHasRoleInProject(*txnHandle(), JOHN, DEVELOPER, PROJECT_ID_ONE_A));
}

} // namespace maps::fw_updater::storage::idm::tests
