#!/usr/bin/python3
# coding: utf-8

#################################################################
#
# Copyright (C) ИП Пуляев Григорий Васильевич, 2024
# email: rodegast@xmail.ru
# telegram: t.me/kybyc_meta_test
#
# Расширение добавляет поддержку браузера "Google Chrome"
# в платформу автоматизированного тестирования Meta Test.
#
# Этот файл НЕ является частью платформы Meta Test.
# Лицензия расширения - Apache License 2.0
# https://www.apache.org/licenses/LICENSE-2.0
#
#################################################################

import os

from lxml import etree
from functools import lru_cache

from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService

from core_brower import WebBrowser, install
from errors import InstallError, InstallExistsError, NetError
from parameter import CONFIG, LOG_TYPES, LOG_LEVEL, BROWSER_VERSION, ItemConfig
from utilites import Version, get_platform, get_api_data, get_api_json, x_path

__all__ = (
	"get_chromedriver",
)

@lru_cache
def get_latest_version_chromedriver():
	"""
	Возвращает последнюю стабильную версию chromedriver-а
	"""
	endpoint = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
	try:
		data = get_api_json(endpoint, timeout=CONFIG.REMOTE_TIMEOUT)
	except NetError:
		raise InstallError("Не удалось получить информацию о последней версии chromedriver:\n "+endpoint)
	except Exception as err:
		raise InstallError(err)
	if data:
		try:
			return data["channels"]["Stable"]["version"]
		except KeyError:
			raise InstallError("Невозможно получить последнюю версию chromedriver")
	raise InstallError("Невозможно получить последнюю версию chromedriver")

@lru_cache
def get_url_chromedriver(version, platform):
	"""
	Возвращает URL на скачивание драйвера
	"""
	short_version = ".".join(version.split(".")[:2])
	if Version(version) < Version(114):
		endpoint = "https://chromedriver.storage.googleapis.com/"
		try:
			data = get_api_data(endpoint, timeout=CONFIG.REMOTE_TIMEOUT)
		except NetError:
			raise InstallError("Не удалось получить информацию о chromedriver:\n "+endpoint)
		if data:
			data = tuple( x[0].text for x in etree.fromstring(data) if "Contents" in x.tag )
			data = sorted([ x for x in data if platform in x and x.startswith(short_version) ], key=lambda x: len(x.split("/")[0]))
			if data:
				return endpoint+data[-1]
	else:
		endpoint = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"
		try:
			data = get_api_json(endpoint, timeout=CONFIG.REMOTE_TIMEOUT)
		except NetError:
			raise InstallError("Не удалось получить информацию о chromedriver:\n "+endpoint)
		for x in ( x["downloads"]["chromedriver"] for x in data["versions"] if ".".join(x["version"].split(".")[:2]) == short_version and "chromedriver" in x["downloads"] ):
			for y in x:
				if y["platform"] == platform:
					return y["url"]
	raise InstallError("Невозможно получить chromedriver для версии "+version)

@lru_cache
def get_chromedriver(version, short_version, browser_name):
	"""
	Возвращает путь к файлу chromedriver-а
	"""
	if not CONFIG.CHROME_DRIVER_PATH or not os.path.isfile(CONFIG.CHROME_DRIVER_PATH):
		if version.upper() in BROWSER_VERSION:
			version = get_latest_version_chromedriver()
		platform      = get_platform()
		install_path  = os.path.join(CONFIG.BROWSERS_DIR, browser_name, short_version)
		driver_file   = os.path.join(install_path, "chromedriver")
		if platform == "win64":
			driver_file += ".exe"
		if not os.path.isfile(driver_file):
			url = get_url_chromedriver(version, platform)
			if platform == "win64":
				try:
					install(url, install_path, {"chromedriver-win64/chromedriver.exe": driver_file})
				except InstallExistsError as err:
					install(err.source, install_path, {"chromedriver.exe": driver_file})
			elif platform == "linux64":
				try:
					install(url, install_path, {"chromedriver-linux64/chromedriver": driver_file})
				except InstallExistsError as err:
					install(err.source, install_path, {"chromedriver": driver_file})
			else:
				try:
					install(url, install_path, {"chromedriver-mac-x64/chromedriver": driver_file})
				except InstallExistsError as err:
					install(err.source, install_path, {"chromedriver": driver_file})
		CONFIG.CHROME_DRIVER_PATH = x_path(driver_file)
	return CONFIG.CHROME_DRIVER_PATH

