# --- BEGIN COPYRIGHT BLOCK ---
# Copyright (C) 2015 Red Hat, Inc.
# All rights reserved.
#
# License: GPL (version 3 or any later version).
# See LICENSE for details. 
# --- END COPYRIGHT BLOCK ---
#
import os
import sys
import time
import ldap
import logging
import pytest
from lib389 import DirSrv, Entry, tools
from lib389.tools import DirSrvTools
from lib389._constants import *
from lib389.properties import *
from lib389.idm.domain import Domain

log = logging.getLogger(__name__)

installation_prefix = None

USER_NUM = 10
TEST_USER = 'user'

OTHER_NAME = 'other_entry'
MAX_OTHERS = 10

BIND_NAME  = 'bind_entry'
BIND_DN    = 'cn=%s, %s' % (BIND_NAME, SUFFIX)
BIND_PW    = 'password'

ENTRY_NAME = 'test_entry'
ENTRY_DN   = 'cn=%s, %s' % (ENTRY_NAME, SUFFIX)
ENTRY_OC   = "top person"

# subtrees used in test 
SUBTREE_BASE    = "ou=testbase,%s" %SUFFIX
SUBTREE_GREEN_1 = "ou=green_one,%s" % SUBTREE_BASE
SUBTREE_GREEN_2 = "ou=green_two,%s" % SUBTREE_BASE
SUBTREE_RED     = "ou=red,%s" % SUBTREE_BASE
SUBTREES = (SUBTREE_GREEN_1, SUBTREE_GREEN_2, SUBTREE_RED)

class TopologyStandalone(object):
    def __init__(self, standalone):
        standalone.open()
        self.standalone = standalone


@pytest.fixture(scope="module")
def topology(request):
    global installation_prefix

    if installation_prefix:
        args_instance[SER_DEPLOYED_DIR] = installation_prefix

    standalone = DirSrv(verbose=False)

    # Args for the standalone instance
    args_instance[SER_HOST] = HOST_STANDALONE
    args_instance[SER_PORT] = PORT_STANDALONE
    args_instance[SER_SERVERID_PROP] = SERVERID_STANDALONE
    args_standalone = args_instance.copy()
    standalone.allocate(args_standalone)

    # Get the status of the instance and restart it if it exists
    instance_standalone = standalone.exists()

    # Remove the instance
    if instance_standalone:
        standalone.delete()

    # Create the instance
    standalone.create()

    # Used to retrieve configuration information (dbdir, confdir...)
    standalone.open()

    # clear the tmp directory
    standalone.clearTmpDir(__file__)

    # Here we have standalone instance up and running
    return TopologyStandalone(standalone)


