#/usr/bin/env python3.4
#
# Copyright (C) 2016 The Android Open Source Project
#
# 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.
"""
This test script exercises different Bluetooth 5 specific scan scenarios.
It is expected that the second AndroidDevice is able to advertise.

This test script was designed with this setup in mind:
Shield box one: Android Device, Android Device
"""

from queue import Empty

from acts import asserts
from acts.test_decorators import test_tracker_info
from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
from acts.test_utils.bt.BleEnum import ScanSettingsPhy
from acts.test_utils.bt.BleEnum import ScanSettingsScanMode
from acts.test_utils.bt.bt_test_utils import batch_scan_result
from acts.test_utils.bt.bt_test_utils import cleanup_scanners_and_advertisers
from acts.test_utils.bt.bt_test_utils import generate_ble_advertise_objects
from acts.test_utils.bt.bt_test_utils import generate_ble_scan_objects
from acts.test_utils.bt.bt_test_utils import reset_bluetooth
from acts.test_utils.bt.bt_test_utils import scan_result
from acts.test_utils.bt.bt_test_utils import advertising_set_on_own_address_read
from acts.test_utils.bt.bt_test_utils import advertising_set_started
from acts import signals


class Bt5ScanTest(BluetoothBaseTest):
    default_timeout = 10
    max_scan_instances = 28
    report_delay = 2000
    scan_callbacks = []
    adv_callbacks = []
    active_scan_callback_list = []
    big_adv_data = {
        "includeDeviceName": True,
        "manufacturerData": [0x0123, "00112233445566778899AABBCCDDEE"],
        "manufacturerData2":
        [0x2540, [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0xFF]],
        "serviceData": [
            "b19d42dc-58ba-4b20-b6c1-6628e7d21de4",
            "00112233445566778899AABBCCDDEE"
        ],
        "serviceData2": [
            "000042dc-58ba-4b20-b6c1-6628e7d21de4",
            [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0xFF]
        ]
    }

    def __init__(self, controllers):
        BluetoothBaseTest.__init__(self, controllers)
        self.scn_ad = self.android_devices[0]
        self.adv_ad = self.android_devices[1]

    def setup_class(self):
        if not self.scn_ad.droid.bluetoothIsLeExtendedAdvertisingSupported():
            raise signals.TestSkipClass(
                "Scanner does not support LE Extended Advertising")

        if not self.adv_ad.droid.bluetoothIsLeExtendedAdvertisingSupported():
            raise signals.TestSkipClass(
                "Advertiser does not support LE Extended Advertising")

    def teardown_test(self):
        cleanup_scanners_and_advertisers(
            self.scn_ad, self.active_scan_callback_list, self.adv_ad, [])
        self.active_scan_callback_list = []

    def on_exception(self, test_name, begin_time):
        reset_bluetooth(self.android_devices)

    # This one does not relly test anything, but display very helpful
    # information that might help with debugging.
    @BluetoothBaseTest.bt_test_wrap
    @test_tracker_info(uuid='787e0877-269f-4b9b-acb0-b98a8bb3770a')
    def test_capabilities(self):
        """Test capabilities

        Test BT 5.0 scan scapabilities

        Steps:
        1. Test various vapabilities.

        Expected Result:
        Pass

        Returns:
          Pass if True
          Fail if False

        TAGS: BT5.0, Scanning
        Priority: 2
        """
        d = self.scn_ad.droid
        sup2M = d.bluetoothIsLe2MPhySupported()
        supCoded = d.bluetoothIsLeCodedPhySupported()
        supExt = d.bluetoothIsLeExtendedAdvertisingSupported()
        supPeriodic = d.bluetoothIsLePeriodicAdvertisingSupported()
        maxDataLen = d.bluetoothGetLeMaximumAdvertisingDataLength()
        self.log.info("Scanner capabilities:")
        self.log.info("LE 2M: " + str(sup2M) + ", LE Coded: " + str(
            supCoded) + ", LE Extended Advertising: " + str(
                supExt) + ", LE Periodic Advertising: " + str(supPeriodic) +
                      ", maximum advertising data length: " + str(maxDataLen))
        d = self.adv_ad.droid
        sup2M = d.bluetoothIsLe2MPhySupported()
        supCoded = d.bluetoothIsLeCodedPhySupported()
        supExt = d.bluetoothIsLeExtendedAdvertisingSupported()
        supPeriodic = d.bluetoothIsLePeriodicAdvertisingSupported()
        maxDataLen = d.bluetoothGetLeMaximumAdvertisingDataLength()
        self.log.info("Advertiser capabilities:")
        self.log.info("LE 2M: " + str(sup2M) + ", LE Coded: " + str(
            supCoded) + ", LE Extended Advertising: " + str(
                supExt) + ", LE Periodic Advertising: " + str(supPeriodic) +
                      ", maximum advertising data length: " + str(maxDataLen))
        return True

    @BluetoothBaseTest.bt_test_wrap
    @test_tracker_info(uuid='62d36679-bb91-465e-897f-2635433aac2f')
    def test_1m_1m_extended_scan(self):
        """Test scan on LE 1M PHY using LE 1M PHY as secondary.

        Tests test verify that device is able to receive extended advertising
        on 1M PHY when secondary is 1M PHY.

        Steps:
        1. Start advertising set on dut1
        2. Start scanning on dut0, scan filter set to advertiser's device name
        3. Try to find an event, expect found
        4. Stop advertising

        Expected Result:
        Scan finds a advertisement.

        Returns:
          Pass if True
          Fail if False

        TAGS: LE Advertising Extension, BT5, LE, Advertising, Scanning
        Priority: 1
        """
        adv_callback = self.adv_ad.droid.bleAdvSetGenCallback()
        self.adv_ad.droid.bleAdvSetStartAdvertisingSet({
            "connectable": True,
            "legacyMode": False,
            "primaryPhy": "PHY_LE_1M",
            "secondaryPhy": "PHY_LE_1M",
            "interval": 320
        }, self.big_adv_data, None, None, None, 0, 0, adv_callback)

        self.scn_ad.droid.bleSetScanSettingsLegacy(False)
        self.scn_ad.droid.bleSetScanSettingsPhy(
            ScanSettingsPhy.PHY_LE_1M.value)

        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
            self.scn_ad.droid)

        adv_device_name = self.adv_ad.droid.bluetoothGetLocalName()
        self.scn_ad.droid.bleSetScanFilterDeviceName(adv_device_name)
        self.scn_ad.droid.bleBuildScanFilter(filter_list)
        self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
                                          scan_callback)
        self.active_scan_callback_list.append(scan_callback)

        try:
            self.scn_ad.ed.pop_event(
                scan_result.format(scan_callback), self.default_timeout)
        except Empty:
            self.log.error("Scan result not found")
            self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
            return False

        self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
        return True

    @BluetoothBaseTest.bt_test_wrap
    @test_tracker_info(uuid='3e3c9757-f7b6-4d1d-a2d6-8e2330d1a18e')
    def test_1m_2m_extended_scan(self):
        """Test scan on LE 1M PHY using LE 2M PHY as secondary.

        Tests test verify that device is able to receive extended advertising
        on 1M PHY when secondary is 2M PHY.

        Steps:
        1. Start advertising set on dut1
        2. Start scanning on dut0, scan filter set to advertiser's device name
        3. Try to find an event, expect found
        4. Stop advertising

        Expected Result:
        Scan finds a advertisement.

        Returns:
          Pass if True
          Fail if False

        TAGS: LE Advertising Extension, BT5, LE, Advertising, Scanning
        Priority: 1
        """
        adv_callback = self.adv_ad.droid.bleAdvSetGenCallback()
        self.adv_ad.droid.bleAdvSetStartAdvertisingSet({
            "connectable": True,
            "legacyMode": False,
            "primaryPhy": "PHY_LE_1M",
            "secondaryPhy": "PHY_LE_2M",
            "interval": 320
        }, self.big_adv_data, None, None, None, 0, 0, adv_callback)

        self.scn_ad.droid.bleSetScanSettingsLegacy(False)
        self.scn_ad.droid.bleSetScanSettingsPhy(
            ScanSettingsPhy.PHY_LE_1M.value)

        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
            self.scn_ad.droid)

        adv_device_name = self.adv_ad.droid.bluetoothGetLocalName()
        self.scn_ad.droid.bleSetScanFilterDeviceName(adv_device_name)
        self.scn_ad.droid.bleBuildScanFilter(filter_list)
        self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
                                          scan_callback)
        self.active_scan_callback_list.append(scan_callback)

        try:
            self.scn_ad.ed.pop_event(
                scan_result.format(scan_callback), self.default_timeout)
        except Empty:
            self.log.error("Scan result not found")
            self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
            return False

        self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
        return True

    @BluetoothBaseTest.bt_test_wrap
    @test_tracker_info(uuid='236e9e5b-3853-4762-81ae-e88db03d74f3')
    def test_legacy_scan_result_raw_length(self):
        """Test that raw scan record data in legacy scan is 62 bytes long.

        This is required for compability with older apps that make this
        assumption.

        Steps:
        1. Start legacy advertising set on dut1
        2. Start scanning on dut0, scan filter set to advertiser's device name
        3. Try to find an event, expect found, verify scan recurd data length
        4. Stop advertising

        Expected Result:
        Scan finds a legacy advertisement of proper size

        Returns:
          Pass if True
          Fail if False

        TAGS: LE Advertising Extension, BT5, LE, Advertising, Scanning
        Priority: 1
        """
        adv_callback = self.adv_ad.droid.bleAdvSetGenCallback()
        self.adv_ad.droid.bleAdvSetStartAdvertisingSet({
            "connectable": True,
            "scannable": True,
            "legacyMode": True,
            "interval": 320
        }, {"includeDeviceName": True}, None, None, None, 0, 0, adv_callback)

        self.scn_ad.droid.bleSetScanSettingsLegacy(True)
        self.scn_ad.droid.bleSetScanSettingsPhy(
            ScanSettingsPhy.PHY_LE_1M.value)

        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
            self.scn_ad.droid)

        adv_device_name = self.adv_ad.droid.bluetoothGetLocalName()
        self.scn_ad.droid.bleSetScanFilterDeviceName(adv_device_name)
        self.scn_ad.droid.bleBuildScanFilter(filter_list)
        self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
                                          scan_callback)
        self.active_scan_callback_list.append(scan_callback)

        try:
            evt = self.scn_ad.ed.pop_event(
                scan_result.format(scan_callback), self.default_timeout)
            rawData = evt['data']['Result']['scanRecord']
            asserts.assert_true(62 == len(rawData.split(",")),
                                "Raw data should be 62 bytes long.")
        except Empty:
            self.log.error("Scan result not found")
            self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
            return False

        self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
        return True

    @BluetoothBaseTest.bt_test_wrap
    @test_tracker_info(uuid='04632d8d-4303-476f-8f83-52c16be3713a')
    def test_duration(self):
        """Test scanning duration

        Tests BT5.0 scanning duration

        Steps:
        1. Start advertising set
        2. Start 5.0 scan
        3. Scan for advertisement event

        Expected Result:
        Scan finds a legacy advertisement of proper size

        Returns:
          Pass if True
          Fail if False

        TAGS: BT5.0, LE, Advertising, Scanning
        Priority: 1
        """
        adv_callback = self.adv_ad.droid.bleAdvSetGenCallback()
        self.adv_ad.droid.bleAdvSetStartAdvertisingSet({
            "connectable": True,
            "legacyMode": False,
            "primaryPhy": "PHY_LE_1M",
            "secondaryPhy": "PHY_LE_2M",
            "interval": 320
        }, self.big_adv_data, None, None, None, 0, 0, adv_callback)

        self.scn_ad.droid.bleSetScanSettingsLegacy(False)
        self.scn_ad.droid.bleSetScanSettingsPhy(
            ScanSettingsPhy.PHY_LE_1M.value)

        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
            self.scn_ad.droid)

        adv_device_name = self.adv_ad.droid.bluetoothGetLocalName()
        self.scn_ad.droid.bleSetScanFilterDeviceName(adv_device_name)
        self.scn_ad.droid.bleBuildScanFilter(filter_list)
        self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
                                          scan_callback)
        self.active_scan_callback_list.append(scan_callback)

        try:
            self.scn_ad.ed.pop_event(
                scan_result.format(scan_callback), self.default_timeout)
        except Empty:
            self.log.error("Scan result not found")
            self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
            return False

        self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
        return True

    @BluetoothBaseTest.bt_test_wrap
    @test_tracker_info(uuid='a3704083-0f5c-4a46-b979-32ebc594d6ee')
    def test_anonymous_advertising(self):
        """Test anonymous advertising.

        Tests test verify that device is able to receive anonymous advertising
        on 1M PHY when secondary is 2M PHY.

        Steps:
        1. Start anonymous advertising set on dut1
        2. Start scanning on dut0, scan filter set to advertiser's device name
        3. Try to find an event, expect found
        4. Stop advertising

        Expected Result:
        Scan finds a advertisement.

        Returns:
          Pass if True
          Fail if False

        TAGS: LE Advertising Extension, BT5, LE, Advertising, Scanning
        Priority: 1
        """
        adv_callback = self.adv_ad.droid.bleAdvSetGenCallback()
        self.adv_ad.droid.bleAdvSetStartAdvertisingSet({
            "connectable": False,
            "anonymous": True,
            "legacyMode": False,
            "primaryPhy": "PHY_LE_1M",
            "secondaryPhy": "PHY_LE_2M",
            "interval": 320
        }, self.big_adv_data, None, None, None, 0, 0, adv_callback)

        self.scn_ad.droid.bleSetScanSettingsLegacy(False)
        self.scn_ad.droid.bleSetScanSettingsPhy(
            ScanSettingsPhy.PHY_LE_1M.value)

        filter_list, scan_settings, scan_callback = generate_ble_scan_objects(
            self.scn_ad.droid)

        adv_device_name = self.adv_ad.droid.bluetoothGetLocalName()
        self.scn_ad.droid.bleSetScanFilterDeviceName(adv_device_name)
        self.scn_ad.droid.bleBuildScanFilter(filter_list)
        self.scn_ad.droid.bleStartBleScan(filter_list, scan_settings,
                                          scan_callback)
        self.active_scan_callback_list.append(scan_callback)

        try:
            evt = self.scn_ad.ed.pop_event(
                scan_result.format(scan_callback), self.default_timeout)
            address = evt['data']['Result']['deviceInfo']['address']
            asserts.assert_true(
                '00:00:00:00:00:00' == address,
                "Anonymous address should be 00:00:00:00:00:00, but was " +
                str(address))
        except Empty:
            self.log.error("Scan result not found")
            self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
            return False

        self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
        return True

    @BluetoothBaseTest.bt_test_wrap
    @test_tracker_info(uuid='e3277355-eebf-4760-9502-e49a9289f6ab')
    def test_get_own_address(self):
        """Test obtaining own address for PTS.

        Test obtaining own address.

        Steps:
        1. Start advertising set dut1
        2. Grab address
        3. Stop advertising

        Expected Result:
        Callback with address is received.

        Returns:
          Pass if True
          Fail if False

        TAGS: LE Advertising Extension, BT5, LE, Advertising
        Priority: 1
        """
        adv_callback = self.adv_ad.droid.bleAdvSetGenCallback()
        self.adv_ad.droid.bleAdvSetStartAdvertisingSet({
            "connectable": False,
            "anonymous": True,
            "legacyMode": False,
            "primaryPhy": "PHY_LE_1M",
            "secondaryPhy": "PHY_LE_2M",
            "interval": 320
        }, self.big_adv_data, None, None, None, 0, 0, adv_callback)

        set_id = -1

        try:
            evt = self.adv_ad.ed.pop_event(
                advertising_set_started.format(adv_callback),
                self.default_timeout)
            self.log.info("data: " + str(evt['data']))
            set_id = evt['data']['setId']
        except Empty:
            self.log.error("did not receive the set started event!")
            self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
            return False

        self.adv_ad.droid.bleAdvSetGetOwnAddress(set_id)

        try:
            evt = self.adv_ad.ed.pop_event(
                advertising_set_on_own_address_read.format(set_id),
                self.default_timeout)
            address = evt['data']['address']
            self.log.info("Advertiser address is: " + str(address))
        except Empty:
            self.log.error("onOwnAddressRead not received.")
            self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
            return False

        self.adv_ad.droid.bleAdvSetStopAdvertisingSet(adv_callback)
        return True