#    Copyright 2012 OpenStack Foundation
#
#    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 os
from random import randint
import time

from mock import Mock
from mock import MagicMock
from mockito import mock
from mockito import when
from mockito import any
from mockito import unstub
from mockito import verify
from mockito import contains
from mockito import never
from mockito import matchers
from mockito import inorder, verifyNoMoreInteractions
from trove.extensions.mysql.models import RootHistory
import sqlalchemy
import testtools
from testtools.matchers import Is
from testtools.matchers import Equals
from testtools.matchers import Not
import trove
from trove.common.context import TroveContext
from trove.common import utils
import trove.guestagent.manager.mysql_service as dbaas
from trove.guestagent.dbaas import to_gb
from trove.guestagent.dbaas import get_filesystem_volume_stats
from trove.guestagent.manager.mysql_service import MySqlAdmin
from trove.guestagent.manager.mysql_service import MySqlRootAccess
from trove.guestagent.manager.mysql_service import MySqlApp
from trove.guestagent.manager.mysql_service import MySqlAppStatus
from trove.guestagent.manager.mysql_service import KeepAliveConnection
from trove.guestagent.db import models
from trove.instance.models import ServiceStatuses
from trove.instance.models import InstanceServiceStatus
from trove.tests.unittests.util import util


"""
Unit tests for the classes and functions in dbaas.py.
"""

FAKE_DB = {"_name": "testDB", "_character_set": "latin2",
           "_collate": "latin2_general_ci"}
FAKE_DB_2 = {"_name": "testDB2", "_character_set": "latin2",
             "_collate": "latin2_general_ci"}
FAKE_USER = [{"_name": "random", "_password": "guesswhat",
              "_databases": [FAKE_DB]}]


class FakeAppStatus(MySqlAppStatus):

    def __init__(self, id, status):
        self.id = id
        self.next_fake_status = status

    def _get_actual_db_status(self):
        return self.next_fake_status

    def _load_status(self):
        return InstanceServiceStatus.find_by(instance_id=self.id)

    def set_next_status(self, next_status):
        self.next_fake_status = next_status


class DbaasTest(testtools.TestCase):

    def setUp(self):
        super(DbaasTest, self).setUp()
        self.orig_utils_execute_with_timeout = dbaas.utils.execute_with_timeout
        self.orig_utils_execute = dbaas.utils.execute

    def tearDown(self):
        super(DbaasTest, self).tearDown()
        dbaas.utils.execute_with_timeout = self.orig_utils_execute_with_timeout
        dbaas.utils.execute = self.orig_utils_execute

    def test_get_auth_password(self):

        dbaas.utils.execute_with_timeout = \
            Mock(return_value=("password    ", None))

        password = dbaas.get_auth_password()

        self.assertEqual("password", password)

    def test_get_auth_password_error(self):

        dbaas.utils.execute_with_timeout = \
            Mock(return_value=("password", "Error"))

        self.assertRaises(RuntimeError, dbaas.get_auth_password)

    def test_load_mysqld_options(self):

        output = "mysqld would've been started with the these args:\n"\
                 "--user=mysql --port=3306 --basedir=/usr "\
                 "--tmpdir=/tmp --skip-external-locking"
        dbaas.utils.execute = Mock(return_value=(output, None))

        options = dbaas.load_mysqld_options()

        self.assertEqual(5, len(options))
        self.assertEqual(options["user"], "mysql")
        self.assertEqual(options["port"], "3306")
        self.assertEqual(options["basedir"], "/usr")
        self.assertEqual(options["tmpdir"], "/tmp")
        self.assertTrue("skip-external-locking" in options)

    def test_load_mysqld_options_error(self):

        from trove.common.exception import ProcessExecutionError
        dbaas.utils.execute = Mock(side_effect=ProcessExecutionError())

        self.assertFalse(dbaas.load_mysqld_options())


class ResultSetStub(object):

    def __init__(self, rows):
        self._rows = rows

    def __iter__(self):
        return self._rows.__iter__()

    @property
    def rowcount(self):
        return len(self._rows)

    def __repr__(self):
        return self._rows.__repr__()


class MySqlAdminMockTest(testtools.TestCase):

    def tearDown(self):
        super(MySqlAdminMockTest, self).tearDown()
        unstub()

    def test_list_databases(self):
        mock_conn = mock_admin_sql_connection()

        when(mock_conn).execute(
            TextClauseMatcher('schema_name as name')).thenReturn(
                ResultSetStub([('db1', 'utf8', 'utf8_bin'),
                               ('db2', 'utf8', 'utf8_bin'),
                               ('db3', 'utf8', 'utf8_bin')]))

        databases, next_marker = MySqlAdmin().list_databases(limit=10)

        self.assertThat(next_marker, Is(None))
        self.assertThat(len(databases), Is(3))