def test_ticket48275_init(topology):
    """
        It creates identical entries in 3 subtrees
        It creates aci which allow access to a set of attrs
            in two of these subtrees for bound users
        It creates a user to be used for test

    """


    topology.standalone.log.info("Add subtree base: %s" % SUBTREE_BASE)
    topology.standalone.add_s(Entry((SUBTREE_BASE, {
                                            'objectclass': "top organizationalunit".split(),
                                            'ou': "testbase"})))
    topology.standalone.log.info("Add subtree: %s" % SUBTREE_GREEN_1)
    topology.standalone.add_s(Entry((SUBTREE_GREEN_1, {
                                            'objectclass': "top organizationalunit".split(),
                                            'ou': "green_one"})))
    topology.standalone.log.info("Add subtree: %s" % SUBTREE_GREEN_2)
    topology.standalone.add_s(Entry((SUBTREE_GREEN_2, {
                                            'objectclass': "top organizationalunit".split(),
                                            'ou': "green_two"})))
    topology.standalone.log.info("Add subtree: %s" % SUBTREE_RED)
    topology.standalone.add_s(Entry((SUBTREE_RED, {
                                            'objectclass': "top organizationalunit".split(),
                                            'ou': "red"})))

    # entry used to bind with
    topology.standalone.log.info("Add %s" % BIND_DN)
    topology.standalone.add_s(Entry((BIND_DN, {
                                            'objectclass': "top person".split(),
                                            'sn':           BIND_NAME,
                                            'cn':           BIND_NAME,
                                            'userpassword': BIND_PW})))

    # enable acl error logging
    # mod = [(ldap.MOD_REPLACE, 'nsslapd-errorlog-level', '128')]
    # topology.standalone.modify_s(DN_CONFIG, mod)

    # get rid of default ACIs
    mod = [(ldap.MOD_DELETE, 'aci', None)]
    topology.standalone.modify_s(SUFFIX, mod)

    # Ok Now add the proper ACIs
    ACI_TARGET       = "(target = \"ldap:///%s\")" % SUBTREE_GREEN_1
    ACI_TARGETATTR   = "(targetattr = \"objectclass || cn || sn || uid || givenname \")"
    ACI_ALLOW        = "(version 3.0; acl \"Allow search-read to green attrs\"; allow (read, search, compare)"
    ACI_SUBJECT      = " userdn = \"ldap:///all\";)"
    ACI_BODY         = ACI_TARGET + ACI_TARGETATTR + ACI_ALLOW + ACI_SUBJECT
    # mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)]
    # topology.standalone.modify_s(SUFFIX, mod)
    domain = Domain(topology.standalone, DEFAULT_SUFFIX)
    domain.add('aci', ACI_BODY)

    ACI_TARGET       = "(target = \"ldap:///%s\")" % SUBTREE_GREEN_2
    ACI_TARGETATTR   = "(targetattr != \"mail || employeenumber || mobile || telephonenumber \")"
    ACI_BODY         = ACI_TARGET + ACI_TARGETATTR + ACI_ALLOW + ACI_SUBJECT
    # mod = [(ldap.MOD_ADD, 'aci', ACI_BODY)]
    # topology.standalone.modify_s(SUFFIX, mod)
    domain.add('aci', ACI_BODY)

    log.info("Adding %d test entries..." % USER_NUM)
    for id in range(USER_NUM):
        name = "%s.%d" % (TEST_USER, id)
        mail = "%s@example.com" % name
        for subtree in SUBTREES:
            topology.standalone.add_s(Entry(("cn=%s,%s" % (name, subtree), {
                                         'objectclass': "top person organizationalPerson inetOrgPerson".split(),
                                         'sn': name,
                                         'cn': name,
                                         'uid': name,
                                         'givenname': 'test',
                                         'mail': mail,
                                         'description': 'description',
                                         'employeenumber': "%d" % id,
                                         'telephonenumber': "%d%d%d" % (id,id,id),
                                         'mobile': "%d%d%d" % (id,id,id),
                                         'l': 'MV',
                                         'title': 'Engineer'})))


def gen_filtertests_basic(dm):
    tests = []
    # 1. Simple filter - green attr - not/matching
    tests.append(("(uid=xxxx)",0))
    tests.append(("(uid=user.1)",(3 if dm else 2)))

    # 2. Simple filter -red attr - not/matching
    tests.append(("(mail=user.x@example.com)",0))
    tests.append(("(mail=user.1@example.com)",(3 if dm else 0)))

    # 3. NOT filter -green attr - not/matching
    tests.append(("(!(objectclass=inetorgperson))",(4 if dm else 2)))
    tests.append(("(!(objectclass=dummy))",(34 if dm else 22)))

    # 4. NOT filter - red attr - not/matching
    tests.append(("(!(mail=user.1@example.com))",(31 if dm else 0)))
    tests.append(("(!(mail=user.x@example.com))",(34 if dm else 0)))

    # 5. AND filter - green/green attr - not/matching
    tests.append(("(&(objectclass=inetorgperson)(uid=user.1))",(3 if dm else 2)))
    tests.append(("(&(objectclass=inetorgperson)(uid=user.x))",0))

    # 6. AND filter - green/red attr - not/matching
    tests.append(("(&(objectclass=inetorgperson)(mail=user.1@example.com))",(3 if dm else 0)))
    tests.append(("(&(objectclass=inetorgperson)(mail=user.x@example.com))",0))

    # 7. OR filter - green/green - not/matching/order
    tests.append(("(|(uid=user.1)(sn=user.3))",(6 if dm else 4)))
    tests.append(("(|(uid=user.x)(sn=user.3))",(3 if dm else 2)))
    tests.append(("(|(uid=user.1)(sn=user.x))",(3 if dm else 2)))

    return tests

