# noinspection PyPackageRequirements
import requests
import time
# noinspection PyPackageRequirements
import pytest
import re
from _pytest import runner
from tlib.base import TestHelper
import logging

DEVICE_UNPLUGGED = "disconnected"    # Device is not connected
DEVICE_OFFLINE = "offline"           # Device is connected but offline
DEVICE_UNAUTHORIZED = "unauthorized" # Unauthorized
DEVICE_ONLINE = "online"             # Device is connected and accessible

def is_webdriver_running(tlib_logger, adb_logger, log=True):
    """
    Function to validate if webdriver is running and a connection can be established

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param log: When true, log debugging information
    @type log: bool
    """
    try:
        if log: tlib_logger.debug("Checking if Webdriver is running")
        response = requests.get("http://localhost:8080/wd/hub/status", timeout=10)
    except requests.ConnectionError as e:
        if log: adb_logger.debug("Connection to Webdriver failed:\n%s" % e)
        return False

    return response.status_code == 200


def get_device_status(tlib_logger, adb_logger, serial_id):
    """
    Get status of a device using it's serial id\n
    Serial id can be either a ID (for devices connected to USB) or an IP and port (For devices connected via IP)

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param serial_id: Device's serial number
    @type serial_id: str
    """

    out = TestHelper.run_command(adb_logger, ["adb", "devices"], fail_on_error=False)
    if re.search(serial_id, out[0]) is None:
        tlib_logger.debug("Device {serial_id} is not connected".format(serial_id=serial_id))
        return DEVICE_UNPLUGGED
    elif re.search("{serial_id}\s+offline".format(serial_id=serial_id), out[0]) is not None:
        tlib_logger.debug("Device {serial_id} is offline".format(serial_id=serial_id))
        return DEVICE_OFFLINE
    elif re.search("{serial_id}\s+unauthorized".format(serial_id=serial_id), out[0]) is not None:
        tlib_logger.debug("Device {serial_id} is offline".format(serial_id=serial_id))
        return DEVICE_UNAUTHORIZED
    elif re.search("{serial_id}\s+device".format(serial_id=serial_id), out[0]) is not None:
        tlib_logger.debug("Device {serial_id} is online".format(serial_id=serial_id))
        return DEVICE_ONLINE
    else:
        tlib_logger.error("Unknown device status\n%s" % out[0])
        pytest.fail("Unknown device status\n%s" % out[0])


def setup_ip_connection(tlib_logger, adb_logger, serial_id):
    """
    Connects to an android device via IP and waits for the connection to be established.

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param serial_id: Device's serial number
    @type serial_id: str
    """
    # Validate is device is already connected
    tlib_logger.debug("Setting up IP connection to device {serial_id}".format(serial_id=serial_id))
    status = get_device_status(tlib_logger, adb_logger, serial_id)

    if status == DEVICE_ONLINE:
        tlib_logger.debug("Device is already online".format(serial_id=serial_id))
        return

    # If device is offline or unauthorized, disconnect
    if status == DEVICE_OFFLINE:
        tlib_logger.warn("Device {serial_id} is offline, disconnecting and retrying".format(serial_id=serial_id))
        terminate_ip_connection(tlib_logger, adb_logger, serial_id)
    elif status == DEVICE_UNAUTHORIZED:
        terminate_ip_connection(tlib_logger, adb_logger, serial_id)
        tlib_logger.error("Device {serial_id} is not authorized, aborting".format(serial_id=serial_id))
        pytest.fail("Device {serial_id} is not authorized, aborting".format(serial_id=serial_id))
    elif status == DEVICE_UNPLUGGED:
        tlib_logger.warn("Device {serial_id} is disconecting, resetting connection".format(serial_id=serial_id))
        terminate_ip_connection(tlib_logger, adb_logger, serial_id)

    timeout = 0.5

    #We wait up to 3 seconds
    for i in range(1, int(3 / timeout)):
        out = TestHelper.run_command(adb_logger, ["adb", "connect", serial_id])
        log_adb_output(adb_logger, out)

        if get_device_status(tlib_logger, adb_logger, serial_id) == DEVICE_ONLINE:
            tlib_logger.debug("Connected to device")
            return
        else:
            tlib_logger.debug("Not yet connected")
            time.sleep(timeout)

    #If we're here, it means connection failed
    adb_logger.error(r"Timeout while connecting to device {serial_id}\nADB output:\n{out}".format(serial_id=serial_id, out=out))
    # noinspection PyUnresolvedReferences
    pytest.fail(r"Timeout while connecting to device {serial_id}\nADB output:\n{out}".format(serial_id=serial_id, out=out))


