#!/usr/bin/python3.4
#
#   Copyright 2017 - 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.

import json
import pprint
import queue
import threading
import time

from acts import asserts
from acts.test_decorators import test_tracker_info
from acts_contrib.test_utils.net import connectivity_const as cconsts
from acts_contrib.test_utils.wifi.aware import aware_const as aconsts
from acts_contrib.test_utils.wifi.aware import aware_test_utils as autils
from acts_contrib.test_utils.wifi.aware.AwareBaseTest import AwareBaseTest


class ThroughputTest(AwareBaseTest):
    """Set of tests for Wi-Fi Aware to measure latency of Aware operations."""

    SERVICE_NAME = "GoogleTestServiceXYZ"

    PASSPHRASE = "This is some random passphrase - very very secure!!"
    PASSPHRASE2 = "This is some random passphrase - very very secure - but diff!!"

    def request_network(self, dut, ns):
        """Request a Wi-Fi Aware network.

    Args:
      dut: Device
      ns: Network specifier
    Returns: the request key
    """
        network_req = {"TransportType": 5, "NetworkSpecifier": ns}
        return dut.droid.connectivityRequestWifiAwareNetwork(network_req)

    def run_iperf_single_ndp_aware_only(self, use_ib, results):
        """Measure iperf performance on a single NDP, with Aware enabled and no
    infrastructure connection - i.e. device is not associated to an AP.

    Args:
      use_ib: True to use in-band discovery, False to use out-of-band discovery.
      results: Dictionary into which to place test results.
    """
        init_dut = self.android_devices[0]
        resp_dut = self.android_devices[1]

        if use_ib:
            # note: Publisher = Responder, Subscribe = Initiator
            (resp_req_key, init_req_key, resp_aware_if, init_aware_if,
             resp_ipv6, init_ipv6) = autils.create_ib_ndp(
                 resp_dut, init_dut,
                 autils.create_discovery_config(
                     self.SERVICE_NAME, aconsts.PUBLISH_TYPE_UNSOLICITED),
                 autils.create_discovery_config(
                     self.SERVICE_NAME, aconsts.SUBSCRIBE_TYPE_PASSIVE),
                 self.device_startup_offset)
        else:
            (init_req_key, resp_req_key, init_aware_if, resp_aware_if,
             init_ipv6, resp_ipv6) = autils.create_oob_ndp(init_dut, resp_dut)
        self.log.info("Interface names: I=%s, R=%s", init_aware_if,
                      resp_aware_if)
        self.log.info("Interface addresses (IPv6): I=%s, R=%s", init_ipv6,
                      resp_ipv6)

        # Run iperf3
        result, data = init_dut.run_iperf_server("-D")
        asserts.assert_true(result, "Can't start iperf3 server")

        result, data = resp_dut.run_iperf_client("%s" % init_ipv6, "-6 -J")
        self.log.debug(data)
        asserts.assert_true(result,
                            "Failure starting/running iperf3 in client mode")
        self.log.debug(pprint.pformat(data))

        # clean-up
        resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
        init_dut.droid.connectivityUnregisterNetworkCallback(init_req_key)

        # Collect results
        data_json = json.loads("".join(data))
        if "error" in data_json:
            asserts.fail(
                "iperf run failed: %s" % data_json["error"], extras=data_json)
        results["tx_rate"] = data_json["end"]["sum_sent"]["bits_per_second"]
        results["rx_rate"] = data_json["end"]["sum_received"][
            "bits_per_second"]
        self.log.info("iPerf3: Sent = %d bps Received = %d bps",
                      results["tx_rate"], results["rx_rate"])

    def run_iperf(self, q, dut, peer_dut, peer_aware_if, dut_ipv6, port):
        """Runs iperf and places results in the queue.

    Args:
      q: The queue into which to place the results
      dut: The DUT on which to run the iperf server command.
      peer_dut: The DUT on which to run the iperf client command.
      peer_aware_if: The interface on the DUT.
      dut_ipv6: The IPv6 address of the server.
      port: The port to use for the server and client.
    """
        result, data = dut.run_iperf_server("-D -p %d" % port)
        asserts.assert_true(result, "Can't start iperf3 server")

        result, data = peer_dut.run_iperf_client("%s" % dut_ipv6,
                                                 "-6 -J -p %d" % port)
        self.log.debug(data)
        q.put((result, data))

    def run_iperf_max_ndp_aware_only(self, results):
        """Measure iperf performance on the max number of concurrent OOB NDPs, with
    Aware enabled and no infrastructure connection - i.e. device is not
    associated to an AP.

    Note: the test requires MAX_NDP + 1 devices to be validated. If these are
    not available the test will fail.

    Args:
      results: Dictionary into which to place test results.
    """
        dut = self.android_devices[0]

        # get max NDP: using first available device (assumes all devices are the
        # same)
        max_ndp = dut.aware_capabilities[aconsts.CAP_MAX_NDP_SESSIONS]
        asserts.assert_true(
            len(self.android_devices) > max_ndp,
            'Needed %d devices to run the test, have %d' %
            (max_ndp + 1, len(self.android_devices)))

        # create all NDPs
        dut_aware_if = None
        dut_ipv6 = None
        peers_aware_ifs = []
        peers_ipv6s = []
        dut_requests = []
        peers_requests = []
        for i in range(max_ndp):
            (init_req_key, resp_req_key, init_aware_if, resp_aware_if,
             init_ipv6, resp_ipv6) = autils.create_oob_ndp(
                 dut, self.android_devices[i + 1])
            self.log.info("Interface names: I=%s, R=%s", init_aware_if,
                          resp_aware_if)
            self.log.info("Interface addresses (IPv6): I=%s, R=%s", init_ipv6,
                          resp_ipv6)

            dut_requests.append(init_req_key)
            peers_requests.append(resp_req_key)
            if dut_aware_if is None:
                dut_aware_if = init_aware_if
            else:
                asserts.assert_equal(
                    dut_aware_if, init_aware_if,
                    "DUT (Initiator) interface changed on subsequent NDPs!?")
            if dut_ipv6 is None:
                dut_ipv6 = init_ipv6
            else:
                asserts.assert_equal(
                    dut_ipv6, init_ipv6,
                    "DUT (Initiator) IPv6 changed on subsequent NDPs!?")
            peers_aware_ifs.append(resp_aware_if)
            peers_ipv6s.append(resp_ipv6)

        # create threads, start them, and wait for all to finish
        base_port = 5000
        q = queue.Queue()
        threads = []
        for i in range(max_ndp):
            threads.append(
                threading.Thread(
                    target=self.run_iperf,
                    args=(q, dut, self.android_devices[i + 1],
                          peers_aware_ifs[i], dut_ipv6, base_port + i)))

        for thread in threads:
            thread.start()

        for thread in threads:
            thread.join()

        # cleanup
        for i in range(max_ndp):
            dut.droid.connectivityUnregisterNetworkCallback(dut_requests[i])
            self.android_devices[
                i + 1].droid.connectivityUnregisterNetworkCallback(
                    peers_requests[i])

        # collect data
        for i in range(max_ndp):
            results[i] = {}
            result, data = q.get()
            asserts.assert_true(
                result, "Failure starting/running iperf3 in client mode")
            self.log.debug(pprint.pformat(data))
            data_json = json.loads("".join(data))
            if "error" in data_json:
                asserts.fail(
                    "iperf run failed: %s" % data_json["error"],
                    extras=data_json)
            results[i]["tx_rate"] = data_json["end"]["sum_sent"][
                "bits_per_second"]
            results[i]["rx_rate"] = data_json["end"]["sum_received"][
                "bits_per_second"]
            self.log.info("iPerf3: Sent = %d bps Received = %d bps",
                          results[i]["tx_rate"], results[i]["rx_rate"])

    ########################################################################
    @test_tracker_info(uuid="a628ac08-7a71-4646-9258-8fcd4be6c586")
    def test_iperf_single_ndp_aware_only_ib(self):
        """Measure throughput using iperf on a single NDP, with Aware enabled and
    no infrastructure connection. Use in-band discovery."""
        results = {}
        self.run_iperf_single_ndp_aware_only(use_ib=True, results=results)
        asserts.explicit_pass(
            "test_iperf_single_ndp_aware_only_ib passes", extras=results)

    @test_tracker_info(uuid="26d88e96-2318-4cff-85bb-7961a0b97802")
    def test_iperf_single_ndp_aware_only_oob(self):
        """Measure throughput using iperf on a single NDP, with Aware enabled and
    no infrastructure connection. Use out-of-band discovery."""
        results = {}
        self.run_iperf_single_ndp_aware_only(use_ib=False, results=results)
        asserts.explicit_pass(
            "test_iperf_single_ndp_aware_only_oob passes", extras=results)

    def test_iperf_max_ndp_aware_only_oob(self):
        """Measure throughput using iperf on all possible concurrent NDPs, with
    Aware enabled and no infrastructure connection. Use out-of-band discovery.
    """
        results = {}
        self.run_iperf_max_ndp_aware_only(results=results)
        asserts.explicit_pass(
            "test_iperf_max_ndp_aware_only_oob passes", extras=results)

    ########################################################################

    def run_iperf_max_ndi_aware_only(self, sec_configs, results):
        """Measure iperf performance on multiple NDPs between 2 devices using
    different security configurations (and hence different NDIs). Test with
    Aware enabled and no infrastructure connection - i.e. device is not
    associated to an AP.

    The security configuration can be:
    - None: open
    - String: passphrase
    - otherwise: PMK (byte array)

    Args:
      sec_configs: list of security configurations
      results: Dictionary into which to place test results.
    """
        init_dut = self.android_devices[0]
        init_dut.pretty_name = "Initiator"
        resp_dut = self.android_devices[1]
        resp_dut.pretty_name = "Responder"

        asserts.skip_if(
            init_dut.aware_capabilities[aconsts.CAP_MAX_NDI_INTERFACES] <
            len(sec_configs)
            or resp_dut.aware_capabilities[aconsts.CAP_MAX_NDI_INTERFACES] <
            len(sec_configs),
            "Initiator or Responder do not support multiple NDIs")

        init_id, init_mac = autils.attach_with_identity(init_dut)
        resp_id, resp_mac = autils.attach_with_identity(resp_dut)

        # wait for for devices to synchronize with each other - there are no other
        # mechanisms to make sure this happens for OOB discovery (except retrying
        # to execute the data-path request)
        time.sleep(autils.WAIT_FOR_CLUSTER)

        resp_req_keys = []
        init_req_keys = []
        resp_aware_ifs = []
        init_aware_ifs = []
        resp_aware_ipv6s = []
        init_aware_ipv6s = []

        for sec in sec_configs:
            # Responder: request network
            resp_req_key = autils.request_network(
                resp_dut,
                autils.get_network_specifier(resp_dut, resp_id,
                                             aconsts.DATA_PATH_RESPONDER,
                                             init_mac, sec))
            resp_req_keys.append(resp_req_key)

            # Initiator: request network
            init_req_key = autils.request_network(
                init_dut,
                autils.get_network_specifier(init_dut, init_id,
                                             aconsts.DATA_PATH_INITIATOR,
                                             resp_mac, sec))
            init_req_keys.append(init_req_key)

            # Wait for network
            init_net_event_nc = autils.wait_for_event_with_keys(
                init_dut, cconsts.EVENT_NETWORK_CALLBACK,
                autils.EVENT_NDP_TIMEOUT,
                (cconsts.NETWORK_CB_KEY_EVENT,
                 cconsts.NETWORK_CB_CAPABILITIES_CHANGED),
                (cconsts.NETWORK_CB_KEY_ID, init_req_key))
            resp_net_event_nc = autils.wait_for_event_with_keys(
                resp_dut, cconsts.EVENT_NETWORK_CALLBACK,
                autils.EVENT_NDP_TIMEOUT,
                (cconsts.NETWORK_CB_KEY_EVENT,
                 cconsts.NETWORK_CB_CAPABILITIES_CHANGED),
                (cconsts.NETWORK_CB_KEY_ID, resp_req_key))

            # validate no leak of information
            asserts.assert_false(
                cconsts.NETWORK_CB_KEY_NETWORK_SPECIFIER in init_net_event_nc[
                    "data"], "Network specifier leak!")
            asserts.assert_false(
                cconsts.NETWORK_CB_KEY_NETWORK_SPECIFIER in resp_net_event_nc[
                    "data"], "Network specifier leak!")

            # note that Init <-> Resp since IPv6 are of peer's!
            resp_ipv6 = init_net_event_nc["data"][aconsts.NET_CAP_IPV6]
            init_ipv6 = resp_net_event_nc["data"][aconsts.NET_CAP_IPV6]

            init_net_event_lp = autils.wait_for_event_with_keys(
                init_dut, cconsts.EVENT_NETWORK_CALLBACK,
                autils.EVENT_NDP_TIMEOUT,
                (cconsts.NETWORK_CB_KEY_EVENT,
                 cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED),
                (cconsts.NETWORK_CB_KEY_ID, init_req_key))
            resp_net_event_lp = autils.wait_for_event_with_keys(
                resp_dut, cconsts.EVENT_NETWORK_CALLBACK,
                autils.EVENT_NDP_TIMEOUT,
                (cconsts.NETWORK_CB_KEY_EVENT,
                 cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED),
                (cconsts.NETWORK_CB_KEY_ID, resp_req_key))

            resp_aware_ifs.append(resp_net_event_lp["data"][
                cconsts.NETWORK_CB_KEY_INTERFACE_NAME])
            init_aware_ifs.append(init_net_event_lp["data"][
                cconsts.NETWORK_CB_KEY_INTERFACE_NAME])

            resp_aware_ipv6s.append(resp_ipv6)
            init_aware_ipv6s.append(init_ipv6)

        self.log.info("Initiator interfaces/ipv6: %s / %s", init_aware_ifs,
                      init_aware_ipv6s)
        self.log.info("Responder interfaces/ipv6: %s / %s", resp_aware_ifs,
                      resp_aware_ipv6s)

        # create threads, start them, and wait for all to finish
        base_port = 5000
        q = queue.Queue()
        threads = []
        for i in range(len(sec_configs)):
            threads.append(
                threading.Thread(
                    target=self.run_iperf,
                    args=(q, init_dut, resp_dut, resp_aware_ifs[i],
                          init_aware_ipv6s[i], base_port + i)))

        for thread in threads:
            thread.start()

        for thread in threads:
            thread.join()

        # release requests
        for resp_req_key in resp_req_keys:
            resp_dut.droid.connectivityUnregisterNetworkCallback(resp_req_key)
        for init_req_key in init_req_keys:
            init_dut.droid.connectivityUnregisterNetworkCallback(init_req_key)

        # collect data
        for i in range(len(sec_configs)):
            results[i] = {}
            result, data = q.get()
            asserts.assert_true(
                result, "Failure starting/running iperf3 in client mode")
            self.log.debug(pprint.pformat(data))
            data_json = json.loads("".join(data))
            if "error" in data_json:
                asserts.fail(
                    "iperf run failed: %s" % data_json["error"],
                    extras=data_json)
            results[i]["tx_rate"] = data_json["end"]["sum_sent"][
                "bits_per_second"]
            results[i]["rx_rate"] = data_json["end"]["sum_received"][
                "bits_per_second"]
            self.log.info("iPerf3: Sent = %d bps Received = %d bps",
                          results[i]["tx_rate"], results[i]["rx_rate"])

    @test_tracker_info(uuid="b66faaa5-f1cc-44dd-b22a-610f4fcaf2ca")
    def test_iperf_max_ndi_aware_only_passphrases(self):
        """Test throughput for multiple NDIs configured with different passphrases.
    """
        results = {}
        self.run_iperf_max_ndi_aware_only(
            [self.PASSPHRASE, self.PASSPHRASE2], results=results)
        asserts.explicit_pass(
            "test_iperf_max_ndi_aware_only_passphrases passes", extras=results)

    def run_test_traffic_latency_single_ndp_ib_aware_only_open(self):
        """Measure IPv6 traffic latency performance(ping) on NDP between 2 devices.
        Security config is open.
        """
        p_dut = self.android_devices[0]
        p_dut.pretty_name = "publisher"
        s_dut = self.android_devices[1]
        s_dut.pretty_name = "subscriber"
        ndp_info = autils.create_ib_ndp(p_dut,
                                        s_dut,
                                        autils.create_discovery_config(
                                            self.SERVICE_NAME, aconsts.PUBLISH_TYPE_UNSOLICITED),
                                        autils.create_discovery_config(
                                            self.SERVICE_NAME, aconsts.SUBSCRIBE_TYPE_PASSIVE),
                                        self.device_startup_offset)
        p_req_key = ndp_info[0]
        s_req_key = ndp_info[1]
        p_aware_if = ndp_info[2]
        s_aware_if = ndp_info[3]
        p_ipv6 = ndp_info[4]
        s_ipv6 = ndp_info[5]
        self.log.info("Interface names: P=%s, S=%s", p_aware_if, s_aware_if)
        self.log.info("Interface addresses (IPv6): P=%s, S=%s", p_ipv6, s_ipv6)
        self.log.info("Start ping %s from %s", s_ipv6, p_ipv6)
        latency_result = autils.run_ping6(p_dut, s_ipv6)
        self.log.info("The latency results are %s", latency_result)

    @test_tracker_info(uuid="8a1160fa-8ccf-4015-94dd-a0541793077a")
    def test_traffic_latency_single_ndp_ib_aware_only_open(self):
        """Test IPv6 traffic latency performance on NDP with security config is open.
        """
        self.run_test_traffic_latency_single_ndp_ib_aware_only_open()