def gen_filtertests_ext(dm):
    '''
    the extended tests add OR components to the search filter using attributes
    without access, either matching or not.
    The returned entries have to be the same as without these OR parts
    '''

    tests = []

    # 1. Simple filter - green attr - not/matching
    tests.append(("(|(uid=xxxx)(mail=user.xxxx1@example.com))",0))
    tests.append(("(|(uid=xxxx)(mail=user.1@example.com))",(3 if dm else 0)))
    tests.append(("(|(uid=user.1)(mail=user.xxxx1@example.com))",(3 if dm else 2)))
    tests.append(("(|(uid=user.1)(mail=user.3@example.com))",(6 if dm else 2)))

    # 2. Simple filter - red attr - not/matching
    tests.append(("(|(mail=user.x@example.com)(employeenumber=3))",(3 if dm else 0)))
    tests.append(("(|(mail=user.x@example.com)(employeenumber=100))",0))
    tests.append(("(|(mail=user.1@example.com)(employeenumber=3))",(6 if dm else 0)))
    tests.append(("(|(mail=user.1@example.com)(employeenumber=100))",(3 if dm else 0)))

    # 3. NOT filter - green attr - not/matching
    tests.append(("(!(|(objectclass=inetorgperson)(employeenumber=3)))",(4 if dm else 2)))
    tests.append(("(!(|(objectclass=inetorgperson)(employeenumber=100)))",(4 if dm else 2)))
    tests.append(("(|(!(objectclass=inetorgperson))(employeenumber=3))",(7 if dm else 2)))
    tests.append(("(|(!(objectclass=inetorgperson))(employeenumber=100))",(4 if dm else 2)))

    # 4. NOT filter - red attr - not/matching
    tests.append(("(!(|(mail=user.1@example.com)(employeenumber=3)))",(28 if dm else 0)))
    tests.append(("(!(|(mail=user.1@example.com)(employeenumber=100)))",(31 if dm else 0)))
    tests.append(("(!(|(mail=user.x@example.com)(employeenumber=3)))",(31 if dm else 0)))
    tests.append(("(!(|(mail=user.x@example.com)(employeenumber=100)))",(34 if dm else 0)))

    # 5. AND filter - green/green attr - not/matching
    tests.append(("(|(&(objectclass=inetorgperson)(uid=user.1))(employeenumber=3))",(6 if dm else 2)))
    tests.append(("(|(&(objectclass=inetorgperson)(uid=user.1))(employeenumber=100))",(3 if dm else 2)))
    tests.append(("(&(objectclass=inetorgperson)(|(employeenumber=3)(uid=user.1)))",(6 if dm else 2)))
    tests.append(("(&(objectclass=inetorgperson)(|(employeenumber=100)(uid=user.1)))",(3 if dm else 2)))
    tests.append(("(&(|(employeenumber=3)(objectclass=inetorgperson))(uid=user.1))",(3 if dm else 2)))
    tests.append(("(&(|(employeenumber=100)(objectclass=inetorgperson))(uid=user.1))",(3 if dm else 2)))
    tests.append(("(|(&(objectclass=inetorgperson)(uid=user.x))(employeenumber=3))",(3 if dm else 0)))
    tests.append(("(|(&(objectclass=inetorgperson)(uid=user.x))(employeenumber=100))", 0))
    tests.append(("(&(objectclass=inetorgperson)(|(employeenumber=3)(uid=user.x)))",(3 if dm else 0)))
    tests.append(("(&(objectclass=inetorgperson)(|(employeenumber=100)(uid=user.x)))",0))
    tests.append(("(&(|(employeenumber=3)(objectclass=inetorgperson))(uid=user.x))",0))
    tests.append(("(&(|(employeenumber=100)(objectclass=inetorgperson))(uid=user.x))",0))

    # 6. AND filter - green/red attr - not/matching
    tests.append(("(&(|(employeenumber=3)(objectclass=inetorgperson))(mail=user.1@example.com))",(3 if dm else 0)))
    tests.append(("(&(objectclass=inetorgperson)(|(employeenumber=3)(mail=user.1@example.com)))",(6 if dm else 0)))
    tests.append(("(|(employeenumber=3)(&(objectclass=inetorgperson)(mail=user.1@example.com)))",(6 if dm else 0)))
    tests.append(("(&(|(employeenumber=100)(objectclass=inetorgperson))(mail=user.1@example.com))",(3 if dm else 0)))
    tests.append(("(&(objectclass=inetorgperson)(|(employeenumber=100)(mail=user.1@example.com)))",(3 if dm else 0)))
    tests.append(("(|(employeenumber=100)(&(objectclass=inetorgperson)(mail=user.1@example.com)))",(3 if dm else 0)))
    tests.append(("(&(objectclass=inetorgperson)(mail=user.x@example.com))",0))

    # 7. OR filter - green/green - not/matching/order
    tests.append(("(|(uid=user.1)(employeenumber=3)(sn=user.3))",(6 if dm else 4)))
    tests.append(("(|(uid=user.1)(sn=user.3)(employeenumber=3))",(6 if dm else 4)))
    tests.append(("(|(uid=user.x)(employeenumber=3)(sn=user.3))",(3 if dm else 2)))
    tests.append(("(|(uid=user.x)(sn=user.3)(employeenumber=3))",(3 if dm else 2)))
    tests.append(("(|(uid=user.1)(employeenumber=3)(sn=user.x))",(6 if dm else 2)))
    tests.append(("(|(uid=user.1)(sn=user.x)(employeenumber=3))",(6 if dm else 2)))
    tests.append(("(|(uid=user.1)(unknown=3)(sn=user.3))",(6 if dm else 4)))
    tests.append(("(|(uid=user.1)(sn=user.3)(unknown=3))",(6 if dm else 4)))
    tests.append(("(|(uid=user.x)(unknown=3)(sn=user.3))",(3 if dm else 2)))
    tests.append(("(|(uid=user.x)(sn=user.3)(unknown=3))",(3 if dm else 2)))
    tests.append(("(|(uid=user.1)(unknown=3)(sn=user.x))",(3 if dm else 2)))
    tests.append(("(|(uid=user.1)(sn=user.x)(unknown=3))",(3 if dm else 2)))

    return tests

