# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2017 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details.
# --- END COPYRIGHT BLOCK ---
#
import pytest
from lib389.tasks import *
from lib389.utils import *
from lib389.topologies import topology_m2c2 as topo

from lib389._constants import (PLUGIN_ACCT_POLICY, DN_PLUGIN, DN_CONFIG, DN_DM, PASSWORD,
                              DEFAULT_SUFFIX, SUFFIX)

pytestmark = pytest.mark.tier2

DEBUGGING = os.getenv("DEBUGGING", default=False)
if DEBUGGING:
    logging.getLogger(__name__).setLevel(logging.DEBUG)
else:
    logging.getLogger(__name__).setLevel(logging.INFO)
log = logging.getLogger(__name__)

ACCPOL_DN = "cn={},{}".format(PLUGIN_ACCT_POLICY, DN_PLUGIN)
ACCP_CONF = "{},{}".format(DN_CONFIG, ACCPOL_DN)
USER_PW = 'Secret123'


def _last_login_time(topo, userdn, inst_name, last_login):
    """Find lastLoginTime attribute value for a given master/consumer"""

    if 'master' in inst_name:
        if (last_login == 'bind_n_check'):
            topo.ms[inst_name].simple_bind_s(userdn, USER_PW)
        topo.ms[inst_name].simple_bind_s(DN_DM, PASSWORD)
        entry = topo.ms[inst_name].search_s(userdn, ldap.SCOPE_BASE, 'objectClass=*', ['lastLoginTime'])
    else:
        if (last_login == 'bind_n_check'):
            topo.cs[inst_name].simple_bind_s(userdn, USER_PW)
        topo.cs[inst_name].simple_bind_s(DN_DM, PASSWORD)
        entry = topo.cs[inst_name].search_s(userdn, ldap.SCOPE_BASE, 'objectClass=*', ['lastLoginTime'])
    lastLogin = entry[0].lastLoginTime
    time.sleep(1)
    return lastLogin


def _enable_plugin(topo, inst_name):
    """Enable account policy plugin and configure required attributes"""

    log.info('Enable account policy plugin and configure required attributes')
    if 'master' in inst_name:
        log.info('Configure Account policy plugin on {}'.format(inst_name))
        topo.ms[inst_name].simple_bind_s(DN_DM, PASSWORD)
        try:
            topo.ms[inst_name].plugins.enable(name=PLUGIN_ACCT_POLICY)
            topo.ms[inst_name].modify_s(ACCPOL_DN, [(ldap.MOD_REPLACE, 'nsslapd-pluginarg0', ensure_bytes(ACCP_CONF))])
            topo.ms[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'alwaysrecordlogin', b'yes')])
            topo.ms[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'stateattrname', b'lastLoginTime')])
            topo.ms[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'altstateattrname', b'createTimestamp')])
            topo.ms[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'specattrname', b'acctPolicySubentry')])
            topo.ms[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'limitattrname', b'accountInactivityLimit')])
            topo.ms[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'accountInactivityLimit', b'3600')])
        except ldap.LDAPError as e:
            log.error('Failed to configure {} plugin for inst-{} error: {}'.format(PLUGIN_ACCT_POLICY, inst_name, str(e)))
        topo.ms[inst_name].restart(timeout=10)
    else:
        log.info('Configure Account policy plugin on {}'.format(inst_name))
        topo.cs[inst_name].simple_bind_s(DN_DM, PASSWORD)
        try:
            topo.cs[inst_name].plugins.enable(name=PLUGIN_ACCT_POLICY)
            topo.cs[inst_name].modify_s(ACCPOL_DN, [(ldap.MOD_REPLACE, 'nsslapd-pluginarg0', ensure_bytes(ACCP_CONF))])
            topo.cs[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'alwaysrecordlogin', b'yes')])
            topo.cs[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'stateattrname', b'lastLoginTime')])
            topo.cs[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'altstateattrname', b'createTimestamp')])
            topo.cs[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'specattrname', b'acctPolicySubentry')])
            topo.cs[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'limitattrname', b'accountInactivityLimit')])
            topo.cs[inst_name].modify_s(ACCP_CONF, [(ldap.MOD_REPLACE, 'accountInactivityLimit', b'3600')])
        except ldap.LDAPError as e:
            log.error('Failed to configure {} plugin for inst-{} error {}'.format(PLUGIN_ACCT_POLICY, inst_name, str(e)))
        topo.cs[inst_name].restart(timeout=10)