class MySqlAdminTest(testtools.TestCase):

    def setUp(self):

        super(MySqlAdminTest, self).setUp()

        self.orig_get_engine = dbaas.get_engine
        self.orig_LocalSqlClient = dbaas.LocalSqlClient
        self.orig_LocalSqlClient_enter = dbaas.LocalSqlClient.__enter__
        self.orig_LocalSqlClient_exit = dbaas.LocalSqlClient.__exit__
        self.orig_LocalSqlClient_execute = dbaas.LocalSqlClient.execute
        self.orig_MySQLUser_is_valid_user_name = \
            models.MySQLUser._is_valid_user_name
        dbaas.get_engine = MagicMock(name='get_engine')
        dbaas.LocalSqlClient = Mock
        dbaas.LocalSqlClient.__enter__ = Mock()
        dbaas.LocalSqlClient.__exit__ = Mock()
        dbaas.LocalSqlClient.execute = Mock()
        self.mySqlAdmin = MySqlAdmin()

    def tearDown(self):

        super(MySqlAdminTest, self).tearDown()
        dbaas.get_engine = self.orig_get_engine
        dbaas.LocalSqlClient = self.orig_LocalSqlClient
        dbaas.LocalSqlClient.__enter__ = self.orig_LocalSqlClient_enter
        dbaas.LocalSqlClient.__exit__ = self.orig_LocalSqlClient_exit
        dbaas.LocalSqlClient.execute = self.orig_LocalSqlClient_execute
        models.MySQLUser._is_valid_user_name = \
            self.orig_MySQLUser_is_valid_user_name

    def test_create_database(self):

        databases = []
        databases.append(FAKE_DB)

        self.mySqlAdmin.create_database(databases)

        args, _ = dbaas.LocalSqlClient.execute.call_args_list[0]
        expected = ("CREATE DATABASE IF NOT EXISTS "
                    "`testDB` CHARACTER SET = 'latin2' "
                    "COLLATE = 'latin2_general_ci';")
        self.assertEquals(args[0].text, expected,
                          "Create database queries are not the same")

        self.assertEqual(1, dbaas.LocalSqlClient.execute.call_count,
                         "The client object was not 2 times")

    def test_create_database_more_than_1(self):

        databases = []
        databases.append(FAKE_DB)
        databases.append(FAKE_DB_2)

        self.mySqlAdmin.create_database(databases)

        args, _ = dbaas.LocalSqlClient.execute.call_args_list[0]
        expected = ("CREATE DATABASE IF NOT EXISTS "
                    "`testDB` CHARACTER SET = 'latin2' "
                    "COLLATE = 'latin2_general_ci';")
        self.assertEquals(args[0].text, expected,
                          "Create database queries are not the same")

        args, _ = dbaas.LocalSqlClient.execute.call_args_list[1]
        expected = ("CREATE DATABASE IF NOT EXISTS "
                    "`testDB2` CHARACTER SET = 'latin2' "
                    "COLLATE = 'latin2_general_ci';")
        self.assertEquals(args[0].text, expected,
                          "Create database queries are not the same")

        self.assertEqual(2, dbaas.LocalSqlClient.execute.call_count,
                         "The client object was not 2 times")

    def test_create_database_no_db(self):

        databases = []

        self.mySqlAdmin.create_database(databases)

        self.assertFalse(dbaas.LocalSqlClient.execute.called,
                         "The client object was called when it wasn't " +
                         "supposed to")

    def test_delete_database(self):

        database = {"_name": "testDB"}

        self.mySqlAdmin.delete_database(database)

        args, _ = dbaas.LocalSqlClient.execute.call_args
        expected = "DROP DATABASE `testDB`;"
        self.assertEquals(args[0].text, expected,
                          "Delete database queries are not the same")

        self.assertTrue(dbaas.LocalSqlClient.execute.called,
                        "The client object was not called")

    def test_delete_user(self):

        user = {"_name": "testUser"}

        self.mySqlAdmin.delete_user(user)

        # For some reason, call_args is None.
        call_args = dbaas.LocalSqlClient.execute.call_args
        if call_args is not None:
            args, _ = call_args
            expected = "DROP USER `testUser`;"
            self.assertEquals(args[0].text, expected,
                              "Delete user queries are not the same")

            self.assertTrue(dbaas.LocalSqlClient.execute.called,
                            "The client object was not called")

    def test_create_user(self):
        self.mySqlAdmin.create_user(FAKE_USER)
        expected = ("GRANT ALL PRIVILEGES ON `testDB`.* TO `random`@`%` "
                    "IDENTIFIED BY 'guesswhat' "
                    "WITH GRANT OPTION;")
        # For some reason, call_args is None.
        call_args = dbaas.LocalSqlClient.execute.call_args
        if call_args is not None:
            args, _ = call_args
            self.assertEquals(args[0].text.strip(), expected,
                              "Create user queries are not the same")
            self.assertEqual(2, dbaas.LocalSqlClient.execute.call_count)

    def test_list_databases(self):
        self.mySqlAdmin.list_databases()
        args, _ = dbaas.LocalSqlClient.execute.call_args
        expected = ["SELECT schema_name as name,",
                    "default_character_set_name as charset,",
                    "default_collation_name as collation",
                    "FROM information_schema.schemata",
                    ("schema_name NOT IN ("
                     "'mysql', 'information_schema', "
                     "'lost+found', '#mysql50#lost+found'"
                     ")"),
                    "ORDER BY schema_name ASC",
                    ]
        for text in expected:
            self.assertTrue(text in args[0].text, "%s not in query." % text)
        self.assertFalse("LIMIT " in args[0].text)

    def test_list_databases_with_limit(self):
        limit = 2
        self.mySqlAdmin.list_databases(limit)
        args, _ = dbaas.LocalSqlClient.execute.call_args
        expected = ["SELECT schema_name as name,",
                    "default_character_set_name as charset,",
                    "default_collation_name as collation",
                    "FROM information_schema.schemata",
                    ("schema_name NOT IN ("
                     "'mysql', 'information_schema', "
                     "'lost+found', '#mysql50#lost+found'"
                     ")"),
                    "ORDER BY schema_name ASC",
                    ]
        for text in expected:
            self.assertTrue(text in args[0].text, "%s not in query." % text)

        self.assertTrue("LIMIT " + str(limit + 1) in args[0].text)

    def test_list_databases_with_marker(self):
        marker = "aMarker"
        self.mySqlAdmin.list_databases(marker=marker)
        args, _ = dbaas.LocalSqlClient.execute.call_args
        expected = ["SELECT schema_name as name,",
                    "default_character_set_name as charset,",
                    "default_collation_name as collation",
                    "FROM information_schema.schemata",
                    ("schema_name NOT IN ("
                     "'mysql', 'information_schema', "
                     "'lost+found', '#mysql50#lost+found'"
                     ")"),
                    "ORDER BY schema_name ASC",
                    ]

        for text in expected:
            self.assertTrue(text in args[0].text, "%s not in query." % text)

        self.assertFalse("LIMIT " in args[0].text)

        self.assertTrue("AND schema_name > '" + marker + "'" in args[0].text)

    def test_list_databases_with_include_marker(self):
        marker = "aMarker"
        self.mySqlAdmin.list_databases(marker=marker, include_marker=True)
        args, _ = dbaas.LocalSqlClient.execute.call_args
        expected = ["SELECT schema_name as name,",
                    "default_character_set_name as charset,",
                    "default_collation_name as collation",
                    "FROM information_schema.schemata",
                    ("schema_name NOT IN ("
                     "'mysql', 'information_schema', "
                     "'lost+found', '#mysql50#lost+found'"
                     ")"),
                    "ORDER BY schema_name ASC",
                    ]
        for text in expected:
            self.assertTrue(text in args[0].text, "%s not in query." % text)

        self.assertFalse("LIMIT " in args[0].text)

        self.assertTrue(("AND schema_name >= '%s'" % marker) in args[0].text)

    def test_list_users(self):
        self.mySqlAdmin.list_users()
        args, _ = dbaas.LocalSqlClient.execute.call_args

        expected = ["SELECT User, Host",
                    "FROM mysql.user",
                    "WHERE Host != 'localhost'",
                    "ORDER BY User",
                    ]
        for text in expected:
            self.assertTrue(text in args[0].text, "%s not in query." % text)

        self.assertFalse("LIMIT " in args[0].text)
        self.assertFalse("AND Marker > '" in args[0].text)

    def test_list_users_with_limit(self):
        limit = 2
        self.mySqlAdmin.list_users(limit)
        args, _ = dbaas.LocalSqlClient.execute.call_args

        expected = ["SELECT User, Host",
                    "FROM mysql.user",
                    "WHERE Host != 'localhost'",
                    "ORDER BY User",
                    ("LIMIT " + str(limit + 1)),
                    ]
        for text in expected:
            self.assertTrue(text in args[0].text, "%s not in query." % text)

    def test_list_users_with_marker(self):
        marker = "aMarker"
        self.mySqlAdmin.list_users(marker=marker)
        args, _ = dbaas.LocalSqlClient.execute.call_args

        expected = ["SELECT User, Host, Marker",
                    "FROM mysql.user",
                    "WHERE Host != 'localhost'",
                    "ORDER BY User",
                    ]

        for text in expected:
            self.assertTrue(text in args[0].text, "%s not in query." % text)

        self.assertFalse("LIMIT " in args[0].text)
        self.assertTrue("AND Marker > '" + marker + "'" in args[0].text)

    def test_list_users_with_include_marker(self):
        marker = "aMarker"
        self.mySqlAdmin.list_users(marker=marker, include_marker=True)
        args, _ = dbaas.LocalSqlClient.execute.call_args

        expected = ["SELECT User, Host",
                    "FROM mysql.user",
                    "WHERE Host != 'localhost'",
                    "ORDER BY User",
                    ]

        for text in expected:
            self.assertTrue(text in args[0].text, "%s not in query." % text)

        self.assertFalse("LIMIT " in args[0].text)

        self.assertTrue("AND Marker >= '" + marker + "'" in args[0].text)


