#
# Copyright IBM Corp. 2019-
# SPDX-License-Identifier: MIT
#

from __future__ import annotations
import os
from abc import ABC, abstractmethod
from appium.options.android import UiAutomator2Options
from appium.options.common import AppiumOptions
from appium.options.ios import XCUITestOptions
from appium.webdriver.webdriver import WebDriver as MobileRemote
from appium.webdriver import Remote as NativeRemote
from selenium.webdriver.remote.webdriver import WebDriver as Remote
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.chrome.webdriver import WebDriver as Chrome
from selenium.webdriver.common.options import ArgOptions
from selenium.webdriver.edge.options import Options as EdgeOptions
from selenium.webdriver.edge.service import Service as EdgeService
from selenium.webdriver.edge.webdriver import WebDriver as Edge
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.firefox.service import Service as FirefoxService
from selenium.webdriver.firefox.webdriver import WebDriver as Firefox
from selenium.webdriver.ie.options import Options as IeOptions
from selenium.webdriver.ie.service import Service as IeService
from selenium.webdriver.ie.webdriver import WebDriver as Ie
from selenium.webdriver.safari.options import Options as SafariOptions
from selenium.webdriver.safari.service import Service as SafariService
from selenium.webdriver.safari.webdriver import WebDriver as Safari

from ..config import common_config as config
from ..config import local_config
from ..config import saucelabs_config
from .browsers import Browser
from .operating_system import OperatingSystem, SaucelabsOperatingSystem


class DriverFactory(ABC):

    """
    Abstract parent factory for all driver factories.
    """

    @staticmethod
    def create_driver_factory(spec_name: str, suite_id: str) -> DriverFactory:
        if config.get_platform().is_remote():
            return SaucelabsDriverFactory(spec_name, suite_id)
        elif config.get_platform().is_local():
            return LocalDriverFactory()
        else:
            raise RuntimeError("Given driver options are not supported")

    @abstractmethod
    def create_driver(self) -> Remote:
        pass

    def _create_browser_options(self) -> ArgOptions | AppiumOptions:
        if config.get_operating_system().is_mobile():
            operating_system = config.get_operating_system()
            return {
                OperatingSystem.ANDROID: UiAutomator2Options,
                OperatingSystem.IOS: XCUITestOptions,
            }[operating_system]()
        else:
            browser = config.get_browser()
            return {
                Browser.CHROME: ChromeOptions,
                Browser.EDGE: EdgeOptions,
                Browser.FIREFOX: FirefoxOptions,
                Browser.INTERNET_EXPLORER: IeOptions,
                Browser.OPERA: ChromeOptions,
                Browser.SAFARI: SafariOptions,
            }[browser]()