def test_ticket48944(topo):
    """On a read only replica invalid state info can accumulate

    :id: 833be131-f3bf-493e-97c6-3121438a07b1
    :feature: Account Policy Plugin
    :setup: Two master and two consumer setup
    :steps: 1. Configure Account policy plugin with alwaysrecordlogin set to yes
            2. Check if entries are synced across masters and consumers
            3. Stop all masters and consumers
            4. Start master1 and bind as user1 to create lastLoginTime attribute
            5. Start master2 and wait for the sync of lastLoginTime attribute
            6. Stop master1 and bind as user1 from master2
            7. Check if lastLoginTime attribute is updated and greater than master1
            8. Stop master2, start consumer1, consumer2 and then master2
            9. Check if lastLoginTime attribute is updated on both consumers
            10. Bind as user1 to both consumers and check the value is updated
            11. Check if lastLoginTime attribute is not updated from consumers
            12. Start master1 and make sure the lastLoginTime attribute is not updated on consumers
            13. Bind as user1 from master1 and check if all masters and consumers have the same value
            14. Check error logs of consumers for "deletedattribute;deleted" message
    :expectedresults: No accumulation of replica invalid state info on consumers
    """

    log.info("Ticket 48944 - On a read only replica invalid state info can accumulate")
    user_name = 'newbzusr'
    tuserdn = 'uid={}1,ou=people,{}'.format(user_name, SUFFIX)
    inst_list = ['master1', 'master2', 'consumer1', 'consumer2']
    for inst_name in inst_list:
        _enable_plugin(topo, inst_name)

    log.info('Sleep for 10secs for the server to come up')
    time.sleep(10)
    log.info('Add few entries to server and check if entries are replicated')
    for nos in range(10):
        userdn = 'uid={}{},ou=people,{}'.format(user_name, nos, SUFFIX)
        try:
            topo.ms['master1'].add_s(Entry((userdn, {
                'objectclass': 'top person'.split(),
                'objectclass': 'inetorgperson',
                'cn': user_name,
                'sn': user_name,
                'userpassword': USER_PW,
                'mail': '{}@redhat.com'.format(user_name)})))
        except ldap.LDAPError as e:
            log.error('Failed to add {} user: error {}'.format(userdn, e.message['desc']))
            raise e

    log.info('Checking if entries are synced across masters and consumers')
    entries_m1 = topo.ms['master1'].search_s(SUFFIX, ldap.SCOPE_SUBTREE, 'uid={}*'.format(user_name), ['uid=*'])
    exp_entries = str(entries_m1).count('dn: uid={}*'.format(user_name))
    entries_m2 = topo.ms['master2'].search_s(SUFFIX, ldap.SCOPE_SUBTREE, 'uid={}*'.format(user_name), ['uid=*'])
    act_entries = str(entries_m2).count('dn: uid={}*'.format(user_name))
    assert act_entries == exp_entries
    inst_list = ['consumer1', 'consumer2']
    for inst in inst_list:
        entries_other = topo.cs[inst].search_s(SUFFIX, ldap.SCOPE_SUBTREE, 'uid={}*'.format(user_name), ['uid=*'])
        act_entries = str(entries_other).count('dn: uid={}*'.format(user_name))
        assert act_entries == exp_entries

    topo.ms['master2'].stop(timeout=10)
    topo.ms['master1'].stop(timeout=10)
    topo.cs['consumer1'].stop(timeout=10)
    topo.cs['consumer2'].stop(timeout=10)

    topo.ms['master1'].start(timeout=10)
    lastLogin_m1_1 = _last_login_time(topo, tuserdn, 'master1', 'bind_n_check')

    log.info('Start master2 to sync lastLoginTime attribute from master1')
    topo.ms['master2'].start(timeout=10)
    time.sleep(5)
    log.info('Stop master1')
    topo.ms['master1'].stop(timeout=10)
    log.info('Bind as user1 to master2 and check if lastLoginTime attribute is greater than master1')
    lastLogin_m2_1 = _last_login_time(topo, tuserdn, 'master2', 'bind_n_check')
    assert lastLogin_m2_1 > lastLogin_m1_1

    log.info('Start all servers except master1')
    topo.ms['master2'].stop(timeout=10)
    topo.cs['consumer1'].start(timeout=10)
    topo.cs['consumer2'].start(timeout=10)
    topo.ms['master2'].start(timeout=10)
    time.sleep(10)
    log.info('Check if consumers are updated with lastLoginTime attribute value from master2')
    lastLogin_c1_1 = _last_login_time(topo, tuserdn, 'consumer1', 'check')
    assert lastLogin_c1_1 == lastLogin_m2_1

    lastLogin_c2_1 = _last_login_time(topo, tuserdn, 'consumer2', 'check')
    assert lastLogin_c2_1 == lastLogin_m2_1

    log.info('Check if lastLoginTime update in consumers not synced to master2')
    lastLogin_c1_2 = _last_login_time(topo, tuserdn, 'consumer1', 'bind_n_check')
    assert lastLogin_c1_2 > lastLogin_m2_1

    lastLogin_c2_2 = _last_login_time(topo, tuserdn, 'consumer2', 'bind_n_check')
    assert lastLogin_c2_2 > lastLogin_m2_1

    time.sleep(10)  # Allow replication to kick in
    lastLogin_m2_2 = _last_login_time(topo, tuserdn, 'master2', 'check')
    assert lastLogin_m2_2 == lastLogin_m2_1

    log.info('Start master1 and check if its updating its older lastLoginTime attribute to consumers')
    topo.ms['master1'].start(timeout=10)
    time.sleep(10)
    lastLogin_c1_3 = _last_login_time(topo, tuserdn, 'consumer1', 'check')
    assert lastLogin_c1_3 == lastLogin_c1_2

    lastLogin_c2_3 = _last_login_time(topo, tuserdn, 'consumer2', 'check')
    assert lastLogin_c2_3 == lastLogin_c2_2

    log.info('Check if lastLoginTime update from master2 is synced to all masters and consumers')
    lastLogin_m2_3 = _last_login_time(topo, tuserdn, 'master2', 'bind_n_check')
    time.sleep(10)  # Allow replication to kick in
    lastLogin_m1_2 = _last_login_time(topo, tuserdn, 'master1', 'check')
    lastLogin_c1_4 = _last_login_time(topo, tuserdn, 'consumer1', 'check')
    lastLogin_c2_4 = _last_login_time(topo, tuserdn, 'consumer2', 'check')
    assert lastLogin_m2_3 == lastLogin_m1_2 == lastLogin_c2_4 == lastLogin_c1_4

    log.info('Checking consumer error logs for replica invalid state info')
    assert not topo.cs['consumer2'].ds_error_log.match('.*deletedattribute;deleted.*')
    assert not topo.cs['consumer1'].ds_error_log.match('.*deletedattribute;deleted.*')


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