# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2025 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
#
import logging
import pytest
import os
import time
import signal
import threading
import ldap
from datetime import datetime
from random import sample
from lib389.utils import ds_is_older, ensure_list_bytes, ensure_bytes, ensure_str
from lib389.topologies import topology_m1h1c1 as topo, topology_st, topology_m2 as topo_m2
from lib389._constants import *
from lib389.plugins import MemberOfPlugin, RetroChangelogPlugin
from lib389.backend import Backends
from lib389.mappingTree import MappingTrees
from lib389 import Entry
from lib389.idm.user import UserAccount, UserAccounts, TEST_USER_PROPERTIES
from lib389.idm.group import Groups, Group, UniqueGroups
from lib389.replica import ReplicationManager
from lib389.tasks import *
from lib389.idm.nscontainer import nsContainers
from lib389.idm.domain import Domain
from lib389.dirsrv_log import DirsrvErrorLog
from lib389.utils import get_default_db_lib
from . import check_membership


# Skip on older versions
pytestmark = [pytest.mark.tier1,
              pytest.mark.skipif(ds_is_older('1.3.7'), reason="Not implemented")]

USER_CN = 'user_'
GROUP_CN = 'group1'
DEBUGGING = os.getenv('DEBUGGING', False)
SUBTREE_1 = 'cn=sub1,%s' % SUFFIX
SUBTREE_2 = 'cn=sub2,%s' % SUFFIX


if DEBUGGING:
    logging.getLogger(__name__).setLevel(logging.DEBUG)
else:
    logging.getLogger(__name__).setLevel(logging.INFO)
log = logging.getLogger(__name__)


@pytest.fixture(scope="function")
def users(request, topo_m2):
    """Add users to the default suffix
    Return the list of added user DNs.
    """
    users_num = request.param
    users_list = []
    users = UserAccounts(topo_m2.ms["supplier1"], DEFAULT_SUFFIX, rdn=None)
    log.info('Adding %d users' % users_num)
    for num in sample(list(range(1000)), users_num):
        num_ran = int(round(num))
        USER_NAME = 'test%05d' % num_ran
        user = users.create(properties={
            'uid': USER_NAME,
            'sn': USER_NAME,
            'cn': USER_NAME,
            'uidNumber': '%s' % num_ran,
            'gidNumber': '%s' % num_ran,
            'homeDirectory': '/home/%s' % USER_NAME,
            'mail': '%s@redhat.com' % USER_NAME,
            'userpassword': 'pass%s' % num_ran,
        })
        users_list.append(user)

    def fin():
        for user in users_list:
            user.delete()

    request.addfinalizer(fin)
    return users_list


def config_memberof(server):
    # Configure fractional to prevent total init to send memberof
    memberof = MemberOfPlugin(server)
    memberof.enable()
    memberof.set_autoaddoc('nsMemberOf')
    server.restart()
    ents = server.agreement.list(suffix=DEFAULT_SUFFIX)
    for ent in ents:
        log.info('update %s to add nsDS5ReplicatedAttributeListTotal' % ent.dn)
        server.agreement.setProperties(agmnt_dn=ents[0].dn,
                                       properties={RA_FRAC_EXCLUDE: '(objectclass=*) $ EXCLUDE memberOf',
                                                   RA_FRAC_EXCLUDE_TOTAL_UPDATE: '(objectclass=*) $ EXCLUDE '})


def send_updates_now(server):
    ents = server.agreement.list(suffix=DEFAULT_SUFFIX)
    for ent in ents:
        server.agreement.pause(ent.dn)
        server.agreement.resume(ent.dn)


def _find_memberof(server, member_dn, group_dn):
    # To get the specific server's (M1, C1 and H1) user and group
    user = UserAccount(server, member_dn)
    assert user.exists()
    group = Group(server, group_dn)
    assert group.exists()

    # test that the user entry should have memberof attribute with specified group dn value
    assert group._dn.lower() in user.get_attr_vals_utf8_l('memberOf')


def test_memberof_with_repl(topo):
    """Test that we allowed to enable MemberOf plugin in dedicated consumer

    :id: ef71cd7c-e792-41bf-a3c0-b3b38391cbe5
    :setup: 1 Supplier - 1 Hub - 1 Consumer
    :steps:
        1. Configure replication to EXCLUDE memberof
        2. Enable memberof plugin
        3. Create users/groups
        4. Make user_0 member of group_0
        5. Checks that user_0 is memberof group_0 on M,H,C
        6. Make group_0 member of group_1 (nest group)
        7. Checks that user_0 is memberof group_0 and group_1 on M,H,C
        8. Check group_0 is memberof group_1 on M,H,C
        9. Remove group_0 from group_1
        10. Check group_0 and user_0 are NOT memberof group_1 on M,H,C
        11. Remove user_0 from group_0
        12. Check user_0 is not memberof group_0 and group_1 on M,H,C
        13. Disable memberof on C
        14. make user_0 member of group_1
        15. Checks that user_0 is memberof group_0 on M,H but not on C
        16. Enable memberof on C
        17. Checks that user_0 is memberof group_0 on M,H but not on C
        18. Run memberof fixup task
        19. Checks that user_0 is memberof group_0 on M,H,C
    :expectedresults:
        1. Configuration should be successful
        2. Plugin should be enabled
        3. Users and groups should be created
        4. user_0 should be member of group_0
        5. user_0 should be memberof group_0 on M,H,C
        6. group_0 should be member of group_1
        7. user_0 should be memberof group_0 and group_1 on M,H,C
        8. group_0 should be memberof group_1 on M,H,C
        9. group_0 from group_1 removal should be successful
        10. group_0 and user_0 should not be memberof group_1 on M,H,C
        11. user_0 from group_0 remove should be successful
        12. user_0 should not be memberof group_0 and group_1 on M,H,C
        13. memberof should be disabled on C
        14. user_0 should be member of group_1
        15. user_0 should be memberof group_0 on M,H and should not on C
        16. Enable memberof on C should be successful
        17. user_0 should be memberof group_0 on M,H should not on C
        18. memberof fixup task should be successful
        19. user_0 should be memberof group_0 on M,H,C
    """

    M1 = topo.ms["supplier1"]
    H1 = topo.hs["hub1"]
    C1 = topo.cs["consumer1"]
    repl = ReplicationManager(DEFAULT_SUFFIX)

    # Step 1 & 2
    M1.config.enable_log('audit')
    config_memberof(M1)
    M1.restart()

    H1.config.enable_log('audit')
    config_memberof(H1)
    H1.restart()

    C1.config.enable_log('audit')
    config_memberof(C1)
    C1.restart()

    #Declare lists of users and groups
    test_users = []
    test_groups = []

    # Step 3
    # In for loop create users and add them in the user list
    # it creates user_0 to user_9 (range is fun)
    for i in range(10):
        CN = '%s%d' % (USER_CN, i)
        users = UserAccounts(M1, SUFFIX)
        user_props = TEST_USER_PROPERTIES.copy()
        user_props.update({'uid': CN, 'cn': CN, 'sn': '_%s' % CN})
        testuser = users.create(properties=user_props)
        time.sleep(2)
        test_users.append(testuser)

    # In for loop create groups and add them to the group list
    # it creates group_0 to group_2 (range is fun)
    for i in range(3):
        CN = '%s%d' % (GROUP_CN, i)
        groups = Groups(M1, SUFFIX)
        testgroup = groups.create(properties={'cn': CN})
        time.sleep(2)
        test_groups.append(testgroup)

    # Step 4
    # Now start testing by adding differnt user to differn group
    if not ds_is_older('1.3.7'):
        test_groups[0].remove('objectClass', 'nsMemberOf')

    member_dn = test_users[0].dn
    grp0_dn = test_groups[0].dn
    grp1_dn = test_groups[1].dn

    test_groups[0].add_member(member_dn)
    repl.wait_while_replication_is_progressing(M1, C1)

    # Step 5
    for i in [M1, H1, C1]:
        _find_memberof(i, member_dn, grp0_dn)

    # Step 6
    test_groups[1].add_member(test_groups[0].dn)
    repl.wait_while_replication_is_progressing(M1, C1)

    # Step 7
    for i in [grp0_dn, grp1_dn]:
        for inst in [M1, H1, C1]:
            _find_memberof(inst, member_dn, i)

    # Step 8
    for i in [M1, H1, C1]:
        _find_memberof(i, grp0_dn, grp1_dn)

    # Step 9
    test_groups[1].remove_member(test_groups[0].dn)
    time.sleep(2)

    # Step 10
    # For negative testcase, we are using assertionerror
    for inst in [M1, H1, C1]:
        for i in [grp0_dn, member_dn]:
            with pytest.raises(AssertionError):
                _find_memberof(inst, i, grp1_dn)

    # Step 11
    test_groups[0].remove_member(member_dn)
    time.sleep(2)

    # Step 12
    for inst in [M1, H1, C1]:
        for grp in [grp0_dn, grp1_dn]:
            with pytest.raises(AssertionError):
                _find_memberof(inst, member_dn, grp)

    # Step 13
    C1.plugins.disable(name=PLUGIN_MEMBER_OF)
    C1.restart()

    # Step 14
    test_groups[0].add_member(member_dn)
    repl.wait_while_replication_is_progressing(M1, C1)

    # Step 15
    for i in [M1, H1]:
        _find_memberof(i, member_dn, grp0_dn)
    with pytest.raises(AssertionError):
        _find_memberof(C1, member_dn, grp0_dn)

    # Step 16
    memberof = MemberOfPlugin(C1)
    memberof.enable()
    C1.restart()

    # Step 17
    for i in [M1, H1]:
        _find_memberof(i, member_dn, grp0_dn)
    with pytest.raises(AssertionError):
        _find_memberof(C1, member_dn, grp0_dn)

    # Step 18
    memberof.fixup(SUFFIX)
    # have to sleep instead of task.wait() because the task opens a thread and exits
    time.sleep(5)

    # Step 19
    for i in [M1, H1, C1]:
        _find_memberof(i, member_dn, grp0_dn)


