"""Tests for synchronisation of user profiles with external sources.

Re-uses the ZODBMutablePropertyProvider as a example foreign property
provider.

Re-uses the 'source_users' plugin in order to simulate a primary
external user source.

"""
import io
from datetime import datetime
import unittest

from Products.PlonePAS.Extensions.Install import activatePluginInterfaces
from Products.PlonePAS.plugins.property import ZODBMutablePropertyProvider

from plone import api
from ploneintranet import api as pi_api
from ploneintranet.userprofile.sync import AllUsersPropertySync
from ploneintranet.userprofile.sync import AllUsersSync
from ploneintranet.userprofile.sync import IUserProfileManager
from ploneintranet.userprofile.sync import NO_VALUE
from ploneintranet.userprofile.sync import get_last_sync
from ploneintranet.userprofile.sync import record_last_sync
from ploneintranet.userprofile.sync import undouble_unicode_escape
from ploneintranet.userprofile.tests.base import BaseTestCase

TESTING_PLUGIN_ID = 'mock_ldap'
TEST_USER_PREFIX = 'sync-user'


def install_mock_pas_plugin():
    out = io.BytesIO()
    pp = ZODBMutablePropertyProvider(TESTING_PLUGIN_ID,
                                     'Mock LDAP',
                                     schema=(('email', 'string', NO_VALUE), ))
    pp.meta_type = 'Mock External PAS Users'
    acl_users = api.portal.get_tool('acl_users')
    acl_users[TESTING_PLUGIN_ID] = pp
    activatePluginInterfaces(api.portal.get(), TESTING_PLUGIN_ID, out)


class SyncBaseTestCase(BaseTestCase):

    def setUp(self):
        super(SyncBaseTestCase, self).setUp()
        install_mock_pas_plugin()

    def _get_member_ids(self):
        user_ids = {user.getId() for user in api.user.get_users()}
        return {user_id
                for user_id in user_ids
                if user_id.startswith(TEST_USER_PREFIX)}

    def _get_userprofiles(self):
        return map(pi_api.userprofile.get, self._get_member_ids())

    def _create_userprofile(self):
        user = pi_api.userprofile.create(
            username='johndoe',
            email='johndoe@example.com',
            properties={
                'first_name': u'John',
                'last_name': u'Doe',
            },
            approve=True,
        )
        return user

    def _check_last_sync_set(self, context):
        self.assertIsInstance(context.last_sync, datetime)

    def _set_foreign_property(self, member, key, value):
        acl_users = api.portal.get_tool(name='acl_users')
        plugin = acl_users[TESTING_PLUGIN_ID]
        sheet = plugin.getPropertiesForUser(member)
        sheet.setProperty(member, key, value)


class TestUserPropertyManager(SyncBaseTestCase):

    def test_sync(self):
        self.login_as_portal_owner()
        user = self._create_userprofile()
        member = api.user.get(username=user.username)
        email = 'test@foo.com'
        self._set_foreign_property(member, 'email', email)

        user = pi_api.userprofile.get(user.username)
        upm = IUserProfileManager(user)
        upm.sync()
        self.assertEqual(user.email, email)
        catalog = api.portal.get_tool(name='portal_catalog')
        brains = catalog.searchResults(portal_type=user.portal_type)
        self.assertEqual(len(brains), 1)
        self.assertEqual(brains[0]['email'], email)
        self._check_last_sync_set(brains[0].getObject())

    def test_sync_local_user(self):
        self.login_as_portal_owner()
        user = self._create_userprofile()
        ad = IUserProfileManager(user)
        ad.sync()
        self.assertFalse(
            hasattr(user, 'last_sync'),
            'User should not have been synced',
        )


class TestAllUsersPropertySync(SyncBaseTestCase):

    def test__call__(self):
        self.login_as_portal_owner()
        self._create_userprofile()
        view = AllUsersPropertySync(self.profiles, self.request)
        view()
        self._check_last_sync_set(self.profiles)


