# Copyright 2016-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Test the collation module."""

import functools
import warnings

from pymongo.collation import (
    Collation,
    CollationCaseFirst, CollationStrength, CollationAlternate,
    CollationMaxVariable)
from pymongo.errors import ConfigurationError
from pymongo.operations import (DeleteMany, DeleteOne, IndexModel, ReplaceOne,
                                UpdateMany, UpdateOne)
from pymongo.write_concern import WriteConcern
from test import unittest, client_context
from test.utils import EventListener, ignore_deprecations, rs_or_single_client


class TestCollationObject(unittest.TestCase):

    def test_constructor(self):
        self.assertRaises(TypeError, Collation, locale=42)
        # Fill in a locale to test the other options.
        _Collation = functools.partial(Collation, 'en_US')
        # No error.
        _Collation(caseFirst=CollationCaseFirst.UPPER)
        self.assertRaises(TypeError, _Collation, caseLevel='true')
        self.assertRaises(ValueError, _Collation, strength='six')
        self.assertRaises(TypeError, _Collation,
                          numericOrdering='true')
        self.assertRaises(TypeError, _Collation, alternate=5)
        self.assertRaises(TypeError, _Collation, maxVariable=2)
        self.assertRaises(TypeError, _Collation, normalization='false')
        self.assertRaises(TypeError, _Collation, backwards='true')

        # No errors.
        Collation('en_US', future_option='bar', another_option=42)
        collation = Collation(
            'en_US',
            caseLevel=True,
            caseFirst=CollationCaseFirst.UPPER,
            strength=CollationStrength.QUATERNARY,
            numericOrdering=True,
            alternate=CollationAlternate.SHIFTED,
            maxVariable=CollationMaxVariable.SPACE,
            normalization=True,
            backwards=True)

        self.assertEqual({
            'locale': 'en_US',
            'caseLevel': True,
            'caseFirst': 'upper',
            'strength': 4,
            'numericOrdering': True,
            'alternate': 'shifted',
            'maxVariable': 'space',
            'normalization': True,
            'backwards': True
        }, collation.document)

        self.assertEqual({
            'locale': 'en_US',
            'backwards': True
        }, Collation('en_US', backwards=True).document)


def raisesConfigurationErrorForOldMongoDB(func):
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        if client_context.version.at_least(3, 3, 9):
            return func(self, *args, **kwargs)
        else:
            with self.assertRaises(ConfigurationError):
                return func(self, *args, **kwargs)
    return wrapper


