class MongoDBInstanceTestCase(TestCase):
    """
    Test cases for MongoDBInstanceMixin and OpenEdXDatabaseMixin
    """

    def setUp(self):
        super().setUp()
        self.instance = None

    def tearDown(self):
        if self.instance:
            with patch(
                    'instance.tests.models.factories.openedx_instance.OpenEdXInstance._write_metadata_to_consul',
                    return_value=(1, True)
            ):
                self.instance.deprovision_mongo()
        super().tearDown()

    def check_mongo(self):
        """
        Check that the instance mongo user has access to the external mongo database
        """
        mongo = pymongo.MongoClient(settings.DEFAULT_INSTANCE_MONGO_URL)
        for database in self.instance.mongo_database_names:
            self.assertTrue(mongo[database].authenticate(self.instance.mongo_user, self.instance.mongo_pass))

    def check_mongo_vars_not_set(self, appserver):
        """
        Check that the given OpenEdXAppServer does not point to a mongo database
        """
        for var in ('EDXAPP_MONGO_USER',
                    'EDXAPP_MONGO_PASSWORD'
                    'EDXAPP_MONGO_HOSTS',
                    'EDXAPP_MONGO_PORT',
                    'EDXAPP_MONGO_DB_NAME',
                    'FORUM_MONGO_USER',
                    'FORUM_MONGO_PASSWORD',
                    'FORUM_MONGO_HOSTS',
                    'FORUM_MONGO_PORT',
                    'FORUM_MONGO_DATABASE'):
            self.assertNotIn(var, appserver.configuration_settings)

    def check_mongo_vars_set(self, appserver, expected_hosts, expected_replica_set=None):
        """
        Check that the given OpenEdXAppServer is using the expected mongo settings.
        """
        ansible_vars = appserver.configuration_settings
        ansible_vars_data = yaml.safe_load(ansible_vars)
        self.assertIn('EDXAPP_MONGO_USER: {0}'.format(self.instance.mongo_user), ansible_vars)
        self.assertIn('EDXAPP_MONGO_PASSWORD: {0}'.format(self.instance.mongo_pass), ansible_vars)
        self.assertIn('EDXAPP_MONGO_PORT: {0}'.format(MONGODB_SERVER_DEFAULT_PORT), ansible_vars)
        self.assertIn('EDXAPP_MONGO_DB_NAME: {0}'.format(self.instance.mongo_database_name), ansible_vars)
        if isinstance(ansible_vars_data['EDXAPP_MONGO_HOSTS'], list):
            rendered_edxapp_mongo_hosts = ansible_vars_data['EDXAPP_MONGO_HOSTS']
        else:
            rendered_edxapp_mongo_hosts = ansible_vars_data['EDXAPP_MONGO_HOSTS'].split(',')
        self.assertSetEqual(set(expected_hosts), set(rendered_edxapp_mongo_hosts))
        if expected_replica_set:
            self.assertIn('EDXAPP_MONGO_REPLICA_SET: {0}'.format(expected_replica_set), ansible_vars)
            self.assertIn('FORUM_MONGO_REPLICA_SET: {0}'.format(expected_replica_set), ansible_vars)
            self.assertIn('FORUM_MONGO_URL', ansible_vars_data)
            self.assertSetEqual(set(ansible_vars_data['FORUM_MONGO_HOSTS']), set(expected_hosts))
        else:
            self.assertNotIn('EDXAPP_MONGO_REPLICA_SET', ansible_vars)
            self.assertNotIn('FORUM_MONGO_REPLICA_SET', ansible_vars)
            self.assertEqual(ansible_vars_data['FORUM_MONGO_HOSTS'], expected_hosts)

        self.assertIn('FORUM_MONGO_USER: {0}'.format(self.instance.mongo_user), ansible_vars)
        self.assertIn('FORUM_MONGO_PASSWORD: {0}'.format(self.instance.mongo_pass), ansible_vars)
        self.assertIn('FORUM_MONGO_PORT: {0}'.format(MONGODB_SERVER_DEFAULT_PORT), ansible_vars)
        self.assertIn('FORUM_MONGO_DATABASE: {0}'.format(self.instance.forum_database_name), ansible_vars)

    def test_provision_mongo(self, mock_consul):
        """
        Provision mongo databases
        """
        self.instance = OpenEdXInstanceFactory()
        self.instance.provision_mongo()
        self.check_mongo()

    def test_provision_mongo_again(self, mock_consul):
        """
        Only create the databases once
        """
        self.instance = OpenEdXInstanceFactory()
        self.instance.provision_mongo()
        self.assertIs(self.instance.mongo_provisioned, True)

        mongo_user = self.instance.mongo_user
        mongo_pass = self.instance.mongo_pass
        self.instance.provision_mongo()
        self.assertEqual(self.instance.mongo_user, mongo_user)
        self.assertEqual(self.instance.mongo_pass, mongo_pass)
        self.check_mongo()

    def test_provision_mongo_no_mongodb_server(self, mock_consul):
        """
        Don't provision a mongo database if instance has no MongoDB server
        """
        mongo = pymongo.MongoClient(settings.DEFAULT_INSTANCE_MONGO_URL)
        self.instance = OpenEdXInstanceFactory()
        self.instance.mongodb_server = None
        self.instance.save()
        self.instance.provision_mongo()
        databases = mongo.database_names()
        for database in self.instance.mongo_database_names:
            self.assertNotIn(database, databases)

    @override_settings(DEFAULT_INSTANCE_MONGO_URL='mongodb://*****:*****@mongo.opencraft.com')
    def test_ansible_settings_mongo(self, mock_consul):
        """
        Add mongo ansible vars if instance has a MongoDB server
        """
        # Delete MongoDBServer object created during the migrations to allow the settings override
        # to take effect.
        MongoDBServer.objects.all().delete()
        self.instance = OpenEdXInstanceFactory()
        appserver = make_test_appserver(self.instance)
        self.check_mongo_vars_set(appserver, expected_hosts=['mongo.opencraft.com'])

    @override_settings(
        DEFAULT_INSTANCE_MONGO_URL=None,
        DEFAULT_MONGO_REPLICA_SET_NAME="test_name",
        DEFAULT_MONGO_REPLICA_SET_USER="******",
        DEFAULT_MONGO_REPLICA_SET_PASSWORD="******",
        DEFAULT_MONGO_REPLICA_SET_PRIMARY="test.opencraft.hosting",
        DEFAULT_MONGO_REPLICA_SET_HOSTS="test.opencraft.hosting,test1.opencraft.hosting,test2.opencraft.hosting"
    )
    @ddt.data(
        ('open-release/ficus', 'open-release/ficus'),
        ('open-release/ficus', 'opencraft-release/ficus'),
        ('open-release/ginkgo', 'open-release/ginkgo'),
    )
    @ddt.unpack
    def test_ansible_settings_no_replica_set(self, openedx_release, configuration_version, mock_consul):
        """
        Prior to Hawthorn, edx configuration does not support MongoDB replica sets,
        and the mongo hosts must be a single host, provided as a list of strings.
        """
        # Delete MongoDBServer object created during the migrations to allow the settings override
        # to take effect.
        MongoDBServer.objects.all().delete()
        self.instance = OpenEdXInstanceFactory(openedx_release=openedx_release,
                                               configuration_version=configuration_version)
        appserver = make_test_appserver(self.instance)
        self.check_mongo_vars_set(appserver, expected_hosts=["test.opencraft.hosting"])

    @override_settings(
        DEFAULT_INSTANCE_MONGO_URL=None,
        DEFAULT_MONGO_REPLICA_SET_NAME="test_name",
        DEFAULT_MONGO_REPLICA_SET_USER="******",
        DEFAULT_MONGO_REPLICA_SET_PASSWORD="******",
        DEFAULT_MONGO_REPLICA_SET_PRIMARY="test.opencraft.hosting",
        DEFAULT_MONGO_REPLICA_SET_HOSTS="test.opencraft.hosting,test1.opencraft.hosting,test2.opencraft.hosting"
    )
    @ddt.data(
        ('open-release/ginkgo', 'opencraft-release/ginkgo'),
        (settings.OPENEDX_RELEASE_STABLE_REF, settings.STABLE_CONFIGURATION_VERSION),
        (settings.DEFAULT_OPENEDX_RELEASE, settings.DEFAULT_CONFIGURATION_VERSION),
    )
    @ddt.unpack
    def test_ansible_settings_use_replica_set(self, openedx_release, configuration_version, mock_consul):
        """
        Add mongo ansible vars if instance has a MongoDB replica set
        Also, the mongo hosts are provied as a comma-separated string.
        """
        # Delete MongoDBServer object created during the migrations to allow the settings override
        # to take effect.
        MongoDBServer.objects.all().delete()
        self.instance = OpenEdXInstanceFactory(openedx_release=openedx_release,
                                               configuration_version=configuration_version)
        appserver = make_test_appserver(self.instance)
        self.check_mongo_vars_set(appserver,
                                  expected_hosts=['test.opencraft.hosting',
                                                  'test1.opencraft.hosting',
                                                  'test2.opencraft.hosting'],
                                  expected_replica_set='test_name')

    def test_ansible_settings_no_mongo_server(self, mock_consul):
        """
        Don't add mongo ansible vars if instance has no MongoDB server
        """
        self.instance = OpenEdXInstanceFactory()
        self.instance.mongodb_server = None
        self.instance.save()
        appserver = make_test_appserver(self.instance)
        self.check_mongo_vars_not_set(appserver)

    @override_settings(
        DEFAULT_INSTANCE_MONGO_URL=None,
        DEFAULT_MONGO_REPLICA_SET_NAME="test_name",
        DEFAULT_MONGO_REPLICA_SET_USER="******",
        DEFAULT_MONGO_REPLICA_SET_PASSWORD="******",
        DEFAULT_MONGO_REPLICA_SET_PRIMARY="test.opencraft.hosting",
        DEFAULT_MONGO_REPLICA_SET_HOSTS="test.opencraft.hosting,test1.opencraft.hosting,test2.opencraft.hosting"
    )
    def test__get_main_database_url(self, mock_consul):
        """
        Main database url should be extracted from primary replica set MongoDBServer
        """
        self.instance = OpenEdXInstanceFactory()
        self.assertEqual(
            self.instance._get_main_database_url(),
            "mongodb://*****:*****@test.opencraft.hosting"
        )

    @patch('instance.models.mixins.database.MongoDBInstanceMixin._get_main_database_url')
    @patch('instance.models.mixins.database.pymongo.MongoClient', autospec=True)
    def test_deprovision_mongo(self, mock_mongo_client_cls, mock_get_main_db_url, mock_consul):
        """
        Test deprovision_mongo calls drop_database.
        """
        self.instance = OpenEdXInstanceFactory()
        self.instance.mongo_provisioned = True
        self.instance.deprovision_mongo()
        for database in self.instance.mongo_database_names:
            mock_mongo_client_cls().drop_database.assert_any_call(database)

    @patch('instance.models.mixins.database.pymongo.MongoClient', autospec=True)
    @patch('instance.models.mixins.database.MongoDBInstanceMixin._get_main_database_url')
    @patch('instance.models.mixins.database.pymongo.MongoClient.drop_database', side_effect=PyMongoError())
    def test_ignore_errors_deprovision_mongo(self, mock_mongo_client_cls, *mock_methods):
        """
        Test mongo is set as deprovision when ignoring errors.
        """
        self.instance = OpenEdXInstanceFactory()
        self.instance.mongo_provisioned = True
        self.instance.deprovision_mongo(ignore_errors=True)
        self.assertFalse(self.instance.mongo_provisioned)