class Chrome(WebBrowser):
	@lru_cache
	def installed_versions(self):
		"""
		Возвращает описание локально установленных браузеров
		{"версия браузера": "путь к исполняемому файлу"}
		"""
		platform = get_platform()
		if platform == "linux64":
			return self.linux_installed_version(("google-chrome", "google-chrome-stable", "google-chrome-beta", "google-chrome-dev"))
		elif platform == "mac-x64":
			return self.mac_installed_version("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
		return self.windows_installed_version((
			r"PROGRAMFILES\\Google\\Chrome\\Application\\chrome.exe"
			, r"PROGRAMFILES(X86)\\Google\\Chrome\\Application\\chrome.exe"
			, r"LOCALAPPDATA\\Google\\Chrome\\Application\\chrome.exe"
			, r"PROGRAMW6432\\Google\\Chrome\\Application\\chrome.exe"
		))
	
	def linux_install_browser(self, install_path):
		"""
		Скачиваем и устанавливает браузер для linux
		Возвращает путь к исполняемому файлу
		"""
		chrome_path = os.path.join(install_path, "chrome-linux64", "chrome")
		if not os.path.isfile(chrome_path):
			url = "https://storage.googleapis.com/chrome-for-testing-public/%s/linux64/chrome-linux64.zip"%self.version
			install(url, install_path)
			x_path(chrome_path)
			x_path(os.path.join(install_path, "chrome-linux64", "chrome_sandbox"))
			x_path(os.path.join(install_path, "chrome-linux64", "chrome_crashpad_handler"))
		return x_path(chrome_path)
	
	def windows_install_browser(self, install_path):
		"""
		Скачиваем и устанавливает браузер для windows
		Возвращает путь к исполняемому файлу
		"""
		chrome_path = os.path.join(install_path, "chrome-win64", "chrome.exe")
		if not os.path.isfile(chrome_path):
			url = "https://storage.googleapis.com/chrome-for-testing-public/%s/win64/chrome-win64.zip"%self.version
			install(url, install_path)
		return chrome_path
	
	def mac_install_browser(self, install_path):
		"""
		Скачиваем и устанавливает браузер для mac-64
		Возвращает путь к исполняемому файлу
		"""
		chrome_path = os.path.join(install_path, "chrome-mac-x64", "Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing")
		if not os.path.isfile(chrome_path):
			url = "https://storage.googleapis.com/chrome-for-testing-public/%s/mac-x64/chrome-mac-x64.zip"%self.version
			install(url, install_path)
			x_path(chrome_path)
			app_path = os.path.join(install_path, "chrome-mac-x64", "Google Chrome for Testing.app"
				, "Contents", "Frameworks", "Google Chrome for Testing Framework.framework"
				, "Versions", self.version
			)
			x_path(os.path.join(app_path, "Google Chrome for Testing Framework"))
			x_path(os.path.join(app_path, "Libraries", "libEGL.dylib"))
			x_path(os.path.join(app_path, "Libraries", "libGLESv2.dylib"))
			x_path(os.path.join(app_path, "Libraries", "libvk_swiftshader.dylib"))
			app_path = x_path(os.path.join(app_path, "Helpers"))
			x_path(os.path.join(app_path, "chrome_crashpad_handler"))
			x_path(os.path.join(app_path, "app_mode_loader"))
			x_path(os.path.join(app_path, "web_app_shortcut_copier"))
			x_path(os.path.join(app_path, "Google Chrome for Testing Helper (Alerts).app"
				, "Contents", "MacOS", "Google Chrome for Testing Helper (Alerts)"
			))
			x_path(os.path.join(app_path, "Google Chrome for Testing Helper (GPU).app"
				, "Contents", "MacOS", "Google Chrome for Testing Helper (GPU)"
			))
			x_path(os.path.join(app_path, "Google Chrome for Testing Helper (Plugin).app"
				, "Contents", "MacOS", "Google Chrome for Testing Helper (Plugin)"
			))
			x_path(os.path.join(app_path, "Google Chrome for Testing Helper (Renderer).app"
				, "Contents", "MacOS", "Google Chrome for Testing Helper (Renderer)"
			))
			x_path(os.path.join(app_path, "Google Chrome for Testing Helper.app"
				, "Contents", "MacOS", "Google Chrome for Testing Helper"
			))
		return x_path(chrome_path)
	
	@lru_cache
	def available_versions(self):
		"""
		Возвращает список версий браузеров которые возможно установить
		"""
		endpoint = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions.json"
		try:
			data = get_api_json(endpoint, timeout=CONFIG.REMOTE_TIMEOUT)
		except NetError:
			raise InstallError("Не удалось получить список браузеров chrome:\n "+endpoint)
		except Exception:
			return []
		if data:
			return [ x["version"] for x in data.get("versions", []) if "version" in x ]
		return []
	
	def latest_version(self):
		"""
		Возвращает последнюю стабильную версию браузера
		"""
		endpoint = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json"
		try:
			data = get_api_json(endpoint, timeout=CONFIG.REMOTE_TIMEOUT)
		except NetError:
			raise InstallError("Не удалось получить список браузеров chrome:\n "+endpoint)
		except Exception:
			return ""
		if data:
			version = self.version.upper()
			try:
				if version == BROWSER_VERSION.BETA:
					return data["channels"]["Beta"]["version"]
				elif version == BROWSER_VERSION.DEV:
					return data["channels"]["Dev"]["version"]
				elif version == BROWSER_VERSION.CANARY:
					return data["channels"]["Canary"]["version"]
				return data["channels"]["Stable"]["version"]
			except KeyError:
				return ""
		return ""
	
	def make_options(self):
		"""
		Возвращает объект конфигурации драйвера
		"""
		# https://peter.sh/experiments/chromium-command-line-switches/
		# https://gist.github.com/dodying/34ea4760a699b47825a766051f47d43b
		options = webdriver.ChromeOptions()
		options.page_load_strategy = CONFIG.PAGE_LOAD_STRATEGY.lower()
		options.add_experimental_option("excludeSwitches", ["enable-logging", "enable-automation"])
		
		if self.extensions():
			if Version(self.version) >= Version(137):
				options.add_argument("--disable-features=DisableLoadExtensionCommandLineSwitch")
		else:
			options.add_argument("--disable-extensions")
			options.add_experimental_option("useAutomationExtension", False)
		
		platform = get_platform()
		if platform == "linux64":
			user_agent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0 Safari/537.36"
		elif platform == "mac-x64":
			user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0 Safari/537.36"
		else:
			user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0 Safari/537.36"
		options.add_argument("--user-agent='%s'"%(user_agent % self.short_version))
		
		options.add_argument("--silent")
		options.add_argument("--no-pings")
		options.add_argument("--no-sandbox")
		options.add_argument("--mute-audio")
		options.add_argument("--disable-fre")
		options.add_argument("--no-first-run")
		options.add_argument("--no-crash-upload")
		options.add_argument("--no-service-autorun")
		options.add_argument("--password-store=basic")
		options.add_argument("--disable-prompt-on-repost")
		options.add_argument("--disable-password-generation")
		options.add_argument("--disable-background-networking")
		options.add_argument("--ignore-ssl-errors=yes")
		options.add_argument("--ignore-certificate-errors")
		options.add_argument("--ignore-urlfetcher-cert-requests")
		
		options.add_argument("--lang="+CONFIG.BROWSER_LANG)
		options.add_argument("--accept-lang="+CONFIG.BROWSER_LANG)
		
		options.add_argument("--disable-web-security")
		options.add_argument("--deny-permission-prompts")
		options.add_argument("--no-default-browser-check")
		options.add_argument("--allow-insecure-localhost")
		options.add_argument("--reduce-security-for-testing")
		options.add_argument("--disable-site-isolation-trials")
		
		options.add_argument("--disable-breakpad")
		options.add_argument("--disable-infobars")
		options.add_argument("--disable-translate")
		options.add_argument("--disable-top-sites")
		options.add_argument("--disable-auto-reload")
		options.add_argument("--disable-notifications")
		options.add_argument("--disable-dev-shm-usage")
		options.add_argument("--disable-popup-blocking")
		options.add_argument("--disable-crash-reporter")
		options.add_argument("--disable-smooth-scrolling")
		options.add_argument("--disable-plugins-discovery")
		options.add_argument("--disable-oopr-debug-crash-dump")
		options.add_argument("--disable-session-crashed-bubble")
		options.add_argument("--disable-ipc-flooding-protection")
		options.add_argument("--disable-blink-features=AutomationControlled")
		options.add_argument("--unsafely-allow-protected-media-identifier-for-domain")
		if Version(self.version) >= Version(127):
			options.add_argument("--disable-search-engine-choice-screen")
		
		options.add_argument("--disable-features=IsolateOrigins,site-per-process,Translate" + \
			",InsecureDownloadWarnings,DownloadBubble,DownloadBubbleV2" + \
			",OptimizationTargetPrediction,OptimizationGuideModelDownloading" + \
			",SidePanelPinning,UserAgentClientHint,PrivacySandboxSettings4" + \
			",OptimizationHintsFetching,OptimizationHints,InterestFeedContentSuggestions"
		)
		
		if Version(self.version) >= Version(117):
			options.add_argument("--remote-debugging-pipe")
		if self.enable_logs:
			options.set_capability("goog:loggingPrefs" if Version(self.version) > Version(75) else "loggingPrefs", {
				LOG_TYPES.BROWSER.lower():       CONFIG.LOG_LEVEL
				, LOG_TYPES.CLIENT.lower():      CONFIG.LOG_LEVEL
				, LOG_TYPES.DRIVER.lower():      CONFIG.LOG_LEVEL
				, LOG_TYPES.SERVER.lower():      CONFIG.LOG_LEVEL
				, LOG_TYPES.PROFILER.lower():    CONFIG.LOG_LEVEL
				, LOG_TYPES.PERFORMANCE.lower(): CONFIG.LOG_LEVEL
			})
		
		prefs = {
			"intl.accept_languages": CONFIG.BROWSER_LANG
			, "intl.selected_languages": CONFIG.BROWSER_LANG
		}
		if self.download_dir:
			prefs.update({
				"profile.default_content_settings.popups": 0
				, "download.prompt_for_download": False
				, "download.directory_upgrade": True
				, "disable-popup-blocking": True
				, "safebrowsing.enabled": True
				, "credentials_enable_service": False
				, "profile.password_manager_enabled": False
				, "profile.password_manager_leak_detection": False
				, "plugins.always_open_pdf_externally": True
				, "plugins.plugins_disabled": ["Chrome PDF Viewer"]
				, "profile.default_content_setting_values.automatic_downloads": 1
			})
			options.enable_downloads = True
			if not self.is_local_browser:
				options.set_capability("se:downloadsEnabled", True)
			else:
				prefs["download.default_directory"] = self.download_dir
			
			if CONFIG.BASE_URL:
				options.add_argument("--unsafely-treat-insecure-origin-as-secure="+CONFIG.BASE_URL)
			options.add_argument("--allow-running-insecure-content")
		else:
			options.enable_downloads = False
			options.set_capability("se:downloadsEnabled", False)
			if CONFIG.INCOGNITO:
				options.add_argument("--incognito")
		options.add_experimental_option("prefs", prefs)
		
		if self.cache_dir and not CONFIG.INCOGNITO:
			options.add_argument("--disk-cache-dir="+self.cache_dir)
		else:
			options.add_argument("--disable-cache")
			options.add_argument("--disk-cache-size=0")
			options.add_argument("--media-cache-size=0")
			options.add_argument("--disable-application-cache")
		if CONFIG.PROXY:
			if CONFIG.PROXY.lower().endswith("pac"):
				options.add_argument("--proxy-pac-url="+CONFIG.PROXY)
			else:
				options.add_argument("--proxy-server="+CONFIG.PROXY)
		else:
			options.add_argument("--no-proxy-server")
		if self.profile_dir:
			options.add_argument("--user-data-dir="+self.profile_dir)
			options.add_argument("--allow-profiles-outside-user-dir")
			options.add_argument("--enable-profile-shortcut-manager")
			options.add_argument("--profile-directory=Default")
		if CONFIG.HEADLESS and Version(self.version) >= Version(58):
			if Version(self.version) >= Version(108):
				options.add_argument("--headless=new")
			elif Version(self.version) >= Version(96):
				options.add_argument("--headless=chrome")
			else:
				options.add_argument("--headless")
			if not platform == "win64":
				options.add_argument("--disable-gpu")
				options.add_argument("--disable-software-rasterizer")
			options.add_argument("--hide-scrollbars")
			options.add_argument("--disable-low-res-tiling")
			options.add_argument("--wm-window-animations-disabled")
			options.add_argument("--disable-renderer-backgrounding")
			options.add_argument("--disable-background-timer-throttling")
			options.add_argument("--disable-backgrounding-occluded-windows")
			options.add_argument("--disable-client-side-phishing-detection")
		
		if self.is_local_browser:
			if self.binary_location:
				options.binary_location = self.binary_location
		else:
			options.set_capability("browserName", self.browser_name)
			options.set_capability("browserVersion", self.version)
			options.set_capability("acceptInsecureCerts", True)
		return options
	
	def make_service(self):
		"""
		Возвращает объект обслуживания
		"""
		return ChromeService(self.driver_path)
	
	def extensions(self):
		"""
		Возвращает список путей к файлам расширения браузера
		"""
		return []
	
	@property
	def web_driver_class(self):
		"""
		Возвращает класс веб-драйвера
		"""
		return webdriver.Chrome
	
	@property
	def user_version(self):
		"""
		Версия браузера которую указал пользователь в качестве используемой
		"""
		return CONFIG.CHROME_VERSION
	
	@property
	def user_local_path(self):
		"""
		Путь к исполняемому файлу браузера указанный пользователем
		"""
		return CONFIG.CHROME_LOCAL_PATH
	
	@property
	def browser_name(self):
		"""
		Возвращает имя браузера
		"""
		return "chrome"
	
	@property
	def driver_path(self):
		"""
		Возвращает путь к файлу драйвера
		"""
		return get_chromedriver(self.version, self.short_version, self.browser_name)

ItemConfig.registrator(
	ItemConfig("CHROME_DRIVER_PATH",  str, "", "Путь к chromedriver")
	, ItemConfig("CHROME_VERSION",    str, "", "CHROME: Версия chrome")
	, ItemConfig("CHROME_LOCAL_PATH", str, "", "CHROME: Путь к исполняемому файлу chrome")
)