def terminate_ip_connection(tlib_logger, adb_logger, serial_id):
    """
    Disconnects from device

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param serial_id: Device's serial number
    @type serial_id: str
    """
    tlib_logger.debug("Disconnecting from device {serial_id}".format(serial_id=serial_id))
    out = TestHelper.run_command(adb_logger, ["adb", "disconnect", serial_id])
    log_adb_output(adb_logger, out)


def close_webdriver(tlib_logger, adb_logger, serial_id):
    """
    Stops Webdriver app o the device

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param serial_id: Device's serial number
    @type serial_id: str
    """
    tlib_logger.debug("Closing Webdriver on {serial_id}".format(serial_id=serial_id))
    if serial_id is None or serial_id == "":
        out = TestHelper.run_command(adb_logger,
                "adb shell am force-stop org.openqa.selenium.android.app", shell=True)
    else:
        out = TestHelper.run_command(adb_logger,
                "adb -s %s shell am force-stop org.openqa.selenium.android.app" % serial_id, shell=True)
    log_adb_output(adb_logger, out)


def setup_port_forwarding(tlib_logger, adb_logger, serial_id):
    """
    Setup port forwarding between computer and device.

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param serial_id: Device's serial number
    @type serial_id: str
    """
    tlib_logger.info("Setting up port forwarding")
    if serial_id is None or serial_id == "":
        out = TestHelper.run_command(adb_logger, "adb forward tcp:8080 tcp:8080", shell=True)
    else:
        out = TestHelper.run_command(adb_logger, "adb -s %s forward tcp:8080 tcp:8080" % serial_id, shell=True)
    log_adb_output(adb_logger, out)


def teardown_port_forwarding(tlib_logger, adb_logger):
    """
    Terminates all port forwarding connections

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    """
    tlib_logger.info("Tearing down port forwarding")
    out = TestHelper.run_command(adb_logger, "adb forward --remove-all", shell=True)
    log_adb_output(adb_logger, out)

def start_adb_server(tlib_logger, adb_logger):
    """
    Stops adb on the machine\n
    This can be required by TeamCity so some folders are not locked

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    """
    tlib_logger.info("Starting ADB")
    out = TestHelper.run_command(adb_logger, ["adb", "start-server"], fail_on_error=False)
    log_adb_output(adb_logger, out)

def stop_adb_server(tlib_logger, adb_logger):
    """
    Stops adb on the machine\n
    This can be required by TeamCity so some folders are not locked

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    """
    tlib_logger.info("Stopping ADB")
    out = TestHelper.run_command(adb_logger, ["adb", "kill-server"], fail_on_error=False)
    log_adb_output(adb_logger, out)


def start_webdriver(tlib_logger, adb_logger, serial_id):
    """
    Starts Webdriver app on  the device

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param serial_id: Device's serial number
    @type serial_id: str
    """
    tlib_logger.info("Starting webdriver on the device")
    if serial_id is None or serial_id == "":
        out = TestHelper.run_command(adb_logger,
                "adb shell am start -a android.intent.action.MAIN -n org.openqa.selenium.android.app/.MainActivity -e debug true",
                shell=True)
    else:
        out = TestHelper.run_command(adb_logger,
                "adb -s %s shell am start -a android.intent.action.MAIN -n org.openqa.selenium.android.app/.MainActivity -e debug true" % serial_id,
                shell=True)
    log_adb_output(adb_logger, out)


def wait_for_connection_to_webdriver(tlib_logger, adb_logger):
    """
    Waits up to 3 seconds for a connection to Webdriver

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    """
    #Check connection to webdriver can be established. Retry 3 times
    tlib_logger.debug("Waiting for connection to Webdriver")
    timeout = 0.5
    for i in range(1, int(3 / timeout)):
        if is_webdriver_running(tlib_logger, adb_logger, False):
            tlib_logger.debug("Webdriver started successfully")
            break
        tlib_logger.debug("Can't connect to Webdriver, retrying in {timeout} seconds".format(timeout=timeout))
        time.sleep(timeout)

    if not is_webdriver_running(tlib_logger, adb_logger, False):
        tlib_logger.error("Couldn't start Webdriver. Make sure it's installed and running\n"
                     "See https://code.google.com/p/selenium/wiki/AndroidDriver#Setup_the_Emulator for more details")
        # noinspection PyUnresolvedReferences
        pytest.fail("Couldn't start Webdriver. Make sure it's installed and running\n"
                    "See https://code.google.com/p/selenium/wiki/AndroidDriver#Setup_the_Emulator for more details")


