# -*- coding: utf-8 -*-

__author__ = 'n_kovganko@wargaming.net'

import asyncio
import builtins
import ctypes
import glob
import html
import itertools
import json
import os
import random
import re
from typing import Dict, Any, Optional, List

from _ctypes import COMError
from binascii import hexlify
from datetime import datetime
from math import ceil
from os import path, listdir, getenv, makedirs
from pathlib import Path
from string import ascii_lowercase, digits
from time import sleep, time
from urllib.parse import urljoin

import pywintypes
import win32api
import win32con
import win32gui
import xmltodict
from PIL import ImageGrab
from linqp import Linqp
from uisoup import uisoup, TooSaltyUISoupException
from win32api import PostMessage, GetSystemMetrics
from win32con import WM_CLOSE

from clippy.helpers.cmd_helper import CmdHelper
from clippy.helpers.waiter import PollingTimeoutExpired
from wgc_core.config import WGCConfig
from wgc_core.exceptions import WGCException, WGCRunningTimeoutException, \
    WGCDoesntGetResponseException, WGCIncorrectFileHash, WebSocketConnectionClosed, UnsupportedProtocolVersion, \
    WGCCrash, WG_TRACK, AbnormalTermination
from wgc_core.file_templates import get_file_template
from wgc_helpers.artifacts_helper import ArtifactsHelper
from wgc_helpers.browser_helper import BrowserHelper
from wgc_helpers.dump_helper import DumpHelper
from wgc_helpers.encrypt_helper import EncryptHelper
from wgc_helpers.localization_helper import Localization
from wgc_helpers.network_helper import NetworkHelper
from wgc_helpers.os_helper import OSHelper
from wgc_helpers.regex_helper import RegexHelper
from wgc_helpers.registry import RegistryHelper
from wgc_helpers.requirements_helper import RequirementHelper
from wgc_helpers.screen_helper import ScreenHelper
from wgc_helpers.sevenzip import SevenZipHelper
from wgc_helpers.sys_internals import SysInternalsHelper
from wgc_helpers.torrents_helper import TorrentsHelper
from wgc_helpers.ui_map_parser import UIMapParser, UIMapElement
from wgc_helpers.waiter import Waiter
from wgc_helpers.wgc_settings import WGCSettingsHelper
from wgc_helpers.winapi_helper import WinApi
from wgc_helpers.wnc_notification import WNCNotificationHelper
from wgc_core.logger import get_logger
from wgc_core.logger.handlers import StepsToReproduceFileHandler
from wgc_mocks import GameMocks
from wgc_mocks.wgni import WGNIUsersDB
from wgc_third_party import get_third_party_path
from wgc_client.ui.elements.native_element import NativeElement
from wgc_client.ui.elements.web_element import WebElement

log = get_logger(__name__)


class WGCLogRecord(object):

    def __init__(self, log_level, pid, timestamp, message):
        self.log_level = log_level
        self.pid = pid
        self.timestamp = timestamp
        self.message = message