class MySqlAppTest(testtools.TestCase):

    def setUp(self):
        super(MySqlAppTest, self).setUp()
        self.orig_utils_execute_with_timeout = dbaas.utils.execute_with_timeout
        self.orig_time_sleep = time.sleep
        util.init_db()
        self.FAKE_ID = randint(1, 10000)
        InstanceServiceStatus.create(instance_id=self.FAKE_ID,
                                     status=ServiceStatuses.NEW)
        self.appStatus = FakeAppStatus(self.FAKE_ID, ServiceStatuses.NEW)
        self.mySqlApp = MySqlApp(self.appStatus)
        time.sleep = Mock()

    def tearDown(self):
        super(MySqlAppTest, self).tearDown()
        dbaas.utils.execute_with_timeout = self.orig_utils_execute_with_timeout
        time.sleep = self.orig_time_sleep
        InstanceServiceStatus.find_by(instance_id=self.FAKE_ID).delete()

    def assert_reported_status(self, expected_status):
        service_status = InstanceServiceStatus.find_by(
            instance_id=self.FAKE_ID)
        self.assertEqual(expected_status, service_status.status)

    def mysql_starts_successfully(self):
        def start(update_db=False):
            self.appStatus.set_next_status(ServiceStatuses.RUNNING)

        self.mySqlApp.start_mysql.side_effect = start

    def mysql_starts_unsuccessfully(self):
        def start():
            raise RuntimeError("MySQL failed to start!")

        self.mySqlApp.start_mysql.side_effect = start

    def mysql_stops_successfully(self):
        def stop():
            self.appStatus.set_next_status(ServiceStatuses.SHUTDOWN)

        self.mySqlApp.stop_db.side_effect = stop

    def mysql_stops_unsuccessfully(self):
        def stop():
            raise RuntimeError("MySQL failed to stop!")

        self.mySqlApp.stop_db.side_effect = stop

    def test_stop_mysql(self):

        dbaas.utils.execute_with_timeout = Mock()
        self.appStatus.set_next_status(ServiceStatuses.SHUTDOWN)

        self.mySqlApp.stop_db()
        self.assert_reported_status(ServiceStatuses.NEW)

    def test_stop_mysql_with_db_update(self):

        dbaas.utils.execute_with_timeout = Mock()
        self.appStatus.set_next_status(ServiceStatuses.SHUTDOWN)

        self.mySqlApp.stop_db(True)
        self.assert_reported_status(ServiceStatuses.SHUTDOWN)

    def test_stop_mysql_error(self):

        dbaas.utils.execute_with_timeout = Mock()
        self.appStatus.set_next_status(ServiceStatuses.RUNNING)
        self.mySqlApp.state_change_wait_time = 1
        self.assertRaises(RuntimeError, self.mySqlApp.stop_db)

    def test_restart_is_successful(self):

        self.mySqlApp.start_mysql = Mock()
        self.mySqlApp.stop_db = Mock()
        self.mysql_stops_successfully()
        self.mysql_starts_successfully()

        self.mySqlApp.restart()

        self.assertTrue(self.mySqlApp.stop_db.called)
        self.assertTrue(self.mySqlApp.start_mysql.called)
        self.assert_reported_status(ServiceStatuses.RUNNING)

    def test_restart_mysql_wont_start_up(self):

        self.mySqlApp.start_mysql = Mock()
        self.mySqlApp.stop_db = Mock()
        self.mysql_stops_unsuccessfully()
        self.mysql_starts_unsuccessfully()

        self.assertRaises(RuntimeError, self.mySqlApp.restart)

        self.assertTrue(self.mySqlApp.stop_db.called)
        self.assertFalse(self.mySqlApp.start_mysql.called)
        self.assert_reported_status(ServiceStatuses.NEW)

    def test_wipe_ib_logfiles_no_file(self):

        from trove.common.exception import ProcessExecutionError
        processexecerror = ProcessExecutionError('No such file or directory')
        dbaas.utils.execute_with_timeout = Mock(side_effect=processexecerror)

        self.mySqlApp.wipe_ib_logfiles()

    def test_wipe_ib_logfiles_error(self):

        from trove.common.exception import ProcessExecutionError
        mocked = Mock(side_effect=ProcessExecutionError('Error'))
        dbaas.utils.execute_with_timeout = mocked

        self.assertRaises(ProcessExecutionError,
                          self.mySqlApp.wipe_ib_logfiles)

    def test_start_mysql(self):

        dbaas.utils.execute_with_timeout = Mock()
        self.appStatus.set_next_status(ServiceStatuses.RUNNING)

        self.mySqlApp.start_mysql()
        self.assert_reported_status(ServiceStatuses.NEW)

    def test_start_mysql_with_db_update(self):

        dbaas.utils.execute_with_timeout = Mock()
        self.appStatus.set_next_status(ServiceStatuses.RUNNING)

        self.mySqlApp.start_mysql(True)
        self.assert_reported_status(ServiceStatuses.RUNNING)

    def test_start_mysql_runs_forever(self):

        dbaas.utils.execute_with_timeout = Mock()
        self.mySqlApp.state_change_wait_time = 1
        self.appStatus.set_next_status(ServiceStatuses.SHUTDOWN)

        self.assertRaises(RuntimeError, self.mySqlApp.start_mysql)
        self.assert_reported_status(ServiceStatuses.SHUTDOWN)

    def test_start_mysql_error(self):

        self.mySqlApp._enable_mysql_on_boot = Mock()
        from trove.common.exception import ProcessExecutionError
        mocked = Mock(side_effect=ProcessExecutionError('Error'))
        dbaas.utils.execute_with_timeout = mocked

        self.assertRaises(RuntimeError, self.mySqlApp.start_mysql)

    def test_start_db_with_conf_changes(self):

        self.mySqlApp.start_mysql = Mock()
        self.mySqlApp._write_mycnf = Mock()
        self.mysql_starts_successfully()

        self.appStatus.status = ServiceStatuses.SHUTDOWN
        self.mySqlApp.start_db_with_conf_changes(Mock())

        self.assertTrue(self.mySqlApp._write_mycnf.called)
        self.assertTrue(self.mySqlApp.start_mysql.called)
        self.assertEqual(self.appStatus._get_actual_db_status(),
                         ServiceStatuses.RUNNING)

    def test_start_db_with_conf_changes_mysql_is_running(self):

        self.mySqlApp.start_mysql = Mock()
        self.mySqlApp._write_mycnf = Mock()

        self.appStatus.status = ServiceStatuses.RUNNING
        self.assertRaises(RuntimeError,
                          self.mySqlApp.start_db_with_conf_changes,
                          Mock())


