# Lint as: python2, python3
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Bluetooth DBus API tests."""

from __future__ import absolute_import

import logging

import common
from autotest_lib.server.cros.bluetooth import bluetooth_adapter_tests

# Assigning local names for some frequently used long method names.
method_name = bluetooth_adapter_tests.method_name
_test_retry_and_log = bluetooth_adapter_tests.test_retry_and_log

DEFAULT_START_DELAY_SECS = 2
DEFAULT_HOLD_INTERVAL = 10
DEFAULT_HOLD_TIMEOUT = 60

# String representation of DBus exceptions
DBUS_ERRORS  = {
    'InProgress' : 'org.bluez.Error.InProgress: Operation already in progress',
    'NotReady' : 'org.bluez.Error.NotReady: Resource Not Ready',
    'Failed': {
        'discovery_start' : 'org.bluez.Error.Failed: No discovery started',
        'discovery_unpause' : 'org.bluez.Error.Failed: Discovery not paused'
              }
               }


class BluetoothDBusAPITests(bluetooth_adapter_tests.BluetoothAdapterTests):
    """Bluetooth DBus API Test

       These test verifies return values and functionality of various Bluetooth
       DBus APIs. It tests both success and failures cases of each API. It
       checks the following
       - Expected return value
       - Expected exceptions for negative cases
       - Expected change in Dbus variables
       - TODO Expected change in (hci) state of the adapter
    """

    def _reset_state(self):
        """ Reset adapter to a known state.
        These tests changes adapter state. This function resets the adapter
        to known state

        @returns True if reset was successful False otherwise

        """
        logging.debug("resetting state of the adapter")
        power_off = self._wait_till_power_off()
        power_on = self._wait_till_power_on()
        not_discovering = self._wait_till_discovery_stops()
        reset_results = {'power_off' : power_off,
                         'power_on' : power_on,
                         'not_discovering' : not_discovering}
        if not all(reset_results.values()):
            logging.error('_reset_state failed %s',reset_results)
            return False
        else:
            return True

    def _compare_error(self, actual, expected):
        """ Helper function to compare error and log. """
        if expected in actual:
            return True
        else:
            logging.debug("Expected error is %s Actual error is %s",expected,
                          actual)
            return False

    def _get_hci_state(self, msg=''):
        """ get state of bluetooth controller. """
        hci_state = self.log_flags(msg, self.get_dev_info()[3])
        logging.debug("hci_state is %s", hci_state)
        return hci_state

    def _wait_till_hci_state_inquiry(self):
        """ Wait till adapter is in INQUIRY state.

        @return: True if adapter does INQUIRY before timeout, False otherwise
        """
        return self._wait_for_condition(
            lambda: 'INQUIRY' in self._get_hci_state('Expecting INQUIRY'),
            method_name(),
            start_delay = DEFAULT_START_DELAY_SECS)

    def _wait_till_hci_state_no_inquiry_holds(self):
        """ Wait till adapter does not enter INQUIRY for a period of time

        @return : True if adapter is not in INQUIRY for a period of time before
                  timeout. Otherwise False.
        """
        return self._wait_till_condition_holds(
            lambda: 'INQUIRY' not in self._get_hci_state('Expecting NOINQUIRY'),
            method_name(),
            hold_interval = DEFAULT_HOLD_INTERVAL,
            timeout = DEFAULT_HOLD_TIMEOUT,
            start_delay = DEFAULT_START_DELAY_SECS)



    def _wait_till_discovery_stops(self, stop_discovery=True):
        """stop discovery if specified and wait for discovery to stop

        @params: stop_discovery: Specifies whether stop_discovery should be
                 executed
        @returns: True if discovery is stopped
        """
        if stop_discovery:
            self.bluetooth_facade.stop_discovery()
        is_not_discovering = self._wait_for_condition(
            lambda: not self.bluetooth_facade.is_discovering(),
            method_name())
        return is_not_discovering

    def _wait_till_discovery_starts(self, start_discovery=True):
        """start discovery if specified and wait for discovery to start

        @params: start_discovery: Specifies whether start_discovery should be
                 executed
        @returns: True if discovery is started
        """

        if start_discovery:
            self.bluetooth_facade.start_discovery()
        is_discovering = self._wait_for_condition(
            self.bluetooth_facade.is_discovering, method_name())
        return is_discovering

    def _wait_till_power_off(self):
        """power off the adapter and wait for it to be powered off

        @returns: True if adapter can be powered off
        """

        power_off = self.bluetooth_facade.set_powered(False)
        is_powered_off = self._wait_for_condition(
                lambda: not self.bluetooth_facade.is_powered_on(),
                method_name())
        return is_powered_off

    def _wait_till_power_on(self):
        """power on the adapter and wait for it to be powered on

        @returns: True if adapter can be powered on
        """
        power_on = self.bluetooth_facade.set_powered(True)
        is_powered_on = self._wait_for_condition(
            self.bluetooth_facade.is_powered_on, method_name())
        return is_powered_on


########################################################################
# dbus call : start_discovery
#
#####################################################
# Positive cases
# Case 1
# preconditions: Adapter powered on AND
#                Currently not discovering
# result: Success
######################################################
# Negative cases
#
# Case 1
# preconditions: Adapter powered off
# result: Failure
# error : NotReady
#
# Case 2
# precondition: Adapter power on AND
#               Currently discovering
# result: Failure
# error: Inprogress
#########################################################################

    @_test_retry_and_log(False)
    def test_dbus_start_discovery_success(self):
        """ Test success case of start_discovery call. """
        reset = self._reset_state()
        is_power_on = self._wait_till_power_on()
        is_not_discovering = self._wait_till_discovery_stops()

        start_discovery, error =  self.bluetooth_facade.start_discovery()

        is_discovering = self._wait_till_discovery_starts(start_discovery=False)
        inquiry_state = self._wait_till_hci_state_inquiry()

        self.results = {'reset' : reset,
                        'is_power_on' : is_power_on,
                        'is_not_discovering': is_not_discovering,
                        'start_discovery' : start_discovery,
                        'is_discovering': is_discovering,
                        'inquiry_state' : inquiry_state
                        }
        return all(self.results.values())

    @_test_retry_and_log(False)
    def test_dbus_start_discovery_fail_discovery_in_progress(self):
        """ Test Failure case of start_discovery call.

        start discovery when discovery is in progress and confirm it fails with
        'org.bluez.Error.InProgress: Operation already in progress'.
        """
        reset = self._reset_state()
        is_discovering = self._wait_till_discovery_starts()

        start_discovery, error =  self.bluetooth_facade.start_discovery()


        self.results = {'reset' : reset,
                        'is_discovering' : is_discovering,
                        'start_discovery_failed' : not start_discovery,
                        'error_matches' : self._compare_error(error,
                                                    DBUS_ERRORS['InProgress'])
        }
        return all(self.results.values())

    @_test_retry_and_log(False)
    def test_dbus_start_discovery_fail_power_off(self):
        """ Test Failure case of start_discovery call.

        start discovery when adapter is turned off and confirm it fails with
        'NotReady' : 'org.bluez.Error.NotReady: Resource Not Ready'.
        """
        reset = self._reset_state()
        is_power_off = self._wait_till_power_off()

        start_discovery, error =  self.bluetooth_facade.start_discovery()

        is_power_on = self._wait_till_power_on()
        self.results = {'reset' : reset,
                        'power_off' : is_power_off,
                        'start_discovery_failed' : not start_discovery,
                        'error_matches' : self._compare_error(error,
                                                    DBUS_ERRORS['NotReady']),
                        'power_on' : is_power_on}
        return all(self.results.values())


########################################################################
# dbus call : stop_discovery
#
#####################################################
# Positive cases
# Case 1
# preconditions: Adapter powered on AND
#                Currently discovering
# result: Success
#####################################################
# Negative cases
#
# Case 1
# preconditions: Adapter powered off
# result: Failure
# error : NotReady
#
# Case 2
# precondition: Adapter power on AND
#               Currently not discovering
# result: Failure
# error: Failed
#
#TODO
#Case 3  org.bluez.Error.NotAuthorized
#########################################################################

    @_test_retry_and_log(False)
    def test_dbus_stop_discovery_success(self):
        """ Test success case of stop_discovery call. """
        reset = self._reset_state()
        is_power_on = self._wait_till_power_on()
        is_discovering = self._wait_till_discovery_starts()

        stop_discovery, error =  self.bluetooth_facade.stop_discovery()
        is_not_discovering = self._wait_till_discovery_stops(
            stop_discovery=False)
        self._wait_till_hci_state_no_inquiry_holds()
        self.results = {'reset' : reset,
                        'is_power_on' : is_power_on,
                        'is_discovering': is_discovering,
                        'stop_discovery' : stop_discovery,
                        'is_not_discovering' : is_not_discovering}
        return all(self.results.values())

    @_test_retry_and_log(False)
    def test_dbus_stop_discovery_fail_discovery_not_in_progress(self):
        """ Test Failure case of stop_discovery call.

        stop discovery when discovery is not in progress and confirm it fails
        with 'org.bluez.Error.Failed: No discovery started'.
        """
        reset = self._reset_state()
        is_not_discovering = self._wait_till_discovery_stops()

        stop_discovery, error =  self.bluetooth_facade.stop_discovery()

        still_not_discovering = self._wait_till_discovery_stops(
            stop_discovery=False)

        self.results = {
            'reset' : reset,
            'is_not_discovering' : is_not_discovering,
            'stop_discovery_failed' : not stop_discovery,
            'error_matches' : self._compare_error(error,
                                DBUS_ERRORS['Failed']['discovery_start']),
            'still_not_discovering': still_not_discovering}
        return all(self.results.values())

    @_test_retry_and_log(False)
    def test_dbus_stop_discovery_fail_power_off(self):
        """ Test Failure case of stop_discovery call.

        stop discovery when adapter is turned off and confirm it fails with
        'NotReady' : 'org.bluez.Error.NotReady: Resource Not Ready'.
        """
        reset = self._reset_state()
        is_power_off = self._wait_till_power_off()

        stop_discovery, error =  self.bluetooth_facade.stop_discovery()

        is_power_on = self._wait_till_power_on()
        self.results = {'reset' : reset,
                        'is_power_off' : is_power_off,
                        'stop_discovery_failed' : not stop_discovery,
                        'error_matches' : self._compare_error(error,
                                                    DBUS_ERRORS['NotReady']),
                        'is_power_on' : is_power_on}
        return all(self.results.values())


########################################################################
# dbus call: get_suppported_capabilities
# arguments: None
# returns : The dictionary is following the format
#           {capability : value}, where:
#
#           string capability:  The supported capability under
#                       discussion.
#           variant value:      A more detailed description of
#                       the capability.
#####################################################
# Positive cases
# Case 1
# Precondition: Adapter Powered on
# results: Result dictionary returned
#
# Case 2
# Precondition: Adapter Powered Off
# result : Result dictionary returned
################################################################################

    @_test_retry_and_log(False)
    def test_dbus_get_supported_capabilities_success(self):
        """ Test success case of get_supported_capabilities call. """
        reset = self._reset_state()
        is_power_on = self._wait_till_power_on()

        capabilities, error = self.bluetooth_facade.get_supported_capabilities()
        logging.debug('supported capabilities is %s', capabilities)

        self.results = {'reset' : reset,
                        'is_power_on' : is_power_on,
                        'get_supported_capabilities': error is None
                        }
        return all(self.results.values())

    @_test_retry_and_log(False)
    def test_dbus_get_supported_capabilities_success_power_off(self):
        """ Test success case of get_supported_capabilities call.
        Call get_supported_capabilities call with adapter powered off and
        confirm that it succeeds
        """

        reset = self._reset_state()
        is_power_off = self._wait_till_power_off()

        capabilities, error = self.bluetooth_facade.get_supported_capabilities()
        logging.debug('supported capabilities is %s', capabilities)

        self.results = {'reset' : reset,
                        'is_power_off' : is_power_off,
                        'get_supported_capabilities': error is None,
                        }
        return all(self.results.values())