@pytest.mark.skipif(ds_is_older('1.3.7'), reason="Not implemented")
def test_scheme_violation_errors_logged(topo_m2):
    """Check that ERR messages are verbose enough, if a member entry
    doesn't have the appropriate objectclass to support 'memberof' attribute

    :id: e2af0aaa-447e-4e85-a5ce-57ae66260d0b
    :setup: Standalone instance
    :steps:
        1. Enable memberofPlugin and set autoaddoc to nsMemberOf
        2. Restart the instance
        3. Add a user without nsMemberOf attribute
        4. Create a group and add the user to the group
        5. Check that user has memberOf attribute
        6. Check the error log for ".*oc_check_allowed_sv.*USER_DN.*memberOf.*not allowed.*"
           and ".*schema violation caught - repair operation.*" patterns
    :expectedresults:
        1. Should be successful
        2. Should be successful
        3. Should be successful
        4. Should be successful
        5. User should have the attribute
        6. Errors should be logged
    """

    inst = topo_m2.ms["supplier1"]
    memberof = MemberOfPlugin(inst)
    memberof.enable()
    memberof.set_autoaddoc('nsMemberOf')
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        delay = 3
    else:
        delay = 0
    inst.restart()

    users = UserAccounts(inst, SUFFIX)
    user_props = TEST_USER_PROPERTIES.copy()
    user_props.update({'uid': USER_CN, 'cn': USER_CN, 'sn': USER_CN})
    testuser = users.create(properties=user_props)
    testuser.remove('objectclass', 'nsMemberOf')

    groups = Groups(inst, SUFFIX)
    testgroup = groups.create(properties={'cn': GROUP_CN})

    testgroup.add('member', testuser.dn)

    time.sleep(delay)
    user_memberof_attr = testuser.get_attr_val_utf8('memberof')
    assert user_memberof_attr
    log.info('memberOf attr value - {}'.format(user_memberof_attr))

    pattern = ".*oc_check_allowed_sv.*{}.*memberOf.*not allowed.*".format(testuser.dn)
    log.info("pattern = %s" % pattern)
    assert inst.ds_error_log.match(pattern)

    pattern = ".*schema violation caught - repair operation.*"
    assert inst.ds_error_log.match(pattern)


@pytest.mark.parametrize("users", [999], indirect=True)
def test_memberof_with_changelog_reset(topo_m2, users):
    """Test that replication does not break, after DS stop-start, due to changelog reset

    :id: 60c11636-55a1-4704-9e09-2c6bcc828de4
    :setup: 2 Suppliers, 999 user entries on supplier 1 (created by users fixture)
    :steps:
        1. On M1 and M2, Enable memberof
        2. On M1, add a group with these 999 user entries as members
        3. Stop M1 in between,
           when add the group memberof is called and before it is finished the
           add, so step 3 should be executed after memberof has started and
           before the add has finished
        4. Check that replication is working fine
    :expectedresults:
        1. memberof should be enabled
        2. Add operation should start
        3. M1 should be stopped
        4. Replication should be working fine
    """
    m1 = topo_m2.ms["supplier1"]
    m2 = topo_m2.ms["supplier2"]

    log.info("Configure memberof on M1 and M2")
    memberof = MemberOfPlugin(m1)
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        # too difficult to make a large update work at shutdown
        # need a dedicated test
        return
    memberof.enable()
    memberof.set_autoaddoc('nsMemberOf')
    m1.restart()

    memberof = MemberOfPlugin(m2)
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        # too difficult to make a large update work at shutdown
        # need a dedicated test
        return
    memberof.enable()
    memberof.set_autoaddoc('nsMemberOf')
    m2.restart()

    log.info("On M1, add a group with these 999 entries as members")
    dic_of_attributes = {'cn': ensure_bytes('testgroup'),
                         'objectclass': ensure_list_bytes(['top', 'groupOfNames'])}

    for user in users:
        dic_of_attributes.setdefault('member', [])
        dic_of_attributes['member'].append(user.dn)

    log.info('Adding the test group using async function')
    groupdn = 'cn=testgroup,%s' % DEFAULT_SUFFIX
    m1.add(Entry((groupdn, dic_of_attributes)))

    #shutdown the server in-between adding the group
    m1.stop()

    #start the server
    m1.start()

    log.info("Check the log messages for error")
    error_msg = "ERR - NSMMReplicationPlugin - ruv_compare_ruv"
    assert not m1.ds_error_log.match(error_msg)

    log.info("Check that the replication is working fine both ways, M1 <-> M2")
    repl = ReplicationManager(DEFAULT_SUFFIX)
    repl.test_replication_topology(topo_m2)


def add_container(inst, dn, name, sleep=False):
    """Creates container entry"""
    conts = nsContainers(inst, dn)
    cont = conts.create(properties={'cn': name})
    if sleep:
        time.sleep(1)
    return cont


def add_member(server, cn, subtree):
    dn = subtree
    users = UserAccounts(server, dn, rdn=None)
    users.create(properties={'uid': 'test_%s' % cn,
                             'cn': "%s" % cn,
                             'sn': 'SN',
                             'description': 'member',
                             'uidNumber': '1000',
                             'gidNumber': '2000',
                             'homeDirectory': '/home/testuser'
                             })


def add_group(server, cn, subtree):
    group = Groups(server, subtree, rdn=None)
    group.create(properties={'cn': "%s" % cn,
                             'member': ['uid=test_m1,%s' % SUBTREE_1, 'uid=test_m2,%s' % SUBTREE_1],
                             'description': 'group'})


def rename_entry(server, cn, from_subtree, to_subtree):
    dn = '%s,%s' % (cn, from_subtree)
    nrdn = '%s-new' % cn
    log.fatal('Renaming user (%s): new %s' % (dn, nrdn))
    server.rename_s(dn, nrdn, newsuperior=to_subtree, delold=0)


def _find_memberof_ext(server, user_dn=None, group_dn=None, find_result=True):
    assert (server)
    assert (user_dn)
    assert (group_dn)
    ent = server.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
    found = False
    if ent.hasAttr('memberof'):

        for val in ent.getValues('memberof'):
            server.log.info("!!!!!!! %s: memberof->%s" % (user_dn, val))
            if ensure_str(val) == group_dn:
                found = True
                break

    if find_result:
        assert found
    else:
        assert (not found)