class MySqlAppInstallTest(MySqlAppTest):

    def setUp(self):
        super(MySqlAppInstallTest, self).setUp()
        self.orig_create_engine = sqlalchemy.create_engine
        self.orig_pkg_version = dbaas.packager.pkg_version

    def tearDown(self):
        super(MySqlAppInstallTest, self).tearDown()
        sqlalchemy.create_engine = self.orig_create_engine
        dbaas.packager.pkg_version = self.orig_pkg_version

    def test_install(self):

        self.mySqlApp._install_mysql = Mock()
        self.mySqlApp.is_installed = Mock(return_value=False)
        self.mySqlApp.install_if_needed()
        self.assertTrue(self.mySqlApp._install_mysql.called)
        self.assert_reported_status(ServiceStatuses.NEW)

    def test_secure(self):

        self.mySqlApp.start_mysql = Mock()
        self.mySqlApp.stop_db = Mock()
        self.mySqlApp._write_mycnf = Mock()
        self.mysql_stops_successfully()
        self.mysql_starts_successfully()
        sqlalchemy.create_engine = Mock()

        self.mySqlApp.secure('contents')

        self.assertTrue(self.mySqlApp.stop_db.called)
        self.assertTrue(self.mySqlApp._write_mycnf.called)
        self.assertTrue(self.mySqlApp.start_mysql.called)
        self.assert_reported_status(ServiceStatuses.NEW)

    def test_install_install_error(self):

        from trove.guestagent import pkg
        self.mySqlApp.start_mysql = Mock()
        self.mySqlApp.stop_db = Mock()
        self.mySqlApp.is_installed = Mock(return_value=False)
        self.mySqlApp._install_mysql = \
            Mock(side_effect=pkg.PkgPackageStateError("Install error"))

        self.assertRaises(pkg.PkgPackageStateError,
                          self.mySqlApp.install_if_needed)

        self.assert_reported_status(ServiceStatuses.NEW)

    def test_secure_write_conf_error(self):

        from trove.guestagent import pkg
        self.mySqlApp.start_mysql = Mock()
        self.mySqlApp.stop_db = Mock()
        self.mySqlApp._write_mycnf = \
            Mock(side_effect=IOError("Could not write file"))
        self.mysql_stops_successfully()
        self.mysql_starts_successfully()
        sqlalchemy.create_engine = Mock()

        self.assertRaises(IOError, self.mySqlApp.secure, "foo")

        self.assertTrue(self.mySqlApp.stop_db.called)
        self.assertTrue(self.mySqlApp._write_mycnf.called)
        self.assertFalse(self.mySqlApp.start_mysql.called)
        self.assert_reported_status(ServiceStatuses.NEW)

    def test_is_installed(self):

        dbaas.packager.pkg_version = Mock(return_value=True)

        self.assertTrue(self.mySqlApp.is_installed())

    def test_is_installed_not(self):

        dbaas.packager.pkg_version = Mock(return_value=None)

        self.assertFalse(self.mySqlApp.is_installed())


