import datetime

import requests
import responses

from pillar.tests import AbstractPillarTest

httpmock = responses.RequestsMock()


class AbstractSyncTest(AbstractPillarTest):
    def setUp(self):
        super().setUp()
        self.uid1 = self.create_user(24 * '1')
        self.uid2 = self.create_user(24 * '2')

        # Make sure the users have different auth info.
        with self.app.app_context():
            users_coll = self.app.db('users')
            users_coll.update_one(
                {'_id': self.uid1},
                {'$set': {'auth': [
                    {'provider': 'local', 'user_id': '47', 'token': ''},
                    {'provider': 'blender-id', 'user_id': '1947', 'token': ''},
                ]}})
            users_coll.update_one(
                {'_id': self.uid2},
                {'$set': {'auth': [
                    {'provider': 'blender-id', 'user_id': '4488', 'token': ''},
                    {'provider': 'local', 'user_id': '48', 'token': ''},
                ]}})

        self.create_valid_auth_token(self.uid1, token='find-this-token-uid1',
                                     oauth_scopes=['email', 'badge'])
        self.create_valid_auth_token(self.uid1, token='no-badge-scope',
                                     oauth_scopes=['email'])
        self.create_valid_auth_token(self.uid1, token='expired',
                                     oauth_scopes=['email', 'badge'],
                                     expire_in_days=-1)

        self.create_valid_auth_token(self.uid2, token='find-this-token-uid2',
                                     oauth_scopes=['email', 'badge'])
        self.create_valid_auth_token(self.uid2, token='no-badge-scope',
                                     oauth_scopes=['email'])
        self.create_valid_auth_token(self.uid2, token='expired',
                                     oauth_scopes=['email', 'badge'],
                                     expire_in_days=-1)

        from pillar import badge_sync
        self.sync_user1 = badge_sync.SyncUser(self.uid1, 'find-this-token-uid1', '1947')
        self.sync_user2 = badge_sync.SyncUser(self.uid2, 'find-this-token-uid2', '4488')

    def _update_badge_expiry(self, delta_minutes1, delta_minutes2):
        """Make badges of userN expire in delta_minutesN minutes."""
        from pillar.api.utils import utcnow, remove_private_keys
        now = utcnow()

        # Do the update via Eve so that that flow is covered too.
        with self.app.app_context():
            users_coll = self.app.db('users')
            db_user1 = users_coll.find_one(self.uid1)
            db_user1['badges'] = {
                'html': 'badge for user 1',
                'expires': now + datetime.timedelta(minutes=delta_minutes1)
            }
            r, _, _, status = self.app.put_internal('users',
                                                    remove_private_keys(db_user1),
                                                    _id=self.uid1)
        self.assertEqual(200, status, r)

        with self.app.app_context():
            db_user2 = users_coll.find_one(self.uid2)
            db_user2['badges'] = {
                'html': 'badge for user 2',
                'expires': now + datetime.timedelta(minutes=delta_minutes2)
            }
            r, _, _, status = self.app.put_internal('users',
                                                    remove_private_keys(db_user2),
                                                    _id=self.uid2)
        self.assertEqual(200, status, r)


class FindUsersToSyncTest(AbstractSyncTest):
    def test_no_badge_fetched_yet(self):
        from pillar import badge_sync
        with self.app.app_context():
            found = set(badge_sync.find_users_to_sync())
        self.assertEqual({self.sync_user1, self.sync_user2}, found)

    def test_badge_fetched_recently(self):
        from pillar import badge_sync

        # Badges of user1 expired, user2 didn't yet.
        with self.app.app_context():
            self._update_badge_expiry(-5, 5)
            found = list(badge_sync.find_users_to_sync())
        self.assertEqual([self.sync_user1], found)

        # Badges of both users expired, but user2 expired longer ago.
        with self.app.app_context():
            self._update_badge_expiry(-5, -10)
            found = list(badge_sync.find_users_to_sync())
        self.assertEqual([self.sync_user2, self.sync_user1], found)

        # Badges of both not expired yet.
        with self.app.app_context():
            self._update_badge_expiry(2, 3)
            found = list(badge_sync.find_users_to_sync())
        self.assertEqual([], found)


class FindUserSyncInfoTest(AbstractSyncTest):
    def test_no_badge_fetched_yet(self):
        from pillar import badge_sync

        with self.app.app_context():
            found1 = badge_sync.find_user_to_sync(self.uid1)
            found2 = badge_sync.find_user_to_sync(self.uid2)
        self.assertEqual(self.sync_user1, found1)
        self.assertEqual(self.sync_user2, found2)

    def test_badge_fetched_recently(self):
        # This should be the same for all cases, as a single user
        # is always refreshed.
        from pillar import badge_sync

        # Badges of user1 expired, user2 didn't yet.
        with self.app.app_context():
            self._update_badge_expiry(-5, 5)
            found1 = badge_sync.find_user_to_sync(self.uid1)
            found2 = badge_sync.find_user_to_sync(self.uid2)
        self.assertEqual(self.sync_user1, found1)
        self.assertEqual(self.sync_user2, found2)

        # Badges of both users expired, but user2 expired longer ago.
        with self.app.app_context():
            self._update_badge_expiry(-5, -10)
            found1 = badge_sync.find_user_to_sync(self.uid1)
            found2 = badge_sync.find_user_to_sync(self.uid2)
        self.assertEqual(self.sync_user1, found1)
        self.assertEqual(self.sync_user2, found2)

        # Badges of both not expired yet.
        with self.app.app_context():
            self._update_badge_expiry(2, 3)
            found1 = badge_sync.find_user_to_sync(self.uid1)
            found2 = badge_sync.find_user_to_sync(self.uid2)
        self.assertEqual(self.sync_user1, found1)
        self.assertEqual(self.sync_user2, found2)


