#   Copyright 2017 GoDaddy
# Copyright 2018 Rackspace US Inc.  All rights reserved.
#
#    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 time

from oslo_log import log as logging
from tempest import config
from tempest.lib.common.utils import test_utils
from tempest.lib import exceptions

from octavia_tempest_plugin.common import constants as const

CONF = config.CONF
LOG = logging.getLogger(__name__)


def wait_for_status(show_client, id, status_key, status,
                    check_interval, check_timeout, root_tag=None,
                    error_ok=False, **kwargs):
    """Waits for an object to reach a specific status.

    :param show_client: The tempest service client show method.
                        Ex. cls.os_primary.servers_client.show_server
    :param id: The id of the object to query.
    :param status_key: The key of the status field in the response.
                       Ex. provisioning_status
    :param status: The status to wait for. Ex. "ACTIVE"
    :check_interval: How often to check the status, in seconds.
    :check_timeout: The maximum time, in seconds, to check the status.
    :root_tag: The root tag on the response to remove, if any.
    :error_ok: When true, ERROR status will not raise an exception.
    :raises CommandFailed: Raised if the object goes into ERROR and ERROR was
                           not the desired status.
    :raises TimeoutException: The object did not achieve the status or ERROR in
                              the check_timeout period.
    :returns: The object details from the show client.
    """
    start = int(time.time())
    LOG.info('Waiting for %s status to update to %s',
             show_client.__name__, status)
    while True:
        if status == const.DELETED:
            try:
                response = show_client(id, **kwargs)
            except exceptions.NotFound:
                return
        else:
            response = show_client(id, **kwargs)

        if root_tag:
            object_details = response[root_tag]
        else:
            object_details = response

        if object_details[status_key] == status:
            LOG.info('%s\'s status updated to %s.',
                     show_client.__name__, status)
            return object_details
        elif object_details[status_key] == 'ERROR' and not error_ok:
            message = ('{name} {field} updated to an invalid state of '
                       'ERROR'.format(name=show_client.__name__,
                                      field=status_key))
            caller = test_utils.find_test_caller()
            if caller:
                message = '({caller}) {message}'.format(caller=caller,
                                                        message=message)
            raise exceptions.UnexpectedResponseCode(message)

        if int(time.time()) - start >= check_timeout:
            message = (
                '{name} {field} failed to update to {expected_status} within '
                'the required time {timeout}. Current status of {name}: '
                '{status}'.format(
                    name=show_client.__name__,
                    timeout=check_timeout,
                    status=object_details[status_key],
                    expected_status=status,
                    field=status_key
                ))
            caller = test_utils.find_test_caller()
            if caller:
                message = '({caller}) {message}'.format(caller=caller,
                                                        message=message)
            raise exceptions.TimeoutException(message)

        time.sleep(check_interval)


def wait_for_not_found(delete_func, show_func, *args, **kwargs):
    """Call the delete function, then wait for it to be 'NotFound'

    :param delete_func: The delete function to call.
    :param show_func: The show function to call looking for 'NotFound'.
    :param ID: The ID of the object to delete/show.
    :raises TimeoutException: The object did not achieve the status or ERROR in
                              the check_timeout period.
    :returns: None
    """
    try:
        delete_func(*args, **kwargs)
    except exceptions.NotFound:
        return
    start = int(time.time())
    LOG.info('Waiting for object to be NotFound')
    while True:
        try:
            show_func(*args, **kwargs)
        except exceptions.NotFound:
            return
        if int(time.time()) - start >= CONF.load_balancer.check_timeout:
            message = ('{name} did not raise NotFound in {timeout} '
                       'seconds.'.format(
                           name=show_func.__name__,
                           timeout=CONF.load_balancer.check_timeout))
            raise exceptions.TimeoutException(message)
        time.sleep(CONF.load_balancer.check_interval)


def wait_for_deleted_status_or_not_found(
        show_client, id, status_key, check_interval, check_timeout,
        root_tag=None, **kwargs):
    """Waits for an object to reach a DELETED status or be not found (404).

    :param show_client: The tempest service client show method.
                        Ex. cls.os_primary.servers_client.show_server
    :param id: The id of the object to query.
    :param status_key: The key of the status field in the response.
                       Ex. provisioning_status
    :check_interval: How often to check the status, in seconds.
    :check_timeout: The maximum time, in seconds, to check the status.
    :root_tag: The root tag on the response to remove, if any.
    :raises CommandFailed: Raised if the object goes into ERROR and ERROR was
                           not the desired status.
    :raises TimeoutException: The object did not achieve the status or ERROR in
                              the check_timeout period.
    :returns: None
    """
    start = int(time.time())
    LOG.info('Waiting for %s status to update to DELETED or be not found(404)',
             show_client.__name__)
    while True:
        try:
            response = show_client(id, **kwargs)
        except exceptions.NotFound:
            return

        if root_tag:
            object_details = response[root_tag]
        else:
            object_details = response

        if object_details[status_key] == const.DELETED:
            LOG.info('%s\'s status updated to DELETED.',
                     show_client.__name__)
            return
        elif int(time.time()) - start >= check_timeout:
            message = (
                '{name} {field} failed to update to DELETED or become not '
                'found (404) within the required time {timeout}. Current '
                'status of {name}: {status}'.format(
                    name=show_client.__name__,
                    timeout=check_timeout,
                    status=object_details[status_key],
                    field=status_key
                ))
            caller = test_utils.find_test_caller()
            if caller:
                message = '({caller}) {message}'.format(caller=caller,
                                                        message=message)
            raise exceptions.TimeoutException(message)

        time.sleep(check_interval)


def wait_for_spare_amps(list_func, check_interval, check_timeout):
    """Waits for amphorae in spare pool.

    :param list_func: The tempest service client amphora list method.
                        Ex. cls.os_admin.amphora_client.list_amphorae
    :check_interval: How often to check the status, in seconds.
    :check_timeout: The maximum time, in seconds, to check the status.
    :raises TimeoutException: No amphora available in spare pool in the
                              check_timeout period.
    :returns: A list of amphorae in spare pool.
    """

    LOG.info('Waiting for amphorae in spare pool')
    start = int(time.time())
    while True:
        spare_amps = list_func(
            query_params='{status}={status_ready}'.format(
                status=const.STATUS, status_ready=const.STATUS_READY))
        if len(spare_amps) >= 1:
            return spare_amps
        if int(time.time()) - start >= check_timeout:
            message = ("No available amphorae in spare pool within the "
                       "required time {timeout}.".format(
                           timeout=check_timeout))
            raise exceptions.TimeoutException(message)
        time.sleep(check_interval)


def wait_until_true(func, timeout=60, sleep=1, **kwargs):
    """Wait until callable predicate is evaluated as True

    :param func: Callable deciding whether waiting should continue.
    :param timeout: Timeout in seconds how long should function wait.
    :param sleep: Polling interval for results in seconds.
    """
    start = int(time.time())
    while True:
        try:
            ret = func(**kwargs)
            if ret:
                return
        except Exception as e:
            LOG.error(e)

        if int(time.time()) - start >= timeout:
            message = "Timed out after {timeout} seconds waiting".format(
                timeout=timeout)
            raise exceptions.TimeoutException(message)
        time.sleep(sleep)