class TextClauseMatcher(matchers.Matcher):
    def __init__(self, text):
        self.contains = contains(text)

    def __repr__(self):
        return "TextClause(%s)" % self.contains.sub

    def matches(self, arg):
        print("Matching %s" % arg.text)
        return self.contains.matches(arg.text)


def mock_sql_connection():
    mock_engine = mock()
    when(sqlalchemy).create_engine("mysql://root:@localhost:3306",
                                   echo=True).thenReturn(mock_engine)
    mock_conn = mock()
    when(dbaas.LocalSqlClient).__enter__().thenReturn(mock_conn)
    when(dbaas.LocalSqlClient).__exit__(any(), any(), any()).thenReturn(None)
    return mock_conn


def mock_admin_sql_connection():
    when(utils).execute_with_timeout("sudo", "awk", any(), any()).thenReturn(
        ['fake_password', None])
    mock_engine = mock()
    when(sqlalchemy).create_engine("mysql://root:@localhost:3306",
                                   pool_recycle=any(), echo=True,
                                   listeners=[any()]).thenReturn(mock_engine)
    mock_conn = mock()
    when(dbaas.LocalSqlClient).__enter__().thenReturn(mock_conn)
    when(dbaas.LocalSqlClient).__exit__(any(), any(), any()).thenReturn(None)
    return mock_conn