class LocalDriverFactory(DriverFactory):

    """
    This factory creates drivers for a browser which is installed on the machine of the user.
    """

    def create_driver(self) -> Remote:
        """Creates and returns a driver for local environment."""
        if config.get_operating_system().is_desktop():
            return self._create_desktop_driver()
        elif config.get_operating_system().is_mobile():
            return self._create_mobile_driver()
        else:
            raise RuntimeError(f"Operating system {config.get_operating_system()} is not supported")

    def _create_desktop_driver(self) -> Remote:
        browser = config.get_browser()
        operating_system = config.get_operating_system()
        assert browser.is_supported(operating_system), f"Browser {browser} not supported by {operating_system}."
        browser_options = self._create_options()
        executable_path = config.get_driver_executable_path()
        service = {
            Browser.CHROME: lambda: ChromeService(executable_path=executable_path),
            Browser.EDGE: lambda: EdgeService(executable_path=executable_path),
            Browser.FIREFOX: lambda: FirefoxService(executable_path=executable_path, log_output=self._firefox_log_path()),
            Browser.INTERNET_EXPLORER: lambda: IeService(executable_path=executable_path),
            Browser.OPERA: lambda: ChromeService(executable_path=executable_path),
            Browser.SAFARI: lambda: SafariService() # Driver executable for Mac/Safari comes pre-installed
        }[browser]()
        driver: Remote = None
        driver = {
            Browser.CHROME: lambda: Chrome(service=service, options=browser_options),
            Browser.EDGE: lambda: Edge(service=service, options=browser_options),
            Browser.FIREFOX: lambda: Firefox(service=service, options=browser_options),
            Browser.INTERNET_EXPLORER: lambda: Ie(service=service, options=browser_options),
            Browser.OPERA: lambda: Chrome(service=service, options=browser_options),
            Browser.SAFARI: lambda: Safari(service=service, options=browser_options)
        }[browser]()
        driver.implicitly_wait(config.get_implicit_timeout())
        driver.set_page_load_timeout(config.get_page_load_timeout())
        return driver

    def _create_mobile_driver(self) -> MobileRemote:
        operating_system = config.get_operating_system()
        browser = config.get_browser()
        assert browser.is_supported(operating_system), f"Browser {browser} not supported by {operating_system}."
        capabilities = {
            OperatingSystem.IOS: self._create_ios_capabilities,
            OperatingSystem.ANDROID: self._create_android_capabilities,
        }[operating_system]()
        options = self._create_browser_options()
        capabilities_options = options.load_capabilities(capabilities)
        driver = MobileRemote(local_config.get_mobile_appium_server_url(), options=capabilities_options)
        driver.implicitly_wait(config.get_implicit_timeout())
        driver.set_page_load_timeout(config.get_page_load_timeout())
        return driver

    def _create_options(self) -> ArgOptions:
        browser = config.get_browser()
        options: ArgOptions = None
        if browser == Browser.CHROME:
            options = self._create_chrome_options()
        elif browser == Browser.FIREFOX:
            options = self._create_firefox_options()
        elif browser == Browser.OPERA:
            options = self._create_chrome_options()
            # following is needed for Opera to run
            options.add_experimental_option('w3c', True)
        else:
            options = self._create_browser_options()
        for arg in config.get_custom_args():
            options.add_argument(arg)
        return options

    def _create_firefox_options(self) -> FirefoxOptions:
        options = FirefoxOptions()
        if config.is_headless():
            options.add_argument('-headless')
        return options

    def _create_chrome_options(self) -> ChromeOptions:
        options = ChromeOptions()
        options.add_argument('--no-sandbox')
        options.add_argument('--hide-scrollbars')
        options.add_argument('--disable-extensions')
        if config.is_headless():
            # to run chrome headless in a docker container without any GUI environment
            options.add_argument('--headless')
            options.add_argument('--disable-dev-shm-usage')
            options.add_argument('--start-maximized')
        return options

    def _firefox_log_path(self) -> str:
        project_root = os.environ.get("GAUGE_PROJECT_ROOT")
        return os.path.join(project_root, "logs", "geckodriver.log")

    def _create_android_capabilities(self) -> dict:
        capabilities = {
            "deviceName": local_config.get_mobile_device_name(),
            "platformName": OperatingSystem.ANDROID.value,
            "platformVersion": config.get_operating_system_version(),
            "browserName": config.get_browser().value,
            "nativeWebScreenshot": "true",
            "goog:chromeOptions": {
                "w3c": True,
                "extensions": []
            }
        }
        if local_config.get_mobile_real_device():
            # a real device needs a UDID instead of a name
            capabilities["udid"] = local_config.get_mobile_device_udid()
        custom_args = config.get_custom_args()
        if custom_args:
            capabilities['goog:chromeOptions']['args'] = custom_args
        return capabilities

    def _create_ios_capabilities(self) -> dict:
        capabilities = {
            "automationName": "XCUITest",
            "deviceName": local_config.get_mobile_device_name(),
            "platformName": OperatingSystem.IOS.value,
            "platformVersion": config.get_operating_system_version(),
            "browserName": Browser.SAFARI.value
        }
        custom_args = config.get_custom_args()
        if custom_args:
            # not sure, if this works with safari. Documentation is scarce
            capabilities['safariOptions'] = {}
            capabilities['safariOptions']['args'] = custom_args
        return capabilities


class RemoteDriverFactory(DriverFactory):

    """
    This factory creates remote drivers for browsers. It is not bound to any specific proved of an environment.
    """

    def create_driver(self) -> Remote:
        options = self._create_options()
        return Remote(command_executor=local_config.get_mobile_appium_server_url(), options=options)

    def _create_options(self) -> ArgOptions:
        browser = config.get_browser()
        headless = config.is_headless()
        options = self._create_browser_options()
        if browser == Browser.CHROME or browser == Browser.OPERA:
            options.add_argument('--no-sandbox')
            options.add_argument('--hide-scrollbars')
            options.add_argument('--disable-extensions')
            if headless:
                # to run chrome headless in a docker container without any GUI environment
                options.add_argument('--headless')
                options.add_argument('--disable-dev-shm-usage')
                options.add_argument('--start-maximized')
            if browser == Browser.OPERA:
                options.add_experimental_option('w3c', True)
        elif browser == Browser.EDGE and headless:
            options.add_argument('--headless')
        elif browser == Browser.FIREFOX and headless:
            options.add_argument('-headless')
        return options