# noinspection PyUnresolvedReferences
def setup_webdriver(tlib_logger, adb_logger, serial_id):
    """
    Connects to a device and starts webdriver

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param serial_id: Device's serial number
    @type serial_id: str
    """
    #Try to connect to Webdriver and exit if success
    if is_webdriver_running(tlib_logger, adb_logger, log=False):
        tlib_logger.debug("Already connected to device")
        return

    tlib_logger.info("Connecting to Webdriver")

    #Connect to device and setup port forwarding.
    if TestHelper.is_valid_ip(serial_id):
        setup_ip_connection(tlib_logger, adb_logger, serial_id)

    setup_port_forwarding(tlib_logger, adb_logger, serial_id)

    #Try again to connect to Webdriver and exit if success
    if is_webdriver_running(tlib_logger, adb_logger):
        tlib_logger.debug("Connected to Webdriver")
        return

    # Webdriver not running, start it
    start_webdriver(tlib_logger, adb_logger, serial_id)

    #Ensure we're connected
    wait_for_connection_to_webdriver(tlib_logger, adb_logger)

    tlib_logger.info("Connection to Webdriver established")


# noinspection PyUnresolvedReferences
def teardown_webdriver(tlib_logger, adb_logger, serial_id):
    """
    Closes webdriver and disconnects from device

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param serial_id: Device's serial number
    @type serial_id: str
    """
    #close selenium WebDriver
    tlib_logger.info("Disconnecting from Webdriver")
    close_webdriver(tlib_logger, adb_logger, serial_id)

    # Terminates IP connection if an IP was given
    if TestHelper.is_valid_ip(serial_id):
        terminate_ip_connection(tlib_logger, adb_logger, serial_id)

    #Stop adb service so it won't lock files required by TeamCity
    try:
        stop_adb_server(tlib_logger, adb_logger)
    except runner.Failed:
        #If ADB fails to stop, don't abort test
        adb_logger.warn("Error stopping ADB server")

    tlib_logger.info("Disconnected from Webdriver")


# noinspection PyUnresolvedReferences
def start_selendroid_server(tlib_logger, adb_logger, app_path):
    """
    Start selendroid server and point to apk file

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: Adb logger
    @type adb_logger: logging.logger
    @param app_path: Location of the apk file
    @type app_path: str
    """
    tlib_logger.info("Starting selendroid server")
    stop_adb_server(tlib_logger, adb_logger)
    start_adb_server(tlib_logger, adb_logger)
    process = TestHelper.start_process(tlib_logger, "java -jar %s -aut %s" % (TestHelper.selendroid_server_jar(), app_path), shell=True)
    trial_left = 20
    while not is_selendroid_running(tlib_logger, adb_logger, False) and trial_left > 0:
        time.sleep(0.5)
        trial_left -= 1
    if trial_left > 0:
        return process
    return None


def is_selendroid_running(tlib_logger, adb_logger, log=True):
    """
    Function to validate if webdriver is running and a connection can be established

    @param tlib_logger: TLib logger
    @type tlib_logger: logging.logger
    @param adb_logger: ADB logger
    @type adb_logger: logging.logger
    @param log: When true, log debugging information
    @type log: bool
    """
    try:
        if log: tlib_logger.debug("Checking if Selendroid server is running")
        response = requests.get("http://localhost:4444/wd/hub/status", timeout=10)
    except requests.ConnectionError as e:
        if log: adb_logger.debug("Connection to Selendroid failed:\n%s" % e)
        return False

    return response.status_code == 200


def get_android_app_id():
    response = requests.get("http://localhost:4444/wd/hub/status")
    if not response:
        return None
    from jsonpath_rw import jsonpath
    apps = jsonpath(response.json(), '$.value.supportedApps[*].appId')
    for app in apps:
        if not app.startswith('io.selendroid.androiddriver'):
            return app
    return None

def log_adb_output(logger, out):
    """
    Logs ADB output

    @param logger: ADB logger
    @type logger: logging.logger
    """
    if out[0] is not None and out[0] != '':
        logger.debug("\n" + out[0])

    if out[1] is not None and out[1] != '':
        logger.debug("\n" + out[1])