class MySqlAppMockTest(testtools.TestCase):

    def tearDown(self):
        super(MySqlAppMockTest, self).tearDown()
        unstub()

    def test_secure_with_mycnf_error(self):
        mock_conn = mock_sql_connection()
        when(mock_conn).execute(any()).thenReturn(None)
        when(utils).execute_with_timeout("sudo", any(str), "stop").thenReturn(
            None)
        # skip writing the file for now
        when(os.path).isfile(any()).thenReturn(False)
        mock_status = mock(MySqlAppStatus)
        when(mock_status).wait_for_real_status_to_change_to(
            any(), any(), any()).thenReturn(True)
        app = MySqlApp(mock_status)

        self.assertRaises(TypeError, app.secure, None)

        verify(mock_conn, atleast=2).execute(any())
        inorder.verify(mock_status).wait_for_real_status_to_change_to(
            ServiceStatuses.SHUTDOWN, any(), any())
        verifyNoMoreInteractions(mock_status)

    def test_secure_keep_root(self):
        mock_conn = mock_sql_connection()

        when(mock_conn).execute(any()).thenReturn(None)
        when(utils).execute_with_timeout("sudo", any(str), "stop").thenReturn(
            None)
        # skip writing the file for now
        when(os.path).isfile(any()).thenReturn(False)
        when(utils).execute_with_timeout(
            "sudo", "chmod", any(), any()).thenReturn(None)
        mock_status = mock(MySqlAppStatus)
        when(mock_status).wait_for_real_status_to_change_to(
            any(), any(), any()).thenReturn(True)
        app = MySqlApp(mock_status)
        when(app)._write_mycnf(any(), any()).thenReturn(True)
        app.secure('foo')
        verify(mock_conn, never).execute(TextClauseMatcher('root'))


class MySqlRootStatusTest(testtools.TestCase):

    def tearDown(self):
        super(MySqlRootStatusTest, self).tearDown()
        unstub()

    def test_root_is_enabled(self):
        mock_conn = mock_admin_sql_connection()

        mock_rs = mock()
        mock_rs.rowcount = 1
        when(mock_conn).execute(
            TextClauseMatcher(
                "User = 'root' AND Host != 'localhost'")).thenReturn(mock_rs)

        self.assertThat(MySqlRootAccess().is_root_enabled(), Is(True))

    def test_root_is_not_enabled(self):
        mock_conn = mock_admin_sql_connection()

        mock_rs = mock()
        mock_rs.rowcount = 0
        when(mock_conn).execute(
            TextClauseMatcher(
                "User = 'root' AND Host != 'localhost'")).thenReturn(mock_rs)

        self.assertThat(MySqlRootAccess.is_root_enabled(), Equals(False))

    def test_enable_root(self):
        mock_conn = mock_admin_sql_connection()
        when(mock_conn).execute(any()).thenReturn(None)
        # invocation
        user_ser = MySqlRootAccess.enable_root()
        # verification
        self.assertThat(user_ser, Not(Is(None)))
        verify(mock_conn).execute(TextClauseMatcher('CREATE USER'),
                                  user='root', host='%')
        verify(mock_conn).execute(TextClauseMatcher(
            'GRANT ALL PRIVILEGES ON *.*'))
        verify(mock_conn).execute(TextClauseMatcher('UPDATE mysql.user'))

    def test_enable_root_failed(self):
        when(models.MySQLUser)._is_valid_user_name(any()).thenReturn(False)
        self.assertRaises(ValueError, MySqlAdmin().enable_root)

    def test_report_root_enabled(self):
        mock_db_api = mock()
        when(trove.extensions.mysql.models).get_db_api().thenReturn(
            mock_db_api)
        when(mock_db_api).find_by(any(), id=None).thenReturn(None)
        root_history = RootHistory('x', 'root')
        when(mock_db_api).save(any(RootHistory)).thenReturn(root_history)
        # invocation
        history = MySqlRootAccess.report_root_enabled(TroveContext())
        # verification
        self.assertThat(history, Is(root_history))
        verify(mock_db_api).save(any(RootHistory))