def run_ticket48275_search(topology, dm, basic):

    if basic:
        tests = gen_filtertests_basic(dm)
    else:
        tests = gen_filtertests_ext(dm)

    topology.standalone.log.info("\n\n######################### SEARCH ######################\n")

    cnt = 0
    passed = 0
    for (fstr, exp) in tests:
        cnt += 1
        ents = topology.standalone.search_s(SUBTREE_BASE, ldap.SCOPE_SUBTREE, fstr)
        rec = len(ents)
        topology.standalone.log.info("Test %d: filter \"%s\", expected: %d, received: %d\n" % (cnt, fstr, exp, rec))
        if exp == rec:
            passed += 1

    assert(cnt==passed)

def test_ticket48275_search_basic_dm(topology):

    topology.standalone.simple_bind_s(DN_DM, PASSWORD)
    run_ticket48275_search(topology, dm=True, basic=True)

def test_ticket48275_search_basic_user(topology):

    topology.standalone.simple_bind_s(BIND_DN, BIND_PW)
    run_ticket48275_search(topology, dm=False, basic=True)

def test_ticket48275_search_ext_dm(topology):

    topology.standalone.simple_bind_s(DN_DM, PASSWORD)
    run_ticket48275_search(topology, dm=True, basic=False)

def test_ticket48275_search_ext_user(topology):

    topology.standalone.simple_bind_s(BIND_DN, BIND_PW)
    run_ticket48275_search(topology, dm=False, basic=False)


def test_ticket48275_final(topology):
    topology.standalone.delete()
    log.info('Testcase PASSED')


def run_isolated():
    '''
        run_isolated is used to run these test cases independently of a test scheduler (xunit, py.test..)
        To run isolated without py.test, you need to
            - edit this file and comment '@pytest.fixture' line before 'topology' function.
            - set the installation prefix
            - run this program
    '''
    global installation_prefix
    installation_prefix = None

    topo = topology(True)
    test_ticket48275_init(topo)

    test_ticket48275_search_dm(topo)

    test_ticket48275_final(topo)


if __name__ == '__main__':
    run_isolated()

