# Copyright 2015 Tesora 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.

import logging
from unittest import mock

import django
from django.conf import settings
from django.urls import reverse

from trove_dashboard import api
from trove_dashboard.content.database_configurations \
    import config_param_manager
from trove_dashboard.test import helpers as test


INDEX_URL = reverse('horizon:project:database_configurations:index')
CREATE_URL = reverse('horizon:project:database_configurations:create')
DETAIL_URL = 'horizon:project:database_configurations:detail'
ADD_URL = 'horizon:project:database_configurations:add'


class DatabaseConfigurationsTests(test.TestCase):
    @test.create_mocks({api.trove: ('configuration_list',)})
    def test_index(self):
        self.mock_configuration_list.return_value = (
            self.database_configurations.list())
        res = self.client.get(INDEX_URL)
        self.mock_configuration_list.assert_called_once_with(
            test.IsHttpRequest())
        self.assertTemplateUsed(res,
                                'project/database_configurations/index.html')

    @test.create_mocks({api.trove: ('configuration_list',)})
    def test_index_exception(self):
        self.mock_configuration_list.side_effect = self.exceptions.trove
        res = self.client.get(INDEX_URL)
        self.mock_configuration_list.assert_called_once_with(
            test.IsHttpRequest())
        self.assertTemplateUsed(
            res, 'project/database_configurations/index.html')
        self.assertEqual(res.status_code, 200)
        self.assertMessageCount(res, error=1)

    @test.create_mocks({
        api.trove: ('datastore_list', 'datastore_version_list')})
    def test_create_configuration(self):
        self.mock_datastore_list.return_value = self.datastores.list()
        self.mock_datastore_version_list.return_value = (
            self.datastore_versions.list())
        res = self.client.get(CREATE_URL)
        self.mock_datastore_list.assert_called_once_with(test.IsHttpRequest())
        self.assert_mock_multiple_calls_with_same_arguments(
            self.mock_datastore_version_list, 4,
            mock.call(test.IsHttpRequest(), test.IsA(str)))
        self.assertTemplateUsed(res,
                                'project/database_configurations/create.html')

    @test.create_mocks({api.trove: ('datastore_list',)})
    def test_create_configuration_exception_on_datastore(self):
        self.mock_datastore_list.side_effect = self.exceptions.trove
        toSuppress = ["trove_dashboard.content."
                      "database_configurations.forms", ]

        # Suppress expected log messages in the test output
        loggers = []
        for cls in toSuppress:
            logger = logging.getLogger(cls)
            loggers.append((logger, logger.getEffectiveLevel()))
            logger.setLevel(logging.CRITICAL)

        try:
            res = self.client.get(CREATE_URL)
            self.mock_datastore_list.assert_called_once_with(
                test.IsHttpRequest())
            self.assertEqual(res.status_code, 302)

        finally:
            # Restore the previous log levels
            for (log, level) in loggers:
                log.setLevel(level)

    @test.create_mocks({
        api.trove: ('datastore_list', 'datastore_version_list',
                    'configuration_create')})
    def _test_create_test_configuration(
            self, config_description=u''):
        self.mock_datastore_list.return_value = self.datastores.list()
        self.mock_datastore_version_list.return_value = (
            self.datastore_versions.list())

        self.mock_configuration_create.return_value = (
            self.database_configurations.first())

        name = u'config1'
        values = "{}"
        ds = self._get_test_datastore('mysql')
        dsv = self._get_test_datastore_version(ds.id, '5.5')
        config_datastore = ds.name
        config_datastore_version = dsv.name

        post = {
            'method': 'CreateConfigurationForm',
            'name': name,
            'description': config_description,
            'datastore': (config_datastore + ',' + config_datastore_version)}

        res = self.client.post(CREATE_URL, post)
        self.mock_datastore_list.assert_called_once_with(test.IsHttpRequest())
        self.assert_mock_multiple_calls_with_same_arguments(
            self.mock_datastore_version_list, 4,
            mock.call(test.IsHttpRequest(), test.IsA(str)))
        self.mock_configuration_create.assert_called_once_with(
            test.IsHttpRequest(),
            name,
            values,
            description=config_description,
            datastore=config_datastore,
            datastore_version=config_datastore_version)
        self.assertNoFormErrors(res)
        self.assertMessageCount(success=1)

    def test_create_test_configuration(self):
        self._test_create_test_configuration(u'description of config1')

    def test_create_test_configuration_with_no_description(self):
        self._test_create_test_configuration()

    @test.create_mocks({
        api.trove: ('datastore_list', 'datastore_version_list',
                    'configuration_create')})
    def test_create_test_configuration_exception(self):
        self.mock_datastore_list.return_value = self.datastores.list()
        self.mock_datastore_version_list.return_value = (
            self.datastore_versions.list())

        self.mock_configuration_create.side_effect = self.exceptions.trove

        name = u'config1'
        values = "{}"
        config_description = u'description of config1'
        ds = self._get_test_datastore('mysql')
        dsv = self._get_test_datastore_version(ds.id, '5.5')
        config_datastore = ds.name
        config_datastore_version = dsv.name

        post = {'method': 'CreateConfigurationForm',
                'name': name,
                'description': config_description,
                'datastore': config_datastore + ',' + config_datastore_version}

        res = self.client.post(CREATE_URL, post)
        self.mock_datastore_list.assert_called_once_with(test.IsHttpRequest())
        self.assert_mock_multiple_calls_with_same_arguments(
            self.mock_datastore_version_list, 4,
            mock.call(test.IsHttpRequest(), test.IsA(str)))
        self.mock_configuration_create.assert_called_once_with(
            test.IsHttpRequest(),
            name,
            values,
            description=config_description,
            datastore=config_datastore,
            datastore_version=config_datastore_version)
        self.assertRedirectsNoFollow(res, INDEX_URL)

    @test.create_mocks({api.trove: ('configuration_get',
                                    'configuration_instances',)})
    def test_details_tab(self):
        config = self.database_configurations.first()
        self.mock_configuration_get.return_value = config
        details_url = self._get_url_with_arg(DETAIL_URL, config.id)
        url = details_url + '?tab=configuration_details__details'
        res = self.client.get(url)
        self.mock_configuration_get.assert_called_once_with(
            test.IsHttpRequest(), config.id)
        self.assertTemplateUsed(res,
                                'project/database_configurations/details.html')

    @test.create_mocks({api.trove: ('configuration_get',)})
    def test_overview_tab_exception(self):
        config = self.database_configurations.first()
        self.mock_configuration_get.side_effect = self.exceptions.trove
        details_url = self._get_url_with_arg(DETAIL_URL, config.id)
        url = details_url + '?tab=configuration_details__overview'
        res = self.client.get(url)
        self.mock_configuration_get.assert_called_once_with(
            test.IsHttpRequest(), config.id)
        self.assertRedirectsNoFollow(res, INDEX_URL)

    @test.create_mocks({
        api.trove: ('configuration_parameters_list',),
        config_param_manager.ConfigParamManager:
            ('get_configuration', 'configuration_get',)})
    def test_add_parameter(self):
        config = self.database_configurations.first()
        self.mock_get_configuration.return_value = config

        self.mock_configuration_get.return_value = config
        ds = self._get_test_datastore('mysql')
        dsv = self._get_test_datastore_version(ds.id, '5.5')
        self.mock_configuration_parameters_list.return_value = (
            self.configuration_parameters.list())
        res = self.client.get(self._get_url_with_arg(ADD_URL, 'id'))
        self.mock_get_configuration.assert_called_once()
        self.mock_configuration_get.assert_called_once_with(
            test.IsHttpRequest())
        self.mock_configuration_parameters_list.assert_called_once_with(
            test.IsHttpRequest(),
            ds.name,
            dsv.name)
        self.assertTemplateUsed(
            res, 'project/database_configurations/add_parameter.html')

    @test.create_mocks({
        api.trove: ('configuration_parameters_list',),
        config_param_manager.ConfigParamManager:
            ('get_configuration', 'configuration_get',)})
    def test_add_parameter_exception_on_parameters(self):
        try:
            config = self.database_configurations.first()
            self.mock_get_configuration.return_value = config

            self.mock_configuration_get.return_value = config

            ds = self._get_test_datastore('mysql')
            dsv = self._get_test_datastore_version(ds.id, '5.5')
            self.mock_configuration_parameters_list.side_effect = (
                self.exceptions.trove)
            toSuppress = ["trove_dashboard.content."
                          "database_configurations.forms", ]

            # Suppress expected log messages in the test output
            loggers = []
            for cls in toSuppress:
                logger = logging.getLogger(cls)
                loggers.append((logger, logger.getEffectiveLevel()))
                logger.setLevel(logging.CRITICAL)

            try:
                res = self.client.get(
                    self._get_url_with_arg(ADD_URL, config.id))
                self.mock_get_configuration.assert_called_once()
                self.mock_configuration_get.assert_called_once_with(
                    test.IsHttpRequest())
                (self.mock_configuration_parameters_list
                     .assert_called_once_with(
                         test.IsHttpRequest(),
                         ds.name,
                         dsv.name))
                self.assertEqual(res.status_code, 302)

            finally:
                # Restore the previous log levels
                for (log, level) in loggers:
                    log.setLevel(level)
        finally:
            config_param_manager.delete(config.id)

    @test.create_mocks({
        api.trove: ('configuration_parameters_list',),
        config_param_manager.ConfigParamManager:
            ('get_configuration', 'add_param', 'configuration_get',)})
    def test_add_new_parameter(self):
        config = self.database_configurations.first()
        self.mock_get_configuration.return_value = config
        try:
            self.mock_configuration_get.return_value = config

            ds = self._get_test_datastore('mysql')
            dsv = self._get_test_datastore_version(ds.id, '5.5')
            self.mock_configuration_parameters_list.return_value = (
                self.configuration_parameters.list())

            name = self.configuration_parameters.first().name
            value = 1

            self.mock_add_param.return_value = value

            post = {
                'method': 'AddParameterForm',
                'name': name,
                'value': value}

            res = self.client.post(self._get_url_with_arg(ADD_URL, config.id),
                                   post)
            self.mock_get_configuration.assert_called_once()
            self.mock_configuration_get.assert_called_once_with(
                test.IsHttpRequest())
            self.mock_configuration_parameters_list.assert_called_once_with(
                test.IsHttpRequest(),
                ds.name,
                dsv.name)
            self.mock_add_param.assert_called_once_with(name, value)
            self.assertNoFormErrors(res)
            self.assertMessageCount(success=1)
        finally:
            config_param_manager.delete(config.id)

    @test.create_mocks({
        api.trove: ('configuration_get', 'configuration_parameters_list',),
        config_param_manager: ('get',)})
    def test_add_parameter_invalid_value(self):
        try:
            config = self.database_configurations.first()

            # setup the configuration parameter manager
            config_param_mgr = config_param_manager.ConfigParamManager(
                config.id)
            config_param_mgr.configuration = config
            config_param_mgr.original_configuration_values = \
                dict.copy(config.values)

            self.mock_get.return_value = config_param_mgr
            self.mock_configuration_parameters_list.return_value = (
                self.configuration_parameters.list())

            name = self.configuration_parameters.first().name
            value = "non-numeric"

            post = {
                'method': 'AddParameterForm',
                'name': name,
                'value': value}

            res = self.client.post(self._get_url_with_arg(ADD_URL, config.id),
                                   post)
            self.assert_mock_multiple_calls_with_same_arguments(
                self.mock_get, 2,
                mock.call(test.IsHttpRequest(), test.IsA(str)))
            self.assert_mock_multiple_calls_with_same_arguments(
                self.mock_configuration_parameters_list, 2,
                mock.call(test.IsHttpRequest(), test.IsA(str), test.IsA(str)))
            self.assertFormError(res, "form", 'value',
                                 ['Value must be a number.'])
        finally:
            config_param_manager.delete(config.id)

    @test.create_mocks({api.trove: ('configuration_get',
                                    'configuration_instances',)})
    def test_values_tab_discard_action(self):
        config = self.database_configurations.first()

        self.mock_configuration_get.return_value = config

        details_url = self._get_url_with_arg(DETAIL_URL, config.id)
        url = details_url + '?tab=configuration_details__value'

        self._test_create_altered_config_params(config, url)

        # get the state of the configuration before discard action
        changed_configuration_values = \
            dict.copy(config_param_manager.get(self.request, config.id)
                      .get_configuration().values)

        res = self.client.post(url, {'action': u"values__discard_changes"})
        self.mock_configuration_get.assert_called_once_with(
            test.IsHttpRequest(), config.id)
        if django.VERSION >= (1, 9):
            url = settings.TESTSERVER + url
        self.assertRedirectsNoFollow(res, url)

        # get the state of the configuration after discard action
        restored_configuration_values = \
            dict.copy(config_param_manager.get(self.request, config.id)
                      .get_configuration().values)

        self.assertTrue(config_param_manager.dict_has_changes(
            changed_configuration_values, restored_configuration_values))

    @test.create_mocks({api.trove: ('configuration_instances',
                                    'configuration_update',),
                        config_param_manager: ('get',)})
    def test_values_tab_apply_action(self):
        # NOTE(zhaochao): we cannot use copy.deepcopy() under Python 3,
        # because of the lazy-loading feature of the troveclient Resource
        # objects. copy.deepcopy will use hasattr to search for the
        # '__setstate__' attribute of the resource object. As the resource
        # object is lazy loading, searching attributes relys on the 'is_load'
        # property, unfortunately this property is also not loaded at the
        # moment, then we're getting in an infinite loop there. Python will
        # raise RuntimeError saying "maximum recursion depth exceeded", this is
        # ignored under Python 2.x, but reraised under Python 3 by hasattr().
        #
        # Temporarily importing troveclient and reconstructing a configuration
        # object from the original config object's dict info will make this
        # case (and the next) working under Python 3.
        original_config = self.database_configurations.first()
        from troveclient.v1 import configurations
        config = configurations.Configuration(
            configurations.Configurations(None), original_config.to_dict())
        # Making sure the newly constructed config object is the same as
        # the original one.
        self.assertEqual(config, original_config)

        # setup the configuration parameter manager
        config_param_mgr = config_param_manager.ConfigParamManager(
            config.id)
        config_param_mgr.configuration = config
        config_param_mgr.original_configuration_values = \
            dict.copy(config.values)

        self.mock_get.return_value = config_param_mgr

        self.mock_configuration_update.return_value = None

        details_url = self._get_url_with_arg(DETAIL_URL, config.id)
        url = details_url + '?tab=configuration_details__value'

        self._test_create_altered_config_params(config, url)

        # apply changes
        res = self.client.post(url, {'action': u"values__apply_changes"})
        self.assert_mock_multiple_calls_with_same_arguments(
            self.mock_get, 11, mock.call(test.IsHttpRequest(), config.id))
        self.mock_configuration_update.assert_called_once_with(
            test.IsHttpRequest(),
            config.id,
            config_param_mgr.to_json())
        if django.VERSION >= (1, 9):
            url = settings.TESTSERVER + url
        self.assertRedirectsNoFollow(res, url)

    @test.create_mocks({api.trove: ('configuration_instances',
                                    'configuration_update',),
                        config_param_manager: ('get',)})
    def test_values_tab_apply_action_exception(self):
        # NOTE(zhaochao) Please refer to the comment at the beginning of the
        # 'test_values_tab_apply_action' about not using copy.deepcopy() for
        # details.
        original_config = self.database_configurations.first()
        from troveclient.v1 import configurations
        config = configurations.Configuration(
            configurations.Configurations(None), original_config.to_dict())
        # Making sure the newly constructed config object is the same as
        # the original one.
        self.assertEqual(config, original_config)

        # setup the configuration parameter manager
        config_param_mgr = config_param_manager.ConfigParamManager(
            config.id)
        config_param_mgr.configuration = config
        config_param_mgr.original_configuration_values = \
            dict.copy(config.values)

        self.mock_get.return_value = config_param_mgr

        self.mock_configuration_update.side_effect = self.exceptions.trove

        details_url = self._get_url_with_arg(DETAIL_URL, config.id)
        url = details_url + '?tab=configuration_details__value'

        self._test_create_altered_config_params(config, url)

        # apply changes
        res = self.client.post(url, {'action': u"values__apply_changes"})
        self.assert_mock_multiple_calls_with_same_arguments(
            self.mock_get, 11, mock.call(test.IsHttpRequest(), config.id))
        self.mock_configuration_update.assert_called_once_with(
            test.IsHttpRequest(),
            config.id,
            config_param_mgr.to_json())
        if django.VERSION >= (1, 9):
            url = settings.TESTSERVER + url
        self.assertRedirectsNoFollow(res, url)
        self.assertEqual(res.status_code, 302)

    def _test_create_altered_config_params(self, config, url):
        # determine the number of configuration group parameters in the list
        res = self.client.get(url)

        table_data = res.context['table'].data
        number_params = len(table_data)
        config_param = table_data[0]

        # delete the first parameter
        action_string = u"values__delete__%s" % config_param.name
        form_data = {'action': action_string}
        res = self.client.post(url, form_data)
        self.assertRedirectsNoFollow(res, url)

        # verify the test number of parameters is reduced by 1
        res = self.client.get(url)
        table_data = res.context['table'].data
        new_number_params = len(table_data)

        self.assertEqual((number_params - 1), new_number_params)

    @test.create_mocks({api.trove: ('configuration_instances',),
                        config_param_manager: ('get',)})
    def test_instances_tab(self):
        try:
            config = self.database_configurations.first()

            # setup the configuration parameter manager
            config_param_mgr = config_param_manager.ConfigParamManager(
                config.id)
            config_param_mgr.configuration = config
            config_param_mgr.original_configuration_values = \
                dict.copy(config.values)

            self.mock_get.return_value = config_param_mgr

            self.mock_configuration_instances.return_value = (
                self.configuration_instances.list())

            details_url = self._get_url_with_arg(DETAIL_URL, config.id)
            url = details_url + '?tab=configuration_details__instance'

            res = self.client.get(url)
            self.assert_mock_multiple_calls_with_same_arguments(
                self.mock_get, 2, mock.call(test.IsHttpRequest(), config.id))
            self.mock_configuration_instances.assert_called_once_with(
                test.IsHttpRequest(), config.id)
            table_data = res.context['instances_table'].data
            self.assertCountEqual(
                self.configuration_instances.list(), table_data)
            self.assertTemplateUsed(
                res, 'project/database_configurations/details.html')
        finally:
            config_param_manager.delete(config.id)

    @test.create_mocks({api.trove: ('configuration_instances',),
                        config_param_manager: ('get',)})
    def test_instances_tab_exception(self):
        try:
            config = self.database_configurations.first()

            # setup the configuration parameter manager
            config_param_mgr = config_param_manager.ConfigParamManager(
                config.id)
            config_param_mgr.configuration = config
            config_param_mgr.original_configuration_values = \
                dict.copy(config.values)

            self.mock_get.return_value = config_param_mgr

            self.mock_configuration_instances.side_effect = (
                self.exceptions.trove)

            details_url = self._get_url_with_arg(DETAIL_URL, config.id)
            url = details_url + '?tab=configuration_details__instance'

            res = self.client.get(url)
            self.assert_mock_multiple_calls_with_same_arguments(
                self.mock_get, 2, mock.call(test.IsHttpRequest(), config.id))
            self.mock_configuration_instances.assert_called_once_with(
                test.IsHttpRequest(), config.id)
            table_data = res.context['instances_table'].data
            self.assertNotEqual(len(self.configuration_instances.list()),
                                len(table_data))
            self.assertTemplateUsed(
                res, 'project/database_configurations/details.html')
        finally:
            config_param_manager.delete(config.id)

    def _get_url_with_arg(self, url, arg):
        return reverse(url, args=[arg])

    def _get_test_datastore(self, datastore_name):
        for ds in self.datastores.list():
            if ds.name == datastore_name:
                return ds
        return None

    def _get_test_datastore_version(self, datastore_id,
                                    datastore_version_name):
        for dsv in self.datastore_versions.list():
            if (dsv.datastore == datastore_id and
                    dsv.name == datastore_version_name):
                return dsv
        return None