class MockStats:
    f_blocks = 1024 ** 2
    f_bsize = 4096
    f_bfree = 512 * 1024


class InterrogatorTest(testtools.TestCase):

    def tearDown(self):
        super(InterrogatorTest, self).tearDown()
        unstub()

    def test_to_gb(self):
        result = to_gb(123456789)
        self.assertEqual(result, 0.11)

    def test_to_gb_zero(self):
        result = to_gb(0)
        self.assertEqual(result, 0.0)

    def test_get_filesystem_volume_stats(self):
        when(os).statvfs(any()).thenReturn(MockStats)
        result = get_filesystem_volume_stats('/some/path/')

        self.assertEqual(result['block_size'], 4096)
        self.assertEqual(result['total_blocks'], 1048576)
        self.assertEqual(result['free_blocks'], 524288)
        self.assertEqual(result['total'], 4294967296)
        self.assertEqual(result['free'], 2147483648)
        self.assertEqual(result['used'], 2.0)

    def test_get_filesystem_volume_stats_error(self):
        when(os).statvfs(any()).thenRaise(OSError)
        self.assertRaises(
            RuntimeError,
            get_filesystem_volume_stats, '/nonexistent/path')


class KeepAliveConnectionTest(testtools.TestCase):

    class OperationalError(Exception):
        def __init__(self, value):
            self.args = [value]

        def __str__(self):
            return repr(self.value)

    def setUp(self):
        super(KeepAliveConnectionTest, self).setUp()
        self.orig_utils_execute_with_timeout = dbaas.utils.execute_with_timeout
        self.orig_LOG_err = dbaas.LOG

    def tearDown(self):
        super(KeepAliveConnectionTest, self).tearDown()
        dbaas.utils.execute_with_timeout = self.orig_utils_execute_with_timeout
        dbaas.LOG = self.orig_LOG_err

    def test_checkout_type_error(self):

        dbapi_con = Mock()
        dbapi_con.ping = Mock(side_effect=TypeError("Type Error"))

        self.keepAliveConn = KeepAliveConnection()
        self.assertRaises(TypeError, self.keepAliveConn.checkout,
                          dbapi_con, Mock(), Mock())

    def test_checkout_disconnection_error(self):

        from sqlalchemy import exc
        dbapi_con = Mock()
        dbapi_con.OperationalError = self.OperationalError
        dbapi_con.ping = Mock(side_effect=dbapi_con.OperationalError(2013))

        self.keepAliveConn = KeepAliveConnection()
        self.assertRaises(exc.DisconnectionError, self.keepAliveConn.checkout,
                          dbapi_con, Mock(), Mock())

    def test_checkout_operation_error(self):

        dbapi_con = Mock()
        dbapi_con.OperationalError = self.OperationalError
        dbapi_con.ping = Mock(side_effect=dbapi_con.OperationalError(1234))

        self.keepAliveConn = KeepAliveConnection()
        self.assertRaises(self.OperationalError, self.keepAliveConn.checkout,
                          dbapi_con, Mock(), Mock())