class TestAllUsersSync(SyncBaseTestCase):

    def _create_external_user(self, test_user_suffix):
        """Return a userid for tests."""
        username = '{}-{}'.format(TEST_USER_PREFIX, test_user_suffix)
        email = '{}@ploneintranet.org'.format(username)
        member = api.user.create(username=username, email=email)
        self._set_foreign_property(member, 'email', email)
        return username

    def _create_external_users(self, n):
        for i in range(n):
            self._create_external_user(i)

    def _check_profiles_synced(self, expected_userids, since=datetime.min):
        for userid in expected_userids:
            self.assertIn(userid, self.profiles)
            profile = self.profiles[userid]
            self._check_last_sync_set(profile)
            self.assertGreater(profile.last_sync, since)

    def _check_sync_dates(self):
        profiles = self._get_userprofiles()
        # We want all the profiles that:
        # - have been synced
        # - are not disabled
        synced_profiles = [
            profile for profile in profiles
            if (
                get_last_sync(profile) and
                api.content.get_state(obj=profile) != 'disabled'
            )
        ]
        self.assertTrue(
            all(
                self.profiles.last_sync < profile.last_sync
                for profile in synced_profiles
            )
        )

    def _call_view_under_test(self):
        view = AllUsersSync(self.profiles, self.request)
        view()

    def setUp(self):
        super(SyncBaseTestCase, self).setUp()
        self.login_as_portal_owner()
        install_mock_pas_plugin()

    def test_no_profiles_synced(self):
        self._create_external_users(10)
        self._check_profiles_synced([])
        sync_dt = datetime.utcnow()
        self._call_view_under_test()
        member_ids = self._get_member_ids()
        self._check_profiles_synced(member_ids, sync_dt)
        self._check_sync_dates()

    def test_some_profiles_synced(self):
        self._create_external_users(9)

        # Simulate 1 profile already synced
        username = '{}-9'.format(TEST_USER_PREFIX)
        profile = pi_api.userprofile.create(username=username,
                                            approve=True)
        record_last_sync(profile)
        self._check_profiles_synced({profile.getId()})

        self._call_view_under_test()
        self._check_profiles_synced(self._get_member_ids())
        self._check_sync_dates()

    def test_all_profiles_synced(self):
        for n in range(3):
            username = '{}-{}'.format(TEST_USER_PREFIX, n)
            profile = pi_api.userprofile.create(username=username,
                                                approve=True)
            record_last_sync(profile)

        sync_dt = datetime.utcnow()
        self._call_view_under_test()
        self._check_profiles_synced([], sync_dt)

        # Simulate a new remote user being added
        sync_dt = datetime.utcnow()
        userid = self._create_external_user(3)
        self._call_view_under_test()
        self._check_profiles_synced({userid}, since=sync_dt)
        self._check_sync_dates()

    def test_external_user_removed_disables_profile(self):
        """Test the case where a remote user been deleted.

        The local membrane profile should be disabled.
        """
        username = self._create_external_user(0)
        self._call_view_under_test()
        api.user.delete(username=username)
        self._call_view_under_test()
        profile = pi_api.userprofile.get(username)
        self.assertIsNotNone(profile)
        self.assertEqual(api.content.get_state(obj=profile), 'disabled')
        self._check_sync_dates()

    def test_local_profile_is_not_disabled(self):
        """Test a "local" profile that has never been sync'd is not disabled.
        """
        username = TEST_USER_PREFIX + '-0'
        profile = pi_api.userprofile.create(username=username,
                                            approve=True)
        api.user.delete(username=username)
        self._call_view_under_test()
        profile = pi_api.userprofile.get(username)
        self.assertIsNotNone(profile)
        self.assertEqual(api.content.get_state(obj=profile), 'enabled')
        self.assertIsNone(get_last_sync(profile))


class TestUndoubleEscape(unittest.TestCase):

    def test_double_unicode(self):
        garbled = u'K\xc4\xb1l\xc4\xb1\xc3\xa7aslan'
        goodname = u'K\u0131l\u0131\xe7aslan'
        self.assertEquals(undouble_unicode_escape(garbled), goodname)

    def test_normal_unicode(self):
        goodname = u'K\u0131l\u0131\xe7aslan'
        self.assertEquals(undouble_unicode_escape(goodname), goodname)

    def test_string(self):
        goodstring = 'K\xc3\x84\xc2\xb1l\xc3\x84\xc2\xb1\xc3\x83\xc2\xa7aslan'
        self.assertEquals(undouble_unicode_escape(goodstring), goodstring)

    def test_object(self):
        NO_VALUE = object()
        self.assertEquals(undouble_unicode_escape(NO_VALUE), NO_VALUE)