class TestCollation(unittest.TestCase):

    @classmethod
    @client_context.require_connection
    def setUpClass(cls):
        cls.listener = EventListener()
        cls.client = rs_or_single_client(event_listeners=[cls.listener])
        cls.db = cls.client.pymongo_test
        cls.collation = Collation('en_US')
        cls.warn_context = warnings.catch_warnings()
        cls.warn_context.__enter__()
        warnings.simplefilter("ignore", DeprecationWarning)

    @classmethod
    def tearDownClass(cls):
        cls.warn_context.__exit__()
        cls.warn_context = None
        cls.client.close()

    def tearDown(self):
        self.listener.results.clear()

    def last_command_started(self):
        return self.listener.results['started'][-1].command

    def assertCollationInLastCommand(self):
        self.assertEqual(
            self.collation.document,
            self.last_command_started()['collation'])

    @raisesConfigurationErrorForOldMongoDB
    def test_create_collection(self):
        self.db.test.drop()
        self.db.create_collection('test', collation=self.collation)
        self.assertCollationInLastCommand()

        # Test passing collation as a dict as well.
        self.db.test.drop()
        self.listener.results.clear()
        self.db.create_collection('test', collation=self.collation.document)
        self.assertCollationInLastCommand()

    def test_index_model(self):
        model = IndexModel([('a', 1), ('b', -1)], collation=self.collation)
        self.assertEqual(self.collation.document, model.document['collation'])

    @raisesConfigurationErrorForOldMongoDB
    def test_create_index(self):
        self.db.test.create_index('foo', collation=self.collation)
        ci_cmd = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            ci_cmd['indexes'][0]['collation'])

    @raisesConfigurationErrorForOldMongoDB
    def test_ensure_index(self):
        self.db.test.ensure_index('foo', collation=self.collation)
        ci_cmd = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            ci_cmd['indexes'][0]['collation'])

    @raisesConfigurationErrorForOldMongoDB
    def test_aggregate(self):
        self.db.test.aggregate([{'$group': {'_id': 42}}],
                               collation=self.collation)
        self.assertCollationInLastCommand()

    @raisesConfigurationErrorForOldMongoDB
    @ignore_deprecations
    def test_count(self):
        self.db.test.count(collation=self.collation)
        self.assertCollationInLastCommand()

        self.listener.results.clear()
        self.db.test.find(collation=self.collation).count()
        self.assertCollationInLastCommand()

    @raisesConfigurationErrorForOldMongoDB
    def test_count_documents(self):
        self.db.test.count_documents({}, collation=self.collation)
        self.assertCollationInLastCommand()

    @raisesConfigurationErrorForOldMongoDB
    def test_distinct(self):
        self.db.test.distinct('foo', collation=self.collation)
        self.assertCollationInLastCommand()

        self.listener.results.clear()
        self.db.test.find(collation=self.collation).distinct('foo')
        self.assertCollationInLastCommand()

    @raisesConfigurationErrorForOldMongoDB
    def test_find_command(self):
        self.db.test.insert_one({'is this thing on?': True})
        self.listener.results.clear()
        next(self.db.test.find(collation=self.collation))
        self.assertCollationInLastCommand()

    @raisesConfigurationErrorForOldMongoDB
    def test_explain_command(self):
        self.listener.results.clear()
        self.db.test.find(collation=self.collation).explain()
        # The collation should be part of the explained command.
        self.assertEqual(
            self.collation.document,
            self.last_command_started()['explain']['collation'])

    @raisesConfigurationErrorForOldMongoDB
    @client_context.require_version_max(4, 1, 0, -1)
    def test_group(self):
        self.db.test.group('foo', {'foo': {'$gt': 42}}, {},
                           'function(a, b) { return a; }',
                           collation=self.collation)
        self.assertCollationInLastCommand()

    @raisesConfigurationErrorForOldMongoDB
    def test_map_reduce(self):
        self.db.test.map_reduce('function() {}', 'function() {}', 'output',
                                collation=self.collation)
        self.assertCollationInLastCommand()

    @raisesConfigurationErrorForOldMongoDB
    def test_delete(self):
        self.db.test.delete_one({'foo': 42}, collation=self.collation)
        command = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            command['deletes'][0]['collation'])

        self.listener.results.clear()
        self.db.test.delete_many({'foo': 42}, collation=self.collation)
        command = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            command['deletes'][0]['collation'])

        self.listener.results.clear()
        self.db.test.remove({'foo': 42}, collation=self.collation)
        command = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            command['deletes'][0]['collation'])

    @raisesConfigurationErrorForOldMongoDB
    def test_update(self):
        self.db.test.update({'foo': 42}, {'$set': {'foo': 'bar'}},
                            collation=self.collation)
        command = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            command['updates'][0]['collation'])

        self.listener.results.clear()
        self.db.test.save({'_id': 12345}, collation=self.collation)
        command = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            command['updates'][0]['collation'])

        self.listener.results.clear()
        self.db.test.replace_one({'foo': 42}, {'foo': 43},
                                 collation=self.collation)
        command = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            command['updates'][0]['collation'])

        self.listener.results.clear()
        self.db.test.update_one({'foo': 42}, {'$set': {'foo': 43}},
                                collation=self.collation)
        command = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            command['updates'][0]['collation'])

        self.listener.results.clear()
        self.db.test.update_many({'foo': 42}, {'$set': {'foo': 43}},
                                 collation=self.collation)
        command = self.listener.results['started'][0].command
        self.assertEqual(
            self.collation.document,
            command['updates'][0]['collation'])

    @raisesConfigurationErrorForOldMongoDB
    def test_find_and(self):
        self.db.test.find_and_modify({'foo': 42}, {'$set': {'foo': 43}},
                                     collation=self.collation)
        self.assertCollationInLastCommand()

        self.listener.results.clear()
        self.db.test.find_one_and_delete({'foo': 42}, collation=self.collation)
        self.assertCollationInLastCommand()

        self.listener.results.clear()
        self.db.test.find_one_and_update({'foo': 42}, {'$set': {'foo': 43}},
                                         collation=self.collation)
        self.assertCollationInLastCommand()

        self.listener.results.clear()
        self.db.test.find_one_and_replace({'foo': 42}, {'foo': 43},
                                          collation=self.collation)
        self.assertCollationInLastCommand()

    @raisesConfigurationErrorForOldMongoDB
    def test_bulk_write(self):
        self.db.test.collection.bulk_write([
            DeleteOne({'noCollation': 42}),
            DeleteMany({'noCollation': 42}),
            DeleteOne({'foo': 42}, collation=self.collation),
            DeleteMany({'foo': 42}, collation=self.collation),
            ReplaceOne({'noCollation': 24}, {'bar': 42}),
            UpdateOne({'noCollation': 84}, {'$set': {'bar': 10}}, upsert=True),
            UpdateMany({'noCollation': 45}, {'$set': {'bar': 42}}),
            ReplaceOne({'foo': 24}, {'foo': 42}, collation=self.collation),
            UpdateOne({'foo': 84}, {'$set': {'foo': 10}}, upsert=True,
                      collation=self.collation),
            UpdateMany({'foo': 45}, {'$set': {'foo': 42}},
                       collation=self.collation)
        ])

        delete_cmd = self.listener.results['started'][0].command
        update_cmd = self.listener.results['started'][1].command

        def check_ops(ops):
            for op in ops:
                if 'noCollation' in op['q']:
                    self.assertNotIn('collation', op)
                else:
                    self.assertEqual(self.collation.document,
                                     op['collation'])

        check_ops(delete_cmd['deletes'])
        check_ops(update_cmd['updates'])

    @raisesConfigurationErrorForOldMongoDB
    def test_bulk(self):
        bulk = self.db.test.initialize_ordered_bulk_op()
        bulk.find({'noCollation': 42}).remove_one()
        bulk.find({'noCollation': 42}).remove()
        bulk.find({'foo': 42}, collation=self.collation).remove_one()
        bulk.find({'foo': 42}, collation=self.collation).remove()
        bulk.find({'noCollation': 24}).replace_one({'bar': 42})
        bulk.find({'noCollation': 84}).upsert().update_one(
            {'$set': {'foo': 10}})
        bulk.find({'noCollation': 45}).update({'$set': {'bar': 42}})
        bulk.find({'foo': 24}, collation=self.collation).replace_one(
            {'foo': 42})
        bulk.find({'foo': 84}, collation=self.collation).upsert().update_one(
            {'$set': {'foo': 10}})
        bulk.find({'foo': 45}, collation=self.collation).update({
            '$set': {'foo': 42}})
        bulk.execute()

        delete_cmd = self.listener.results['started'][0].command
        update_cmd = self.listener.results['started'][1].command

        def check_ops(ops):
            for op in ops:
                if 'noCollation' in op['q']:
                    self.assertNotIn('collation', op)
                else:
                    self.assertEqual(self.collation.document,
                                     op['collation'])

        check_ops(delete_cmd['deletes'])
        check_ops(update_cmd['updates'])

    @client_context.require_version_max(3, 3, 8)
    def test_mixed_bulk_collation(self):
        bulk = self.db.test.initialize_unordered_bulk_op()
        bulk.find({'foo': 42}).upsert().update_one(
            {'$set': {'bar': 10}})
        bulk.find({'foo': 43}, collation=self.collation).remove_one()
        with self.assertRaises(ConfigurationError):
            bulk.execute()
        self.assertIsNone(self.db.test.find_one({'foo': 42}))

    @raisesConfigurationErrorForOldMongoDB
    def test_indexes_same_keys_different_collations(self):
        self.db.test.drop()
        usa_collation = Collation('en_US')
        ja_collation = Collation('ja')
        self.db.test.create_indexes([
            IndexModel('fieldname', collation=usa_collation),
            IndexModel('fieldname', name='japanese_version',
                       collation=ja_collation),
            IndexModel('fieldname', name='simple')
        ])
        indexes = self.db.test.index_information()
        self.assertEqual(usa_collation.document['locale'],
                         indexes['fieldname_1']['collation']['locale'])
        self.assertEqual(ja_collation.document['locale'],
                         indexes['japanese_version']['collation']['locale'])
        self.assertNotIn('collation', indexes['simple'])
        self.db.test.drop_index('fieldname_1')
        indexes = self.db.test.index_information()
        self.assertIn('japanese_version', indexes)
        self.assertIn('simple', indexes)
        self.assertNotIn('fieldname', indexes)

    def test_unacknowledged_write(self):
        unacknowledged = WriteConcern(w=0)
        collection = self.db.get_collection(
            'test', write_concern=unacknowledged)
        with self.assertRaises(ConfigurationError):
            collection.update_one(
                {'hello': 'world'}, {'$set': {'hello': 'moon'}},
                collation=self.collation)
        bulk = collection.initialize_ordered_bulk_op()
        bulk.find({'hello': 'world'}, collation=self.collation).update_one(
            {'$set': {'hello': 'moon'}})
        with self.assertRaises(ConfigurationError):
            bulk.execute()
        update_one = UpdateOne({'hello': 'world'}, {'$set': {'hello': 'moon'}},
                               collation=self.collation)
        with self.assertRaises(ConfigurationError):
            collection.bulk_write([update_one])

    @raisesConfigurationErrorForOldMongoDB
    def test_cursor_collation(self):
        self.db.test.insert_one({'hello': 'world'})
        next(self.db.test.find().collation(self.collation))
        self.assertCollationInLastCommand()