def _check_membership(server, entry, expected_members, expected_memberof):
    assert server
    assert entry

    memberof = entry.get_attr_vals('memberof')
    member = entry.get_attr_vals('member')
    assert len(member) == len(expected_members)
    assert len(memberof) == len(expected_memberof)
    for e in expected_members:
        server.log.info("Checking %s has member %s" % (entry.dn, e.dn))
        assert e.dn.encode() in member
    for e in expected_memberof:
        server.log.info("Checking %s is member of %s" % (entry.dn, e.dn))
        assert e.dn.encode() in memberof


def test_memberof_group(topology_st):
    """Test memberof does not fail if group is moved into scope

    :id: d1d276ae-6375-4ad8-9437-6a0afcbee7d2

    :setup: Single instance

    :steps:
         1. Enable memberof plugin and set memberofentryscope
         2. Restart the server
         3. Add test sub-suffixes
         4. Add test users
         5. Add test groups
         6. Check for memberof attribute added to the test users
         7. Rename the group entry
         8. Check the new name is reflected in memberof attribute of user

    :expectedresults:
         1. memberof plugin should be enabled and memberofentryscope should be set
         2. Server should be restarted
         3. Sub-suffixes should be added
         4. Test users should be added
         5. Test groups should be added
         6. memberof attribute should be present in the test users
         7. Group entry should be renamed
         8. New group name should be present in memberof attribute of user
    """

    inst = topology_st.standalone
    log.info('Enable memberof plugin and set the scope as cn=sub1,dc=example,dc=com')
    memberof = MemberOfPlugin(inst)
    memberof.enable()
    memberof.replace('memberOfEntryScope', SUBTREE_1)
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        delay = 3
    else:
        delay = 0
    inst.restart()

    add_container(inst, SUFFIX, 'sub1')
    add_container(inst, SUFFIX, 'sub2')
    add_member(inst, 'm1', SUBTREE_1)
    add_member(inst, 'm2', SUBTREE_1)
    add_group(inst, 'g1', SUBTREE_1)
    add_group(inst, 'g2', SUBTREE_2)

    time.sleep(delay)
    # _check_memberof
    dn1 = '%s,%s' % ('uid=test_m1', SUBTREE_1)
    dn2 = '%s,%s' % ('uid=test_m2', SUBTREE_1)
    g1 = '%s,%s' % ('cn=g1', SUBTREE_1)
    g2 = '%s,%s' % ('cn=g2', SUBTREE_2)
    _find_memberof_ext(inst, dn1, g1, True)
    _find_memberof_ext(inst, dn2, g1, True)
    _find_memberof_ext(inst, dn1, g2, False)
    _find_memberof_ext(inst, dn2, g2, False)

    rename_entry(inst, 'cn=g2', SUBTREE_2, SUBTREE_1)

    time.sleep(delay)
    g2n = '%s,%s' % ('cn=g2-new', SUBTREE_1)
    _find_memberof_ext(inst, dn1, g1, True)
    _find_memberof_ext(inst, dn2, g1, True)
    _find_memberof_ext(inst, dn1, g2n, True)
    _find_memberof_ext(inst, dn2, g2n, True)

def test_multipaths(topology_st, request):
    """Test memberof succeeds to update memberof when
    there are multiple paths from a leaf to an intermediate node

    :id: 35aa704a-b895-4153-9dcb-1e8a13612ebf

    :setup: Single instance

    :steps:
         1. Create a graph G1->U1, G2->G21->U1
         2. Add G2 as member of G1: G1->U1, G1->G2->G21->U1
         3. Check members and memberof in entries G1,G2,G21,User1

    :expectedresults:
         1. Graph should be created
         2. succeed
         3. Membership is okay
    """

    inst = topology_st.standalone
    memberof = MemberOfPlugin(inst)
    memberof.enable()
    memberof.replace('memberOfEntryScope', SUFFIX)
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        delay = 3
    else:
        delay = 0
    inst.restart()

    #
    # Create the hierarchy
    #
    #
    #  Grp1 ---------------> User1
    #                           ^
    #                          /
    #  Grp2 ----> Grp21 ------/
    #
    users = UserAccounts(inst, SUFFIX, rdn=None)
    user1 = users.create(properties={'uid': "user1",
                             'cn': "user1",
                             'sn': 'SN',
                             'description': 'leaf',
                             'uidNumber': '1000',
                             'gidNumber': '2000',
                             'homeDirectory': '/home/user1'
                             })
    group = Groups(inst, SUFFIX, rdn=None)
    g0 = group.create(properties={'cn': 'group0',
                             'description': 'group0'})
    g1 = group.create(properties={'cn': 'group1',
                             'member': user1.dn,
                             'description': 'group1'})
    g21 = group.create(properties={'cn': 'group21',
                             'member': user1.dn,
                             'description': 'group21'})
    g2 = group.create(properties={'cn': 'group2',
                             'member': [g21.dn],
                             'description': 'group2'})

    # Enable debug logs if necessary
    #inst.config.replace('nsslapd-errorlog-level', '65536')
    #inst.config.set('nsslapd-accesslog-level','260')
    #inst.config.set('nsslapd-plugin-logging', 'on')
    #inst.config.set('nsslapd-auditlog-logging-enabled','on')
    #inst.config.set('nsslapd-auditfaillog-logging-enabled','on')

    #
    # Update the hierarchy
    #
    #
    #  Grp1 ----------------> User1
    #    \                       ^
    #     \                     /
    #      --> Grp2 --> Grp21 --
    #
    g1.add_member(g2.dn)
    time.sleep(delay)

    #
    # Check G1, G2, G21 and User1 members and memberof
    #
    _check_membership(inst, g1, expected_members=[g2, user1], expected_memberof=[])
    _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g1])
    _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2, g1])
    _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])

    #inst.config.replace('nsslapd-errorlog-level', '65536')
    #inst.config.set('nsslapd-accesslog-level','260')
    #inst.config.set('nsslapd-plugin-logging', 'on')
    #inst.config.set('nsslapd-auditlog-logging-enabled','on')
    #inst.config.set('nsslapd-auditfaillog-logging-enabled','on')
    #
    # Update the hierarchy
    #
    #
    #  Grp1 ----------------> User1
    #                            ^
    #                           /
    #          Grp2 --> Grp21 --
    #
    g1.remove_member(g2.dn)
    time.sleep(delay)

    #
    # Check G1, G2, G21 and User1 members and memberof
    #
    _check_membership(inst, g1, expected_members=[user1], expected_memberof=[])
    _check_membership(inst, g2, expected_members=[g21], expected_memberof=[])
    _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2])
    _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])

    #
    # Update the hierarchy
    #
    #
    #  Grp1 ----------------> User1
    #      \__________         ^
    #                 |       /
    #                 v      /
    #     Grp2 --> Grp21 ----
    #
    g1.add_member(g21.dn)
    time.sleep(delay)

    #
    # Check G1, G2, G21 and User1 members and memberof
    #
    _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[])
    _check_membership(inst, g2, expected_members=[g21], expected_memberof=[])
    _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2, g1])
    _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])

    #
    # Update the hierarchy
    #
    #
    #  Grp1 ----------------> User1
    #                            ^
    #                           /
    #          Grp2 --> Grp21 --
    #
    g1.remove_member(g21.dn)
    time.sleep(delay)

    #
    # Check G1, G2, G21 and User1 members and memberof
    #
    _check_membership(inst, g1, expected_members=[user1], expected_memberof=[])
    _check_membership(inst, g2, expected_members=[g21], expected_memberof=[])
    _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g2])
    _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1])

    #
    # Update the hierarchy
    #
    #
    #       Grp1 ----------------> User1
    #                                 ^
    #                                /
    #   Grp0 ---> Grp2 ---> Grp21 ---
    #
    g0.add_member(g2.dn)
    time.sleep(delay)

    #
    # Check G0,G1, G2, G21 and User1 members and memberof
    #
    _check_membership(inst, g0, expected_members=[g2], expected_memberof=[])
    _check_membership(inst, g1, expected_members=[user1], expected_memberof=[])
    _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0])
    _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g2])
    _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0])

    #
    # Update the hierarchy
    #
    #
    #       Grp1 ----------------> User1
    #       ^                         ^
    #      /                         /
    #   Grp0 ---> Grp2 ---> Grp21 ---
    #
    g0.add_member(g1.dn)
    time.sleep(delay)

    #
    # Check G0,G1, G2, G21 and User1 members and memberof
    #
    _check_membership(inst, g0, expected_members=[g1,g2], expected_memberof=[])
    _check_membership(inst, g1, expected_members=[user1], expected_memberof=[g0])
    _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0])
    _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g2])
    _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0])

    #
    # Update the hierarchy
    #
    #
    #       Grp1 ----------------> User1
    #       ^  \_____________        ^
    #      /                 |       /
    #     /                  V      /
    #   Grp0 ---> Grp2 ---> Grp21 ---
    #
    g1.add_member(g21.dn)
    time.sleep(delay)

    #
    # Check G0,G1, G2, G21 and User1 members and memberof
    #
    _check_membership(inst, g0, expected_members=[g1, g2], expected_memberof=[])
    _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[g0])
    _check_membership(inst, g2, expected_members=[g21], expected_memberof=[g0])
    _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g1, g2])
    _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g2, g1, g0])

    #
    # Update the hierarchy
    #
    #
    #       Grp1 ----------------> User1
    #       ^  \_____________        ^
    #      /                 |       /
    #     /                  V      /
    #   Grp0 ---> Grp2      Grp21 ---
    #
    g2.remove_member(g21.dn)
    time.sleep(delay)

    #
    # Check G0,G1, G2, G21 and User1 members and memberof
    #
    _check_membership(inst, g0, expected_members=[g1, g2], expected_memberof=[])
    _check_membership(inst, g1, expected_members=[user1, g21], expected_memberof=[g0])
    _check_membership(inst, g2, expected_members=[], expected_memberof=[g0])
    _check_membership(inst, g21, expected_members=[user1], expected_memberof=[g0, g1])
    _check_membership(inst, user1, expected_members=[], expected_memberof=[g21, g1, g0])

    def fin():
        try:
            user1.delete()
            g1.delete()
            g2.delete()
            g21.delete()
        except:
            pass
    request.addfinalizer(fin)