class WGCClient:
    ui_map_parser = UIMapParser()
    websocket_path_wgc = '/websocket/wgc'
    websocket_path_overlay = '/websocket/overlay'
    websocket_path_game_overlay = '/websocket/game-overlay'
    websocket_path_news = '/websocket/news'
    websocket_path_survey = '/websocket/survey'
    websocket_path_game_event = '/websocket/game-event'
    websocket_path_custom_login = '/websocket/custom-login'
    websocket_path_dialog_with_iframe = '/websocket/dialog-with-iframe'
    websocket_clients = {websocket_path_wgc: None, websocket_path_overlay: None,
                         websocket_path_news: None, websocket_path_survey: None, websocket_path_game_event: None,
                         websocket_path_game_overlay: None, websocket_path_custom_login: None,
                         websocket_path_dialog_with_iframe: None}
    websocket_closed_connection = {websocket_path_wgc: 0, websocket_path_overlay: 0,
                                   websocket_path_news: 0, websocket_path_survey: 0,
                                   websocket_path_game_event: 0,
                                   websocket_path_game_overlay: 0, websocket_path_custom_login: 0,
                                   websocket_path_dialog_with_iframe: 0}
    _socket_state = None
    _current_body_content = None
    _cached_src_crc = {}
    _hash_calculated = False

    @classmethod
    def get_wgc_element(cls, element_name, template=None, index=0, provider=None):
        if isinstance(element_name, UIMapElement):
            name = ''
            element = element_name
        else:
            element = cls.ui_map_parser.get_element(element_name, template)
            name = element_name
        if element.type == 'native':
            result = NativeElement(cls, name, element.full_selector, index)
        else:
            result = WebElement(cls, name, element.full_selector, index, provider=provider)
        return result

    @classmethod
    def reports_dir(cls):
        return path.join(WGCConfig.temp_folder, 'wgc_report_dir')

    @classmethod
    def get_wgc_env(cls):
        return WGCConfig.wgc_branch, WGCConfig.wgc_build
    
    @classmethod
    def wait_for_close_websocket(cls):
        return Waiter.poll(60, lambda: cls.websocket_closed_connection.get(cls.websocket_path_wgc) > 0)
    
    @classmethod
    def is_game_center_running(cls, check_process=False):
        return cls.is_wgc_running(check_process)

    @classmethod
    def is_wgc_running(cls, check_process=False):
        """
        Verifies is WGC application running or not.

        :rtype: bool
        :return: True if WGC application is running False if not.
        """
        if check_process:
            result = cls.websocket_clients.get(cls.websocket_path_wgc) is not None and cls.is_wgc_process_exist()
            log.debug(f'is_wgc_running -> {result}')
            return result
        else:
            return cls.websocket_clients.get(cls.websocket_path_wgc) is not None

    @classmethod
    def is_overlay_running(cls):
        """
        Verifies is Overlay application running or not.

        :rtype: bool
        :return: True if Overlay application is running False if not.
        """
        return cls.websocket_clients.get(cls.websocket_path_overlay) is not None
    
    @classmethod
    def is_news_running(cls):
        """
        Verifies is Overlay application running or not.

        :rtype: bool
        :return: True if Overlay application is running False if not.
        """
        return cls.websocket_clients.get(cls.websocket_path_news) is not None
    
    @classmethod
    def is_survey_running(cls):
        """
        Verifies is Overlay application running or not.

        :rtype: bool
        :return: True if Overlay application is running False if not.
        """
        return cls.websocket_clients.get(cls.websocket_path_survey) is not None
    
    @classmethod
    def is_custom_login_running(cls):
        """
        Verifies is Overlay application running or not.

        :rtype: bool
        :return: True if Overlay application is running False if not.
        """
        return cls.websocket_clients.get(cls.websocket_path_custom_login) is not None
    
    @classmethod
    def is_dialog_with_iframe_running(cls):
        """
        Verifies is Overlay application running or not.

        :rtype: bool
        :return: True if Overlay application is running False if not.
        """
        return cls.websocket_clients.get(cls.websocket_path_dialog_with_iframe) is not None
    
    @classmethod
    def is_game_event_running(cls):
        """
        Verifies is Overlay application running or not.

        :rtype: bool
        :return: True if Overlay application is running False if not.
        """
        return cls.websocket_clients.get(cls.websocket_path_game_event) is not None
    
    @classmethod
    def is_game_overlay_running(cls):
        """
        Verifies is Overlay application running or not.

        :rtype: bool
        :return: True if Overlay application is running False if not.
        """
        return cls.websocket_clients.get(cls.websocket_path_game_overlay) is not None

    @classmethod
    def find_js_post_message_record(cls, search_string):
        """
        Find some [UI] [JS] [PostMessageService] records in WGC current log file.
        :param str search_string: log message.
        :rtype: list[str].
        :return: List of records.
        """
        data = cls.parse_wgc_log()
        result = Linqp(data).where(
            lambda x: '[UI] [JS] [PostMessageService]' in x.message and search_string in x.message).select_all()
        return [x.message for x in result]

    @classmethod
    def ping(cls):
        """
        Sends ping command to WGC websocket client.

        :rtype: bool
        :return: True if server answered to ping command otherwise False.
        """
        result = False
        try:
            # check if application was crashed
            cls.execute_js('ping()', timeout=1)
            result = True
        except WGCException:
            pass

        return result

    @classmethod
    def start_full_screen_app(cls):
        RequirementHelper.append_requirement('LLR.2.2')
        app = get_third_party_path('full_screen_app.exe')
        CmdHelper.execute(f'"{app}"', dont_wait=True)
        pid = Waiter.poll(15, lambda: OSHelper.is_process_exists('full_screen_app.exe'),
                          'Expected that full screen app was started.')
        log.debug(f'full_screen_app.exe pid-> {pid}')
        sleep(WGCConfig.TIMEOUT_BEFORE_ACTION)
        hwnd = Waiter.poll(7, lambda: WinApi.get_hwnd_by_pid(pid))
        sleep(3 * WGCConfig.TIMEOUT_AFTER_ACTION)
        WinApi.set_active(hwnd)
        WinApi.set_topmost(hwnd)

    @classmethod
    def terminate_full_screen_app(cls):
        RequirementHelper.append_requirement('LLR.2.2')
        OSHelper.terminate_process('full_screen_app.exe')
        Waiter.poll(4, lambda: not OSHelper.is_process_exists('full_screen_app.exe'))
        sleep(WGCConfig.TIMEOUT_AFTER_ACTION)

    @classmethod
    def wait_for_game_center_running(cls, home_folder=None):
        """
        Waits while Game Center will be running.
        """
        cls.wait_for_wgc_running(home_folder)

    @classmethod
    def wait_for_wgc_running(cls, home_folder=None):
        """
        Waits while WGC will be running.
        """
        log.info('Wait until WGC is running.')
        home_folder = home_folder or WGCConfig.application.folder
        Waiter.poll(120, cls.is_wgc_running,
                    'Expected that WGC is running.')
        cls.wait_for_element('Notifications', timeout=4) and Waiter.poll(
            4, lambda: 'Postload initialization' in cls.get_current_wgc_log_file_content(home_folder))

    @classmethod
    def is_wgc_update_in_progress(cls):
        """
        Verifies is WGC application running or not.

        :rtype: bool
        :return: True if WGC update is in progress False if not.
        """
        RequirementHelper.append_requirement('LLR.2.7')
        updater_title_i18n = f'*{Localization.i18n("SELFUPDATE_TITLE")}'
        updater_title_ru = f'*процесс обновления'
        if WinApi.get_top_level_window([updater_title_i18n, updater_title_ru], 'Qt51510QWindowIcon'):
            return True
        return False

    @classmethod
    def wait_for_websocket_closing(cls, timeout=30):
        log.debug('Waiting for websoket closing connection and WGC restarted.')
        Waiter.poll(timeout, lambda: WGCClient._socket_state == 'closed', sleep_seconds=.1)

    @classmethod
    def is_binary_process_exist(cls):
        return cls.is_wgc_process_exist()
        
    @classmethod
    def is_wgc_process_exist(cls):
        """
        Verifies is WGC process exist.

        :rtype: bool
        :return: True if WGC progress exist False if not.
        """
        return OSHelper.is_process_exists(WGCConfig.application.binary_name) or OSHelper.is_process_exists(
            WGCConfig.application.renderer_process) or OSHelper.is_process_exists('helper_process.exe')

    @classmethod
    def is_wgc_processes_exist(cls):
        """
        Verifies is WGC processes exist.

        :rtype: bool
        :return: True if WGC progress exist False if not.
        """
        return bool(OSHelper.is_process_exists(WGCConfig.application.binary_name) or OSHelper.is_process_exists(
            WGCConfig.application.watchdog_process) or OSHelper.is_process_exists(
            WGCConfig.application.renderer_process))
    
    @classmethod
    def _copy_cached(cls, destination_folder, publisher=None):
        """
        Copies WGC binaries to the destination folder using the local cache.
        The cache is filled from a network share (priority) or from the web (fallback) if it is empty.
        The method prefers patch-based distributions if available.
        
        :param str destination_folder: The destination folder for WGC binaries.
        :param str publisher: The publisher of the WGC build (e.g. 'wargaming', 'steam').
        """
        
        final_publisher = publisher or WGCConfig.wgc_publisher
        prefix = 'wgcs' if final_publisher == 'steam' else 'wgc'
        arch_dir = 'x64' if WGCConfig.wgc_arch != 'win32' else 'win32'
        patch_arch = 'win32' if WGCConfig.wgc_arch == 'win32' else 'win64'
        patch_dir_name = f'{prefix}_{WGCConfig.wgc_build}_{WGCConfig.wgc_region}'
        if publisher:
            cache_bin_folder = path.join(WGCConfig.cached_folder_for(final_publisher), 'bin')
            cache_patch_folder = path.join(WGCConfig.cached_folder_for(final_publisher), patch_dir_name)
            cache_api_folder = path.join(WGCConfig.cached_folder_for(final_publisher), 'wgc_api')
        else:
            cache_bin_folder = path.join(WGCConfig.cached_folder, 'bin')
            cache_patch_folder = WGCConfig.cache_patch_folder
            cache_api_folder = path.join(WGCConfig.cached_folder, 'wgc_api')
        
        if not path.exists(cache_bin_folder):
            source_type = None
            network_source_path_bin = None
            network_source_path_patch = None
            dx_dst = path.join(path.dirname(WGCConfig.temp_folder), WGCConfig.test_data.redist_exe_name)
            
            net_share_config = WGCConfig.full_config.get('network_share', {})
            host = net_share_config.get('host')
            share_folder = net_share_config.get('folder')
            
            if host:
                try:
                    SHARE_FULL_NAME = path.sep * 2 + path.sep.join((host, share_folder))
                    with NetworkHelper(host=SHARE_FULL_NAME, user=net_share_config.get('user'),
                                       password=net_share_config.get('password')) as net_helper:
                        if net_helper.is_connected():
                            root_path = path.join('wgc', 'demo', WGCConfig.wgc_branch, WGCConfig.wgc_build)
                            network_source_path_bin = Path(net_helper.host).joinpath(
                                root_path, final_publisher, 'bin', arch_dir)
                            network_source_path_patch = Path(net_helper.host).joinpath(
                                root_path, final_publisher, 'patch', patch_dir_name)
                            dx_src = Path(net_helper.host).joinpath(
                                'test_artifacts', 'test_data', WGCConfig.test_data.redist_exe_name)
                            
                            # Copy 1507640920_directx_Jun2010_redist.exe
                            OSHelper.copy(str(dx_src), str(dx_dst), cast_path=False)
                            
                            if network_source_path_patch.exists() and any(network_source_path_patch.iterdir()):
                                source_type = 'network_patch'
                            elif network_source_path_bin.exists() and any(network_source_path_bin.iterdir()):
                                source_type = 'network_bin'
                            else:
                                log.warning("The network folder is connected, "
                                            "but artifacts were not found at the expected paths..")
                except Exception as e:
                    msg = str(e).replace(net_share_config.get('password'), '******')
                    log.error(
                        f"An error occurred while accessing the network folder: {msg}. Using fallback (web).")
            else:
                log.info("Network share not configured in YAML. Fallback (web) used")
                
                # Copy 1507640920_directx_Jun2010_redist.exe
                OSHelper.copy(WGCConfig.share_redist_exe, str(dx_dst))
            
            if source_type is None:
                source_type = 'web_patch' if WGCConfig.patches_available else 'web_bin'
            
            is_patch_source = 'patch' in source_type
            
            try:
                if is_patch_source:
                    OSHelper.make_dirs(cache_patch_folder, exist_ok=True)
                    
                    if source_type == 'network_patch':
                        log.info(f"Copying patches from the network to '{cache_patch_folder}'...")
                        OSHelper.copy(str(network_source_path_patch), cache_patch_folder, cast_path=False)
                    
                    elif source_type == 'web_patch':
                        log.info("Downloading patches from the web...")
                        win32_patch_file_name = f'{prefix}_{WGCConfig.wgc_build}_win32.wgpkg'
                        win64_patch_file_name = f'{prefix}_{WGCConfig.wgc_build}_win64.wgpkg'
                        path_to_patch_win32 = urljoin(WGCConfig.patch_folder(final_publisher), win32_patch_file_name)
                        path_to_patch_win64 = urljoin(WGCConfig.patch_folder(final_publisher), win64_patch_file_name)
                        patch_destination_win32 = path.join(cache_patch_folder, win32_patch_file_name)
                        patch_destination_win64 = path.join(cache_patch_folder, win64_patch_file_name)
                        
                        if OSHelper.is_web_path_exist(path_to_patch_win32):
                            OSHelper.copy(path_to_patch_win32, patch_destination_win32)
                        if OSHelper.is_web_path_exist(path_to_patch_win64):
                            OSHelper.copy(path_to_patch_win64, patch_destination_win64)
                    
                    patch_to_unzip = path.join(cache_patch_folder, f'{prefix}_{WGCConfig.wgc_build}_{patch_arch}.wgpkg')
                    
                    SevenZipHelper.unzip(patch_to_unzip, cache_bin_folder)
                    OSHelper.remove_file_or_directory(str(Path(cache_bin_folder, '_Service')))
                    if path.exists(str(Path(cache_bin_folder, 'wgc_info.xml'))):
                        OSHelper.remove_file_or_directory(str(Path(cache_bin_folder, 'wgc_info.xml')))
                        SevenZipHelper.delete(patch_to_unzip, 'wgc_info.xml')
                    
                    torrent_name = f'{prefix}_{WGCConfig.wgc_build}_{WGCConfig.wgc_region}.torrent'
                    torrent_path = path.join(cache_patch_folder, '..', torrent_name)
                    TorrentsHelper.generate_torrent_with_maketorrent(cache_patch_folder, torrent_path=torrent_path)
                
                else:
                    source_path = None
                    if source_type == 'network_bin':
                        source_path = str(network_source_path_bin)
                        log.info(f"Copying binary files from the network: '{source_path}' -> '{cache_bin_folder}'...")
                    elif source_type == 'web_bin':
                        source_path = urljoin(WGCConfig.test_data.source_folder, f'{final_publisher}/bin/{arch_dir}/')
                        if not OSHelper.is_web_path_exist(source_path):
                            raise WGCException(f"Web source not found: '{source_path}'")
                        log.info(f"Copying binaries from the web: '{source_path}' -> '{cache_bin_folder}'...")
                    
                    start_time = time()
                    OSHelper.copy(source_path, cache_bin_folder, cast_path=False)
                    log.debug(f"Copying binary files took {time() - start_time:.2f} sec.")
                    
                # Copy wgc_api.dll
                makedirs(cache_api_folder, exist_ok=True)
                if WGCConfig.wgc_arch == 'win32':
                    OSHelper.copy(WGCConfig.application.api.source_win32_dir_path, cache_api_folder)
                else:
                    OSHelper.copy(WGCConfig.application.api.source_x64_dir_path, cache_api_folder)
            
            except Exception as e:
                log.error(f"Error '{source_type}': {e}")
                OSHelper.remove_file_or_directory(cache_bin_folder)
                OSHelper.remove_file_or_directory(cache_patch_folder)
        
        if not path.exists(cache_bin_folder):
            raise WGCException(f"Failed to populate cache for build {WGCConfig.wgc_build}. "
                               f"Web and network sources are unavailable or have failed.")
        
        log.info(f"Synchronizing destination folder '{destination_folder}' from local cache '{cache_bin_folder}'.")
        
        OSHelper.remove_file_or_directory(path.join(destination_folder, 'cache'))
        OSHelper.remove_file_or_directory(path.join(destination_folder, 'ntf_storage'))
        
        src_files = []
        for p in Path(cache_bin_folder).glob('**/*.*'):
            rel_path = path.relpath(str(p), cache_bin_folder)
            src_files.append(rel_path)
            if not cls._hash_calculated:
                cls._cached_src_crc[rel_path] = OSHelper.get_file_crc(str(p))
        cls._hash_calculated = True
        
        try:
            dst_glob = Path(destination_folder).glob('**/*.*')
            dst_files = [path.relpath(str(p), destination_folder) for p in dst_glob]
        except OSError as e:
            log.debug(f"Error accessing destination folder, folder will be cleared: {e}")
            OSHelper.remove_file_or_directory(destination_folder)
            dst_files = []
        
        need_remove = list(set(dst_files) - set(src_files))
        
        log.debug(f"Sync: {len(src_files)} in source, {len(dst_files)} in destination, {len(need_remove)} to remove.")
        start_sync_time = time()
        
        # Copy/update files
        for file_rel_path in src_files:
            src_file_abs = path.join(cache_bin_folder, file_rel_path)
            dst_file_abs = path.join(destination_folder, file_rel_path)
            
            dir_name = path.dirname(dst_file_abs)
            try:
                OSHelper.make_dirs(dir_name, exist_ok=True)
            except PermissionError:
                cls.terminate_all_wgc_related_processes()
                OSHelper.unlock_file_or_folder(dir_name)
                OSHelper.make_dirs(dir_name, exist_ok=True)
            
            try:
                if not path.exists(dst_file_abs) or OSHelper.get_file_crc(dst_file_abs) != cls._cached_src_crc.get(
                        file_rel_path):
                    # Using prefix for long paths to avoid MAX_PATH issues
                    OSHelper.copy(f'\\\\?\\{src_file_abs}', dst_file_abs)
            except (OSError, PermissionError) as e:
                log.debug(
                    f"Permission error copying '{file_rel_path}', terminating processes and trying again. Error: {e}")
                Waiter.poll(10, lambda: not OSHelper.terminate_process(cls.get_all_related_wgc_process()))
                sleep(0.5)
                OSHelper.unlock_file_or_folder(dst_file_abs)
                OSHelper.copy(src_file_abs, dst_file_abs)
        
        for file_rel_path in need_remove:
            OSHelper.remove_file_or_directory(path.join(destination_folder, file_rel_path))
        
        log.debug(f'File synchronization completed in {time() - start_sync_time:.2f} sec.')

    @classmethod
    def create_dump(cls):
        if cls.is_binary_process_exist():
            try:
                cls.set_focus_window()
                kc = uisoup.keyboard.codes
                uisoup.keyboard.send(kc.CONTROL.modify(kc.F12))
                sleep(1.5)
            except (TooSaltyUISoupException, COMError, OSError):
                pass

    @classmethod
    def copy_wgc_from_source(cls, destination_folder=None, publisher=None):
        """
        Copies WGC binaries from source.

        :param str publisher: WGC publisher.
        :param str destination_folder: WGC destination folder.
        """
        log.debug('Copying WGC from source.')
        print('Copying WGC from source.')
        start_time = time()
        cls._copy_cached(destination_folder or WGCConfig.application.folder, publisher)
        # Need to save current WGC version to cache.
        elapsed_time = time() - start_time
        log.debug('Copying WGC version %s from source finished. '
                  'Elapsed: %s sec' %
                  (WGCSettingsHelper.get_wgc_current_version(), elapsed_time))
        print('Copying WGC version %s from source finished. '
                  'Elapsed: %s sec' %
                  (WGCSettingsHelper.get_wgc_current_version(), elapsed_time))

    @classmethod
    def click_first_notification(cls):
        """
        Clicks on first Tray notification.
        """
        RequirementHelper.append_requirement('LLR.2.3')
        log.info('Click on first tray notification.')

        def _find_notification():
            try:
                result = WinApi.get_top_level_window(None, 'Qt51510QWindowToolTipSaveBits', only_first=False)
                if not isinstance(result, list):
                    result = [result] if result > 0 else []
                for hwnd in result:
                    x, y, w, h = WinApi.get_location(hwnd)
                    if w > 0 and h > 0:
                        return hwnd, x, y, w, h
            except Exception:
                pass
            return None

        # Poll for notification
        window_info = None
        for _ in range(20):  # 10 seconds approx
            window_info = _find_notification()
            if window_info:
                break
            sleep(0.5)

        if window_info:
            hwnd, x, y, w, h = window_info
            log.info(f"Notification found at {x},{y} ({w}x{h}). Clicking.")
            
            # Try to ensure it's on top just in case
            try:
                win32gui.BringWindowToTop(hwnd)
            except Exception:
                pass

            cx = int(x + w / 2)
            cy = int(y + h / 2)
            uisoup.mouse.move(cx, cy, smooth=False)
            WinApi.click(cx, cy)
            return True
        
        log.warning("No tray notification found.")
        return False
    
    @classmethod
    def get_position_tray_notification(cls, text_string):
        Waiter.poll(7, lambda: cls.contains_in_tray_notification(text_string))
        try:
            result = WinApi.get_top_level_window(None, 'Qt51510QWindowToolTipSaveBits', only_first=False)
            if not isinstance(result, list):
                result = [result] if result > 0 else []
            for hwnd in result:
                try:
                    x, y, w, h = WinApi.get_location(hwnd)
                    if w == 0 and h == 0:
                        continue
                    obj = uisoup.get_window(hwnd)
                    v = [i for i in obj.findall(False, role_name='lbl') if i.acc_name]
                    if v:
                        text = v[0].acc_name
                        if text and text_string in text:
                            return x, y, w, h
                except (pywintypes.error, WindowsError, COMError) as e:
                    log.error(e)
        except (pywintypes.error, WindowsError, COMError) as e:
            log.error(e)
        return 0, 0, 0, 0

    @classmethod
    def click_on_tray_notification(cls, text_string):
        Waiter.poll(7, lambda: cls.contains_in_tray_notification(text_string))
        try:
            result = WinApi.get_top_level_window(None, 'Qt51510QWindowToolTipSaveBits', only_first=False)
            if not isinstance(result, list):
                result = [result] if result > 0 else []
            for hwnd in result:
                try:
                    x, y, w, h = WinApi.get_location(hwnd)
                    if w == 0 and h == 0:
                        continue
                    obj = uisoup.get_window(hwnd)
                    v = [i for i in obj.findall(False, role_name='lbl') if i.acc_name]
                    if v:
                        text = v[0].acc_name
                        if text and text_string in text:
                            uisoup.mouse.move(int(x + w / 2), int(y + h / 2), smooth=False)
                            sleep(.5)
                            WinApi.click(int(x + w / 2), int(y + h / 2))
                except (pywintypes.error, WindowsError, COMError) as e:
                    log.error(e)
        except (OSError, COMError) as e:
            log.error(e)

    @classmethod
    def click_on_notification_button_in_tray(cls, button_positions: str = 'right'):
        log.info(f'Click on button in tray notification with position: {button_positions}')
        sleep(1)
        text = cls.get_tray_notifications_text()[0]
        x, y, w, h = cls.get_position_tray_notification(text)
        if button_positions == 'left':
            uisoup.mouse.move(int(x + w / 2 - 40), int(y + h / 2 + 140), smooth=False)
            WinApi.click(int(x + w / 2 - 40), int(y + h / 2 + 140))
            uisoup.mouse.move(int(x + w / 2 - 240), int(y + h / 2 + 140), smooth=False)
        if button_positions == 'right':
            uisoup.mouse.move(int(x + w / 2 + 100), int(y + h / 2 + 140), smooth=False)
            WinApi.click(int(x + w / 2 + 100), int(y + h / 2 + 140))
            uisoup.mouse.move(int(x + w / 2 - 240), int(y + h / 2 + 140), smooth=False)

    @classmethod
    def click_on_notification_in_wnc(cls, click_on: str = 'text'):
        log.info('Open WNC')
        sleep(1)
        uisoup.mouse.move(1890, 1060, smooth=False)
        WinApi.click(1890, 1060)
        sleep(WGCConfig.TIMEOUT_AFTER_ACTION * 2)
        log.info(f'Click on notification {click_on} in WNC')
        y_offset_1 = 455
        y_offset_2 = 315
        y_offset_3 = 150
        if OSHelper.is_windows_11():
            y_offset_1 = 880
            y_offset_2 = 625
            y_offset_3 = 460
            # expand notification
            uisoup.mouse.move(1645, 635, smooth=False)
            WinApi.click(1645, 635)
            sleep(WGCConfig.TIMEOUT_AFTER_ACTION * 2)
        if click_on == 'left_button':
            sleep(WGCConfig.TIMEOUT_BEFORE_ACTION)
            uisoup.mouse.move(1625, y_offset_1, smooth=False)
            WinApi.click(1625, y_offset_1)
            sleep(WGCConfig.TIMEOUT_AFTER_ACTION)
        if click_on == 'right_button':
            sleep(WGCConfig.TIMEOUT_BEFORE_ACTION)
            uisoup.mouse.move(1825, y_offset_1, smooth=False)
            WinApi.click(1825, y_offset_1)
            sleep(WGCConfig.TIMEOUT_AFTER_ACTION)
        if click_on == 'text':
            sleep(WGCConfig.TIMEOUT_BEFORE_ACTION)
            uisoup.mouse.move(1725, y_offset_2, smooth=False)
            WinApi.click(1725, y_offset_2)
            sleep(WGCConfig.TIMEOUT_AFTER_ACTION)
        if click_on == 'news':
            sleep(WGCConfig.TIMEOUT_BEFORE_ACTION)
            uisoup.mouse.move(1725, y_offset_3, smooth=False)
            WinApi.click(1725, y_offset_3)
        log.info('Close WNC')
        sleep(WGCConfig.TIMEOUT_BEFORE_ACTION)
        uisoup.mouse.move(5, 5, smooth=False)
        WinApi.click(5, 5)

    @classmethod
    def get_notifications_list_in_wnc_with_text(cls, text):
        log.info('')
        wnc_notification = []
        for item in WNCNotificationHelper.list():
            if text in item:
                wnc_notification.append(item)
        return wnc_notification

    @classmethod
    def hover_mouse_on_tray_notification(cls):
        log.info(f'Hover mouse on tray notification')
        text = cls.get_tray_notifications_text()[0]
        x, y, w, h = cls.get_position_tray_notification(text)
        uisoup.mouse.move(int(x + w / 2), int(y + h / 2), smooth=False)

    @classmethod
    def get_tray_notification_count(cls):
        RequirementHelper.append_requirement('LLR.2.3')
        result = WinApi.get_top_level_window(None, 'Qt51510QWindowToolTipSaveBits', only_first=False)
        if isinstance(result, list):
            return len(result)
        else:
            return 1 if result > 0 else 0

    @classmethod
    def get_tray_notifications_text(cls):
        RequirementHelper.append_requirement('LLR.2.3')
        texts = []
        result = []
        try:
            result = WinApi.get_top_level_window(None, 'Qt51510QWindowToolTipSaveBits', only_first=False)
            if not isinstance(result, list):
                result = [result] if result > 0 else []
            for hwnd in result:
                try:
                    x, y, w, h = WinApi.get_location(hwnd)
                    if w == 0 and h == 0:
                        continue
                    obj = uisoup.get_window(hwnd)
                    v = [i for i in obj.findall(False, role_name='lbl') if i.acc_name]
                    if v:
                        texts.append(v[0].acc_name)
                except (pywintypes.error, WindowsError, COMError) as e:
                    log.error(e)
        except (OSError, COMError) as e:
            log.error(e)
        for te in ['Language bar', 'NotificationChevron']:
            if te in texts:
                texts.remove(te)
        log.debug(f'TRAY NOTIFICATIONS COUNT: {len(result)}, TEXTS: {texts}')
        return texts

    @classmethod
    def contains_in_tray_notification(cls, match):
        RequirementHelper.append_requirement('LLR.2.3')
        notifies = cls.get_tray_notifications_text()
        for notify in notifies:
            if match in notify:
                return True
        return False

    @classmethod
    def find_records_in_log(cls, record, home_folder=None):
        RequirementHelper.append_requirement('LLR.2.65')
        home_folder = home_folder or WGCConfig.application.folder
        log_file = cls.get_current_wgc_log_file(home_folder)
        result = []
        if log_file:
            with open(log_file, 'r', encoding='utf-8') as fp:
                content = fp.read()
                return re.findall(record, content)
        return result

    @classmethod
    def clean_wgc_home_dir(cls):
        """
        Remove WGC home directory.
        """
        log.info('Remove WGC home directory.')
        OSHelper.remove_file_or_directory(WGCConfig.application.folder)

    @classmethod
    def change_wgc_build_realm(cls, realm, home_folder=None, production=False):
        log.info('Change WGC build realm to %s' % realm)
        home_folder = home_folder or WGCConfig.application.folder
        file_name = path.join(home_folder, WGCConfig.application.wgc_info_file)
        content = WGCSettingsHelper.read_xml_file(file_name)
        content['wgc_info']['wgc_id'] = 'WGC.%s.PRODUCTION' % realm
        content['wgc_info']['wgc_realm_id'] = realm.upper()
        if production:
            for index, val in enumerate(content['wgc_info']['platform_realms']['realm']):
                content['wgc_info']['platform_realms']['realm'][index]['@content_service_url'] = \
                    'http://wguscs-wgc%s.wargaming.net/' % realm.lower()
        WGCSettingsHelper.write_xml_file(file_name, content)

    @classmethod
    def create_user_info(cls, account, protocol_version=None,
                         instance_id='N2', steam_account_uid=0, steam_game_id=0, renaming_in_progress_flag=0,
                         ingoing_teleport_realm_id='', stated_country_code='', scope='',
                         persona_creds_conflict_flag=False, steam_restricted_app_codes='',
                         enlisted_default_accounts=''):
        """
        Create user information XML file.

        Args:
            account (Account): The account information.
            protocol_version (str, optional): The protocol version. Defaults to None.
            default_realm (str, optional): The default realm. Defaults to 'dev1'.
            instance_id (str, optional): The instance ID. Defaults to 'N2'.
            game_path (str, optional): The game path. Defaults to None.
            steam_account_uid (int, optional): The Steam account UID. Defaults to 0.
            steam_game_id (int, optional): The Steam game ID. Defaults to 0.
            renaming_in_progress_flag (int, optional): The renaming in progress flag. Defaults to 0.
            ingoing_teleport_realm_id (str, optional): The ingoing teleport realm ID. Defaults to ''.
            stated_country_code (str, optional): The stated country code. Defaults to ''.
            scope (str, optional): The scope. Defaults to ''.
            persona_creds_conflict_flag (bool, optional): The persona credentials conflict flag. Defaults to False.
            steam_restricted_app_codes (str, optional): The Steam restricted app codes. Defaults to ''.
            enlisted_default_accounts (str, optional): Describes games for which account is set as default
        """
        protocol_version = protocol_version or str(WGCConfig.application.user_info_protocol)
        user_info_file = WGCConfig.application.user_info_file
        pattern = get_file_template('user_info.xml')
        hwid = '76C11821D59482932FD7CEFD8D4A15CD'
        client_id = WGCConfig.wgni_client_id
      
        def prepare_realm(account):
            oauth_token = WGNIUsersDB.generate_oauth_token()
            account.oauth_token = oauth_token
            account.client_id = client_id
            account.oauth_token_exp_time = ceil(time() + 3600 * 24)
            account.exchange_code = hwid
            login = hexlify(account.username.encode('utf-8')).decode()
            nickname = hexlify(account.nickname.encode('utf-8')).decode()
            oauth_token = account.oauth_token
            demo_flag = 0
            scopes = scope.split() or account.scopes
            personaLink = ''.join(random.choice(ascii_lowercase + digits) for _ in range(6))
            timestamp = 1775175524917824100  # Friday, 9 May 2026 г., 20:027
            data = f'{login}/{account.id}/{oauth_token}/Bearer/{nickname}//{demo_flag}/' \
                   f'{timestamp}///' \
                   f'{login}/{WGNIUsersDB.generate_oauth_token()}/Bearer/{steam_account_uid}/{steam_game_id}/' \
                   f'{renaming_in_progress_flag}///' \
                   f'{ingoing_teleport_realm_id}/{stated_country_code}/{" ".join(scopes)}/' \
                   f'{personaLink}/{int(persona_creds_conflict_flag)}/{steam_restricted_app_codes}/' \
                   f'{enlisted_default_accounts}/Unknown'
            return data
        
        data_list = []
        for persona_account in WGNIUsersDB.get_persona_by_persona_id(account.persona_id).accounts:
            data = prepare_realm(persona_account)
            data_list.append((persona_account, data))

        game_tag = ''
        
        acc, user_data = data_list[0]
        content = pattern % {
            'protocol': protocol_version,
            'instance_id': instance_id,
            'game_tag': game_tag,
            'hwid': EncryptHelper.encode(hwid.encode()),
            'realm': acc.realm.upper(),
            'data': EncryptHelper.encode(user_data.encode()),
            'stats': EncryptHelper.encode(b'1'),
            'game_attr': EncryptHelper.encode(json.dumps(account.game_fields).encode())
        }
        xml_content = xmltodict.parse(content)
        sessions = []
        for acc, user_data in data_list:
            session = {'realm': acc.realm.upper(), 'data': EncryptHelper.encode(user_data.encode()),
                       'stats': EncryptHelper.encode(b'1'),
                       'game_attr': EncryptHelper.encode(json.dumps(acc.game_fields).encode()),
                       'games_data': '',
                       'instances': ''}
            sessions.append(session)
        xml_content['protocol']['sessions']['session'] = sessions
        content = xmltodict.unparse(xml_content)
        OSHelper.make_dirs(path.dirname(user_info_file), exist_ok=True)
        with open(user_info_file, 'w', encoding='utf-8') as info_file:
            info_file.write(content)

    @classmethod
    def create_wgc_info(cls, home_folder=None, protocol_version=None, wgc_id=None, use_wgcheck_link=False):
        """
        Creates wgc_info.xml file with mocked servers.

        :param str home_folder: path to home folder
        """
        RequirementHelper.append_requirement('INT.4.3')
        wgc_id = wgc_id or WGCConfig.application.wgc_id
        protocol_version = protocol_version or str(WGCConfig.application.wgc_info_protocol)
        home_folder = home_folder or WGCConfig.application.folder
        if not path.isdir(home_folder):
            OSHelper.make_dirs(home_folder, exist_ok=True)
        file_name = path.join(home_folder, WGCConfig.application.wgc_info_file)
        log.debug('Creating %s with mocked data.' % file_name)
        pattern = get_file_template('wgc_info.%s.xml' % protocol_version)
        mock_url = 'https://%s/' % GameMocks.host_https
        product_mode_switch = False if WGCConfig.wgc_configuration.lower() == 'final' else True
        ignore_us_certificates = 'true'
        ignore_cs_certificates = 'true'
        wgc_info_content = pattern % {
            'update_url': mock_url + 'wgc',
            'run_all_tests': 'true',
            'run_probability': '100',
            'game_update_url': f'https://{GameMocks.host_https}/',
            'wgc_id': wgc_id,
            'statistic_url': mock_url + 'statistic',
            'content_service_url': GameMocks.content_service_host,
            'ignore_us_certificates': ignore_us_certificates,
            'ignore_cs_certificates': ignore_cs_certificates,
            'product_mode_switch': str(product_mode_switch).lower(),
            'product_modes_access_marker': '',
            'wgni_url': WGCConfig.wgni_url,
            'redirect': GameMocks.host_http,
            'wgcps_url': 'https://%s/' % WGCConfig.wgcps_host,
            'wgni_client_id': GameMocks.wgni_client_id}
        content = xmltodict.parse(wgc_info_content)
        if not use_wgcheck_link:
            del content['wgc_info']['install_wgcheck_link']
        if path.isfile(file_name):
            OSHelper.remove_file_or_directory(file_name)
        try:
            with open(file_name, 'w', encoding='utf-8') as wgc_info_file:
                wgc_info_file.write(xmltodict.unparse(
                    content, pretty=True, encoding='utf-8'))
            return True
        except (OSError, PermissionError) as e:
            log.error(f'{e}, {file_name}')
            return False

    @classmethod
    def create_prod_wgc_info(cls, region='EU', home_folder=None):
        """
        Creates a Production wgc_info.xml file with US/SS mocked servers.

        :param str region: WGC default region.
        """
        home_folder = home_folder or WGCConfig.application.folder
        RequirementHelper.append_requirement('INT.4.3')
        file_name = path.join(home_folder, WGCConfig.application.wgc_info_file)
        log.debug('Creating %s with mocked data.' % file_name)
        pattern = get_file_template('wgc_info_prod_%s.xml' % region.lower())
        mock_url = 'https://%s/' % GameMocks.host_https
        stat_url = 'https://%s/' % GameMocks.host_https
        wgc_info_content = pattern % {
            'wgc_id': WGCConfig.application.wgc_id,
            'update_url': mock_url + 'wgc',
            'statistic_url': stat_url + 'statistic'}
        OSHelper.remove_file_or_directory(file_name)
        content = xmltodict.parse(wgc_info_content)
        with open(file_name, 'w', encoding='utf-8') as wgc_info_file:
            wgc_info_file.write(xmltodict.unparse(
                content, pretty=True, encoding='utf-8'))

    @classmethod
    def change_wgc_info_protocol(cls, protocol, home_folder=None):
        RequirementHelper.append_requirement('INT.4.3')
        home_folder = home_folder or WGCConfig.application.folder
        wgc_info = path.join(home_folder, 'wgc_info.xml')
        if path.isfile(wgc_info):
            with open(wgc_info, 'r') as info:
                content = info.read()
            content = xmltodict.parse(content)
            content['wgc_info']['@version'] = protocol
            with open(wgc_info, 'w', encoding='utf-8') as wgc_info_file:
                wgc_info_file.write(xmltodict.unparse(
                    content, pretty=True, encoding='utf-8'))

    @classmethod
    def create_wgc_info_ct(cls, home_folder=None, update_url=None):
        """
        Creates wgc_info.xml file with mocked servers with support CT and Login.
        :param str update_url: update service url for update.
        :param str home_folder: path to home folder
        """
        RequirementHelper.append_requirement('INT.4.3')
        home_folder = home_folder or WGCConfig.application.folder
        cls.create_wgc_info(home_folder)
        update_url = update_url or f'https://{GameMocks.host_https}/wgc'
        file_name = path.join(home_folder, WGCConfig.application.wgc_info_file)
        content = WGCSettingsHelper.read_xml_file(file_name)
        content['wgc_info']['update_url_ct'] = update_url
        content['wgc_info']['wgc_stat_realm_id_ct'] = 'CT'
        with open(file_name, 'w', encoding='utf-8') as wgc_info_file:
            wgc_info_file.write(xmltodict.unparse(
                content, pretty=True, encoding='utf-8'))

    @classmethod
    def create_wgc_info_360(cls, home_folder=None):
        """
        Creates wgc_info.xml file with mocked servers with support CT and Login.
        :param str home_folder: path to home folder
        """
        RequirementHelper.append_requirement('INT.4.3')
        home_folder = home_folder or WGCConfig.application.folder
        file_name = path.join(home_folder, WGCConfig.application.wgc_info_file)
        OSHelper.remove_file_or_directory(file_name)
        pattern = get_file_template('wgc_info_360.xml') % {
            'statistic_url': GameMocks.ss_url,
            'wgni_url': WGCConfig.wgni_url,
            'update_url': f'https://{GameMocks.host_https}/wgc',
            'content_service_url': GameMocks.content_service_host,
            'wgni_client_id': GameMocks.wgni_client_id,
            'wgcps_url': 'https://%s/' % WGCConfig.wgcps_host,
        }
        with open(file_name, 'w', encoding='utf-8') as wgc_info_file:
            wgc_info_file.write(pattern)

    @classmethod
    def execute_js(cls, code, dont_wait_response=False, timeout=15, check_crash=False, provider=None):
        """
        Execute js code on WGC.
        """
        # execute js code
        result = ''
        chars = ascii_lowercase + digits
        msg_prefix = \
            ''.join(random.choice(chars) for _ in range(6))
        if hasattr(builtins, 'provider'):
            provider = builtins.provider
        else:
            if provider is None:
                provider = cls.websocket_path_wgc
        
        checked_method = cls.is_wgc_running
        if provider == cls.websocket_path_overlay:
            checked_method = cls.is_overlay_running
        if provider == cls.websocket_path_game_overlay:
            checked_method = cls.is_game_overlay_running
        if provider == cls.websocket_path_news:
            checked_method = cls.is_news_running
        if provider == cls.websocket_path_survey:
            checked_method = cls.is_survey_running
        if provider == cls.websocket_path_custom_login:
            checked_method = cls.is_custom_login_running
        if provider == cls.websocket_path_dialog_with_iframe:
            checked_method = cls.is_dialog_with_iframe_running
        if provider == cls.websocket_path_game_event:
            checked_method = cls.is_game_event_running
        
        retries = 5 if provider == cls.websocket_path_game_event else 3
        poll_timeout = 20 if provider == cls.websocket_path_game_event else 10
        
        for counter in range(retries):
            if not checked_method():
                log.debug(f'Wait for setup websocket connection[provider={provider}].')
                result = Waiter.poll(poll_timeout, checked_method)
                if not result:
                    if dont_wait_response:
                        break
                    cls.search_crash_and_assert()
                    if counter == retries - 1:
                        raise WebSocketConnectionClosed(
                            f'WebSocket Server cannot establish connection with client [{provider}].')
                    continue
            
            log.debug('Executing js code: %s' % code)
            
            ws_client = cls.websocket_clients.get(provider)
            if ws_client and ws_client.get('ws'):
                if ws_client['ws'].closed:
                    log.debug(f'WebSocket [{provider}] is logically closed. Cleaning up and retrying.')
                    cls.websocket_clients[provider] = None
                    continue
            
            # sending message to WGC
            try:
                asyncio.run(WGCClient.websocket_clients[provider]['ws'].send_str(msg_prefix + code))
            except (ConnectionResetError, AttributeError, BrokenPipeError, Exception) as e:
                cls.search_crash_and_assert()
                log.debug(f'Websocket connection [{provider}] error: {e}. Trying to reconnect.')
                cls.websocket_clients[provider] = None
                
                if counter == retries - 1:
                    raise WGCDoesntGetResponseException(
                        f'Provider[{provider}] failed to send message after retries. Error: {e}')
                continue
            except RuntimeError:
                sleep(3)
                continue
            
            if not dont_wait_response:
                response_received = Waiter.poll(
                    timeout,
                    lambda: (
                        cls.websocket_clients[provider] is not None and
                        msg_prefix in cls.websocket_clients[provider]['messages_storage'].keys()
                    ),
                    sleep_seconds=.001, inside=check_crash)
                if response_received:
                    result = cls.websocket_clients[provider]['messages_storage'][msg_prefix]
                    del cls.websocket_clients[provider]['messages_storage'][msg_prefix]
                else:
                    cls.search_crash_and_assert()
                    if counter == retries - 1:
                        cls.websocket_clients[provider] = None
                        raise WGCDoesntGetResponseException(
                            f'Provider[{provider}] is not responding in {timeout} seconds')
                    continue
                if 'getElementsDump()' not in result \
                        and 'getLog()' not in result:
                    log.debug(
                        'Received message from '
                        'WGC client: %s' % result[:300] +
                        '..' * bool(len(result) > 300))
            return result
        return result

    @classmethod
    def activate_ui(cls, provider=None):
        prefix = 'WGC'
        if provider is None:
            provider = cls.websocket_path_wgc
        log.info(f'Activate {prefix} window.')
        cls.execute_js('wgc.app.activateUI()', dont_wait_response=True, provider=provider)

    @classmethod
    def activate_wgc(cls):
        """
        Activate WGC window without mouse input.

        Strategy:
          1) Resolve top-level WGC HWND.
          2) Defocus from WGC (helps in cases when UI is stuck with stale focus).
          3) Trigger in-app activation via JS.
          4) Strictly bring WGC to foreground.
        """
        RequirementHelper.append_requirement('LLR.2.2')
        log.info('Activate WGC window.')

        if not cls.is_wgc_running():
            return False

        provider = cls.websocket_path_wgc

        hwnd = cls.get_wgc_hwnd()
        if not hwnd:
            return False

        hwnd = WinApi.get_top_level_parent(hwnd)

        # Ensure WGC loses focus first (no mouse clicks).
        WinApi.defocus_from(hwnd_to_avoid=hwnd, timeout_s=1.0)
        sleep(0.2)

        # Trigger UI activation inside the app (fire-and-forget).
        try:
            cls.execute_js('wgc.app.activateUI()', dont_wait_response=True, provider=provider)
        except Exception as e:
            log.debug(f'activate_wgc: execute_js failed: {e}')

        # Bring WGC to the foreground.
        ok = WinApi.set_foreground_strict(hwnd, timeout_s=2.5)
        if not ok:
            log.warning('Activate WGC window: failed to set foreground strictly.')
        return ok

    @classmethod
    def set_focus_window(cls):
        """
        Set WGC window in focus without mouse input.
        Automatically restores window from tray or minimized state.
        """
        RequirementHelper.append_requirement('LLR.2.2')
        log.info('Activate WGC window (set focus).')

        if not cls.is_wgc_running():
            return False

        # Prefer existing resolver if it is stable in your project:
        try:
            hwnd = cls.get_wgc_hwnd()
        except Exception:
            hwnd = 0

        # Fallback: search by title/class if needed.
        if not hwnd:
            try:
                hwnd = WinApi.get_top_level_window(
                    WGCConfig.application.wgc_window_title_name,
                    'Chrome_WidgetWin_0'
                )
            except Exception:
                hwnd = 0

        if not hwnd:
            return False

        if win32gui.IsIconic(hwnd) or not win32gui.IsWindowVisible(hwnd):
            log.info(f'Window {hwnd} is minimized or hidden (tray). Restoring...')

            # SW_RESTORE (9)
            win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
            sleep(0.5)
        # -------------------------------------------------

        hwnd = WinApi.get_top_level_parent(hwnd)

        # Optional: defocus from current foreground to improve SetForegroundWindow success.
        WinApi.defocus_from(hwnd_to_avoid=hwnd, timeout_s=0.8)
        sleep(0.05)

        return WinApi.set_foreground_strict(hwnd, timeout_s=2.5)

    @classmethod
    def set_focus_self_update_window(cls):
        """
        Set WGC self-update window in focus without mouse input.
        """
        RequirementHelper.append_requirement('LLR.2.2')
        log.info('Activate WGC self-update window (set focus).')

        if not cls.is_wgc_update_in_progress():
            return False

        updater_title = f'*{WGCConfig.application.app_name}*'
        hwnd = WinApi.get_top_level_window(updater_title, 'Qt5QWindowIcon')
        if not hwnd:
            return False

        hwnd = WinApi.get_top_level_parent(hwnd)

        WinApi.defocus_from(hwnd_to_avoid=hwnd, timeout_s=0.8)
        sleep(0.05)

        return WinApi.set_foreground_strict(hwnd, timeout_s=2.5)

    @classmethod
    def kill_focus(cls):
        """Kill focus from WGC window (no mouse moves/clicks)."""
        RequirementHelper.append_requirement('LLR.2.2')
        log.info('Kill focus from WGC window.')

        hwnd_wgc = 0
        try:
            if cls.is_wgc_running():
                hwnd_wgc = cls.get_wgc_hwnd()
        except Exception:
            hwnd_wgc = 0

        WinApi.defocus_from(hwnd_to_avoid=hwnd_wgc, timeout_s=1.5)

    @classmethod
    def set_wgc_focus(cls):
        """Set focus on WGC window (no mouse moves/clicks)."""
        RequirementHelper.append_requirement('LLR.2.2')
        log.info('Set focus on WGC window.')

        if not cls.is_wgc_running():
            return False

        hwnd = cls.get_wgc_hwnd()
        if not hwnd:
            return False

        hwnd = WinApi.get_top_level_parent(cls.get_wgc_hwnd())
        return WinApi.set_foreground_strict(hwnd, timeout_s=2.5)

    @classmethod
    def minimize_wgc_window_to_tray(cls):
        """
        Simulate clicking on Windows X button.
        """
        log.info('Click X in header to minimize main window.')
        hwnd = Waiter.poll(5, lambda: WinApi.get_top_level_window(
            WGCConfig.application.wgc_window_title_name, 'Chrome_WidgetWin_0'))
        # Get the system menu of the window
        win32gui.GetSystemMenu(hwnd, False)

        # Send the SC_CLOSE command to simulate clicking the close button
        win32gui.PostMessage(hwnd, win32con.WM_SYSCOMMAND, win32con.SC_CLOSE, 0)
        sleep(1.0)

    @classmethod
    def maximize_wgc_window_from_tray(cls):
        """
        Restore WGC windows from tray.
        """
        log.info('Restore WGC window from tray.')
        hwnd = Waiter.poll(5, lambda: WinApi.get_top_level_window(
            WGCConfig.application.wgc_window_title_name, 'Chrome_WidgetWin_0'))
        # Check if the window is minimized, and if so, restore it
        if win32gui.IsIconic(hwnd):
            win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)

        # Maximize the window
        win32gui.ShowWindow(hwnd, win32con.SW_MAXIMIZE)
        sleep(1.0)

    @classmethod
    def close_crash_window(cls):
        hwnd = WinApi.get_top_level_window(WGCConfig.application.wgc_window_title_name, '*32770')
        WinApi.set_active(hwnd)
        win32gui.SendMessage(
            WinApi.find_control(WGCConfig.application.wgc_window_title_name, '*32770',
                                'Button', 'Close'), win32con.BM_CLICK, 0, 0)

    @classmethod
    def safe_quit_wgc(cls, timeout=2, confirm_required=False, force_close=True, trigger_enabled=False):
        """
        Close WGC by X button.

        :param bool confirm_required: wait for confirm popup or not.
        :param float timeout: timeout
        """
        RequirementHelper.append_requirement('LLR.2.10')
        log.info('Click X in header to close WGC.')
        texts = None
        hwnd = Waiter.poll(5, lambda: WinApi.get_top_level_window(
            WGCConfig.application.wgc_window_title_name, 'Chrome_WidgetWin_0'),
                           'Expected that WGC opened.')
        PostMessage(hwnd, WM_CLOSE, 0, 0)
        is_running_func = cls.is_wgc_running
        try:
            if confirm_required and cls.wait_for_element('ExitPopupYes', timeout=2):
                texts = [cls.get_element_text('ExitPopupYes', index=i) for i in range(
                    cls.get_elements_count('ExitPopupYes'))]
                cls.click('ExitPopupYes', wait_response=False, trigger_enabled=trigger_enabled)
            else:
                wgc_not_running = \
                    Waiter.poll(timeout, lambda: not cls.is_binary_process_exist() or not is_running_func(),
                                sleep_seconds=.01)
                try:
                    if not wgc_not_running and cls.is_element_displayed('ExitPopupYes'):
                        cls.click('ExitPopupYes', wait_response=False, trigger_enabled=trigger_enabled)
                        Waiter.poll(3, lambda: not cls.is_wgc_running)
                except WGCException as e:
                    log.info(e)
        except (WebSocketConnectionClosed, PollingTimeoutExpired) as e:
            log.error(e)
            cls.websocket_clients[cls.websocket_path_wgc] = None
        cls.search_crash_and_assert()
        if force_close:
            cls.close_wgc()
        return texts

    @classmethod
    def get_current_wgc_log_file(cls, home_folder=None):
        """
        Get current WGC log file.

        :rtype: str
        :return: Path to current log.
        """
        home_folder = home_folder or WGCConfig.application.folder
        prefix = WGCConfig.application.data_prefix
        if hasattr(builtins, 'provider'):
            if builtins.provider == cls.websocket_path_wgc:
                prefix = 'wgc'

        try:
            files = glob.glob(path.join(home_folder, 'logs', prefix + '_*'))
            latest_file = None
            if files:
                latest_file = max(files, key=path.getctime)
            return latest_file
        except (FileNotFoundError, OSError):
            pass

    @classmethod
    def get_2_latest_wgc_log_files(cls, home_folder=None):
        """
        Get 2 current WGC log files.

        :rtype: str
        :return: Path to current log.
        """
        home_folder = home_folder or WGCConfig.application.folder
        try:
            files = glob.glob(path.join(home_folder, 'logs', WGCConfig.application.data_prefix + '_*'))
            latest_file = pre_last = None
            if files:
                latest_file = max(files, key=path.getctime)
                if len(files) > 1:
                    files.remove(latest_file)
                    pre_last = max(files, key=path.getctime)
            return latest_file, pre_last
        except (FileNotFoundError, OSError):
            pass

    @classmethod
    def get_current_wgc_log_file_content(cls, wgc_folder=None):
        wgc_folder = wgc_folder or WGCConfig.application.folder
        files = glob.glob(path.join(wgc_folder, 'logs', WGCConfig.application.data_prefix + '_*'))
        result_content = ''
        if files:
            for file_ in files:
                try:
                    with open(file_, 'r', encoding='utf-8') as fp:
                        content = fp.read()
                except (OSError, PermissionError, IOError) as e:
                    log.debug(e)
                    content = ''
                result_content += content
        else:
            log.debug('There is no WGC logs in logs folder.')
            return result_content
        return result_content

    @classmethod
    def get_cwlfc(cls, wgc_folder=None):
        return cls.get_current_wgc_log_file_content(wgc_folder)

    @classmethod
    def get_current_cef_log_file_content(cls, wgc_folder=None):
        wgc_folder = wgc_folder or WGCConfig.application.folder
        file_ = cls.get_current_cef_log_file(wgc_folder)
        if file_:
            try:
                with open(file_, 'r', encoding='utf-8') as fp:
                    return fp.read()
            except (OSError, PermissionError, IOError) as e:
                log.debug(e)
                return ''
        else:
            log.debug('There is no WGC logs in logs folder.')
            return ''

    @classmethod
    def get_latest_line_cef_log_file_content(cls, wgc_folder=None, pattern=None):
        wgc_folder = wgc_folder or WGCConfig.application.folder
        file_ = cls.get_current_cef_log_file(wgc_folder)
        if file_:
            try:
                with open(file_, 'r', encoding='utf-8') as fp:
                    lines = fp.readlines()
                    if pattern is None:
                        return lines[-1]
                    else:
                        result = []
                        for line in lines:
                            if re.match(RegexHelper.convert_wildcard_to_regex(pattern), line):
                                result.append(line)
                        if result:
                            return result[-1]
                        return ''
            except (OSError, PermissionError, IOError) as e:
                log.debug(e)
                return ''
        else:
            log.debug('There is no WGC logs in logs folder.')
            return ''

    @classmethod
    def get_current_pliginhost_log_file_content(cls, wgc_folder=None):
        wgc_folder = wgc_folder or WGCConfig.application.folder
        file_ = cls.get_current_pluginhost_log_file(wgc_folder)
        if file_:
            try:
                with open(file_, 'r', encoding='utf-8') as fp:
                    return fp.read()
            except (OSError, PermissionError, IOError) as e:
                log.debug(e)
                return ''
        else:
            log.debug('There is no WGC logs in logs folder.')
            return ''

    @classmethod
    def get_current_cef_log_file(cls, home_folder=None):
        """
        Get current WGC log file.

        :rtype: str
        :return: Path to current log.
        """
        home_folder = home_folder or WGCConfig.application.folder
        try:
            files = glob.glob(path.join(home_folder, 'logs', 'cef_*'))
            latest_file = None
            if files:
                latest_file = max(files, key=path.getctime)
            return latest_file
        except (FileNotFoundError, OSError):
            pass

    @classmethod
    def get_current_pluginhost_log_file(cls, home_folder=None):
        """
		Get current WGC pluginhost log file.

		:rtype: str
		:return: Path to current log.
		"""
        home_folder = home_folder or WGCConfig.application.folder
        try:
            files = glob.glob(path.join(home_folder, 'logs', 'pluginhost_*'))
            latest_file = None
            if files:
                latest_file = max(files, key=path.getctime)
            return latest_file
        except (FileNotFoundError, OSError):
            pass

    @classmethod
    def get_current_monitor_log_file(cls, home_folder=None):
        """
        Get current monitor log file.

        :rtype: str
        :return: Path to current log.
        """
        home_folder = home_folder or WGCConfig.application.folder
        try:
            files = glob.glob(path.join(home_folder, 'logs', 'monitor_*'))
            latest_file = None
            if files:
                latest_file = max(files, key=path.getctime)
            return latest_file
        except (FileNotFoundError, OSError):
            pass

    @classmethod
    def get_monitor_log_files(cls, home_folder=None):
        """
        Get current monitor log files.

        :rtype: str
        :return: Path to current log.
        """
        home_folder = home_folder or WGCConfig.application.folder
        try:
            return glob.glob(path.join(home_folder, 'logs', 'monitor_*'))
        except (FileNotFoundError, OSError):
            return []

    @classmethod
    def get_exit_codes_for(cls, process_name):
        RequirementHelper.append_requirement('LLR.2.71')
        log_file = cls.get_current_wgc_log_file()
        content = OSHelper.get_file_content(log_file)
        match = re.findall('.+(%s).+exit code = (.+)' % process_name, content, re.MULTILINE)
        result = []
        if match:
            for p in match:
                result.append(p[1].rstrip())
        return result

    @classmethod
    def get_exit_code_for(cls, process_name):
        RequirementHelper.append_requirement('LLR.2.71')
        log_file = cls.get_current_wgc_log_file()
        content = OSHelper.get_file_content(log_file)
        match = re.search('.+(%s).+exit code = (.+)' % process_name, content)
        if match:
            return match.group(2).rstrip()
        return None

    @classmethod
    def get_current_url(cls):
        """
        Gets current WGC page url.

        :rtype: str
        :return: WGC page url.
        """
        if not cls.is_wgc_running():
            raise WGCException('WGC should be run to get page url.')

        result = cls.execute_js('document.URL')
        return result

    @classmethod
    def is_element_displayed(cls, element_name, template=None, index=None, provider=None):
        """
        Verifies is element displayed.

        :param int index: element index.
        :param str element_name: element name.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: bool
        :return: flag that indicates is element is displayed or not.
        """
        element = cls.get_wgc_element(element_name, template, index, provider=provider)
        result = element.is_element_displayed
        log.debug('Check that element "%s" is displayed. Result: %s' %
                  (element_name, result))
        return result

    @classmethod
    def is_checkbox_checked(cls, element_name, template=None, index=0, provider=None):
        """
        Checks is checkbox checked.

        :param str element_name: element name.
        :param int index: element index.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: bool
        :return: flag that indicates is checkbox checked or not.
        """
        log.debug(f'Get "checked" state of "{element_name}" element.')
        element = cls.get_wgc_element(element_name, template, index=index, provider=provider)
        return element.is_checkbox_checked

    @classmethod
    def set_checkbox_state(cls, element_name, checked=True, template=None, index=0, provider=None):
        """
        Sets checkbox state.

        :param str element_name: element name.
        :param int index: element index.
        :param bool checked: flag that indicates should we set state to
        checked or to unchecked.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug(f'Set "{element_name}" checkbox to checked="{checked}" state.')
        checkbox_state = cls.is_checkbox_checked(element_name, template, index, provider=provider)
        if checkbox_state != checked:
            cls.click(element_name, template, index_=index, provider=provider)

    @classmethod
    def get_bounding_client_rect(cls, element_name, template=None, index=0, provider=None):
        cls.wait_for_element(element_name, template=template, index=index, provider=provider)
        element = cls.get_wgc_element(element_name, template, provider=provider)
        data = element.get_bounding_client_rect(index)
        return data

    @classmethod
    def get_element_position(cls, element_name, template=None, index=0, provider=None):
        element = cls.get_bounding_client_rect(element_name, template, index, provider=provider)
        hwnd = WinApi.get_top_level_window(WGCConfig.application.wgc_window_title_name, 'Chrome_WidgetWin_0')
        if hwnd == 0:
            raise WGCException('HWND WGC window does not found.')
        client = win32gui.GetWindowRect(hwnd)
        dpi_scale = WinApi.get_dpi_for_window(hwnd)
        x = client[0] + int(element['left'] * dpi_scale)
        y = client[1] + int(element['top'] * dpi_scale)
        return x, y, client[0] + int(element['right'] * dpi_scale), client[1] + int(element['bottom'] * dpi_scale), \
               int(element['width'] * dpi_scale), int(element['height'] * dpi_scale)

    @classmethod
    def click_element_by_mouse(cls, element_name, template=None,
                               index=0, x_offset=0, y_offset=0, smooth=True, provider=None):
        x, y, r, b, w, h = cls.get_element_position(element_name, template, index, provider=provider)
        uisoup.mouse.move(x + 5 + x_offset, y + 5 + y_offset, smooth=smooth)
        WinApi.click(x + 5 + x_offset, y + 5 + y_offset)

    @classmethod
    def click(cls, element_name, template=None, wait_response=True,
              trigger_enabled=False, timeout=10, index_=0, ignore_exc=False, provider=None):
        """
        Clicks on element.

        :param bool ignore_exc: ignore errors.
        :param int index_: index of web element in list.
        :param int timeout: time to wait response from websocket client.
        :param str element_name: element name.
        :param bool wait_response: flag that indicates should we wait for
        response or not.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :param bool trigger_enabled: flag indicate send trigger event
        mousedown/mouseup or not.
        """
        log.debug('Click on element "%s".' % element_name)
        element = cls.get_wgc_element(element_name, template, provider=provider)
        cls.search_crash_and_assert()
        element.click(wait_response, timeout, index_, ignore_exc)
        if trigger_enabled:
            cls.trigger_mouse_event(element_name, template, index_, provider=provider)

    @classmethod
    def trigger_mouse_event(cls, element_name, template=None, index_=0, provider=None):
        """
        Triggers mousedown + mouseup signals.

        :param str element_name: element name.
        :param int index_: index of web element in list.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug('Sending mousedown + mouseup signal.')
        cls.trigger_event(
            element_name, 'mousedown', '{ which: 1, button: 0 }', template,
            index_, provider=provider)
        sleep(.2)
        cls.trigger_event(
            element_name, 'mouseup', '{ which: 1, button: 0 }', template,
            index_, provider=provider)

    @classmethod
    def trigger_event(cls, element_name, event, params=None, template=None,
                      index=0, provider=None):
        """
        Triggers event on element.

        :param str element_name: element name.
        :param str event: name of event that should be triggered.
        :param str params: params that should be passed with event.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :param int index: index of web element in list.
        """
        log.debug('Triggering event "%s" with params "%s" on element "%s".' %
                  (event, params, element_name))
        element = cls.get_wgc_element(element_name, template, provider=provider)
        element.trigger_event(event, params, index)

    @classmethod
    def set_text(cls, element_name, text, template=None, index=0, provider=None):
        """
        Sets text of element.

        :param str element_name: element name.
        :param int index: index of element.
        :param str text: text that should be set on element.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug('Setting element "%s" text to "%s".' % (element_name, text))
        element = cls.get_wgc_element(element_name, template, index=index, provider=provider)
        element.set_text(text, index=index)

    @classmethod
    def react_set_text(cls, element_name, text, template=None, provider=None, index=0):
        """
        Sets text of element.

        :param str element_name: element name.
        :param str text: text that should be set on element.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug('Setting element "%s" text to "%s".' % (element_name, text))
        element = cls.get_wgc_element(element_name, template, provider=provider, index=index)
        element.react_set_text(text, index=index)

    @classmethod
    def react_mouse_down(cls, element_name, template=None, index=0, provider=None):
        """
        Mouse Down of element.

        :param str element_name: element name.
        :param int index: index that should be set for element.
        :param object provider: provider that should be set for executor.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug(f'Mouse down on element "{element_name}".')
        element = cls.get_wgc_element(element_name, template, provider=provider, index=index)
        element.react_mouse_down()
        
    @classmethod
    def get_display_property(cls, element_name, template=None, index=0, provider=None):
        log.debug(f'Get style "display property" for element "{element_name}".')
        element = cls.get_wgc_element(element_name, template, provider=provider, index=index)
        return element.get_display_property()
    
    @classmethod
    def react_mouseover(cls, element_name, template=None, index=0, provider=None):
        """
        Mouse Hover of element.

        :param str element_name: element name.
        :param int index: index that should be set for element.
        :param object provider: provider that should be set for executor.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug(f'Mouse over on element "{element_name}".')
        element = cls.get_wgc_element(element_name, template, provider=provider, index=index)
        element.react_mouseover()
    
    @classmethod
    def react_mouseleave(cls, element_name, template=None, index=0, provider=None):
        """
        Mouse Hover of element.

        :param str element_name: element name.
        :param int index: index that should be set for element.
        :param object provider: provider that should be set for executor.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug(f'Mouse leave on element "{element_name}".')
        element = cls.get_wgc_element(element_name, template, provider=provider, index=index)
        element.react_mouseleave()
        
    @classmethod
    def wait_for_element(cls, element_name, timeout=30, template=None,
                         message=None, index=0, dont_send_any_events=False, provider=None):
        """
        Waits for element.

        :param str message: assert message if element not displayed.
        :param str element_name: element name.
        :param int index: element index.
        :param int timeout: timeout in seconds.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: bool
        :return: flag that indicate is element was found or not.
        """
        element = cls.get_wgc_element(element_name, template, index=index, provider=provider)
        result = Waiter.poll(timeout, lambda: element.is_element_present,
                             failure_message=message,
                             dont_send_any_events=dont_send_any_events)
        return result

    @classmethod
    def wait_element_disappear(cls, element_name, timeout=15, template=None,
                               message=None, provider=None):
        """
        Waits element disappear.

        :param str element_name: element name.
        :param int timeout: timeout in seconds.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :param str message: assert message.
        :rtype: bool
        :return: flag that indicate is element disappears or not.
        """
        log.debug('Waiting while element "%s" disappear with timeout "%s".' %
                  (element_name, timeout))

        def element_not_displayed():
            return not cls.is_element_displayed(element_name, template, provider=provider)

        result = Waiter.poll(timeout, element_not_displayed, message,
                             sleep_seconds=.3)
        return result

    @classmethod
    def get_element_text(cls, element_name, template=None, index=0, provider=None):
        """
        Get text of element.

        :param int index: element index.
        :param str element_name: element name.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: str
        :returns: attribute text.
        """
        log.debug('Get text of "%s" element.' % element_name)
        element = cls.get_wgc_element(element_name, template, index, provider=provider)
        result = element.get_text()
        return result

    @classmethod
    def get_element_html_text(cls, element_name, template=None, index=0, provider=None):
        """
        Get text of element.

        :param int index: element index.
        :param str element_name: element name.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: str
        :returns: attribute text.
        """
        log.debug('Get text of "%s" element.' % element_name)
        element = cls.get_wgc_element(element_name, template, index, provider=provider)
        result = element.get_html_text()
        return result

    @classmethod
    def get_element_value(cls, element_name, template=None, provider=None, index=0):
        """
        Get an value of element.

        :param str element_name: element name.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: str
        :returns: attribute value.
        """
        log.debug('Get value of "%s" element.' % element_name)
        element = cls.get_wgc_element(element_name, template, provider=provider, index=index)
        result = element.get_value()
        return result

    @classmethod
    def get_element_attribute(cls, element_name, attribute_name,
                              template=None, index=0, provider=None):
        """
        Get an attribute of element.

        :param int index: index of element.
        :param str element_name: element name.
        :param str attribute_name: name of attribute that should be taken.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: str
        :returns: attribute value.
        """
        log.debug('Get attribute "%s" of element "%s".' % (attribute_name,
                                                           element_name))
        element = cls.get_wgc_element(element_name, template, provider=provider)
        result = element.get_attribute(attribute_name, index)
        return result

    @classmethod
    def get_element_computed_style_attribute(cls, element_name, attribute_name, template=None, index=0, provider=None):
        """
        Get a computed style attribute of element.

        :param int index: index of element.
        :param str element_name: element name.
        :param str attribute_name: name of attribute that should be taken.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: str
        :returns: attribute value.
        """
        log.debug(f'Get style attribute "{attribute_name}" of element "{element_name}".')
        element = cls.get_wgc_element(element_name, template, provider=provider)
        result = element.get_property_value(attribute_name, index)
        return result
    
    @classmethod
    def get_element_style_attribute(cls, element_name, attribute_name, template=None, index=0, provider=None):
        """
        Get a style attribute of element.

        :param int index: index of element.
        :param str element_name: element name.
        :param str attribute_name: name of attribute that should be taken.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: str
        :returns: attribute value.
        """
        log.debug(f'Get style attribute "{attribute_name}" of element "{element_name}".')
        element = cls.get_wgc_element(element_name, template, provider=provider)
        result = element.get_style_value(attribute_name, index)
        return result

    @classmethod
    def copy_wgc_helper_exe(cls):
        try:
            log.info('Copy helper_process.exe which always return code 0 after start.')
            wgc_helper = get_third_party_path('helper_process.exe')
            OSHelper.copy(wgc_helper, path.join(
                WGCConfig.application.folder, 'dlls', 'helper_process.exe'))
            return True
        except Exception as e:
            log.debug(e)
            return False

    @classmethod
    def create_games_restrictions_xml(cls, games: list, home_folder=None):
        RequirementHelper.append_requirement('INT.4.10')
        log.info('Create games_restrictions.xml with %s games ids.' % games)
        working_dir = home_folder or WGCConfig.application.folder
        file_ = path.join(working_dir, 'games_restrictions.xml')
        if games:
            data = {'id': games}
        else:
            data = ''
        content = {'protocol': {'allowed': data, '@name': 'games', '@version': '1.0'}}
        if path.isfile(file_):
            OSHelper.remove_file_or_directory(file_)
        with open(file_, 'w') as fp:
            fp.write(xmltodict.unparse(content, pretty=True))
        return file_
    
    @classmethod
    def create_restrictions_xml_v2(
            cls,
            games: Optional[List[str]] = None,
            realms: Optional[List[str]] = None,
            accounts: Optional[List[Dict[str, str]]] = None,
            allow_user_logout_ui: bool = False,
            allow_redeem_bonus_code_ui: bool = False,
            allow_open_ext_link_with_auth: bool = False,
            home_folder: Optional[str] = None
    ) -> str:
        """
        Creates a flexible restrictions.xml version 2.0 file.

        Args:
        games: List of game IDs for the <allowed_games> section.
        realms: List of realm IDs for the <allowed_realms> section.
        accounts: List of dictionaries for <allowed_accounts>,
        each dictionary must contain the keys 'id' and 'realm'.
        Example: [{'id': '11111', 'realm': 'EU'}, {'id': '22222', 'realm': 'NA'}]
        allow_user_logout_ui: Value for the <allow_user_logout_ui> tag.
        allow_redeem_bonus_code_ui: Value for the <allow_redeem_bonus_code_ui> tag.
        allow_open_ext_link_with_auth: Value for the <allow_open_ext_link_with_auth> tag.
        home_folder: Path to the directory where the file is saved. Defaults to the application folder.
        
        Returns:
        Path to the generated restrictions.xml file.
        """
        RequirementHelper.append_requirement('INT.4.10')
        log.info(f'Create restrictions.xml v2.0 with games: {games}, realms: {realms}, accounts: {accounts}')
        
        working_dir = home_folder or WGCConfig.application.folder
        file_ = path.join(working_dir, 'restrictions.xml')
        
        protocol_content: Dict[str, Any] = {
            '@name': 'restrictions',
            '@version': '2.0',
        }
        
        if games:
            protocol_content['allowed_games'] = {'id': games}
        
        if realms:
            protocol_content['allowed_realms'] = {'id': realms}
        
        if accounts:
            account_data = [
                {'@id': acc['id'], '@realm': acc['realm']} for acc in accounts
            ]
            protocol_content['allowed_accounts'] = {'account': account_data}
        
        protocol_content['allow_user_logout_ui'] = str(allow_user_logout_ui).lower()
        protocol_content['allow_redeem_bonus_code_ui'] = str(allow_redeem_bonus_code_ui).lower()
        protocol_content['allow_open_ext_link_with_auth'] = str(allow_open_ext_link_with_auth).lower()

        content = {'protocol': protocol_content}

        if path.isfile(file_):
            OSHelper.remove_file_or_directory(file_)

        with open(file_, 'w', encoding='utf-8') as fp:
            fp.write(xmltodict.unparse(content, pretty=True, indent='    '))
        
        return file_

    @classmethod
    def create_restrictions_xml(cls, enable_games_restrictions: bool, enable_realms_restrictions: bool, games: list,
                                realms: list, home_folder=None):
        RequirementHelper.append_requirement('INT.4.10')
        log.info(f'Create restrictions.xml with {games} games ids and {realms} realms.')
        working_dir = home_folder or WGCConfig.application.folder
        file_ = path.join(working_dir, 'restrictions.xml')
        games_data = ''
        realms_data = ''
        if games:
            games_data = {'id': games}
        if realms:
            realms_data = {'id': realms}
        content = {'protocol':
                   {'@name': 'restrictions',
                    '@version': '1.0',
                    'features': {'enable_games_restrictions': enable_games_restrictions,
                                 'enable_realms_restrictions': enable_realms_restrictions},
                    'allowed_games': games_data,
                    'allowed_realms': realms_data
                    }
                   }
        if path.isfile(file_):
            OSHelper.remove_file_or_directory(file_)
        with open(file_, 'w') as fp:
            fp.write(xmltodict.unparse(content, pretty=True))
        return file_

    @classmethod
    def _is_steam_update_in_progress(cls):
        done_title = r'Готово — Distribution sandbox*'
        update_title = r'Обновление Distribution sandbox*'
        done_title2 = r'Ready - Distribution sandbox*'
        update_title2 = r'Updating Distribution sandbox*'
        class_name = 'vguiPopupWindow'
        return bool(WinApi.get_top_level_window([update_title, update_title2, done_title, done_title2], class_name))

    @classmethod
    def wait_for_finish_steam_update(cls):
        done_title = r'Готово — Distribution sandbox*'
        done_title2 = r'Ready - Distribution sandbox*'
        class_name = 'vguiPopupWindow'
        Waiter.poll(360, lambda: WinApi.get_top_level_window([done_title, done_title2], class_name))

    @classmethod
    def steam_click_play_game(cls):
        done_title = r'Готово — Distribution sandbox*'
        done_title2 = r'Ready - Distribution sandbox*'
        class_name = 'vguiPopupWindow'
        Waiter.poll(5, lambda: WinApi.get_top_level_window([done_title, done_title2], class_name))
        for retry in range(3):
            try:
                hwnd = WinApi.get_top_level_window([done_title, done_title2], class_name)
                log.debug(f'steam_click_play_game -> hwnd={hwnd}')
                x, y, w, h = win32gui.GetWindowRect(hwnd)
                break
            except pywintypes.error as e:
                if retry == 2:
                    raise e
                sleep(1)
                continue
        width = w - x
        height = h - y
        log.debug(f'screen resolution: width={GetSystemMetrics(0)}, height={GetSystemMetrics(1)}')
        uisoup.mouse.move(int(x + width * 0.74), int(y + height * 0.74), smooth=True)
        WinApi.click(int(x + width * 0.74), int(y + height * 0.74))
        sleep(1)
        uisoup.mouse.move(int(x + width * 0.74), int(y + height * 0.6), smooth=False)
        WinApi.click(int(x + width * 0.74), int(y + height * 0.6))
        
    @classmethod
    def check_launch_wgc(cls, check_func, dont_send_any_events):
        log.debug(f'Waiting for {WGCConfig.application.prefix.upper()} will be launched.')
        was_launched = Waiter.poll(
            int(WGCConfig.wgc_launch_timeout), lambda: check_func(
                check_process=True) or cls.is_wgc_update_in_progress() or cls._is_steam_update_in_progress(),
            inside=True, sleep_seconds=1, dont_send_any_events=dont_send_any_events)
        if was_launched and cls._is_steam_update_in_progress():
            cls.wait_for_finish_steam_update()
            cls.steam_click_play_game()
            was_launched = Waiter.poll(
                int(WGCConfig.wgc_launch_timeout), lambda: check_func(
                    check_process=True) or cls.is_wgc_update_in_progress(),
                inside=True, sleep_seconds=1, dont_send_any_events=dont_send_any_events)
        if not was_launched:
            log.debug(f'{WGCConfig.application.prefix.upper()} was not launched.')
            logfile = cls.get_current_wgc_log_file()
            if logfile:
                with open(logfile, 'r', encoding='utf-8') as log_f:
                    content = log_f.read()
                failed = RegexHelper.search_incorrect_file_hash(content)
                if failed:
                    raise WGCIncorrectFileHash(failed[0])
                failed = RegexHelper.search_unsupported_protocol(content)
                if failed:
                    raise UnsupportedProtocolVersion(failed[0])
        return was_launched
    
    @classmethod
    def restart_steam(cls):
        Waiter.poll(7, lambda: not OSHelper.terminate_process(
            ['steam.exe', 'SteamOverlayUI.exe', 'steamwebhelper.exe']))
        sleep(2.5)  # delay between restart processes
        from wgc_helpers.steam_helper import SteamHelper
        SteamHelper.prepare_steam()

    @classmethod
    def run_game_center(cls, home_folder=None, log_debug=True,
                        params='', dont_wait=False, force_run=False, self_update=False,
                        without_make_installed=False, create_preferences=False,
                        log_trace=False, dont_wait_cmd_result=True,
                        skip_verification=False, enable_proxy=False, pre_execution=None,
                        dont_wait_after_init=False, dont_send_any_events=False, binary_name=None,
                        ensure_steam_launched=False):
        """
        Run WGC.

        :param bool dont_wait_cmd_result: wait output from cmd after run.
        :param bool log_trace: trace log level.
        :param str home_folder: specific path to home folder WGC.
        :param bool log_debug: flag that indicates should we launch WGC with
        debug logs or not.
        :param str params: additional params for run WGC.
        :param bool dont_wait: wait for WGC is running.
        :param bool force_run: flag indicate check WGC running or now at start.
        :param bool self_update: flag that indicates should we wait for
        installer or wgc.
        :param bool without_make_installed: flag that indicates should we set
        wgc as installed or not.
        :param bool create_preferences: flag that indicates should we create
        default preferences file before running wgc.
        :param bool enable_proxy: flag that indicats will proxy be supported
        or not.
        """
        RequirementHelper.append_requirement('LLR.2.37')
        if cls.websocket_clients[cls.websocket_path_wgc] is not None:
            cls.websocket_clients[cls.websocket_path_wgc]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_overlay] is not None:
            cls.websocket_clients[cls.websocket_path_overlay]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_news] is not None:
            cls.websocket_clients[cls.websocket_path_news]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_game_overlay] is not None:
            cls.websocket_clients[cls.websocket_path_game_overlay]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_survey] is not None:
            cls.websocket_clients[cls.websocket_path_survey]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_custom_login] is not None:
            cls.websocket_clients[cls.websocket_path_custom_login]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_dialog_with_iframe] is not None:
            cls.websocket_clients[cls.websocket_path_dialog_with_iframe]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_game_event] is not None:
            cls.websocket_clients[cls.websocket_path_game_event]['messages_storage'] = {}  # clear messages storage
            
        binary_name = binary_name or WGCConfig.application.binary_name
        check_func = cls.is_wgc_running
        if log_trace:
            log_debug = False
        params = '--make-wgc-installed' + ' --log debug' * log_debug + \
                 (' %s' % params) * bool(params) + ' --log trace' * log_trace + \
                 ' --enable-proxy' * enable_proxy
        if without_make_installed:
            params = params.replace('--make-wgc-installed ', '')
        home_folder = home_folder or WGCConfig.application.folder
        if skip_verification:
            params += ' --skip-verification'
        if not force_run:
            if check_func():
                raise WGCException(f'{WGCConfig.application.prefix.upper()} is '
                                   f'already running. You need to close it first.')
        if create_preferences:
            cls.create_default_preferences_xml()
        log.info(f'Starting {WGCConfig.application.prefix.upper()} with params={(params * bool(params)).lstrip()}.')
        wgc_binary_path = path.join(home_folder, binary_name)
        if not path.isfile(wgc_binary_path):
            raise WGCException(f'{WGCConfig.application.prefix.upper()} binary was not found at "{wgc_binary_path}".')
        start_proc = time()
        pre_cmd = '' if pre_execution is None else '%s ' % pre_execution
        if pre_execution:
            log.info('%s"%s" %s' % (pre_cmd, wgc_binary_path, params))
        command = f'{pre_cmd}"{wgc_binary_path}" {params}'
        result = CmdHelper.execute(command, dont_wait=dont_wait_cmd_result, dont_redirect_out_error=True)
        if not dont_wait_cmd_result:
            return result
        start = time()
        if not dont_wait:
            was_launched = cls.check_launch_wgc(check_func, dont_send_any_events)
            if not was_launched and ensure_steam_launched:
                cls.restart_steam()
                CmdHelper.execute(command, dont_wait=dont_wait_cmd_result, dont_redirect_out_error=True)
                was_launched = cls.check_launch_wgc(check_func, dont_send_any_events)
            if not was_launched:
                raise WGCRunningTimeoutException(
                    f'Expected that {WGCConfig.application.prefix.upper()} will be running at this '
                    f'moment(elapsed time: {int(time() - start)} seconds.).')

            if not self_update and not cls.is_wgc_update_in_progress() and not dont_wait_after_init \
                    and not force_run:
                Waiter.poll(
                    10, lambda: 'Running script with autotests' in cls.get_current_wgc_log_file_content(
                        home_folder))
        log.debug(f'{WGCConfig.application.prefix.upper()} LAUNCHING TIME: {time() - start_proc}')
        log.debug(f'{WGCConfig.application.prefix.upper()} '
                  f'current version: {WGCSettingsHelper.get_wgc_current_version(home_folder)}')
        sleep(2)

    @classmethod
    def get_wgc_log_filenames(cls, wgc_folder=None, wgc_logs=True, monitor_logs=True,
                              renderer_logs=True, cef_logs=True, overlay_logs=True):
        """
        Gets WGC log filenames.

        :param str wgc_folder: wgc home folder.
        :rtype: list[str]
        :return: WGC log filenames.
        """
        wgc_folder = wgc_folder or WGCConfig.application.folder
        exclude = ['original_app_tree.log', 'application_tree.log',
                   'games_tree.log', 'processes.log']
        try:
            logs = glob.glob(path.join(wgc_folder, 'logs\\**\\*.log'), recursive=True)
        except OSError as e:
            log.debug('Except during get *logs files for WGC.\r\n%s' % str(e))
            logs = []
        
        result = []
        logs = [log_ for log_ in logs if path.basename(log_) not in exclude]
        for log_file in logs:
            if wgc_logs and path.basename(log_file).startswith(f'wgc_'):
                result.append(log_file)
            if monitor_logs and path.basename(log_file).startswith('monitor_'):
                result.append(log_file)
            if renderer_logs and path.basename(log_file).startswith('renderer_'):
                result.append(log_file)
            if cef_logs and path.basename(log_file).startswith('cef_'):
                result.append(log_file)
            if overlay_logs and path.basename(log_file).startswith('overlay_'):
                result.append(log_file)
        return result

    @classmethod
    def get_ver_tmp_folder(cls, wgc_folder=None):
        wgc_folder = wgc_folder or WGCConfig.application.folder
        ver_tmp = list(Path(wgc_folder).glob('ver_tmp*'))
        return ver_tmp

    @classmethod
    def run_wgc_api(cls, wgc_api_folder=None, cmd_line_params='', copy_from_wgc_dir=False):
        """
        Run WGC api.

        :param str wgc_api_folder: folder that have wgc_api binary.
        :param str cmd_line_params: command line parameters.
        """
        RequirementHelper.append_requirement('INT.4.4', 'LLR.2.39', 'LLR.2.37')
        wgc_api_folder = wgc_api_folder or str(path.join(
            WGCConfig.application.folder, WGCConfig.application.api.folder))
        wgc_api_path = path.join(wgc_api_folder, WGCConfig.application.api.binary_name)
        if copy_from_wgc_dir:
            OSHelper.copy(str(path.join(
                WGCConfig.application.folder,
                WGCConfig.application.api.folder,
                WGCConfig.application.api.binary_name)), wgc_api_path)
        if not path.isfile(wgc_api_path):
            raise WGCException('There is not "%s" in "%s" folder.' %
                               (WGCConfig.application.api.binary_name,
                                wgc_api_folder))
        log.info(f'Run "{wgc_api_path}" {cmd_line_params}')
        wgc_api_command = 'chcp 866 & "%s" %s' % (wgc_api_path, cmd_line_params)
        CmdHelper.execute(wgc_api_command, dont_wait=True)

    @classmethod
    def hover_on_element(cls, element_name, template=None, provider=None):
        """
        Hover on element.

        :param str element_name: element name.
        :param str provider: websocket provider.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug('Hover on "%s" element.' % element_name)
        element = cls.get_wgc_element(element_name, template, provider=provider)
        element.hover_on_element()

    @classmethod
    def mouseenter_on_element(cls, element_name, template=None, provider=None):
        """
        Mouseenter on element.

        :param str element_name: element name.
        :param str provider: websocket provider.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug('Mouse enter on "%s" element.' % element_name)
        element = cls.get_wgc_element(element_name, template, provider=provider)
        element.mouseenter_on_element()

    @classmethod
    def mouseleave_on_element(cls, element_name, template=None, provider=None):
        """
        Mouseleave on element.

        :param str element_name: element name.
        :param str provider: websocket provider.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug('Mouse leave on "%s" element.' % element_name)
        element = cls.get_wgc_element(element_name, template, provider=provider)
        element.mouseleave_on_element()

    @classmethod
    def mouseover_on_element(cls, element_name, template=None, provider=None):
        """
        Mouseover on element.

        :param str element_name: element name.
        :param str provider: websocket provider.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        log.debug('Mouse over on "%s" element.' % element_name)
        element = cls.get_wgc_element(element_name, template, provider=provider)
        element.mouseleave_on_element()
        element.mouseover_on_element()

    @classmethod
    def get_elements_count(cls, element_name, template=None, provider=None):
        """
        Gets elements count.

        :param str element_name: element name.
        :param str provider: websocket provider.
        :param dict{str, str} template: template that shows what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: int
        :returns: elements count.
        """
        log.debug('Get count of "%s" element.' % element_name)
        element = cls.get_wgc_element(element_name, template, provider=provider)
        result = element.get_elements_count()
        return result

    @classmethod
    def terminate_wgc_process(cls):
        """
        Terminates WGC process.
        """
        log.debug('Terminating WGC process.')
        processes_to_terminate = ['wgc_helper.exe',
                                  'wgc_helper_host.exe',
                                  f'wgc.exe.update',
                                  'wgc.exe',
                                  'wgc_api.exe', 'wgcs_api.exe',
                                  'WerFault.exe', 'wermgr.exe', 'wgc360_api.exe',
                                  'overlay_test_x64.exe', 'overlay_test_x86.exe',
                                  'wgc_renderer_host.exe',
                                  'helper_process.exe',
                                  'wgctmp_setup.tmp', 'wgctmp_setup.exe',
                                  'pluginhost.exe', 'pluginhost_l.exe',
                                  'WargamingErrorMonitor.exe',
                                  ]
        OSHelper.terminate_process(processes_to_terminate)

    @classmethod
    def game_installers_processes(cls):
        return ['wot_encore_install', 'world_of_warships_pt_install',
                'world_of_warships_pi_install', 'world_of_warships_install',
                'world_of_warplanes_ww_install', 'world_of_tanks_sb_install',
                'world_of_tanks_install', 'world_of_tanks_ct_install',
                'world_of_warships_ww_inst',
                'test_hide_settings_install',
                'wargaming_game_center_ins',
                'test_extrafiles_install',
                'wargaming_game_center_exp'
                f'{WGCConfig.wgc_publisher}_game_center_express_install',
                f'{WGCConfig.wgc_publisher}_game_center_install',
                'steel_division_2_install', 'caliber_install']

    @classmethod
    def get_all_related_wgc_process(cls):
        """
        Terminates WGC process.
        """
        steam_processes = ['steam.exe', 'SteamOverlayUI.exe', 'steamwebhelper.exe']
        processes_to_terminate = ['wgc.exe', 'tasklist.exe',
                                  'wgc_api.exe', 'wgcs_api.exe',
                                  'wgc360_api.exe', 'slui.exe',
                                  'wgc_helper_host.exe',
                                  'WargamingErrorMonitor.exe',
                                  'full_screen_app.exe',
                                  'world_of_tanks_install_*',
                                  'wargaming_game_center_*',
                                  'test_hide_settings_*',
                                  'dxsetup.exe.+',
                                  'dxsetup.exe',
                                  'wgc_renderer_host.exe',
                                  'helper_process.exe',
                                  'wgc.exe.update', 'WerFault.exe',
                                  'wgctmp_setup.tmp', 'wgctmp_setup.exe',
                                  '.+game_center.+',
                                  'qihoowgctmp_setup.tmp', 'qihoowgctmp_setup.exe',
                                  'pluginhost.exe', 'pluginhost_l.exe',
                                  'Dbgview.exe', 'dummy_game.exe', 'setup.*', 'procdump.exe',
                                  'server32-windows.exe',
                                  'game_client.exe',
                                  'overlay_test_x64.exe', 'overlay_test_x86.exe',
                                  'Procmon.exe', 'Procmon64.exe',
                                  'Dbghost.exe', 'rundll32.exe',
                                  '1507640920_directx_Jun2010.+']
        if WGCConfig.terminate_steam_after_test:
            processes_to_terminate.extend(steam_processes)
        processes_to_terminate.extend(cls.game_installers_processes())
        return processes_to_terminate

    @classmethod
    def delete_desktop_links(cls):
        wot = Path(WGCConfig.application.user_desktop_folder).glob('*Tanks*.lnk')
        wowp = Path(WGCConfig.application.user_desktop_folder).glob('*planes*.lnk')
        wows = Path(WGCConfig.application.user_desktop_folder).glob('*ships*.lnk')
        game = Path(WGCConfig.application.user_desktop_folder).glob('*Game RU*.lnk')
        for lnk in Path(WGCConfig.application.user_desktop_folder).glob('**\\*.*'):
            OSHelper.unlock_file_or_folder(str(lnk))
        for lnk in itertools.chain(wot, wowp, wows, game):
            try:
                OSHelper.remove_file_or_directory(str(lnk))
            except WindowsError:
                pass

    @classmethod
    def log_libtorrent(cls, self_update=False):
        if self_update:
            cls.set_focus_self_update_window()
        else:
            cls.set_focus_window()
        kc = uisoup.keyboard.codes
        for _ in range(5):
            uisoup.keyboard.send(kc.CONTROL.modify(kc.KEY_L))
            sleep(.8)

    @classmethod
    def prepare_wgc_before_test_run(cls, wgc_folder=None, publisher=None):
        """
        Prepares wgc before test run.

        :param str publisher: wgc publisher.
        :param str wgc_folder: WGC destination folder.
        """
        publisher = publisher or WGCConfig.wgc_publisher
        WGCConfig.terminate_wgc_after_test = True
        WGCConfig.all_dlc_available = False
        OSHelper.close_error_popups(title_substrings=["Bad Image", "dummy_game.exe"], kill_owners=True)
        cls._socket_state = None
        if cls.websocket_clients[cls.websocket_path_wgc] is not None:
            cls.websocket_clients[cls.websocket_path_wgc]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_overlay] is not None:
            cls.websocket_clients[cls.websocket_path_overlay]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_news] is not None:
            cls.websocket_clients[cls.websocket_path_news]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_game_overlay] is not None:
            cls.websocket_clients[cls.websocket_path_game_overlay]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_survey] is not None:
            cls.websocket_clients[cls.websocket_path_survey]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_custom_login] is not None:
            cls.websocket_clients[cls.websocket_path_custom_login]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_dialog_with_iframe] is not None:
            cls.websocket_clients[cls.websocket_path_dialog_with_iframe]['messages_storage'] = {}  # clear messages storage
        if cls.websocket_clients[cls.websocket_path_game_event] is not None:
            cls.websocket_clients[cls.websocket_path_game_event]['messages_storage'] = {}  # clear messages storage
        cls.websocket_closed_connection = {cls.websocket_path_wgc: 0, cls.websocket_path_overlay: 0,
                                           cls.websocket_path_news: 0,
                                           cls.websocket_path_survey: 0, cls.websocket_path_game_event: 0,
                                           cls.websocket_path_game_overlay: 0, cls.websocket_path_custom_login: 0,
                                           cls.websocket_path_dialog_with_iframe: 0}
        cls.terminate_all_wgc_related_processes()
        cls.reset_stored_users()
        GameMocks.clear_requests()
        RegistryHelper.clear_all_wgc_entries()
        WGNIUsersDB.clear_accounts()
        WGNIUsersDB.demo_create_timeout = 1.5
        WGNIUsersDB.strong_check_language = False
        WGNIUsersDB.change_nickname_after = 1
        WGNIUsersDB.predicted_realm = None
        WGNIUsersDB.predict_timeout = 1
        WGNIUsersDB._wgni_sessions = {}
        WGNIUsersDB._titles = {}
        WGNIUsersDB.action_code_create_binding = None
        WGNIUsersDB.predicted_timer = None
        WGNIUsersDB.custom_scopes = []
        WGNIUsersDB.default_email_code = '12345678'
        WGNIUsersDB.prepare_method = 'tsv.otp'
        WGNIUsersDB.commerce_method = 'tsv.otp'
        WGNIUsersDB.registration_adult_check = None
        WGNIUsersDB.prepare_account_status = 'disabled'
        WGNIUsersDB.prepare_check_required = False
        WGNIUsersDB.do_not_request_steam_api = False
        WGNIUsersDB.use_captcha_on_registration = False
        WGNIUsersDB.create_new_nickname_on_teleport = False
        WGNIUsersDB.billing_mandatory = False
        WGNIUsersDB.billing_applicable = True
        WGNIUsersDB.billing_info_available = True
        WGNIUsersDB.personal_account_banned = False
        WGNIUsersDB.payments_methods_available = True
        WGNIUsersDB._wgnr_sessions = {}
        WGNIUsersDB.variable_price_min = 200
        WGNIUsersDB._wgnp_sessions = {}
        WGNIUsersDB.external_account = None
        WGNIUsersDB.without_email_info = False
        WGNIUsersDB.commit_3ds1 = False
        WGNIUsersDB.pay_pay = False
        WGNIUsersDB.commit_3ds2 = False
        WGNIUsersDB.pow_complexity = 3
        WGNIUsersDB.cache_products = 60
        WGNIUsersDB.use_real_captcha = False
        WGNIUsersDB.use_two_factor_auth = False
        WGNIUsersDB.redirect_url = f'https://localhost/external/payment/'
        WGNIUsersDB.redirect_url_present = True
        WGNIUsersDB.persona_enabled = True
        WGNIUsersDB.single_realm = False
        WGNIUsersDB.persona_conflicted = False
        WGNIUsersDB.persona_available_realms = []
        WGNIUsersDB.wgn_realm_map = {'main': ['cis', 'eu', 'asia'], 'dev': ['dev1', 'dev2'], 'cn360': ['cn360']}
        WGNIUsersDB.clear_products()
        WGNIUsersDB.steam_account_for_oauth = None
        WGNIUsersDB.add_account(
            WGNIUsersDB.email, WGNIUsersDB.password, WGNIUsersDB.nickname, realm='dev1')
        WGNIUsersDB.add_account(
            WGNIUsersDB.email, WGNIUsersDB.password, WGNIUsersDB.nickname, realm='dev2')
        WGNIUsersDB.add_account(
            WGNIUsersDB.email, WGNIUsersDB.password, WGNIUsersDB.nickname, realm='cis')
        WGNIUsersDB.add_account(
            WGNIUsersDB.email, WGNIUsersDB.password, WGNIUsersDB.nickname, realm='asia')
        WGNIUsersDB.prepare_product_timeout = 0
        WGNIUsersDB.allowed_countries = ["BY"]
        WGNIUsersDB.detected_country_code = "BY"
        GameMocks.realm_to_remove_from_meta = None
        GameMocks.time_between_request = 0
        GameMocks.keep_patches_interval = 7
        GameMocks.wrong_web_seed = None
        GameMocks.add_utm_mark = False
        GameMocks.mandatory_account_email = False
        GameMocks.left_premium_banner_url = 'https://google.com/'
        GameMocks.right_premium_banner_url = 'https://google.com/'
        GameMocks.invalid_meta_app_id = None
        GameMocks.demo_accounts_enabled = False
        GameMocks.extended_public_games = []
        GameMocks.wgc_publisher_id = None
        GameMocks.actual_game_center_version = False
        GameMocks.environment_name = 'CIS'
        GameMocks.item_size = '601x300'
        GameMocks.preload_active = False
        GameMocks.installer_check_version = WGCConfig.wgc_build
        GameMocks.wgc_publisher_id = None
        GameMocks.dev1_agreement = 'dev1_eula'
        GameMocks.available_preload_ids = ['WOT.RU.PRODUCTION']
        GameMocks.game_executable = 'dummy_game.exe'
        GameMocks.activate_preinstall_patches = False
        GameMocks.open_dd_url = 'https://agdd-wgt1.wgtest.net'
        GameMocks.force_update_metadata = False
        GameMocks.storefront_name_for_dd = 'wgc_game_shop'
        GameMocks.news_production = False
        GameMocks.get_game_patch_timeout = 0
        GameMocks.api_lang_aliases = None
        GameMocks.report_session_id = ''
        GameMocks.news_show_inside = True
        GameMocks.local_wgc_patch = True
        GameMocks.cookie_samesite = 'None'
        GameMocks.setup_cookies_if_present_in_request = True
        GameMocks.cookie_exp_long_reset = False
        import datetime
        GameMocks.cookie_exp_short = datetime.datetime.now() + datetime.timedelta(seconds=-300)
        GameMocks.cookie_exp_long = datetime.datetime.now() + datetime.timedelta(seconds=-300)
        GameMocks.is_tool = False
        GameMocks.in_game_overlay = False
        GameMocks.premium_spectate_url = f'https://{GameMocks.host_https}/overlay/'
        GameMocks.show_agreement_before_launch = False
        GameMocks.installed_game_news_layout = False
        GameMocks.item_type = "vehicle"
        GameMocks.installed_game_15_layout = False
        GameMocks.item_display_media_map = {
            "image_main": True,
            "image_2_1_shop": True,
            "image_2_1_overriden": True,
            "image_2_1": True,
            "image_1_1_shop": True,
            "image_1_1": True,
            "image_1_1_overriden": True,
            "background_image": True
        }
        GameMocks.news_layout_support_login = True
        GameMocks.main_news_link = "https://worldoftanks.ru/ru/news/wgc-client/special-offers/warhammer/"
        GameMocks.regular_news_link = f"{GameMocks.content_service_host}/custom_mock/"
        GameMocks.regular_news_show_inside = True
        GameMocks.steam_game_first_autorun = False
        GameMocks.show_steam_game_agreements = True
        GameMocks.news_layout_external_link = True
        GameMocks.install_game_main_news_layout = False
        GameMocks.actual_game_version = '2.1'
        GameMocks.create_oauth_token_timeout = 0
        GameMocks.create_id_token_timeout = 0
        GameMocks.content_layout_name = 'wgc_auto_test'
        GameMocks.get_full_inventory_timeout = 1
        GameMocks.count_of_update_meta_ct = 0
        GameMocks.enable_wgc_ct_update = False
        GameMocks.wgc_web_seed = WGCConfig.share_builds_folder
        GameMocks.game_custom_torrents = {}
        GameMocks.game_custom_webseed = WGCConfig.web_seed_url
        GameMocks.game_custom_torrent_hashes = {}
        GameMocks.game_custom_patches = {}
        GameMocks.custom_app_id_news_messages = None
        GameMocks.get_wgc_patch_sleep = 0
        GameMocks.user_notifications = None
        GameMocks.secret = None
        GameMocks.min_supported_os = 'Win7'
        GameMocks.news_messages = {}
        GameMocks.title_id = 'Alice Game'
        GameMocks.category_id = None
        GameMocks.show_age_ratings = False
        GameMocks.arsenal_qsg_enabled = False
        GameMocks.metadata_request_timeout = 0
        GameMocks.default_language = 'RU'
        GameMocks.support_languages = 'RU,EN'
        GameMocks.get_game_page_category_timeout = 0
        GameMocks.amount_download_size = 0
        BrowserHelper.tab = None
        GameMocks.country_legal = 'NO_RESTRICTIONS'
        GameMocks.country_code = 'UA'''
        GameMocks.stated_country = 'UA'
        GameMocks.game_pages_targeting = True
        GameMocks.buy_game = False
        GameMocks.game_metadata_is_actual = False
        GameMocks.enable_login_tag = True
        GameMocks.custom_field = None
        GameMocks.game_is_actual = False
        GameMocks.enable_wgc_update = False
        GameMocks.shop_data_source = 'platform'
        GameMocks.item_finishing_property = None
        GameMocks.item_show_custom_package_content = False
        GameMocks.item_background_image = 'https://dwd.pershastudia.com/test_artifacts/test_data/cs_images/prem_item_bg.jpeg'
        GameMocks.item_image_overridden = None
        GameMocks.item_promotion_text = None
        GameMocks.item_package_content = ('<ul class=\"b-unordered-list\">\r\n<li><span class=\"b-icon-nation flag flag'
                                          '__usa mceNonEditable\">&zwnj;</span>&nbsp;ИС-6;</li><li>слот.</li></ul>')
        GameMocks.item_price_property = 1002.2
        GameMocks.stop_supporting = False
        started = time()
        if path.isdir(WGCConfig.application.folder):
            OSHelper.unlock_file_or_folder(WGCConfig.application.folder)
            for path_ in listdir(WGCConfig.application.folder):
                OSHelper.unlock_file_or_folder(
                    path.join(WGCConfig.application.folder, path_))
        result = Waiter.poll(30, lambda: not cls.is_binary_process_exist(), sleep_seconds=.1)
        if not result:
            OSHelper.terminate_process(WGCConfig.application.binary_name)
        for file_ in Path(WGCConfig.application.folder).glob('**\\*.log'):
            OSHelper.unlock_file_or_folder(str(file_))
            OSHelper.remove_file_or_directory(str(file_))
        OSHelper.remove_file_or_directory(path.dirname(WGCConfig.application.base_folder))
        for path_ in glob.glob(WGCConfig.application.wargaming_data_folder + '\\*.*'):
            OSHelper.unlock_file_or_folder(path_)
        for path_ in Path(path.join(WGCConfig.additional_disk, 'GamesWGC')).rglob('*.*'):
            OSHelper.unlock_file_or_folder(str(path_))
        OSHelper.remove_file_or_directory(path.join(WGCConfig.additional_disk, 'flush.txt'))
        OSHelper.remove_file_or_directory(
            WGCConfig.application.wargaming_data_folder)
        if WGCConfig.remove_game_dir_before_test:
            dir_ = glob.glob(WGCConfig.steam.games_dir + '\\**\\wgc*_api.exe', recursive=True)
            if not dir_:
                dir_ = glob.glob(WGCConfig.steam.games_dir + '\\**\\game_info.xml', recursive=True)
            for d in dir_:
                for df in glob.glob(path.dirname(d) + '\\**\\*', recursive=True):
                    OSHelper.unlock_file_or_folder(df)
                OSHelper.remove_file_or_directory(path.dirname(d))
        OSHelper.remove_file_or_directory('c:\\chrome_html.html')

        for path_ in glob.glob(getenv('TEMP') + '\\Setup Log*.txt'):
            OSHelper.remove_file_or_directory(path_)

        cls.delete_desktop_links()
        cls.copy_wgc_from_source(wgc_folder, publisher)
        finished_copy = time()
        log.debug('Copy artifacts: %s' % (finished_copy - started))
        started = time()
        if WGCConfig.remove_game_dir_before_test:
            OSHelper.unlock_file_or_folder(WGCConfig.games.folder)
            result = OSHelper.remove_file_or_directory(WGCConfig.games.folder)
            finished_deletes_games = time()
            log.debug(f'Delete Games folder: %s, result = {result}' % (finished_deletes_games - started))
            if not result:
                OSHelper.remove_file_or_directory(path.join(WGCConfig.games.folder, '*'), system_delete=True)
                sleep(.5)  # for cmd unlock dir
        OSHelper.remove_file_or_directory('c:\\dumps')
        OSHelper.make_dirs('c:\\dumps', exist_ok=True)
        try:
            OSHelper.remove_file_or_directory(WGCConfig.application.crash_dump_folder)
            OSHelper.make_dirs(WGCConfig.application.crash_dump_folder, exist_ok=True)
        except OSError as e:
            log.error(e)
        if not path.isdir(WGCConfig.games.folder):
            OSHelper.make_dirs(WGCConfig.games.folder)
        OSHelper.remove_file_or_directory(
            WGCConfig.application.alternative_start_menu_folder)

        # reset hanged client
        cls.websocket_clients[cls.websocket_path_wgc] = None
        cls.websocket_clients[cls.websocket_path_overlay] = None
        cls.websocket_clients[cls.websocket_path_news] = None
        cls.websocket_clients[cls.websocket_path_game_overlay] = None
        cls.websocket_clients[cls.websocket_path_survey] = None
        cls.websocket_clients[cls.websocket_path_custom_login] = None
        cls.websocket_clients[cls.websocket_path_dialog_with_iframe] = None
        cls.websocket_clients[cls.websocket_path_game_event] = None
        
        cls.create_wgc_info(wgc_folder)

        OSHelper.unlock_file_or_folder(WGCConfig.application.user_info_file)
        OSHelper.remove_file_or_directory(path.join(getenv('APPDATA'), 'Wargaming.net'))
        OSHelper.remove_file_or_directory(path.join(getenv('APPDATA'), '360 Wargaming'))
        OSHelper.remove_file_or_directory(path.join(getenv('PROGRAMDATA'), 'Wargaming.net'))
        OSHelper.remove_file_or_directory(path.join(getenv('PROGRAMDATA'), '360 Wargaming'))

        # reinit registry entry
        cls.delete_wgc_uninstall_entry()
        cls.add_wgc_uninstall_entry()
        cls.add_wgc_uri_scheme_entry()
        cls.create_wgc_path_dat_file()
        cls.create_wgc_tracking_id_file()
        cls.create_wgc_id_file()
        RegistryHelper.restore_country_code()
        
        cls.create_monitor_config_json(wgc_folder)

        try:
            if not WGCConfig.local_debug:
                hwnd = WinApi.get_top_level_window('git*Application*Error*')
                if hwnd:
                    PostMessage(hwnd, WM_CLOSE, 0, 0)
        except Exception:
            pass

        if path.isfile(WGCConfig.dbg_view_log_file):
            OSHelper.remove_file_or_directory(WGCConfig.dbg_view_log_file)

        if WGCConfig.dbg_view_enabled:
            dbg_view = get_third_party_path('Dbgview.exe')
            command = '%s /t /l %s /o /m 10 /w' % (dbg_view,
                                                   WGCConfig.dbg_view_log_file)
            CmdHelper.execute(command, dont_wait=True)

        if WGCConfig.procmon_enabled:
            logs = glob.glob(WGCConfig.procmon_log_file[:-4] + '*.*')
            for log_ in logs:
                OSHelper.remove_file_or_directory(log_)
            SysInternalsHelper.start_monitor()

    @classmethod
    def enable_dbg_view_logging(cls):
        if not WGCConfig.dbg_view_enabled:
            if path.isfile(WGCConfig.dbg_view_log_file):
                OSHelper.remove_file_or_directory(WGCConfig.dbg_view_log_file)
            dbg_view = get_third_party_path('Dbgview.exe')
            command = '%s /t /l %s /o /m 10 /w' % (dbg_view,
                                                   WGCConfig.dbg_view_log_file)
            CmdHelper.execute(command, dont_wait=True)

    @classmethod
    def prepare_str(cls):
        str_content = StepsToReproduceFileHandler.compose_str()
        str_file = path.join(cls.reports_dir(), 'Test STR.log')
        if str_content.strip() != StepsToReproduceFileHandler.header:
            try:
                if not path.isdir(cls.reports_dir()):
                    OSHelper.make_dirs(cls.reports_dir())
                with open(str_file, 'w', encoding='utf-8') as fp:
                    fp.write(str_content)
            except OSError as e:
                log.error(f'{e} ({str_file})')
        return str_file, str_content

    @classmethod
    def parse_wgc_log(cls, log_path=None):
        log_path = log_path or cls.get_current_wgc_log_file()
        log_result = []
        regex = r'(\w+).+\|.(\d{1,6}).*\|.*(\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s\|\s(.*)'
        if log_path:
            with open(log_path, 'r', encoding='utf-8') as fp:
                for line in fp.readlines():
                    m = re.findall(regex, line.rstrip('\n').rstrip())
                    if m:
                        log_result.append(WGCLogRecord(
                            m[0][0], int(m[0][1]), datetime.strptime(m[0][2], '%d %H:%M:%S.%f'), m[0][3]))
        return log_result

    @classmethod
    def close_wgc(cls, verify=True):
        """
        Close WGC.

        :param bool verify: flag that indicates should we verify that wgc
        was closed or not.
        """
        cls.terminate_wgc_process()
        log.info('Close WGC application.')

        def check():
            if cls.is_binary_process_exist():
                cls.terminate_wgc_process()
                return False
            return True
        
        if verify:
            Waiter.poll(25, check, 'Expected that WGC will be closed at this moment.',
                        sleep_seconds=.1)
        cls.websocket_clients[cls.websocket_path_wgc] = None

    @classmethod
    def terminate_all_wgc_related_processes(cls):
        """
        Terminates all WGC processes such as WGC, installer, launcher e.t.c.
        """
        log.debug('Terminating all WGC related processes.')
        Waiter.poll(10, lambda: not OSHelper.terminate_process(
            cls.get_all_related_wgc_process()))
        if not WGCConfig.local_debug:
            cls.terminate_chrome()
            OSHelper.terminate_process('OneDrive.exe')

    @classmethod
    def remove_wgc_logs(cls):
        log_files = cls.get_wgc_log_filenames()
        try:
            for l in log_files:
                OSHelper.remove_file_or_directory(l)
        except (OSError, PermissionError):
            pass

    @classmethod
    def terminate_chrome(cls):
        if not WGCConfig.local_debug:
            Waiter.poll(15, lambda: OSHelper.terminate_process('chrome.exe') and not OSHelper.is_process_exists('chrome.exe'))
            BrowserHelper.tab = None

    @classmethod
    def terminate_steam(cls):
        OSHelper.terminate_process(['steam.exe', 'steamwebhelper.exe'])
        Waiter.poll(5, lambda: not OSHelper.is_process_exists('steam.exe'))

    @classmethod
    def is_crash_dumps_exists(cls):
        return len(glob.glob(WGCConfig.application.cat_reports_dir)) > 0

    @classmethod
    def create_default_preferences_xml(
            cls,
            max_fs_operation_retries=5, fs_operation_retry_timeout=50,
            update_check_threshold=600000,
            game_update_interval=7200000, protocol_version=None,
            localization=None, start_auto_import=False,
            ct_participating='NotParticipating',
            user_info_stat_event_send_interval=7200000,
            content_update_interval=1800000,
            showcase_update_interval=7200000,
            background_installation=False,
            download_speed_limit=-1, wgc_folder=None,
            push_notifications_receive_interval=20000,
            set_last_user_pc_info_stat_time=True,
            arsenal_mode=False):
        """
        Creates default preferences.xml file.
        """
        RequirementHelper.append_requirement('INT.4.8')
        log.info('Create default preferences.xml.')
        protocol_version = protocol_version or WGCConfig.application.preferences_protocol
        wgc_path = wgc_folder or WGCConfig.application.folder
        preferences_path = str(path.join(
            wgc_path, WGCConfig.application.preferences_file))
        preferences_template = \
            get_file_template(WGCConfig.application.preferences_file)
        localization = localization or WGCConfig.wgc_preferences_language
        preferences_content = \
            preferences_template % \
            {'version': str(protocol_version),
             'current_localization': localization,
             'games_path': WGCConfig.games.folder,
             'max_fs_operation_retries': max_fs_operation_retries,
             'fs_operation_retry_timeout': fs_operation_retry_timeout,
             'update_check_threshold': str(update_check_threshold),
             'game_update_interval': str(game_update_interval),
             'user_info_stat_event_send_interval': str(
                 user_info_stat_event_send_interval),
             'background_installation': str(background_installation).lower(),
             'showcase_update_interval': str(showcase_update_interval)}
        try:
            with open(preferences_path, 'w', encoding='utf-8') as preferences_file:
                preferences_file.write(preferences_content)
        except PermissionError:
            log.error(f'PermissionError: cannot write {preferences_path} file.')
            return False

        content = WGCSettingsHelper.read_xml_file(preferences_path)
        content['protocol']['application']['common_test_participation_state'] = ct_participating  # noqa
        if set_last_user_pc_info_stat_time:
            content['protocol']['application']['last_user_pc_info_stat_time'] = str(int(time()))
        if start_auto_import:
            content['protocol']['application']['games_manager']['start_auto_import'] = 'true'  # noqa
        if float(protocol_version) >= 3.14:
            content['protocol']['application'][
                'push_notifications_receive_interval'] = push_notifications_receive_interval  # noqa
        content['protocol']['application'][
            'download_manager']['download_speed_limit'] = str(download_speed_limit)  # noqa
        content['protocol']['application']['games_manager']['content_update_interval'] = content_update_interval
        if arsenal_mode:
            content['protocol']['application']['product_mode'] = 'Arsenal'
        WGCSettingsHelper.save_preferences_content(content, wgc_folder)
        return preferences_path

    @classmethod
    def edit_preferences_xml(
            cls, wgc_folder=None,
            max_fs_operation_retries=None,
            fs_operation_retry_timeout=None,
            update_check_threshold=None,
            game_update_interval=None,
            protocol_version=None,
            localization=None,
            start_auto_import=None,
            ct_participating=None,
            user_info_stat_event_send_interval=None,
            content_update_interval=None,
            showcase_update_interval=None,
            background_installation=None,
            download_speed_limit=None,
            push_notifications_receive_interval=None,
            set_last_user_pc_info_stat_time=False,
            arsenal_mode=None):
        """
        Edits the existing preferences.xml file with provided values.
        Only non-None parameters will be updated.
        """
        RequirementHelper.append_requirement('INT.4.8')
        log.info('Edit preferences.xml.')

        wgc_path = wgc_folder or WGCConfig.application.folder
        preferences_path = str(path.join(wgc_path, WGCConfig.application.preferences_file))

        if not path.exists(preferences_path):
            log.error(f'Preferences file not found at: {preferences_path}')
            return False

        content = WGCSettingsHelper.read_xml_file(preferences_path)

        app_config = content['protocol']['application']

        # Apply changes
        if protocol_version:
            content['protocol']['version'] = str(protocol_version)
        if localization:
            app_config['current_localization'] = localization
        if max_fs_operation_retries is not None:
            app_config['max_fs_operation_retries'] = max_fs_operation_retries
        if fs_operation_retry_timeout is not None:
            app_config['fs_operation_retry_timeout'] = fs_operation_retry_timeout
        if update_check_threshold is not None:
            app_config['update_check_threshold'] = update_check_threshold
        if game_update_interval is not None:
            app_config['game_update_interval'] = game_update_interval
        if user_info_stat_event_send_interval is not None:
            app_config['user_info_stat_event_send_interval'] = user_info_stat_event_send_interval
        if background_installation is not None:
            app_config['background_installation'] = str(background_installation).lower()
        if showcase_update_interval is not None:
            app_config['showcase_update_interval'] = showcase_update_interval
        if ct_participating:
            app_config['common_test_participation_state'] = ct_participating
        if set_last_user_pc_info_stat_time:
            app_config['last_user_pc_info_stat_time'] = str(int(time()))
        if start_auto_import is not None:
            app_config.setdefault('games_manager', {})['start_auto_import'] = 'true' if start_auto_import else 'false'
        if download_speed_limit is not None:
            app_config.setdefault('download_manager', {})['download_speed_limit'] = str(download_speed_limit)
        if content_update_interval is not None:
            app_config.setdefault('games_manager', {})['content_update_interval'] = content_update_interval
        if push_notifications_receive_interval is not None and float(
                app_config.get('version', protocol_version or 0)) >= 3.14:
            app_config['push_notifications_receive_interval'] = push_notifications_receive_interval
        if arsenal_mode is not None:
            app_config['product_mode'] = 'Arsenal' if arsenal_mode else 'Default'

        WGCSettingsHelper.save_preferences_content(content, wgc_folder)
        return preferences_path

    @classmethod
    def delete_preferences_file(cls):
        """
        Deletes preferences.xml file.
        """
        RequirementHelper.append_requirement('INT.4.8')
        log.info('Delete preferences.xml file.')
        OSHelper.remove_file_or_directory(str(path.join(
            WGCConfig.application.folder,
            WGCConfig.application.preferences_file)))

    @classmethod
    def delete_wgc_info_file(cls):
        """
        Deletes wgc_info.xml file.
        """
        log.info('Delete wgc_info.xml file.')
        OSHelper.remove_file_or_directory(str(path.join(
            WGCConfig.application.folder,
            WGCConfig.application.wgc_info_file)))

    @classmethod
    def create_wgc_tracking_id_file(cls):
        """
        Create wgc_tracking_id file in WGC folder.
        """
        log.debug('Create wgc_tracking_id file in WGC folder.')
        path_file = str(path.join(WGCConfig.application.base_folder,
                              'data', WGCConfig.application.data_prefix + '_tracking_id.dat'))
        OSHelper.unlock_file_or_folder(path_file)
        OSHelper.remove_file_or_directory(path_file)
        data_path = path.join(WGCConfig.application.base_folder, 'data')

        if not path.isdir(data_path):
            OSHelper.make_dirs(data_path)
        else:
            if path.isfile(path_file):
                OSHelper.remove_file_or_directory(path_file)
        OSHelper.create_file(path_file, WGCConfig.wgc_tracking_id)

    @classmethod
    def create_wgc_external_id_file(cls, data):
        """
        Create wgc_external_id.dat file in WGC folder.
        """
        log.debug('Create wgc_external_id.dat file in WGC folder.')
        path_file = str(path.join(WGCConfig.application.base_folder,
                              'data', WGCConfig.application.data_prefix + '_external_id.dat'))
        OSHelper.unlock_file_or_folder(path_file)
        OSHelper.remove_file_or_directory(path_file)
        data_path = path.join(WGCConfig.application.base_folder, 'data')

        if not path.isdir(data_path):
            OSHelper.make_dirs(data_path)
        else:
            if path.isfile(path_file):
                OSHelper.remove_file_or_directory(path_file)
        OSHelper.create_file(path_file, data)

    @classmethod
    def create_wgc_id_file(cls):
        """
        Create wgc_id file in WGC folder.
        """
        log.debug('Create wgc_id file in WGC folder.')
        path_file = str(path.join(
            WGCConfig.application.base_folder, 'data', WGCConfig.application.data_prefix + '_id.dat'))
        OSHelper.unlock_file_or_folder(path_file)
        OSHelper.remove_file_or_directory(path_file)
        data_path = path.join(WGCConfig.application.base_folder, 'data')

        if not path.isdir(data_path):
            OSHelper.make_dirs(data_path)
        else:
            if path.isfile(path_file):
                OSHelper.remove_file_or_directory(path_file)
        OSHelper.create_file(path_file, WGCConfig.application.installation_id)

    @classmethod
    def change_wgc_tracking_id_file(cls, tracking_id):
        """
        Change wgc_tracking_id file in WGC folder.
        """
        log.debug('Change wgc_tracking_id file in WGC folder.')
        path_file = str(path.join(
            WGCConfig.application.base_folder, 'data',
            WGCConfig.application.data_prefix + '_tracking_id.dat'))

        data_path = path.join(WGCConfig.application.base_folder, 'data')

        if not path.isdir(data_path):
            OSHelper.make_dirs(data_path)
        else:
            if path.isfile(path_file):
                OSHelper.remove_file_or_directory(path_file)
        OSHelper.create_file(path_file, tracking_id)

    @classmethod
    def delete_wgc_from_registry(cls):
        """
        Deletes WGC from registry.
        """
        uninstall_registry_path = 'Software\\Microsoft\\Windows\\' \
                                  'CurrentVersion\\Uninstall\\' \
                                  'Wargaming.net Game Center'

        if RegistryHelper.is_key_exist(
                RegistryHelper.HKEY_CURRENT_USER, uninstall_registry_path):
            RegistryHelper.delete_key(RegistryHelper.HKEY_CURRENT_USER,
                                      uninstall_registry_path)

    @classmethod
    def delete_game_from_registry(cls, game_id, key_value=None):
        """
        Deletes game from Windows registry.

        :param str game_id: game id such as "WOT.RU.PRODUCTION".
        :param str key_value: if not, None, only that value will be deleted.
        """
        uninstall_registry_path = \
            'Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s' % \
            game_id

        if RegistryHelper.is_key_exist(
                RegistryHelper.HKEY_CURRENT_USER, uninstall_registry_path):
            if key_value:
                RegistryHelper.delete_value(RegistryHelper.HKEY_CURRENT_USER,
                                            uninstall_registry_path,
                                            key_value)
            else:
                RegistryHelper.delete_key(RegistryHelper.HKEY_CURRENT_USER,
                                          uninstall_registry_path)
        
    @classmethod
    def get_elements_dump(cls, file_path):
        """
        Gets elements dump.
        """
        log.info(f'Getting a dump of elements in {file_path}')
        provider = cls.websocket_path_wgc
        check_method = cls.is_wgc_running
        
        if not check_method():
            log.warning("WGC is not running. Unable to obtain element dump.")
            error_info = "WGC is not running"
            try:
                ui_map_template_str = get_file_template('ui_map.html')
                context = {
                    'elements_json_str': '[]',
                    'screenshot_path': '""',
                    'dump_time': datetime.now().isoformat(),
                    'error': html.escape(error_info)
                }
                # Replace placeholders. Important: Names must match those in ui_map.html
                # Use .replace() instead of % for greater reliability with different data
                html_content = ui_map_template_str.replace('{{ elements_json_str|safe }}',
                                                           context['elements_json_str'])
                html_content = html_content.replace('{{ screenshot_path }}', context['screenshot_path'].strip(
                    '"'))
                html_content = html_content.replace('{{ dump_time }}', context['dump_time'])
                html_content = html_content.replace('{{ error }}', context['error'])
                html_content = html_content.replace('{{ screenshot_width }}', '0')
                html_content = html_content.replace('{{ screenshot_height }}', '0')
                html_content = html_content.replace('{{ screenshot_base64 }}', '""')
                
                with open(file_path, 'w', encoding='utf-8') as ui_map_file:
                    ui_map_file.write(html_content)
                log.warning(f'Saved UI dump with error "{error_info}" to {file_path}')
            except Exception as fallback_err:
                log.exception(f"Even the HTML placeholder failed to save with an error.: {fallback_err}")
            return
        
        screenshot_path_rel = None
        html_content = ""
        error_info = None
        window_origin_x = 0
        window_origin_y = 0
        dpi_scale = 1.0
        hwnd = None
        
        try:
            output_dir = path.dirname(file_path)
            html_filename = path.basename(file_path)
            screenshot_filename = f"{path.splitext(html_filename)[0]}_screenshot.png"
            screenshot_filepath_abs = path.join(output_dir, screenshot_filename)
            makedirs(output_dir, exist_ok=True)
            
            im = None
            try:
                log.debug('Taking a screenshot...')
                im = ImageGrab.grab(include_layered_windows=True)
                im.save(screenshot_filepath_abs)
                screenshot_path_rel = screenshot_filename
                log.debug(f'The screenshot was saved in {screenshot_filepath_abs}')
            except Exception as screen_err:
                log.exception(f"Failed to take or save a screenshot: {screen_err}")
                error_info = f"Screenshot Error: {screen_err}"
            
            # 3. Execute JS
            log.debug('Trying to get a UI dump via JS execution.')
            active_provider = provider
            if cls.websocket_clients.get(cls.websocket_path_game_overlay) is not None:
                active_provider = cls.websocket_path_game_overlay
                log.debug(f'Overlay provider is used: {active_provider}')
            else:
                log.debug(f'The WGC provider is used: {active_provider}')
            
            element_json_str = cls.execute_js('getElementsDump()', timeout=10, provider=active_provider)
            
            # 4. Parsing the JS result
            if element_json_str and element_json_str != 'null':
                try:
                    elements_dump = json.loads(element_json_str)
                    log.debug(f'Successfully received and parsed UI dump ({len(elements_dump)} elements).')
                except json.JSONDecodeError as json_err:
                    log.error(f"Error parsing JSON from JS: {json_err}. JS response: {element_json_str[:200]}...")
                    elements_dump = []
                    json_error_str = f"JSON Decode Error: {json_err}"
                    error_info = f"{error_info}\n{json_error_str}" if error_info else json_error_str
            else:
                log.warning("Received empty or null response from getElementsDump() JS.")
                elements_dump = []
                if not error_info:
                    error_info = "Empty or null response from getElementsDump() JS"
            
            try:
                ui_map_template_str = get_file_template('ui_map.html')
            except Exception as template_load_err:
                log.exception(f"Failed to load template ui_map.html: {template_load_err}")
                raise WGCException(f"Template ui_map.html not found or unreadable: {template_load_err}")

            try:
                hwnd = cls.get_wgc_hwnd()
                rect = WinApi.get_location(hwnd)
                window_origin_x = rect[0]
                window_origin_y = rect[1]
            except Exception as e:
                log.warning(f"Unable to get WGC window coordinates, using (0, 0): {e}")
                window_origin_x, window_origin_y = 0, 0
                
            if hwnd:
                dpi_scale = WinApi.get_dpi_for_window(hwnd)
            else:
                dpi_scale = WinApi.get_dpi_scale_factor_fallback()
                log.warning(f"The system DPI scale is used: {dpi_scale} (since HWND is not available)")
            
            context = {
                'elements_json_str': json.dumps(elements_dump, indent=2),
                'screenshot_path': json.dumps(screenshot_path_rel) if screenshot_path_rel else 'null',
                'dump_time': datetime.now().isoformat(),
                'error': html.escape(error_info) if error_info else "",
                'window_origin_x': window_origin_x,
                'window_origin_y': window_origin_y,
                'dpi_scale': dpi_scale,
                'screenshot_width': str(im.width) if im else '0',
                'screenshot_height': str(im.height) if im else '0',
                'screenshot_base64': '""',
                'objects_json': json.dumps(elements_dump)
            }

            html_content = ui_map_template_str.replace('{{ elements_json_str|safe }}', context['elements_json_str'])
            html_content = html_content.replace('{{ screenshot_path }}', context['screenshot_path'].strip(
                '"'))
            html_content = html_content.replace('{{ dump_time }}', context['dump_time'])
            html_content = html_content.replace('{{ error }}', context['error'])
            html_content = html_content.replace('{{ window_origin_x }}', str(context['window_origin_x']))
            html_content = html_content.replace('{{ window_origin_y }}', str(context['window_origin_y']))
            html_content = html_content.replace('{{ dpi_scale }}', str(context['dpi_scale']))
            
            html_content = html_content.replace('%(screenshot_width)s', context['screenshot_width'])
            html_content = html_content.replace('%(screenshot_height)s', context['screenshot_height'])
            html_content = html_content.replace('%(screenshot_base64)s',
                                                context['screenshot_base64'].strip('"'))
            html_content = html_content.replace('%(objects_json)s', context['objects_json'])
        
        except (TooSaltyUISoupException, WGCException, Exception) as e:
            log.exception(f'Error while getting element dump or processing template: {e}')
            error_info = f"Dump Generation Error: {type(e).__name__} - {e}"
            try:
                ui_map_template_str = get_file_template('ui_map.html')
                context = {
                    'elements_json_str': '[]',
                    'screenshot_path': json.dumps(screenshot_path_rel) if screenshot_path_rel else 'null',
                    'dump_time': datetime.now().isoformat(),
                    'error': html.escape(error_info),
                    'window_origin_x': window_origin_x,
                    'window_origin_y': window_origin_y,
                    'dpi_scale': dpi_scale
                }
                html_content = ui_map_template_str.replace('{{ elements_json_str|safe }}',
                                                           context['elements_json_str'])
                html_content = html_content.replace('{{ screenshot_path }}', context['screenshot_path'].strip('"'))
                html_content = html_content.replace('{{ dump_time }}', context['dump_time'])
                html_content = html_content.replace('{{ error }}', context['error'])
                html_content = html_content.replace('{{ window_origin_x }}', str(context['window_origin_x']))
                html_content = html_content.replace('{{ window_origin_y }}', str(context['window_origin_y']))
                html_content = html_content.replace('{{ dpi_scale }}', str(context['dpi_scale']))
                html_content = html_content.replace('%(screenshot_width)s', '0')
                html_content = html_content.replace('%(screenshot_height)s', '0')
                html_content = html_content.replace('%(screenshot_base64)s', '""')
                html_content = html_content.replace('%(objects_json)s', '[]')
            except Exception as fallback_err:
                log.error(f"Couldn't even create an HTML placeholder with an error: {fallback_err}")
                html_content = (f"<html><body><h1>Failed to get element dump</h1><pre>{html.escape(error_info)}"
                                f"</pre><h2>Fallback error</h2><pre>{html.escape(str(fallback_err))}</pre></body></html>")
        
        finally:
            try:
                with open(file_path, 'w', encoding='utf-8') as ui_map_file:
                    ui_map_file.write(html_content)
            except Exception as write_err:
                log.exception(f"Critical error: Failed to write HTML dump to file {file_path}: {write_err}")

    @classmethod
    def copy_wgc_api(cls, game_folder,
                     home_folder=WGCConfig.application.folder):
        """
        Copy wgc_api from application folder to game folder.

        :param str game_folder: path to game folder.
        :param str home_folder: path to WGC home directory.
        """
        OSHelper.copy(path.join(home_folder, WGCConfig.application.api.folder),
                      game_folder, remove_destination=False)

    @classmethod
    def click_cancel_on_updater(cls):
        """
        Clicks Cancel on updater page.
        """
        RequirementHelper.append_requirement('LLR.2.7')
        log.info('Click Cancel on the update process.')
        cls.search_crash_and_assert()
        Waiter.poll(25, cls.is_wgc_update_in_progress, 'Expected that self-update in a progress.')
        base_selector = f'{WGCConfig.application.publisher_name}*'
        updater_title = f'{base_selector} {Localization.i18n("SELFUPDATE_TITLE")}'
        updater_window = uisoup.get_window(updater_title)
        x, y, w, h = updater_window.acc_location
        uisoup.mouse.move(x + w - 86, y + h - 45, smooth=False)
        sleep(.5)
        WinApi.click(x, y)

    @classmethod
    def click_continue_on_updater(cls):
        """
        Clicks Continue on updater page.
        """
        RequirementHelper.append_requirement('LLR.2.7')
        log.info('Click Continue on the confirmation dialog window update '
                 'process.')
        element_btn = UIMapElement('native', f'btn{Localization.i18n("CONTINUE")}*')
        cls.search_crash_and_assert()
        cls.wait_for_element(element_btn)
        element = cls.get_wgc_element(element_btn)
        try:
            element.click()
        except (TooSaltyUISoupException, COMError):
            sleep(2)
            element.click()

    @classmethod
    def click_exit_on_updater(cls):
        """
        Clicks Exit on updater page.
        """
        RequirementHelper.append_requirement('LLR.2.7')
        log.info('Click Exit on confirmation dialog window updater process.')
        element_btn = UIMapElement('native', f'btn{Localization.i18n("EXIT")}*')
        cls.search_crash_and_assert()
        cls.wait_for_element(element_btn)
        element = cls.get_wgc_element(element_btn)
        try:
            element.click()
        except (TooSaltyUISoupException, COMError):
            sleep(2)
            element.click()

    @classmethod
    def click_cancel_on_blocked_error_dialog(cls):
        """
        Clicks Cancel on updater page.
        """
        RequirementHelper.append_requirement('LLR.2.7')
        log.info('Click Cancel on windows error dialog.')
        cls.search_crash_and_assert()
        element_p = UIMapElement('native', f'btn{Localization.i18n("RETRY_SYS")}*')
        cls.wait_for_element(element_p)
        try:
            element = cls.get_wgc_element(element_p)
            x, y, w, h = element.hover_on_element()
            sleep(1)
            uisoup.mouse.move(x + 110, y + 10, smooth=False)
            WinApi.click(x + 110, y + 10)
        except Exception:
            pass

    @classmethod
    def get_wgc_hwnd(cls):
        try:
            hwnd = WinApi.get_hwnd_by_pid(OSHelper.get_process('wgc.exe').pid)
            return hwnd
        except Exception as e:
            log.debug(f'get_wgc_hwnd - > {e}')
        return 0

    @classmethod
    def is_wgc_minimized(cls):
        """
        Check WGC main window state.

        :rtype: bool
        :return: True or False
        """
        RequirementHelper.append_requirement('LLR.2.2')
        try:
            hwnd_fore = win32gui.GetForegroundWindow()
            log.debug(f'GetForegroundWindow: - {win32gui.GetWindowText(hwnd_fore)}')
            hwnd = ctypes.windll.user32.GetShellWindow()
            log.debug(f'GetShellWindow: - {win32gui.GetWindowText(hwnd)}')
            log.debug(f'IsWindowVisible(foregroundWindow): - {WinApi.is_visible(hwnd_fore)}')
        except Exception as e:
            log.debug(e)
        try:
            hwnd = WinApi.get_top_level_window(WGCConfig.application.wgc_window_title_name, 'Chrome_WidgetWin_0')
            return not WinApi.is_visible(hwnd)
        except (TooSaltyUISoupException, COMError):
            return False

    @classmethod
    def get_element_color(cls, element_name, template=None):
        """
        Gets element color.

        :param str element_name: element name.
        :param dict{str, str} template: template that shows what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: str
        :return: element color string.
        """
        element = cls.get_wgc_element(element_name, template)
        color = element.get_element_color()
        return color

    @classmethod
    def get_element_background_color(cls, element_name, template=None):
        """
        Gets element background color.

        :param str element_name: element name.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        :rtype: str
        :return: element background color string.
        """
        element = cls.get_wgc_element(element_name, template)
        bg_color = element.get_element_background_color()
        return bg_color

    @classmethod
    def create_wgc_path_dat_file(cls, path_to_wgc=None):
        """
        Create wgc_path.dat file in WGC folder.

        :param str path_to_wgc: path to current WGC.
        """
        log.debug('Create wgc_path.dat file in WGC folder.')
        path_to_wgc = path_to_wgc or WGCConfig.application.folder.encode('utf-8')
        path_file = str(path.join(WGCConfig.application.base_folder,
                              'data', WGCConfig.application.wgc_path_file))

        data_path = path.join(WGCConfig.application.base_folder, 'data')

        if not path.isdir(data_path):
            OSHelper.make_dirs(data_path)
        else:
            if path.isfile(path_file):
                OSHelper.unlock_file_or_folder(path_file)
                OSHelper.remove_file_or_directory(path_file)
        OSHelper.create_file(path_file, path_to_wgc)

    @classmethod
    def add_wgc_uri_scheme_entry(cls, path_to_wgc=None):
        path_to_wgc = path_to_wgc or WGCConfig.application.folder
        log.debug('Add class scheme entry to registry.')
        RegistryHelper.add_wgc_uri_scheme_entry(WGCConfig.application.prefix, path_to_wgc)

    @classmethod
    def delete_wgc_uri_scheme_entry(cls):
        log.debug('Delete class scheme entry from registry.')
        RegistryHelper.delete_wgc_uri_scheme_entry(WGCConfig.application.prefix)

    @classmethod
    def add_wgc_uninstall_entry(cls, path_to_wgc=None):
        """
        Add uninstall entry to registry.

        :param str path_to_wgc: path to current WGC.
        """
        path_to_wgc = path_to_wgc or WGCConfig.application.folder
        log.debug('Add uninstall entry to registry.')
        ico_str = path.join(path_to_wgc, 'wgc.exe') + ',0'
        uninstall_str = path_to_wgc + 'setup.exe" /IU'

        wgc_key = f'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\' \
                  f'Uninstall\\{WGCConfig.application.registry_key}'
        if not RegistryHelper.is_key_exist(
                RegistryHelper.HKEY_CURRENT_USER, wgc_key):
            RegistryHelper.create_key(
                RegistryHelper.HKEY_CURRENT_USER, wgc_key)
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'DisplayIcon', ico_str)
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'UninstallString', uninstall_str)
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'DisplayName', f'{WGCConfig.application.display_name}')
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'Publisher', WGCConfig.application.registry_publisher)
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'DisplayVersion', WGCConfig.wgc_build)

    @classmethod
    def add_steam_uninstall_entry(cls, path_to_wgc=None):
        """
        Add Steam uninstall entry to registry.

        :param str path_to_wgc: path to current WGC.
        """
        path_to_wgc = path_to_wgc or WGCConfig.application.folder
        log.debug('Add uninstall entry to registry.')
        ico_str = path.join(path_to_wgc, 'wgc.exe') + ',0'
        uninstall_str = path_to_wgc + 'setup.exe" /IU'

        wgc_key = f'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\' \
                  f'Uninstall\\Wargaming.net Game Center for Steam'
        if not RegistryHelper.is_key_exist(
                RegistryHelper.HKEY_CURRENT_USER, wgc_key):
            RegistryHelper.create_key(
                RegistryHelper.HKEY_CURRENT_USER, wgc_key)
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'DisplayIcon', ico_str)
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 4,
            'EstimatedSize', 327680)
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'UninstallString', uninstall_str)
        sp = WGCConfig.wgc_build.split('.')
        version32 = int(sp[0]) * 10000 + int(sp[1]) * 100 + int(sp[2])
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 4,
            'WGC Installer', version32)
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'DisplayName', f'Wargaming Game Center')
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'Publisher', 'Wargaming.net')
        RegistryHelper.set_value(
            RegistryHelper.HKEY_CURRENT_USER, wgc_key, 1,
            'DisplayVersion', WGCConfig.wgc_build)

    @classmethod
    def delete_wgc_uninstall_entry(cls):
        """
        Delete uninstall entry to registry.
        """
        log.debug('Delete uninstall entry to registry.')
        wgc_key = f'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\' \
                  f'Uninstall\\{WGCConfig.application.registry_key}'
        if RegistryHelper.is_key_exist(
                RegistryHelper.HKEY_CURRENT_USER, wgc_key):
            RegistryHelper.delete_key(RegistryHelper.HKEY_CURRENT_USER,
                                      wgc_key)

    @classmethod
    def reset_stored_users(cls):
        """
        Reset stored users.
        """
        OSHelper.unlock_file_or_folder(WGCConfig.application.user_info_file)
        OSHelper.remove_file_or_directory(WGCConfig.application.user_info_file)
        OSHelper.unlock_file_or_folder(WGCConfig.application.ntf_storage)
        OSHelper.remove_file_or_directory(WGCConfig.application.ntf_storage)
        for path_ in ['DEV1', 'RU', 'ASIA', 'DEV2']:
            OSHelper.unlock_file_or_folder(path.join(WGCConfig.application.ntf_storage, path_))
            OSHelper.remove_file_or_directory(path.join(WGCConfig.application.ntf_storage, path_))
            
    @classmethod
    def react_mouse_click(cls, element_name, template=None, index=0):
        element = cls.get_wgc_element(element_name, template, index=index)
        element.react_click()

    @classmethod
    def set_focus(cls, element_name, template=None, index=0, provider=None):
        """
        Set focus to an element.

        :param str element_name: element name.
        :param dict{str, str} template: template that show what replacements
        should be done e.g. {'some_text': 'text123'} will transform selector
        "//div[text()='%some_text%']" to "//div[text()='text123']".
        """
        element = cls.get_wgc_element(element_name, template, index=index, provider=provider)
        element.set_focus()

    @classmethod
    def is_final_configuration(cls):
        return WGCConfig.application.is_final_configuration
    
    @classmethod
    def create_monitor_config_json(cls, home_folder=None):
        home_folder = home_folder or WGCConfig.application.folder
        with open(path.join(home_folder, 'monitor_config.json'), 'w') as fp:
            fp.write(f'{{"state_reporting_url": "https://{GameMocks.host_https}/monitor"}}')

    @classmethod
    def search_crash_and_assert(cls):
        crash_found = False
        abnormal_found = False
        assertion = None
        wg_track = False
        monitor_files = cls.get_monitor_log_files()
        for log_file in monitor_files:
            try:
                with open(log_file, 'r', encoding='utf-8') as content:
                    log_content = content.read()
                crash_found = RegexHelper.search_crash_in_monitor(log_content)
                abnormal_found = RegexHelper.search_abnormal_termination_in_monitor(log_content)
                wg_track = RegexHelper.search_wg_track_in_monitor(log_content)
            except (PermissionError, FileNotFoundError) as e:
                log.error(f'{e} -> {log_file}')

            if wg_track and WGCConfig.search_events.assert_track:
                raise WG_TRACK('WG_TRACK detected.')

            if abnormal_found or crash_found:
                break

        wgc_files = cls.get_wgc_log_filenames(
            monitor_logs=False, renderer_logs=False, cef_logs=False, overlay_logs=False)
        for log_file in wgc_files:
            if log_file and path.isfile(log_file):
                with open(log_file, 'r', encoding='utf-8') as content:
                    log_content = content.read()
                    crash_found = crash_found or RegexHelper.search_crash_in_wgc(log_content)
                    assertion = RegexHelper.search_assertion_in_wgc(log_content)
                    if assertion or crash_found:
                        break
        if assertion:
            type_ = 'silent_assert' if 'Report' in str(type(assertion)) else 'fatal_assert'
            if (WGCConfig.search_events.assert_silent and type_ == 'silent_assert') or (
                    WGCConfig.search_events.assert_fatal and type_ == 'fatal_assert'):
                raise assertion
        if crash_found and WGCConfig.search_events.crash:
            dmp_pth = DumpHelper.get_cat_dmp_path()
            if dmp_pth and WGCConfig.search_events.artifacts_upload:
                
                try:
                    backup_dmp = path.join(WGCConfig.application.folder, path.basename(dmp_pth[0]))
                    OSHelper.copy(dmp_pth[0], backup_dmp)
                    ArtifactsHelper.push_to_queue(backup_dmp)
                except PermissionError:
                    log.debug('Cannot copy dmp file while it used by WGC.')
                    backup_dmp = dmp_pth[0]
                
                screenshot_full_path = ScreenHelper.save_screen(path.dirname(backup_dmp))
                if screenshot_full_path is not None:
                    ArtifactsHelper.push_to_queue(screenshot_full_path)
                    
                cls.close_crash_window()  # for skip process already used error.
                stack_path, first_line = DumpHelper.resolve_dump(backup_dmp)
                log.debug(f'stack_path = {stack_path}')
                log.debug(f'first_line = {first_line}')
                if stack_path:
                    ArtifactsHelper.push_to_queue(stack_path)
                additional_text = ''
                report_session_id = Waiter.poll(10, lambda: GameMocks.report_session_id)
                if report_session_id:
                    additional_text = f'\nhttps://catweb-wgc.wargaming.net/wgc/report/details/?id={report_session_id}'
                    log.debug(f'GameMocks.report_session_id({GameMocks.report_session_id})')
                if first_line:
                    raise WGCCrash(f'{first_line}{additional_text}')
                else:
                    raise WGCCrash(f'WGC has crashed{additional_text}')
            raise WGCCrash('WGC has crashed')
        if abnormal_found and WGCConfig.search_events.abnormal_termination:
            raise AbnormalTermination('Abnormal termination detected')
        return False

    @classmethod
    def create_html_file_with_custom_uri_link(cls, command):
        html_path = path.join(WGCConfig.temp_folder, 'custom_file.html')
        content = f"""
        <!DOCTYPE html>
        <html lang="en">
        <head>
        <meta charset="UTF-8">
        <title>Custom news</title>
        </head>
        <body>
        <h1>Custom news for WGC template</h1>
        
        <h3><a id="wgc_link" href="{command}">Custom link</a></h3>
        
        </body>
        </html>
        """
        OSHelper.create_file(html_path, content)
        return html_path

    @classmethod
    def close_first_notification(cls):
        """
        Clicks on close control first Tray notification.
        """
        RequirementHelper.append_requirement('LLR.2.3')
        log.info('Click on X of first tray notification.')

        # discover where top left corner of active screen is
        # return is a dictionary list of keys and values
        monitors = win32api.EnumDisplayMonitors()
        for m in monitors:
            if m[2][0] == 0:
                display1 = win32api.GetMonitorInfo(m[0])
                uisoup.mouse.move(display1['Work'][2] - 27, display1['Work'][3] - 56, smooth=False)
                WinApi.click(display1['Work'][2] - 26, display1['Work'][3] - 56)
                break

    @classmethod
    def capture_active_window(cls, timeout: int = 1, name: str = None, quality: int = 7):
        """
        Captures the currently active window as a screenshot and stores it in the test results' folder.
        Screenshots are organized by the caller's package directory (root_test_package_folder).
        """
        if not WGCConfig.enable_screenshots:
            return None

        # Required imports for capture
        import time
        import win32gui
        from PIL import ImageGrab
        import inspect

        time.sleep(timeout)

        try:
            # Resolve caller method and file
            frame = inspect.currentframe()
            # Go back one frame to find the caller
            caller_frame = frame.f_back if frame else None

            # Determine test method name
            test_method = caller_frame.f_code.co_name if caller_frame else "manual"
            if test_method == "<module>":
                test_method = "manual"

            # Determine root_test_package_folder (parent folder name of the calling file)
            if caller_frame:
                caller_file = caller_frame.f_code.co_filename
                # e.g., C:/tests/shop/test_buy.py -> shop
                root_package = os.path.basename(os.path.dirname(caller_file))
            else:
                root_package = "common"

            # Sanity check for empty package name
            if not root_package:
                root_package = "common"

            base_name = name if name else test_method

            # Initialize counters if not present
            if not hasattr(cls, "_counters"):
                cls._counters = {}

            # Increment screenshot counter (scoped to method to help uniqueness)
            cls._counters[test_method] = cls._counters.get(test_method, 0) + 1
            counter_str = f"{cls._counters[test_method]:02d}"

            # Get active window handle and dimensions
            hwnd = win32gui.GetForegroundWindow()
            if not hwnd:
                raise RuntimeError("No active window detected.")

            rect = win32gui.GetWindowRect(hwnd)

            # Capture the active window
            screenshot = ImageGrab.grab(bbox=rect).convert("RGB")

            # Build directory: <results>/manual_screenshots/<root_package>/
            screenshot_dir = os.path.join(
                WGCConfig.test_results_folder,
                'manual_screenshots',
                root_package
            )
            os.makedirs(screenshot_dir, exist_ok=True)

            # Save screenshot
            file_name = f"{counter_str}_{base_name}.jpg"
            screenshot_path = os.path.join(screenshot_dir, file_name)

            # Convert 1-10 scale to 10-100 for PIL
            jpeg_quality = max(10, min(quality * 10, 100))

            screenshot.save(screenshot_path, "JPEG", quality=jpeg_quality)

            log.info(f"Screenshot saved: {screenshot_path}")
            return screenshot_path

        except Exception as e:
            log.error(f"Failed to capture active window: {e}")
            return None


    @classmethod
    def hover_element_by_mouse(cls, element_name, template=None,
                               index=0, x_offset=0, y_offset=0, smooth=True, provider=None):
        """
        Moves the mouse to hover over the specified element without clicking.

        :param element_name: Name of the element to hover over.
        :param template: Optional template for the element.
        :param index: Index of the element if multiple is found.
        :param x_offset: X-axis offset for the mouse position.
        :param y_offset: Y-axis offset for the mouse position.
        :param smooth: If True, the mouse movement will be smooth.
        :param provider: Provider for retrieving the element's position.
        """
        x, y, r, b, w, h = cls.get_element_position(element_name, template, index, provider=provider)
        uisoup.mouse.move(x + 5 + x_offset, y + 5 + y_offset, smooth=smooth)

    @classmethod
    def scroll_screen_bottom(cls, step=200, repeat=50, delay=0.05):
        """
        Scrolls the screen vertically to the bottom using Windows API.

        :param step: Scroll step size (default 120 = one notch). Positive scrolls up, negative scrolls down.
        :param repeat: How many times to scroll down.
        :param delay: Delay between scrolls in seconds.
        """
        cls.set_focus_window()
        for _ in range(repeat):
            win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, 0, 0, -step, 0)
            sleep(delay)