class SaucelabsDriverFactory(DriverFactory):
    """
    This factory creates drivers for the remote Saucelabs environment.
    """

    def __init__(self, spec_name: str, suite_id: str) -> None:
        self.spec_name = spec_name
        self.suite_id = suite_id

    def create_driver(self) -> Remote:
        """Creates and returns a driver for https://saucelabs.com/"""
        operating_system = config.get_operating_system()
        if operating_system.is_desktop():
            return self._create_desktop_driver()
        elif operating_system.is_mobile():
            return self._create_mobile_driver()
        else:
            raise RuntimeError(f"Operating system {operating_system} is not supported")

    def _create_desktop_driver(self) -> Remote:
        operating_system = config.get_operating_system()
        browser = config.get_browser()
        assert browser.is_supported(operating_system), f"Browser {browser} not supported by {operating_system}."
        browser_options = self._create_browser_options()
        browser_options.platform_name = self._get_platform_name(operating_system, config.get_operating_system_version())
        browser_options.browser_version = saucelabs_config.get_browser_version()
        sauce_options = self._get_sauce_options()
        browser_options.set_capability("sauce:options", sauce_options)
        for arg in config.get_custom_args():
            browser_options.add_argument(arg)
        driver = Remote(saucelabs_config.get_executor(), options=browser_options)
        driver.implicitly_wait(config.get_implicit_timeout())
        driver.set_page_load_timeout(config.get_page_load_timeout())
        return driver

    def _create_mobile_driver(self) -> MobileRemote | NativeRemote:
        operating_system = config.get_operating_system()
        capabilities = {
            'platformName': operating_system.value,
            'appium:deviceName': saucelabs_config.get_device_name(),
            'appium:platformVersion': config.get_operating_system_version(),
            'sauce:options': self._get_sauce_options()
        }
        options = self._create_browser_options()
        app_test = config.is_app_test()
        if app_test:
            app_location = config.get_app_location()
            if app_location:
                capabilities['appium:app'] = app_location
            app_package = config.get_app_package()
            if app_package:
                capabilities['appium:appPackage'] = app_package
            app_activity = config.get_app_activity()
            if app_activity:
                capabilities['appium:appActivity'] = app_activity
            capabilities['enableMultiWindows'] = True
        else:
            browser = config.get_browser()
            capabilities['browserName'] = browser.value
        auto_grant_permissions = config.is_auto_grant_permissions() and app_test
        custom_args = config.get_custom_args()
        if operating_system == OperatingSystem.ANDROID:
            capabilities['appium:automationName'] = 'UiAutomator2'
            capabilities["goog:chromeOptions"] = {
                'w3c': True,
                'extensions': []
            }
            if custom_args:
                capabilities['goog:chromeOptions']['args'] = custom_args
            if auto_grant_permissions:
                capabilities['appium:autoGrantPermissions'] = True
        elif operating_system == OperatingSystem.IOS:
            capabilities["appium:automationName"] = "XCUITest"
            capabilities["appium:includeSafariInWebviews"] = True
            capabilities["safariAllowPopups"] = True
            if custom_args:
                # not sure, if this works with safari. Documentation is scarce
                capabilities['safariOptions'] = {}
                capabilities['safariOptions']['args'] = custom_args
            if auto_grant_permissions:
                capabilities['appium:autoAcceptAlerts'] = True
        capabilities_options = options.load_capabilities(capabilities)
        if app_test:
            driver = NativeRemote(saucelabs_config.get_executor(), options=capabilities_options, keep_alive=True)
            if operating_system == OperatingSystem.IOS:
                driver.set_page_load_timeout(config.get_page_load_timeout())
        else:
            driver = MobileRemote(saucelabs_config.get_executor(), options=capabilities_options)
            driver.set_page_load_timeout(config.get_page_load_timeout())
        driver.implicitly_wait(config.get_implicit_timeout())
        return driver

    def _get_platform_name(self, operating_system: OperatingSystem, operating_system_version: str) -> str:
        if operating_system == OperatingSystem.WINDOWS:
            return self._get_windows_platform_name(operating_system_version)
        elif operating_system == OperatingSystem.MACOS:
            return self._get_macos_platform_name(operating_system_version)
        else:
            raise RuntimeError(f"Cannot determine platform name for {operating_system}")

    def _get_windows_platform_name(self, operating_system_version: str) -> str:
        version = {
            "11": SaucelabsOperatingSystem.WINDOWS_11,
            "10": SaucelabsOperatingSystem.WINDOWS_10,
            "8.1": SaucelabsOperatingSystem.WINDOWS_8_1,
            "8": SaucelabsOperatingSystem.WINDOWS_8,
            "7": SaucelabsOperatingSystem.WINDOWS_7,
        }
        return version.get(operating_system_version, SaucelabsOperatingSystem.WINDOWS_DEFAULT).value

    def _get_macos_platform_name(self, operating_system_version: str) -> str:
        if operating_system_version is None:
            return SaucelabsOperatingSystem.MACOS_DEFAULT.value
        operating_system_version = operating_system_version.lower()
        versions = {
            "14": SaucelabsOperatingSystem.MACOS_14,
            "sonoma": SaucelabsOperatingSystem.MACOS_14,
            "13": SaucelabsOperatingSystem.MACOS_13,
            "ventura": SaucelabsOperatingSystem.MACOS_13,
            "12": SaucelabsOperatingSystem.MACOS_12,
            "monterey": SaucelabsOperatingSystem.MACOS_12,
            "11": SaucelabsOperatingSystem.MACOS_11,
            "big sur": SaucelabsOperatingSystem.MACOS_11,
            "big_sur": SaucelabsOperatingSystem.MACOS_11,
            "big-sur": SaucelabsOperatingSystem.MACOS_11,
            "10.15": SaucelabsOperatingSystem.MACOS_10_15,
            "10": SaucelabsOperatingSystem.MACOS_10_15,
            "catalina": SaucelabsOperatingSystem.MACOS_10_15,
            "10.14": SaucelabsOperatingSystem.MACOS_10_14,
            "mojave": SaucelabsOperatingSystem.MACOS_10_14,
            "10.13": SaucelabsOperatingSystem.MACOS_10_13,
            "high sierra": SaucelabsOperatingSystem.MACOS_10_13,
            "high_sierra": SaucelabsOperatingSystem.MACOS_10_13,
            "high-sierra": SaucelabsOperatingSystem.MACOS_10_13,
            "10.12": SaucelabsOperatingSystem.MACOS_10_12,
            "sierra": SaucelabsOperatingSystem.MACOS_10_12,
            "10.11": SaucelabsOperatingSystem.MACOS_10_11,
            "el capitan": SaucelabsOperatingSystem.MACOS_10_11,
            "el_capitan": SaucelabsOperatingSystem.MACOS_10_11,
            "el-capitan": SaucelabsOperatingSystem.MACOS_10_11,
            "10.10": SaucelabsOperatingSystem.MACOS_10_10,
            "yosemite": SaucelabsOperatingSystem.MACOS_10_10,
        }
        version = versions.get(operating_system_version)
        return operating_system_version if version is None else version.value

    def _get_sauce_options(self) -> dict:
        sauce_options = {
            "username": saucelabs_config.get_sauce_user_name(),
            "accessKey": saucelabs_config.get_sauce_access_key(),
            "networkCapture": True,
        }
        if config.is_app_test():
            sauce_options["resigningEnabled"] = True
        if saucelabs_config.is_extended_debugging_enabled():
            sauce_options["extendedDebugging"] = True
        operating_system = config.get_operating_system()
        appium_version = saucelabs_config.get_appium_version()
        if operating_system and operating_system.is_mobile() and appium_version:
            sauce_options["appiumVersion"] = appium_version
        tunnel_name = saucelabs_config.get_tunnel_name()
        if tunnel_name and saucelabs_config.is_sauce_tunnel_active():
            sauce_options["tunnelName"] = tunnel_name
        custom_test_title = saucelabs_config.get_test_title()
        spec_name = self.spec_name
        sauce_options["name"] = f"{spec_name} - {custom_test_title}" if custom_test_title else spec_name
        build = saucelabs_config.get_build()
        if build:
            sauce_options["build"] = build
        if saucelabs_config.is_device_cached():
            sauce_options["cacheId"] = self.suite_id
        return sauce_options