def _config_memberof_entrycache_on_modrdn_failure(server):

    server.plugins.enable(name=PLUGIN_MEMBER_OF)
    peoplebase = 'ou=people,%s' % SUFFIX
    MEMBEROF_PLUGIN_DN = ('cn=' + PLUGIN_MEMBER_OF + ',cn=plugins,cn=config')
    server.modify_s(MEMBEROF_PLUGIN_DN, [(ldap.MOD_REPLACE, 'memberOfAllBackends', b'on'),
                                         (ldap.MOD_REPLACE, 'memberOfEntryScope', peoplebase.encode()),
                                         (ldap.MOD_REPLACE, 'memberOfAutoAddOC', b'nsMemberOf')])


def _disable_auto_oc_memberof(server):
    MEMBEROF_PLUGIN_DN = ('cn=' + PLUGIN_MEMBER_OF + ',cn=plugins,cn=config')
    server.modify_s(MEMBEROF_PLUGIN_DN,
        [(ldap.MOD_REPLACE, 'memberOfAutoAddOC', b'nsContainer')])


def test_entrycache_on_modrdn_failure(topology_st):
    """This test checks that when a modrdn fails, the destination entry is not returned by a search
    This could happen in case the destination entry remains in the entry cache

    :id: a4d8ac0b-2448-406a-9dc2-5a72851e30b6
    :setup: Standalone Instance
    :steps:
        1. configure memberof to only scope ou=people,SUFFIX
        2. Creates 10 users
        3. Create groups0 (in peoplebase) that contain user0 and user1
        4. Check user0 and user1 have memberof=group0.dn
        5. Create group1 (OUT peoplebase) that contain user0 and user1
        6. Check user0 and user1 have NOT memberof=group1.dn
        7. Move group1 IN peoplebase and check users0 and user1 HAVE memberof=group1.dn
        8. Create group2 (OUT peoplebase) that contain user2 and user3. Group2 contains a specific description value
        9. Check user2 and user3 have NOT memberof=group2.dn
        10. configure memberof so that added objectclass does not allow 'memberof' attribute
        11. Move group2 IN peoplebase and check move failed OPERATIONS_ERROR (because memberof failed)
        12. Search all groups and check that the group, having the specific description value,
            has the original DN of group2.dn
    :expectedresults:
        1. should succeed
        2. should succeed
        3. should succeed
        4. should succeed
        5. should succeed
        6. should succeed
        7. should succeed
        8. should succeed
        9. should succeed
        10. should succeed
        11. should fail OPERATION_ERROR because memberof plugin fails to add 'memberof' to members.
        12. should succeed

    """

    # only scopes peoplebase
    _config_memberof_entrycache_on_modrdn_failure(topology_st.standalone)
    memberof = MemberOfPlugin(topology_st.standalone)
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        delay = 3
    else:
        delay = 0
    topology_st.standalone.restart(timeout=10)

    # create 10 users
    peoplebase = 'ou=people,%s' % SUFFIX
    for i in range(10):
        cn = 'user%d' % i
        dn = 'cn=%s,%s' % (cn, peoplebase)
        log.fatal('Adding user (%s): ' % dn)
        topology_st.standalone.add_s(Entry((dn, {'objectclass': ['top', 'person'],
                             'sn': 'user_%s' % cn,
                             'description': 'add on standalone'})))

    # Check that members of group0 (in the scope) have 'memberof
    group0_dn = 'cn=group_in0,%s' % peoplebase
    topology_st.standalone.add_s(Entry((group0_dn, {'objectclass': ['top', 'groupofnames'],
                             'member': [
                                   'cn=user0,%s' % peoplebase,
                                   'cn=user1,%s' % peoplebase,
                                   ],
                             'description': 'mygroup'})))

    time.sleep(delay)
    # Check the those entries have memberof with group0
    for i in range(2):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        assert ent.hasAttr('memberof')
        found = False
        for val in ent.getValues('memberof'):
            topology_st.standalone.log.info("!!!!!!! %s: memberof->%s (vs %s)" % (user_dn, val, group0_dn.encode().lower()))
            if val.lower() == group0_dn.encode().lower():
                found = True
                break
        assert found

    # Create a group1 out of the scope
    group1_dn = 'cn=group_out1,%s' % SUFFIX
    topology_st.standalone.add_s(Entry((group1_dn, {'objectclass': ['top', 'groupofnames'],
                             'member': [
                                   'cn=user0,%s' % peoplebase,
                                   'cn=user1,%s' % peoplebase,
                                   ],
                             'description': 'mygroup'})))

    time.sleep(delay)
    # Check the those entries have not memberof with group1
    for i in range(2):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        assert ent.hasAttr('memberof')
        found = False
        for val in ent.getValues('memberof'):
            topology_st.standalone.log.info("!!!!!!! %s: memberof->%s (vs %s)" % (user_dn, val, group1_dn.encode().lower()))
            if val.lower() == group1_dn.encode().lower():
                found = True
                break
        assert not found

    # move group1 into the scope and check user0 and user1 are memberof group1
    topology_st.standalone.rename_s(group1_dn, 'cn=group_in1', newsuperior=peoplebase, delold=0)
    new_group1_dn = 'cn=group_in1,%s' % peoplebase

    time.sleep(delay)
    for i in range(2):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        assert ent.hasAttr('memberof')
        found = False
        for val in ent.getValues('memberof'):
            topology_st.standalone.log.info("!!!!!!! %s: memberof->%s (vs %s)" % (user_dn, val, new_group1_dn.encode().lower()))
            if val.lower() == new_group1_dn.encode().lower():
                found = True
                break
        assert found

    # Create a group2 out of the scope with a SPECIFIC description value
    entry_description = "this is to check that the entry having this description has the appropriate DN"
    group2_dn = 'cn=group_out2,%s' % SUFFIX
    topology_st.standalone.add_s(Entry((group2_dn, {'objectclass': ['top', 'groupofnames'],
                             'member': [
                                   'cn=user2,%s' % peoplebase,
                                   'cn=user3,%s' % peoplebase,
                                   ],
                             'description': entry_description})))
    time.sleep(delay)
    # Check the those entries have not memberof with group2
    for i in (2, 3):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        assert not ent.hasAttr('memberof')

    # memberof will not add the missing objectclass
    _disable_auto_oc_memberof(topology_st.standalone)
    topology_st.standalone.restart(timeout=10)

    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        # move group2 into the scope and check it succeeds
        topology_st.standalone.rename_s(group2_dn, 'cn=group_in2', newsuperior=peoplebase, delold=0)
        topology_st.standalone.log.info("This is expected, modrdn does not fail only updates of members will fail")
    else:
        # move group2 into the scope and check it fails
        try:
            topology_st.standalone.rename_s(group2_dn, 'cn=group_in2', newsuperior=peoplebase, delold=0)
            topology_st.standalone.log.info("This is unexpected, modrdn should fail as the member entry have not the appropriate objectclass")
            assert False
        except ldap.OBJECT_CLASS_VIOLATION:
            pass

        # retrieve the entry having the specific description value
        # check that the entry DN is the original group2 DN
        ents = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(cn=gr*)')
        found = False
        for ent in ents:
            topology_st.standalone.log.info("retrieve: %s with desc=%s" % (ent.dn, ent.getValue('description')))
            if ent.getValue('description') == entry_description.encode():
                found = True
                assert ent.dn == group2_dn
        assert found


