#!/usr/share/ucs-test/runner pytest-3 -s -l -vv
## desc: "Test the UCS<->AD sync in {read,write,sync} mode for `con_other_attribute`s."
## exposure: dangerous
## packages:
## - univention-s4-connector
## bugs:
##  - 35903
##  - 36480
##  - 45252
## versions:
##  4.2-2: skip

# We skip this since 4.2-2, as the corresponding implementation is not yet committed.
# See https://forge.univention.org/bugzilla/show_bug.cgi?id=45252.

import pytest

import univention.testing.connector_common as tcommon
from univention.testing.connector_common import (
    NormalUser, create_con_user, create_udm_user, delete_con_user, delete_udm_user,
)
from univention.testing.udm import UCSTestUDM

import s4connector
from s4connector import connector_running_on_this_host, connector_setup


MAPPINGS = (
    # ucs_attribute, con_attribute, con_other_attribute
    ('phone', 'telephoneNumber', 'otherTelephone'),
    ('homeTelephoneNumber', 'homePhone', 'otherHomePhone'),
    ('mobileTelephoneNumber', 'mobile', 'otherMobile'),
    ('pagerTelephoneNumber', 'pager', 'otherPager'),
)


def random_number():
    return tcommon.random_string(numeric=True)


# General Information: In Active Directory, for attributes that are split in
# two (e.g. `telephoneNumber` and `otherTelephone`), the administrator is
# responsible for keeping a value in `telephoneNumber`. Imagine the following:
# (a) telephoneNumber = '123', otherTelephone = ['123', '456']
# In this case, if the administrator deletes the value of `telephoneNumber`,
# Active Directory does NOT automatically pull a new value from `otherTelephone`.
#
# This is impossible to support with the connector. Imagine again case (a). If
# we delete `123` from `phone` via UDM, AD would be synced into the following
# state: (b) telephoneNumber = [], otherTelephone = ['456']
# From now on, whenever we add a new value to `phone` via UDM, for example:
# (c) phone = ['456', '789'] it MUST be synced as
# (d) telephoneNumber = [], otherTelephone = ['456', '789'] as '456' came
# before '789' and '456' is definitely in `otherTelephone`.
#
# These tests enforce, that `telephoneNumber` is never empty, as long as there
# are values in `otherTelephone`. If a modification would delete the value of
# `telephoneNumber` and at least one value exists in `otherTelephone`, the
# connector duplicates the first entry of `otherTelephone` into
# `telephoneNumber`.


@pytest.mark.parametrize("attribute", MAPPINGS)
@pytest.mark.parametrize("sync_mode", ["write", "sync"])
@pytest.mark.skipif(not connector_running_on_this_host(), reason="Univention S4 Connector not configured.")
def test_attribute_sync_from_udm_to_s4(attribute, sync_mode):
    (ucs_attribute, con_attribute, con_other_attribute) = attribute
    udm_user = NormalUser(selection=("username", "lastname", ucs_attribute))
    primary_value = udm_user.basic.get(ucs_attribute)
    all_values = (primary_value, random_number(), random_number())
    secondary_values = all_values[1:]

    with connector_setup(sync_mode) as s4, UCSTestUDM() as udm:
        # A single `phone` number must be synced to `telephoneNumber` in AD.
        (udm_user_dn, s4_user_dn) = create_udm_user(udm, s4, udm_user, s4connector.wait_for_sync)

        # Additional `phone` values must be synced to `otherTelephone`,
        # `telephoneNumber` must keep its value.
        print(f"\nModifying UDM user: {ucs_attribute}={all_values}\n")
        udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: all_values})
        s4connector.wait_for_sync()
        s4.verify_object(s4_user_dn, {con_attribute: primary_value, con_other_attribute: secondary_values})
        tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: all_values})

        # If we delete the first `phone` value via UDM, we want to duplicate
        # the first value of `otherTelephone` into `telephoneNumber`.
        (new_primary, next_primary) = secondary_values
        print(f"\nModifying UDM user: {ucs_attribute}={secondary_values}\n")
        udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: secondary_values})
        s4connector.wait_for_sync()
        s4.verify_object(s4_user_dn, {con_attribute: new_primary, con_other_attribute: secondary_values})
        tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: secondary_values})

        # If we delete a `phone` value via UDM that is duplicated in AD, we want
        # it to be deleted from `telephoneNumber` and `otherTelephone`.
        print(f"\nModifying UDM user: {ucs_attribute}={next_primary}\n")
        udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: next_primary})
        s4connector.wait_for_sync()
        s4.verify_object(s4_user_dn, {con_attribute: next_primary, con_other_attribute: next_primary})
        tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: next_primary})

        # Setting a completely new `phone` value via UDM, this must be synced
        # to `telephoneNumber` and `otherTelephone` must be empty.
        new_phone_who_dis = random_number()
        print(f"\nModifying UDM user: {ucs_attribute}={new_phone_who_dis}\n")
        udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: new_phone_who_dis})
        s4connector.wait_for_sync()
        s4.verify_object(s4_user_dn, {con_attribute: new_phone_who_dis, con_other_attribute: []})
        tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_phone_who_dis})

        # No `phone` value via UDM, must result in an empty `telephoneNumber`
        # and `otherTelephone`.
        print(f"\nModifying UDM user: {ucs_attribute}={[]}\n")
        udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: ''})
        s4connector.wait_for_sync()
        s4.verify_object(s4_user_dn, {con_attribute: [], con_other_attribute: []})
        tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: []})

        delete_udm_user(udm, s4, udm_user_dn, s4_user_dn, s4connector.wait_for_sync)


