#    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_lib.common.utils import misc as misc_utils
from tempest_lib import exceptions as lib_exc

from tempest import config
from tempest import exceptions

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


# NOTE(afazekas): This function needs to know a token and a subject.
def wait_for_server_status(client, server_id, status, ready_wait=True,
                           extra_timeout=0, raise_on_error=True):
    """Waits for a server to reach a given status."""

    def _get_task_state(body):
        return body.get('OS-EXT-STS:task_state', None)

    # NOTE(afazekas): UNKNOWN status possible on ERROR
    # or in a very early stage.
    body = client.show_server(server_id)
    old_status = server_status = body['status']
    old_task_state = task_state = _get_task_state(body)
    start_time = int(time.time())
    timeout = client.build_timeout + extra_timeout
    while True:
        # NOTE(afazekas): Now the BUILD status only reached
        # between the UNKNOWN->ACTIVE transition.
        # TODO(afazekas): enumerate and validate the stable status set
        if status == 'BUILD' and server_status != 'UNKNOWN':
            return
        if server_status == status:
            if ready_wait:
                if status == 'BUILD':
                    return
                # NOTE(afazekas): The instance is in "ready for action state"
                # when no task in progress
                # NOTE(afazekas): Converted to string because of the XML
                # responses
                if str(task_state) == "None":
                    # without state api extension 3 sec usually enough
                    time.sleep(CONF.compute.ready_wait)
                    return
            else:
                return

        time.sleep(client.build_interval)
        body = client.show_server(server_id)
        server_status = body['status']
        task_state = _get_task_state(body)
        if (server_status != old_status) or (task_state != old_task_state):
            LOG.info('State transition "%s" ==> "%s" after %d second wait',
                     '/'.join((old_status, str(old_task_state))),
                     '/'.join((server_status, str(task_state))),
                     time.time() - start_time)
        if (server_status == 'ERROR') and raise_on_error:
            if 'fault' in body:
                raise exceptions.BuildErrorException(body['fault'],
                                                     server_id=server_id)
            else:
                raise exceptions.BuildErrorException(server_id=server_id)

        timed_out = int(time.time()) - start_time >= timeout

        if timed_out:
            expected_task_state = 'None' if ready_wait else 'n/a'
            message = ('Server %(server_id)s failed to reach %(status)s '
                       'status and task state "%(expected_task_state)s" '
                       'within the required time (%(timeout)s s).' %
                       {'server_id': server_id,
                        'status': status,
                        'expected_task_state': expected_task_state,
                        'timeout': timeout})
            message += ' Current status: %s.' % server_status
            message += ' Current task state: %s.' % task_state
            caller = misc_utils.find_test_caller()
            if caller:
                message = '(%s) %s' % (caller, message)
            raise exceptions.TimeoutException(message)
        old_status = server_status
        old_task_state = task_state


def wait_for_server_termination(client, server_id, ignore_error=False):
    """Waits for server to reach termination."""
    start_time = int(time.time())
    while True:
        try:
            body = client.show_server(server_id)
        except lib_exc.NotFound:
            return

        server_status = body['status']
        if server_status == 'ERROR' and not ignore_error:
            raise exceptions.BuildErrorException(server_id=server_id)

        if int(time.time()) - start_time >= client.build_timeout:
            raise exceptions.TimeoutException

        time.sleep(client.build_interval)


def wait_for_image_status(client, image_id, status):
    """Waits for an image to reach a given status.

    The client should have a show_image(image_id) method to get the image.
    The client should also have build_interval and build_timeout attributes.
    """
    image = client.show_image(image_id)
    # Compute image client return response wrapped in 'image' element
    # which is not case with glance image client.
    if 'image' in image:
        image = image['image']
    start = int(time.time())

    while image['status'] != status:
        time.sleep(client.build_interval)
        image = client.show_image(image_id)
        # Compute image client return response wrapped in 'image' element
        # which is not case with glance image client.
        if 'image' in image:
            image = image['image']
        status_curr = image['status']
        if status_curr == 'ERROR':
            raise exceptions.AddImageException(image_id=image_id)

        # check the status again to avoid a false negative where we hit
        # the timeout at the same time that the image reached the expected
        # status
        if status_curr == status:
            return

        if int(time.time()) - start >= client.build_timeout:
            message = ('Image %(image_id)s failed to reach %(status)s state'
                       '(current state %(status_curr)s) '
                       'within the required time (%(timeout)s s).' %
                       {'image_id': image_id,
                        'status': status,
                        'status_curr': status_curr,
                        'timeout': client.build_timeout})
            caller = misc_utils.find_test_caller()
            if caller:
                message = '(%s) %s' % (caller, message)
            raise exceptions.TimeoutException(message)


def wait_for_volume_status(client, volume_id, status):
    """Waits for a Volume to reach a given status."""
    body = client.show_volume(volume_id)
    if 'volume' in body:
        body = body['volume']
    volume_status = body['status']
    start = int(time.time())

    while volume_status != status:
        time.sleep(client.build_interval)
        body = client.show_volume(volume_id)
        # TODO(jswarren) always extract 'volume' value
        # once the compute clients also return the full
        # response.
        if 'volume' in body:
            body = body['volume']
        volume_status = body['status']
        if volume_status == 'error':
            raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
        if volume_status == 'error_restoring':
            raise exceptions.VolumeRestoreErrorException(volume_id=volume_id)

        if int(time.time()) - start >= client.build_timeout:
            message = ('Volume %s failed to reach %s status (current %s) '
                       'within the required time (%s s).' %
                       (volume_id, status, volume_status,
                        client.build_timeout))
            raise exceptions.TimeoutException(message)


def wait_for_bm_node_status(client, node_id, attr, status):
    """Waits for a baremetal node attribute to reach given status.

    The client should have a show_node(node_uuid) method to get the node.
    """
    _, node = client.show_node(node_id)
    start = int(time.time())

    while node[attr] != status:
        time.sleep(client.build_interval)
        _, node = client.show_node(node_id)
        status_curr = node[attr]
        if status_curr == status:
            return

        if int(time.time()) - start >= client.build_timeout:
            message = ('Node %(node_id)s failed to reach %(attr)s=%(status)s '
                       'within the required time (%(timeout)s s).' %
                       {'node_id': node_id,
                        'attr': attr,
                        'status': status,
                        'timeout': client.build_timeout})
            message += ' Current state of %s: %s.' % (attr, status_curr)
            caller = misc_utils.find_test_caller()
            if caller:
                message = '(%s) %s' % (caller, message)
            raise exceptions.TimeoutException(message)