def _config_memberof_silent_memberof_failure(server):
    _config_memberof_entrycache_on_modrdn_failure(server)


def test_silent_memberof_failure(topology_st, request):
    """This test checks that if during a MODRDN, the memberof plugin fails
    then MODRDN also fails

    :id: 095aee01-581c-43dd-a241-71f9631a18bb
    :setup: Standalone Instance
    :steps:
        1. configure memberof to only scope ou=people,SUFFIX
        2. Do some cleanup and Creates 10 users
        3. Create groups0 (IN peoplebase) that contain user0 and user1
        4. Check user0 and user1 have memberof=group0.dn
        5. Create group1 (OUT peoplebase) that contain user0 and user1
        6. Check user0 and user1 have NOT memberof=group1.dn
        7. Move group1 IN peoplebase and check users0 and user1 HAVE memberof=group1.dn
        8. Create group2 (OUT peoplebase) that contain user2 and user3.
        9. Check user2 and user3 have NOT memberof=group2.dn
        10. configure memberof so that added objectclass does not allow 'memberof' attribute
        11. Move group2 IN peoplebase and check move failed OPERATIONS_ERROR (because memberof failed)
        12. Check user2 and user3 have NOT memberof=group2.dn
        13. ADD group3 (IN peoplebase) with user4 and user5 members and check add failed OPERATIONS_ERROR (because memberof failed)
        14. Check user4 and user5 have NOT memberof=group2.dn
    :expectedresults:
        1. should succeed
        2. should succeed
        3. should succeed
        4. should succeed
        5. should succeed
        6. should succeed
        7. should succeed
        8. should succeed
        9. should succeed
        10. should succeed
        11. should fail OPERATION_ERROR because memberof plugin fails to add 'memberof' to members.
        12. should succeed
        13. should fail OPERATION_ERROR because memberof plugin fails to add 'memberof' to members
        14. should succeed
    """
    # only scopes peoplebase
    _config_memberof_silent_memberof_failure(topology_st.standalone)
    memberof = MemberOfPlugin(topology_st.standalone)
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        delay = 3
    else:
        delay = 0
    topology_st.standalone.restart(timeout=10)

    # first do some cleanup
    peoplebase = 'ou=people,%s' % SUFFIX
    for i in range(10):
        cn = 'user%d' % i
        dn = 'cn=%s,%s' % (cn, peoplebase)
        try:
            topology_st.standalone.delete_s(dn)
        except ldap.NO_SUCH_OBJECT:
            pass

    for i in range(3):
        try:
            topology_st.standalone.delete_s('cn=group_in%d,%s' % (i, peoplebase))
        except ldap.NO_SUCH_OBJECT:
                pass

    try:
        topology_st.standalone.delete_s('cn=group_out2,%s' % SUFFIX)
    except ldap.NO_SUCH_OBJECT:
            pass

    # create 10 users
    for i in range(10):
        cn = 'user%d' % i
        dn = 'cn=%s,%s' % (cn, peoplebase)
        log.fatal('Adding user (%s): ' % dn)
        topology_st.standalone.add_s(Entry((dn, {'objectclass': ['top', 'person'],
                             'sn': 'user_%s' % cn,
                             'description': 'add on standalone'})))

    # Check that members of group0 (in the scope) have 'memberof
    group0_dn = 'cn=group_in0,%s' % peoplebase
    topology_st.standalone.add_s(Entry((group0_dn, {'objectclass': ['top', 'groupofnames'],
                             'member': [
                                   'cn=user0,%s' % peoplebase,
                                   'cn=user1,%s' % peoplebase,
                                   ],
                             'description': 'mygroup'})))

    time.sleep(delay)
    # Check the those entries have memberof with group0
    for i in range(2):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        assert ent.hasAttr('memberof')
        found = False
        for val in ent.getValues('memberof'):
            topology_st.standalone.log.info("!!!!!!! %s: memberof->%s (vs %s)" % (user_dn, val, group0_dn.encode().lower()))
            if val.lower() == group0_dn.encode().lower():
                found = True
                break
        assert found

    # Create a group1 out of the scope
    group1_dn = 'cn=group_out1,%s' % SUFFIX
    topology_st.standalone.add_s(Entry((group1_dn, {'objectclass': ['top', 'groupofnames'],
                             'member': [
                                   'cn=user0,%s' % peoplebase,
                                   'cn=user1,%s' % peoplebase,
                                   ],
                             'description': 'mygroup'})))

    time.sleep(delay)
    # Check the those entries have not memberof with group1
    for i in range(2):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        assert ent.hasAttr('memberof')
        found = False
        for val in ent.getValues('memberof'):
            topology_st.standalone.log.info("!!!!!!! %s: memberof->%s (vs %s)" % (user_dn, val, group1_dn.encode().lower()))
            if val.lower() == group1_dn.encode().lower():
                found = True
                break
        assert not found

    # move group1 into the scope and check user0 and user1 are memberof group1
    topology_st.standalone.rename_s(group1_dn, 'cn=group_in1', newsuperior=peoplebase, delold=0)
    new_group1_dn = 'cn=group_in1,%s' % peoplebase
    time.sleep(delay)
    for i in range(2):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        assert ent.hasAttr('memberof')
        found = False
        for val in ent.getValues('memberof'):
            topology_st.standalone.log.info("!!!!!!! %s: memberof->%s (vs %s)" % (user_dn, val, new_group1_dn.encode().lower()))
            if val.lower() == new_group1_dn.encode().lower():
                found = True
                break
        assert found

    # Create a group2 out of the scope
    group2_dn = 'cn=group_out2,%s' % SUFFIX
    topology_st.standalone.add_s(Entry((group2_dn, {'objectclass': ['top', 'groupofnames'],
                             'member': [
                                   'cn=user2,%s' % peoplebase,
                                   'cn=user3,%s' % peoplebase,
                                   ],
                             'description': 'mygroup'})))

    time.sleep(delay)
    # Check the those entries have not memberof with group2
    for i in (2, 3):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        assert not ent.hasAttr('memberof')

    # memberof will not add the missing objectclass
    _disable_auto_oc_memberof(topology_st.standalone)
    topology_st.standalone.restart(timeout=10)

    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        # move group2 into the scope and check it succeeds
        topology_st.standalone.rename_s(group2_dn, 'cn=group_in2', newsuperior=peoplebase, delold=0)
        topology_st.standalone.log.info("This is expected, modrdn does not fail only updates of members will fail")
    else:
        # move group2 into the scope and check it fails
        try:
            topology_st.standalone.rename_s(group2_dn, 'cn=group_in2', newsuperior=peoplebase, delold=0)
            topology_st.standalone.log.info("This is unexpected, modrdn should fail as the member entry have not the appropriate objectclass")
            assert False
        except ldap.OBJECT_CLASS_VIOLATION:
            pass

    time.sleep(delay)
    # Check the those entries have not memberof
    for i in (2, 3):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        topology_st.standalone.log.info("Should assert %s has memberof is %s" % (user_dn, ent.hasAttr('memberof')))
        assert not ent.hasAttr('memberof')

    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() == "on"):
        # Create a group3 in the scope
        group3_dn = 'cn=group3_in,%s' % peoplebase
        topology_st.standalone.add_s(Entry((group3_dn, {'objectclass': ['top', 'groupofnames'],
                                                        'member': ['cn=user4,%s' % peoplebase,
                                                                   'cn=user5,%s' % peoplebase,],
                                                        'description': 'mygroup'})))
        topology_st.standalone.log.info("This is expected, add does not fail only updates of members will fail")
    else:
        # Create a group3 in the scope
        group3_dn = 'cn=group3_in,%s' % peoplebase
        try:
            topology_st.standalone.add_s(Entry((group3_dn, {'objectclass': ['top', 'groupofnames'],
                                 'member': [
                                       'cn=user4,%s' % peoplebase,
                                       'cn=user5,%s' % peoplebase,
                                       ],
                                 'description': 'mygroup'})))
            topology_st.standalone.log.info("This is unexpected, ADD should fail as the member entry have not the appropriate objectclass")
            assert False
        except ldap.OBJECT_CLASS_VIOLATION:
            pass
        except ldap.OPERATIONS_ERROR:
            pass

    time.sleep(delay)
    # Check the those entries do not have memberof
    for i in (4, 5):
        user_dn = 'cn=user%d,%s' % (i, peoplebase)
        ent = topology_st.standalone.getEntry(user_dn, ldap.SCOPE_BASE, "(objectclass=*)", ['memberof'])
        topology_st.standalone.log.info("Should assert %s has memberof is %s" % (user_dn, ent.hasAttr('memberof')))
        assert not ent.hasAttr('memberof')

    def fin():
        # Cleanup the user[0-9]* entries
        peoplebase = 'ou=people,%s' % SUFFIX
        for i in range(10):
            cn = 'user%d' % i
            dn = 'cn=%s,%s' % (cn, peoplebase)
            try:
                topology_st.standalone.delete_s(dn)
            except ldap.NO_SUCH_OBJECT:
                pass

        # Cleanup the user_ entries
        ents = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(uid=user_*)')
        for ent in ents:
            try:
                topology_st.standalone.delete_s(ent.dn)
            except ldap.NO_SUCH_OBJECT:
                pass

        # Cleanup the test_ entries
        ents = topology_st.standalone.search_s(DEFAULT_SUFFIX, ldap.SCOPE_SUBTREE, '(uid=test_*)')
        for ent in ents:
            try:
                topology_st.standalone.delete_s(ent.dn)
            except ldap.NO_SUCH_OBJECT:
                pass

        for i in range(3):
            try:
                topology_st.standalone.delete_s('cn=group_in%d,%s' % (i, peoplebase))
            except ldap.NO_SUCH_OBJECT:
                    pass

        try:
            topology_st.standalone.delete_s('cn=group_out2,%s' % SUFFIX)
        except ldap.NO_SUCH_OBJECT:
                pass

    request.addfinalizer(fin)