@pytest.mark.parametrize("attribute", MAPPINGS)
@pytest.mark.parametrize("sync_mode", ["read", "sync"])
@pytest.mark.skipif(not connector_running_on_this_host(), reason="Univention S4 Connector not configured.")
def test_attribute_sync_from_s4_to_udm(attribute, sync_mode):
    (ucs_attribute, con_attribute, con_other_attribute) = attribute
    udm_user = NormalUser(selection=("username", "lastname", ucs_attribute))
    primary_value = udm_user.basic.get(ucs_attribute)
    all_values = (primary_value, random_number(), random_number())
    secondary_values = all_values[1:]

    with connector_setup(sync_mode) as s4:
        # A single `telephoneNumber` must be synced to `phone` in UDM.
        (_basic_s4_user, s4_user_dn, udm_user_dn) = create_con_user(s4, udm_user, s4connector.wait_for_sync)

        # Additional values in `otherTelephone` must be appended to `phone`.
        print(f"\nModifying S4 user: {con_attribute}={primary_value}, {con_other_attribute}={secondary_values}\n")
        s4.set_attributes(s4_user_dn, **{con_attribute: primary_value, con_other_attribute: secondary_values})
        s4connector.wait_for_sync()
        tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: all_values})
        s4.verify_object(s4_user_dn, {con_attribute: primary_value, con_other_attribute: secondary_values})

        if sync_mode == "sync":  # otherwise the connector can't write into AD
            # If we delete the value of `telephoneNumber` from AD, we expect to get
            # the first value of `otherTelephone` duplicated into
            # `telephoneNumber`.
            (new_primary, _) = secondary_values
            print(f"\nModifying S4 user: {con_attribute}={[]}\n")
            s4.set_attributes(s4_user_dn, **{con_attribute: []})
            s4connector.wait_for_sync()
            tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: secondary_values})
            s4.verify_object(s4_user_dn, {con_attribute: new_primary, con_other_attribute: secondary_values})

            # Deleting the duplicate from `otherTelephone` must retain the value of
            # `telephoneNumber` and `phone` in UDM.
            print(f"\nModifying S4 user: {con_other_attribute}={[]}\n")
            s4.set_attributes(s4_user_dn, **{con_other_attribute: []})
            s4connector.wait_for_sync()
            tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_primary})
            s4.verify_object(s4_user_dn, {con_attribute: new_primary, con_other_attribute: []})

        # Setting a new `telephoneNumber` and no `otherTelephone` in AD must
        # result in a single new value in `phone`.
        new_phone_who_dis = random_number()
        print(f"\nModifying S4 user: {con_attribute}={new_phone_who_dis}\n")
        s4.set_attributes(s4_user_dn, **{con_attribute: new_phone_who_dis, con_other_attribute: []})
        s4connector.wait_for_sync()
        tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_phone_who_dis})
        s4.verify_object(s4_user_dn, {con_attribute: new_phone_who_dis, con_other_attribute: []})

        # Setting no `telephoneNumber` and no `otherTelephone` in AD must
        # result in no value in `phone`.
        print(f"\nModifying S4 user: {con_attribute}={[]}\n")
        s4.set_attributes(s4_user_dn, **{con_attribute: [], con_other_attribute: []})
        s4connector.wait_for_sync()
        tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: []})
        s4.verify_object(s4_user_dn, {con_attribute: [], con_other_attribute: []})

        delete_con_user(s4, s4_user_dn, udm_user_dn, s4connector.wait_for_sync)