class MySqlAppStatusTest(testtools.TestCase):

    def setUp(self):
        super(MySqlAppStatusTest, self).setUp()
        util.init_db()
        self.orig_utils_execute_with_timeout = dbaas.utils.execute_with_timeout
        self.orig_load_mysqld_options = dbaas.load_mysqld_options
        self.orig_dbaas_os_path_exists = dbaas.os.path.exists
        self.orig_dbaas_time_sleep = dbaas.time.sleep
        self.FAKE_ID = randint(1, 10000)
        InstanceServiceStatus.create(instance_id=self.FAKE_ID,
                                     status=ServiceStatuses.NEW)
        dbaas.CONF.guest_id = self.FAKE_ID

    def tearDown(self):
        super(MySqlAppStatusTest, self).tearDown()
        dbaas.utils.execute_with_timeout = self.orig_utils_execute_with_timeout
        dbaas.load_mysqld_options = self.orig_load_mysqld_options
        dbaas.os.path.exists = self.orig_dbaas_os_path_exists
        dbaas.time.sleep = self.orig_dbaas_time_sleep
        InstanceServiceStatus.find_by(instance_id=self.FAKE_ID).delete()
        dbaas.CONF.guest_id = None

    def test_being_mysql_install(self):

        self.mySqlAppStatus = MySqlAppStatus()

        self.mySqlAppStatus.begin_mysql_install()

        self.assertEquals(self.mySqlAppStatus.status, ServiceStatuses.BUILDING)

    def test_begin_mysql_restart(self):

        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.restart_mode = False

        self.mySqlAppStatus.begin_mysql_restart()

        self.assertTrue(self.mySqlAppStatus.restart_mode)

    def test_end_install_or_restart(self):

        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus._get_actual_db_status = \
            Mock(return_value=ServiceStatuses.SHUTDOWN)

        self.mySqlAppStatus.end_install_or_restart()

        self.assertEqual(ServiceStatuses.SHUTDOWN, self.mySqlAppStatus.status)
        self.assertFalse(self.mySqlAppStatus.restart_mode)

    def test_get_actual_db_status(self):

        dbaas.utils.execute_with_timeout = Mock(return_value=(None, None))

        self.mySqlAppStatus = MySqlAppStatus()
        status = self.mySqlAppStatus._get_actual_db_status()

        self.assertEqual(ServiceStatuses.RUNNING, status)

    def test_get_actual_db_status_error_shutdown(self):

        from trove.common.exception import ProcessExecutionError
        mocked = Mock(side_effect=ProcessExecutionError())
        dbaas.utils.execute_with_timeout = mocked
        dbaas.load_mysqld_options = Mock()
        dbaas.os.path.exists = Mock(return_value=False)

        self.mySqlAppStatus = MySqlAppStatus()
        status = self.mySqlAppStatus._get_actual_db_status()

        self.assertEqual(ServiceStatuses.SHUTDOWN, status)

    def test_get_actual_db_status_error_crashed(self):

        from trove.common.exception import ProcessExecutionError
        dbaas.utils.execute_with_timeout = \
            MagicMock(side_effect=[ProcessExecutionError(),
                                   ("some output", None)])
        dbaas.load_mysqld_options = Mock()
        dbaas.os.path.exists = Mock(return_value=True)

        self.mySqlAppStatus = MySqlAppStatus()
        status = self.mySqlAppStatus._get_actual_db_status()

        self.assertEqual(ServiceStatuses.BLOCKED, status)

    def test_is_mysql_installed(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.status = ServiceStatuses.RUNNING

        self.assertTrue(self.mySqlAppStatus.is_mysql_installed)

    def test_is_mysql_installed_none(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.status = None

        self.assertFalse(self.mySqlAppStatus.is_mysql_installed)

    def test_is_mysql_installed_building(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.status = ServiceStatuses.BUILDING

        self.assertFalse(self.mySqlAppStatus.is_mysql_installed)

    def test_is_mysql_installed_new(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.status = ServiceStatuses.NEW

        self.assertFalse(self.mySqlAppStatus.is_mysql_installed)

    def test_is_mysql_installed_failed(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.status = ServiceStatuses.FAILED

        self.assertFalse(self.mySqlAppStatus.is_mysql_installed)

    def test_is_mysql_restarting(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.restart_mode = True

        self.assertTrue(self.mySqlAppStatus._is_mysql_restarting)

    def test_is_mysql_running(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.status = ServiceStatuses.RUNNING

        self.assertTrue(self.mySqlAppStatus.is_mysql_running)

    def test_is_mysql_running_not(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus.status = ServiceStatuses.SHUTDOWN

        self.assertFalse(self.mySqlAppStatus.is_mysql_running)

    def test_wait_for_real_status_to_change_to(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus._get_actual_db_status = \
            Mock(return_value=ServiceStatuses.RUNNING)
        dbaas.time.sleep = Mock()

        self.assertTrue(self.mySqlAppStatus.
                        wait_for_real_status_to_change_to
                        (ServiceStatuses.RUNNING, 10))

    def test_wait_for_real_status_to_change_to_timeout(self):
        self.mySqlAppStatus = MySqlAppStatus()
        self.mySqlAppStatus._get_actual_db_status = \
            Mock(return_value=ServiceStatuses.RUNNING)
        dbaas.time.sleep = Mock()

        self.assertFalse(self.mySqlAppStatus.
                         wait_for_real_status_to_change_to
                         (ServiceStatuses.SHUTDOWN, 10))