def check_memberof_consistency(inst, group):
    """This function checks that there is same number of:
         - entries having 'memberOf' attribute
         - members in the group
    """
    suffix = Domain(inst, SUFFIX)
    group_members = len(group.get_attr_vals('member'))
    users_memberof = len(suffix.search(filter='(memberof=*)'))
    assert group_members == users_memberof


def count_global_fixup_message(errlog):
    """This function returns a tuple (nbstarted, nsfinished) telling how many messages
       (about Memberof pluging global fixed task) are found in the error log.
    """
    nbstarted = 0
    nbfinished = 0
    for line in errlog.match('.*Memberof plugin [a-z]* the global fixup task.*'):
        if 'started' in line:
            nbstarted += 1
        elif 'finished' in line:
            nbfinished += 1
    log.info(f'Global fixup start was started {nbstarted} times.')
    log.info(f'Global fixup start was finished {nbfinished} times.')
    return (nbstarted, nbfinished)

def _kill_instance(inst, sig=signal.SIGTERM, delay=None):
    pid = None
    try:
        with open(inst.pid_file(), 'rb') as f:
            for line in f.readlines():
                try:
                    pid = int(line.strip())
                    break
                except ValueError:
                    continue
    except IOError:
        pass

    if not pid or pid == 0:
        pytest.raises(AssertionError)

    if delay:
        time.sleep(delay)
    os.kill(pid, signal.SIGKILL)