class FetchHTMLTest(AbstractSyncTest):
    @httpmock.activate
    def test_happy(self):
        from pillar import badge_sync

        def check_request(request: requests.PreparedRequest):
            if request.headers['Authorization'] != 'Bearer find-this-token-uid1':
                return 403, {}, 'BAD TOKEN'
            return 200, {'Content-Type': 'text/html; charset=utf-8'}, 'твоја мајка'.encode()

        httpmock.add_callback('GET', 'http://id.local:8001/api/badges/1947/html/s', check_request)

        with self.app.app_context():
            badge_html = badge_sync.fetch_badge_html(requests.Session(), self.sync_user1, 's')
        self.assertEqual('твоја мајка', badge_html)

    @httpmock.activate
    def test_internal_server_error(self):
        from pillar import badge_sync

        httpmock.add('GET', 'http://id.local:8001/api/badges/1947/html/s',
                     body='oops', status=500)

        with self.assertRaises(badge_sync.StopRefreshing), self.app.app_context():
            badge_sync.fetch_badge_html(requests.Session(), self.sync_user1, 's')

    @httpmock.activate
    def test_no_badge(self):
        from pillar import badge_sync

        httpmock.add('GET', 'http://id.local:8001/api/badges/1947/html/s',
                     body='', status=204)
        with self.app.app_context():
            badge_html = badge_sync.fetch_badge_html(requests.Session(), self.sync_user1, 's')
        self.assertEqual('', badge_html)

    @httpmock.activate
    def test_no_such_user(self):
        from pillar import badge_sync

        httpmock.add('GET', 'http://id.local:8001/api/badges/1947/html/s',
                     body='Not Found', status=404)
        with self.app.app_context():
            badge_html = badge_sync.fetch_badge_html(requests.Session(), self.sync_user1, 's')
        self.assertEqual('', badge_html)

    @httpmock.activate
    def test_no_connection_possible(self):
        from pillar import badge_sync

        with self.assertRaises(badge_sync.StopRefreshing), self.app.app_context():
            badge_sync.fetch_badge_html(requests.Session(), self.sync_user1, 's')


class RefreshAllTest(AbstractSyncTest):
    @httpmock.activate
    def test_happy(self):
        from pillar import badge_sync

        httpmock.add('GET', 'http://id.local:8001/api/badges/1947/html/s',
                     body='badges for Agent 47')
        httpmock.add('GET', 'http://id.local:8001/api/badges/4488/html/s',
                     body='badges for that other user')

        with self.app.app_context():
            badge_sync.refresh_all_badges(timelimit=datetime.timedelta(seconds=4))

        db_user1 = self.get('/api/users/me', auth_token=self.sync_user1.token).json
        db_user2 = self.get('/api/users/me', auth_token=self.sync_user2.token).json
        self.assertEqual('badges for Agent 47', db_user1['badges']['html'])
        self.assertEqual('badges for that other user', db_user2['badges']['html'])

    @httpmock.activate
    def test_timelimit(self):
        from pillar import badge_sync

        # This shouldn't hit any connection error, because it should immediately
        # hit the time limit, before doing any call to Blender ID.
        with self.app.app_context():
            badge_sync.refresh_all_badges(timelimit=datetime.timedelta(seconds=-4))

    @httpmock.activate
    def test_remove_badges(self):
        from pillar import badge_sync
        from pillar.api.utils import utcnow

        # Make sure the user has a badge before getting the 204 No Content from Blender ID.
        self._update_badge_expiry(-10, 10)

        httpmock.add('GET', 'http://id.local:8001/api/badges/1947/html/s',
                     body='', status=204)

        with self.app.app_context():
            badge_sync.refresh_all_badges(
                only_user_id=self.uid1,
                timelimit=datetime.timedelta(seconds=4))
            expected_expire = utcnow() + self.app.config['BLENDER_ID_BADGE_EXPIRY']

            # Get directly from MongoDB because JSON doesn't support datetimes.
            users_coll = self.app.db('users')
            db_user1 = users_coll.find_one(self.uid1)

        self.assertEqual('', db_user1['badges']['html'])
        margin = datetime.timedelta(minutes=1)
        self.assertLess(expected_expire - margin, db_user1['badges']['expires'])
        self.assertGreater(expected_expire + margin, db_user1['badges']['expires'])


class RefreshSingleTest(AbstractSyncTest):
    @httpmock.activate
    def test_happy(self):
        from pillar import badge_sync

        httpmock.add('GET', 'http://id.local:8001/api/badges/1947/html/s',
                     body='badges for Agent 47')

        db_user1 = self.get('/api/users/me', auth_token=self.sync_user1.token).json
        self.assertNotIn('badges', db_user1)

        with self.app.app_context():
            badge_sync.refresh_single_user(self.uid1)

        db_user1 = self.get('/api/users/me', auth_token=self.sync_user1.token).json
        self.assertEqual('badges for Agent 47', db_user1['badges']['html'])
