#    Copyright 2013 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.

from mock import patch

from trove.backup import models as bkup_models
from trove.backup import state
from trove.common import exception as t_exception
from trove.common.instance import ServiceStatuses
from trove.common import utils
from trove.conductor import manager as conductor_manager
from trove.guestagent.common import timeutils
from trove.instance import models as t_models
from trove.tests.unittests import trove_testtools
from trove.tests.unittests.util import util


# See LP bug #1255178
OLD_DBB_SAVE = bkup_models.DBBackup.save


class ConductorMethodTests(trove_testtools.TestCase):
    def setUp(self):
        # See LP bug #1255178
        bkup_models.DBBackup.save = OLD_DBB_SAVE
        super(ConductorMethodTests, self).setUp()
        util.init_db()
        self.cond_mgr = conductor_manager.Manager()
        self.instance_id = utils.generate_uuid()

    def tearDown(self):
        super(ConductorMethodTests, self).tearDown()

    def _create_iss(self):
        new_id = utils.generate_uuid()
        iss = t_models.InstanceServiceStatus(
            id=new_id,
            instance_id=self.instance_id,
            status=ServiceStatuses.NEW)
        iss.save()
        return new_id

    def _get_iss(self, id):
        return t_models.InstanceServiceStatus.find_by(id=id)

    def _create_backup(self, name='fake backup'):
        new_id = utils.generate_uuid()
        backup = bkup_models.DBBackup.create(
            id=new_id,
            name=name,
            description='This is a fake backup object.',
            tenant_id=utils.generate_uuid(),
            state=state.BackupState.NEW,
            instance_id=self.instance_id)
        backup.save()
        return new_id

    def _get_backup(self, id):
        return bkup_models.DBBackup.find_by(id=id)

    # --- Tests for heartbeat ---

    def test_heartbeat_instance_not_found(self):
        new_id = utils.generate_uuid()
        self.assertRaises(t_exception.ModelNotFoundError,
                          self.cond_mgr.heartbeat, None, new_id, {})

    @patch('trove.conductor.manager.LOG')
    def test_heartbeat_instance_no_changes(self, mock_logging):
        iss_id = self._create_iss()
        old_iss = self._get_iss(iss_id)
        self.cond_mgr.heartbeat(None, self.instance_id, {})
        new_iss = self._get_iss(iss_id)
        self.assertEqual(old_iss.status_id, new_iss.status_id)
        self.assertEqual(old_iss.status_description,
                         new_iss.status_description)

    @patch('trove.conductor.manager.LOG')
    def test_heartbeat_instance_status_bogus_change(self, mock_logging):
        iss_id = self._create_iss()
        old_iss = self._get_iss(iss_id)
        new_status = 'potato salad'
        payload = {
            'service_status': new_status,
        }
        self.assertRaises(ValueError, self.cond_mgr.heartbeat,
                          None, self.instance_id, payload)
        new_iss = self._get_iss(iss_id)
        self.assertEqual(old_iss.status_id, new_iss.status_id)
        self.assertEqual(old_iss.status_description,
                         new_iss.status_description)

    @patch('trove.conductor.manager.LOG')
    def test_heartbeat_instance_status_changed(self, mock_logging):
        iss_id = self._create_iss()
        payload = {'service_status': ServiceStatuses.BUILDING.description}
        self.cond_mgr.heartbeat(None, self.instance_id, payload)
        iss = self._get_iss(iss_id)
        self.assertEqual(ServiceStatuses.BUILDING, iss.status)

    # --- Tests for update_backup ---

    def test_backup_not_found(self):
        new_bkup_id = utils.generate_uuid()
        self.assertRaises(t_exception.ModelNotFoundError,
                          self.cond_mgr.update_backup,
                          None, self.instance_id, new_bkup_id)

    @patch('trove.conductor.manager.LOG')
    def test_backup_instance_id_nomatch(self, mock_logging):
        new_iid = utils.generate_uuid()
        bkup_id = self._create_backup('nomatch')
        old_name = self._get_backup(bkup_id).name
        self.cond_mgr.update_backup(None, new_iid, bkup_id,
                                    name="remains unchanged")
        bkup = self._get_backup(bkup_id)
        self.assertEqual(old_name, bkup.name)

    @patch('trove.conductor.manager.LOG')
    def test_backup_bogus_fields_not_changed(self, mock_logging):
        bkup_id = self._create_backup('bogus')
        self.cond_mgr.update_backup(None, self.instance_id, bkup_id,
                                    not_a_valid_field="INVALID")
        bkup = self._get_backup(bkup_id)
        self.assertFalse(hasattr(bkup, 'not_a_valid_field'))

    @patch('trove.conductor.manager.LOG')
    def test_backup_real_fields_changed(self, mock_logging):
        bkup_id = self._create_backup('realrenamed')
        new_name = "recently renamed"
        self.cond_mgr.update_backup(None, self.instance_id, bkup_id,
                                    name=new_name)
        bkup = self._get_backup(bkup_id)
        self.assertEqual(new_name, bkup.name)

    # --- Tests for discarding old messages ---
    @patch('trove.conductor.manager.LOG')
    def test_heartbeat_newer_timestamp_accepted(self, mock_logging):
        new_p = {'service_status': ServiceStatuses.NEW.description}
        build_p = {'service_status': ServiceStatuses.BUILDING.description}
        iss_id = self._create_iss()
        iss = self._get_iss(iss_id)
        now = timeutils.float_utcnow()
        future = now + 60
        self.cond_mgr.heartbeat(None, self.instance_id, new_p, sent=now)
        self.cond_mgr.heartbeat(None, self.instance_id, build_p, sent=future)
        iss = self._get_iss(iss_id)
        self.assertEqual(ServiceStatuses.BUILDING, iss.status)

    @patch('trove.conductor.manager.LOG')
    def test_heartbeat_older_timestamp_discarded(self, mock_logging):
        new_p = {'service_status': ServiceStatuses.NEW.description}
        build_p = {'service_status': ServiceStatuses.BUILDING.description}
        iss_id = self._create_iss()
        iss = self._get_iss(iss_id)
        now = timeutils.float_utcnow()
        past = now - 60
        self.cond_mgr.heartbeat(None, self.instance_id, new_p, sent=past)
        self.cond_mgr.heartbeat(None, self.instance_id, build_p, sent=past)
        iss = self._get_iss(iss_id)
        self.assertEqual(ServiceStatuses.NEW, iss.status)

    def test_backup_newer_timestamp_accepted(self):
        old_name = "oldname"
        new_name = "renamed"
        bkup_id = self._create_backup(old_name)
        bkup = self._get_backup(bkup_id)
        now = timeutils.float_utcnow()
        future = now + 60
        self.cond_mgr.update_backup(None, self.instance_id, bkup_id,
                                    sent=now, name=old_name)
        self.cond_mgr.update_backup(None, self.instance_id, bkup_id,
                                    sent=future, name=new_name)
        bkup = self._get_backup(bkup_id)
        self.assertEqual(new_name, bkup.name)

    def test_backup_older_timestamp_discarded(self):
        old_name = "oldname"
        new_name = "renamed"
        bkup_id = self._create_backup(old_name)
        bkup = self._get_backup(bkup_id)
        now = timeutils.float_utcnow()
        past = now - 60
        self.cond_mgr.update_backup(None, self.instance_id, bkup_id,
                                    sent=now, name=old_name)
        self.cond_mgr.update_backup(None, self.instance_id, bkup_id,
                                    sent=past, name=new_name)
        bkup = self._get_backup(bkup_id)
        self.assertEqual(old_name, bkup.name)