@pytest.mark.skipif(get_default_db_lib() == "mdb", reason="Not supported over mdb")
def test_shutdown_on_deferred_memberof(topology_st, request):
    """This test checks that shutdown is handled properly if memberof updayes are deferred.

    :id: c5629cae-15a0-11ee-8807-482ae39447e5
    :setup: Standalone Instance
    :steps:
        1. Enable memberof plugin to scope SUFFIX
        2. create 500 users
        3. Create a large groups with 250 members
        4. Restart the instance (using the default 2 minutes timeout)
        5. Check that users memberof and group members are in sync.
        6. Modify the group to have 250 others members.
        7. Restart the instance with short timeout
        8. Check that the instance needs fixup
        9. Check that deferred thread did not run fixup
        10. Allow deferred thread to run fixup
        11. Modify the group to have 250 others members.
        12. Restart the instance with short timeout
        13. Check that the instance needs fixup
        14. Check that deferred thread did run fixup
    :expectedresults:
        1. should succeed
        2. should succeed
        3. should succeed
        4. should succeed
        5. should succeed
        6. should succeed
        7. should succeed
        8. should succeed
        9. should succeed
    """

    inst = topology_st.standalone
    inst.stop()
    lpath = inst.ds_error_log._get_log_path()
    os.unlink(lpath)
    inst.start()
    inst.config.loglevel(vals=(ErrorLog.DEFAULT,ErrorLog.PLUGIN))
    errlog = DirsrvErrorLog(inst)
    test_timeout = 900


    # Step 1. Enable memberof plugin to scope SUFFIX
    memberof = MemberOfPlugin(inst)
    delay=0
    memberof.set_memberofdeferredupdate("on")
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() != "on"):
        pytest.skip("Memberof deferred update not enabled or not supported.");
    else:
        delay=10
    memberof.set_attr('memberOf')
    memberof.replace_groupattr('member')
    memberof.remove_all_entryscope()
    memberof.remove_all_excludescope()
    memberof.remove_configarea()
    memberof.remove_autoaddoc()
    memberof.enable()
    inst.restart()

    #Creates users and groups
    users_dn = []

    # Step 2. create 500 users
    for i in range(500):
        CN = '%s%d' % (USER_CN, i)
        users = UserAccounts(inst, SUFFIX)
        user_props = TEST_USER_PROPERTIES.copy()
        user_props.update({'uid': CN, 'cn': CN, 'sn': '_%s' % CN})
        testuser = users.create(properties=user_props)
        users_dn.append(testuser.dn)

    # Step 3. Create a large groups with 250 members
    groups = Groups(inst, SUFFIX)
    testgroup = groups.create(properties={'cn': 'group50', 'member': users_dn[0:249]})

    # Step 4. Restart the instance (using the default 2 minutes timeout)
    time.sleep(10)
    log.info(f'Stopping instance at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    inst.stop()
    log.info(f'Instance stopped at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    inst.start()

    time.sleep(delay)
    # Step 5. Check that users memberof and group members are in sync.
    check_memberof_consistency(inst, testgroup)

    # Step 6. Modify the group to get another big group.
    testgroup.replace('member', users_dn[250:499])

    # Step 7. Restart the instance with short timeout
    pattern = 'deferred_thread_func - thread has stopped'
    original_nbcleanstop = len(errlog.match(pattern))
    log.info(f'Stopping instance at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    _kill_instance(inst, sig=signal.SIGKILL, delay=5)
    log.info(f'Instance stopped at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    # Double check that timeout occured during shutdown
    #  (i.e: no new 'deferred_thread_func - thread has stopped' message)
    nbcleanstop = len(errlog.match(pattern))
    assert nbcleanstop == original_nbcleanstop

    log.info(f'Instance restarted after timeout at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    inst.restart()
    assert inst.status()
    log.info(f'Restart completed at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')

    # Step 9.
    # Check that memberofneedfixup is present
    # and fixup task was not launched because by default launch_fixup is no
    memberof = MemberOfPlugin(inst)
    memberof.set_memberofdeferredupdate("on")
    if (memberof.get_memberofdeferredupdate() and memberof.get_memberofdeferredupdate().lower() != "on"):
        pytest.skip("Memberof deferred update not enabled or not supported.");
    else:
        delay=10
    value = memberof.get_memberofneedfixup()
    assert ((str(value).lower() == "yes") or (str(value).lower() == "true"))
    assert len(errlog.match('.*It is recommended to launch memberof fixup task.*')) == 1

    # Step 10. allow the server to launch the fixup task
    inst.stop()
    inst.deleteErrorLogs()
    inst.start()
    log.info(f'set memberoflaunchfixup=ON')
    memberof.set_memberoflaunchfixup('on')
    inst.restart()

    # Step 11. Modify the group to get another big group.
    testgroup.replace('member', users_dn[250:499])

    # Step 12. then kill/reset errorlog/restart
    _kill_instance(inst, sig=signal.SIGKILL, delay=5)
    log.info(f'Instance restarted after timeout at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
    inst.restart()
    assert inst.status()
    log.info(f'Restart completed at {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')

    # step 13. Check that memberofneedfixup is present
    memberof = MemberOfPlugin(inst)
    value = memberof.get_memberofneedfixup()
    assert ((str(value).lower() == "yes") or (str(value).lower() == "true"))

    # step 14. Verify the global fixup started/finished messages
    attribute_name = 'memberOf'
    started_lines = errlog.match('.*Memberof plugin started the global fixup task for attribute .*')
    assert len(started_lines) >= 1
    for line in started_lines:
        log.info(f'Started line: {line}')
        assert f'attribute {attribute_name}' in line

    # Wait for finished messages to appear, then verify no nulls are present
    finished_lines = []
    for _ in range(60):
        finished_lines = errlog.match('.*Memberof plugin finished the global fixup task.*')
        if finished_lines:
            break
        time.sleep(1)
    assert len(finished_lines) >= 1
    for line in finished_lines:
        log.info(f'Finished line: {line}')
        assert '(null)' not in line

    # Check that users memberof and group members are in sync.
    time.sleep(delay)
    check_memberof_consistency(inst, testgroup)


    def fin():

        for dn in users_dn:
            try:
                inst.delete_s(dn)
            except ldap.NO_SUCH_OBJECT:
                pass

        try:
            inst.delete_s(testgroup.dn)
        except ldap.NO_SUCH_OBJECT:
                pass

    request.addfinalizer(fin)


def test_memberof_modrdn_to_itself(topology_st, user1, group1):
    """Test that memberOf plugin correctly handles modrdn operations on groups

    :id: a4c7f8e9-2f1a-11ef-8a3c-482ae39447e5
    :setup: Standalone Instance with user1 and group1 fixtures
    :steps:
        1. Enable the memberOf plugin and restart the server
        2. Verify user is not initially a member of the group
        3. Add user to the group
        4. Verify user is now a member of the group
        5. Enable plugin logging to capture modrdn operations
        6. Perform modrdn operation on group (rename to itself)
        7. Verify user is still a member of the group after rename
        8. Verify memberOf plugin logs skip message for identical src/dst rename
    :expectedresults:
        1. MemberOf plugin should be enabled successfully
        2. User should not have memberOf attribute initially
        3. User should be added to group successfully
        4. User should have memberOf attribute pointing to group
        5. Plugin logging should be enabled successfully
        6. Modrdn operation should complete without errors
        7. User should retain memberOf attribute after rename operation
        8. Plugin should log that modrdn was skipped due to identical src/dst
    """

    # Enable the MemberOf plugin
    memberof = MemberOfPlugin(topology_st.standalone)
    memberof.remove_all_entryscope()
    memberof.remove_all_excludescope()
    memberof.remove_configarea()
    memberof.remove_autoaddoc()
    memberof.enable()
    topology_st.standalone.restart()

    # Verify that the user is not a member of the group
    check_membership(user1, group1.dn, False)
    # Add the user to the group
    group1.add_member(user1.dn)
    # Verify that the user is a member of the group
    check_membership(user1, group1.dn, True)

    # Enable the plugin log to capture memberof modrdn callback notification
    topology_st.standalone.config.loglevel(vals=[LOG_PLUGIN, LOG_DEFAULT], service='error')

    # Rename the group on itself
    group1.rename('cn=group1')

    # Verify that the user is still a member of the group
    check_membership(user1, group1.dn, True)

    # Verify that the memberof modrdn callback notification is logged
    assert topology_st.standalone.ds_error_log.match('.*Skip modrdn operation because src/dst identical.*')


def check_memberof_on_server(server, group, users, expected_present=True):
    for user in users:
        entry = server.getEntry(user.dn, ldap.SCOPE_BASE, "(objectclass=*)")
        if expected_present:
            assert entry.hasAttr('memberof') and ensure_str(entry.getValue('memberof')) == group.dn
        else:
            assert not entry.hasAttr('memberof')


@pytest.mark.parametrize("users", [5], indirect=True)
def test_memberof_total_init_with_fractional_replication(topo_m2, users):
    """Test that memberOf attributes survive total initialization when configured
    with fractional replication that excludes memberOf from regular updates but
    NOT from total initialization.

    :id: 2c3f1a88-1e4f-4a97-8b2a-1d5c6e7f8901
    :setup: Two suppliers with memberOf plugin and fractional replication, 5 user entries created by fixture
    :steps:
        1. Configure memberOf plugin on supplier1 with fractional replication
           (excludes memberOf from regular updates, includes in total init)
        2. Enable memberOf plugin on supplier2
        3. Wait for replication of users and verify they exist on supplier2
        4. Create test group with all users as members on supplier1
        5. Wait for group replication and verify group exists on supplier2
        6. Verify memberOf attributes are present on both suppliers
        7. Perform total initialization from supplier1 to supplier2
        8. Verify memberOf attributes survive total initialization on both suppliers
        9. Add new user and verify ongoing replication still works correctly
        10. Reset replication agreement to plain replication and cleanup test objects
    :expectedresults:
        1. MemberOf plugin and fractional replication should be configured successfully
        2. MemberOf plugin should be enabled on supplier2
        3. Users should exist on both suppliers after replication
        4. Test group should be created with members successfully
        5. Group should replicate and exist on both suppliers
        6. MemberOf attributes should be present on both suppliers before total init
        7. Total initialization should complete successfully
        8. MemberOf attributes should survive total initialization despite fractional exclusion
        9. Post-init replication should work correctly with memberOf attributes
        10. Replication agreement should be reset to prevent affecting other tests
    """

    supplier1 = topo_m2.ms["supplier1"]
    supplier2 = topo_m2.ms["supplier2"]

    # Enable memberOf plugin on supplier1 with fractional replication to exclude
    # memberOf from regular updates but NOT from total initialization
    config_memberof(supplier1)

    # Enable memberOf plugin on supplier2
    memberof2 = MemberOfPlugin(supplier2)
    memberof2.enable()
    supplier2.restart()

    # Wait for replication of users (created by fixture)
    repl = ReplicationManager(DEFAULT_SUFFIX)
    repl.wait_while_replication_is_progressing(supplier1, supplier2)

    # Verify users exist on supplier2
    users2 = UserAccounts(supplier2, DEFAULT_SUFFIX)
    for user in users:
        user2 = users2.get(dn=user.dn)
        assert user2.exists()
        log.info(f'Verified user exists on supplier2: {user2.dn}')

    # Create test group with members on supplier1
    groups1 = Groups(supplier1, DEFAULT_SUFFIX)
    group_props = {
        'cn': 'testgroup',
        'member': [user.dn for user in users]
    }
    group1 = groups1.create(properties=group_props)
    log.info(f'Created group with members: {group1.dn}')

    # Wait for replication of group
    repl.wait_while_replication_is_progressing(supplier1, supplier2)

    # Verify group exists on supplier2
    groups2 = Groups(supplier2, DEFAULT_SUFFIX)
    group2 = groups2.get('testgroup')
    assert group2.exists()
    log.info(f'Verified group exists on supplier2: {group2.dn}')

    # Verify memberOf attributes are present on both suppliers
    time.sleep(5)  # Allow time for memberOf plugin to process

    check_memberof_on_server(supplier1, group1, users, True)
    check_memberof_on_server(supplier2, group2, users, True)
    log.info('MemberOf attributes verified on both suppliers before total init')

    # Perform total initialization from supplier1 to supplier2
    log.info('Starting total initialization from supplier1 to supplier2')
    supplier1.agreement.init(DEFAULT_SUFFIX, supplier2.host, supplier2.port)
    log.info('Total initialization initiated')

    # Wait for total initialization to complete
    supplier1.waitForReplInit(supplier1.agreement.list(suffix=DEFAULT_SUFFIX)[0].dn)
    log.info('Total initialization completed')

    # Verify memberOf attributes are still present on both suppliers after total init
    time.sleep(5)  # Allow time for any post-init processing

    check_memberof_on_server(supplier1, group1, users, True)
    check_memberof_on_server(supplier2, group2, users, True)
    log.info('MemberOf attributes verified on both suppliers after total init')

    # Additional verification: ensure replication is still working
    log.info('Verifying replication still works after total init')

    # Add a new member to test ongoing replication
    new_user_props = {
        'uid': 'member',
        'cn': 'member',
        'sn': 'member',
        'uidNumber': '1005',
        'gidNumber': '2005',
        'homeDirectory': '/home/member'
    }
    users1 = UserAccounts(supplier1, DEFAULT_SUFFIX)
    new_user = users1.create(properties=new_user_props)
    group1.add_member(new_user.dn)

    # Wait for replication
    repl.wait_while_replication_is_progressing(supplier1, supplier2)
    time.sleep(5)

    # Verify new member has memberOf on both suppliers
    user1_new = users1.get('member')
    user2_new = users2.get('member')

    memberof1_new = user1_new.get_attr_vals_utf8('memberOf')
    memberof2_new = user2_new.get_attr_vals_utf8('memberOf')

    assert memberof1_new and group1.dn in memberof1_new
    assert memberof2_new and group2.dn in memberof2_new

    # Cleanup: Reset replication agreement to plain replication
    agreements1 = supplier1.agreement.list(suffix=DEFAULT_SUFFIX)
    if agreements1:
        supplier1.agreement.setProperties(agmnt_dn=agreements1[0].dn,
                                         properties={RA_FRAC_EXCLUDE: None,
                                                   RA_FRAC_EXCLUDE_TOTAL_UPDATE: None})

    # Cleanup test objects (users are cleaned up automatically by fixture)
    new_user.delete()
    group1.delete()
    group2.delete()


class ModifySecondBackendThread(threading.Thread):
    """Thread class for continuously modifying entries in the second backend"""

    def __init__(self, inst, second_suffix, timeout, num_operations=5000):
        threading.Thread.__init__(self)
        self.daemon = True
        self.inst = inst
        self.second_suffix = second_suffix
        self.timeout = timeout
        self.num_operations = num_operations

    def run(self):
        """Run continuous modifications on the second backend"""
        try:
            # Create a new connection for this thread
            conn = self.inst.clone()
            # Ensure the connection is properly opened
            conn.open()

            # Set timeout using a try/except to handle connection issues
            try:
                conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
            except (AttributeError, ldap.LDAPError):
                # If setting timeout fails, continue without it
                log.warning('Could not set LDAP timeout for thread connection')

            log.info('Starting modifications on second backend...')

            for x in range(self.num_operations):
                try:
                    conn.modify_s(self.second_suffix,
                                 [(ldap.MOD_REPLACE, 'description',
                                   ensure_bytes(f'modified description {x}'))])
                except ldap.LDAPError as e:
                    log.error(f'Failed to modify second suffix at iteration {x} - error: {e}')
                    # Continue with remaining operations instead of failing completely
                    continue

        except Exception as e:
            log.error(f'Thread connection error: {e}')
            return
        finally:
            # Ensure connection is closed even if errors occur
            try:
                if 'conn' in locals():
                    conn.close()
            except:
                pass

        log.info('Finished modifying second backend')


def test_memberof_retrocl_deadlock(topology_st):
    """Test that memberOf and retrocl plugins do not deadlock in multi-backend scenario

    :id: 47931f8c-e792-41bf-a3c0-b3b38391cb31
    :setup: Standalone instance
    :steps:
        1. Configure memberOf plugin for uniqueMember attribute and enable
        2. Configure retrocl plugin scoping to default backend only and enable
        3. Restart instance to apply plugin configurations
        4. Create a second backend for deadlock testing
        5. Create a test group using UniqueGroups and 1500 test users
        6. In parallel: add users to group (triggers memberOf) while modifying second backend
        7. Verify no timeout/deadlock occurs during parallel operations
        8. Clean up test entries and disable plugins
    :expectedresults:
        1. MemberOf plugin should be configured and enabled successfully
        2. Retrocl plugin should be configured and enabled successfully
        3. Instance restart should complete successfully
        4. Second backend should be created successfully
        5. Test group and users should be created successfully
        6. Parallel operations should complete without deadlock/timeout
        7. No LDAP timeout errors should occur
        8. Cleanup should complete successfully
    """

    inst = topology_st.standalone

    # Test constants
    SECOND_BACKEND = "deadlock"
    SECOND_SUFFIX = f"dc={SECOND_BACKEND}"
    TIME_OUT = 5
    NUM_USERS = 1500

    log.info("Test memberOf and retrocl deadlock scenario")

    # Enable memberOf and retrocl plugins
    log.info("Enabling memberOf and retrocl plugins")
    memberof_plugin = MemberOfPlugin(inst)
    memberof_plugin.replace_groupattr('uniquemember')
    memberof_plugin.enable()

    retrocl_plugin = RetroChangelogPlugin(inst)
    retrocl_plugin.replace('nsslapd-include-suffix', DEFAULT_SUFFIX)
    retrocl_plugin.enable()

    topology_st.standalone.restart()

    # Create second backend
    log.info("Creating second backend for deadlock testing")
    backends = Backends(inst)
    backend = backends.create(properties={
        'cn': SECOND_BACKEND,
        'nsslapd-suffix': SECOND_SUFFIX
    })

    # Create test group and users
    log.info("Creating test group and users")

    # Create group
    groups = UniqueGroups(inst, DEFAULT_SUFFIX)
    test_group = groups.create(properties={
        'cn': 'testgroup',
        'description': 'testgroup'
    })

    # Create 1500 test users
    users = UserAccounts(inst, DEFAULT_SUFFIX)
    user_dns = []

    log.info(f"Creating {NUM_USERS} test users...")
    for idx in range(1, NUM_USERS + 1):
        user_props = TEST_USER_PROPERTIES.copy()
        user_props.update({
            'uid': f'member{idx}',
            'cn': f'member{idx}',
            'sn': f'member{idx}',
            'uidNumber': f'{idx}',
            'gidNumber': f'{idx}',
            'homeDirectory': f'/home/member{idx}'
        })

        try:
            user = users.create(properties=user_props)
            user_dns.append(user.dn)
        except ldap.LDAPError as e:
            log.error(f'Failed to create user member{idx}: {e}')
            raise

    log.info(f"Created {len(user_dns)} test users")

    # Run parallel operations to test for deadlock
    log.info("Starting parallel operations to test deadlock scenario")

    # Start thread to continuously modify second backend
    modify_thread = ModifySecondBackendThread(inst, SECOND_SUFFIX, TIME_OUT)
    modify_thread.start()
    time.sleep(1)  # Give thread time to start

    # Add members to group with timeout detection
    log.info("Adding members to group...")
    inst.set_option(ldap.OPT_TIMEOUT, TIME_OUT)

    try:
        for idx, user_dn in enumerate(user_dns, 1):
            try:
                test_group.add('uniqueMember', user_dn)
            except ldap.TIMEOUT:
                log.error(f'DEADLOCK detected at member {idx}! Test FAILED.')
                raise AssertionError("Deadlock detected - memberOf and retrocl plugins deadlocked")
            except ldap.LDAPError as e:
                log.error(f'Failed to add member {idx} (not a deadlock): {e}')
                raise
    finally:
        # Wait for the modification thread to finish
        modify_thread.join()

    # Verify no deadlock occurred - if we reach here, test passed
    log.info("SUCCESS: No deadlock detected between memberOf and retrocl plugins")

    # Cleanup - disable plugins and remove second backend
    try:
        for user in user_dns:
            user.delete()
        test_group.delete()
        memberof_plugin.disable()
        retrocl_plugin.disable()
        backend.delete()
    except Exception as e:
        log.warning(f"Cleanup warning: {e}")


if __name__ == '__main__':
    # Run isolated
    # -s for DEBUG mode
    CURRENT_FILE = os.path.realpath(__file__)
    pytest.main("-s %s" % CURRENT_FILE)

