# coding: utf-8
"""
Этот модуль содержит полезные декораторы для работы с Appium.
"""
import base64
import sys
import io
import time
import functools
import traceback
from functools import wraps
from datetime import datetime

import allure
import numpy as np
import pytest
from PIL import Image


# TODO make unit test for module

def retry(func):
    """
    Повторяет выполнение метода если он возвращает False или None.
    3 tries hardcode in method
    """
    max_retries = 3

    @wraps(func)
    def wrapper(*args, **kwargs):
        result = None
        for _ in range(max_retries):
            result = func(*args, **kwargs)
            if result is not None and result is not False:
                return result
            time.sleep(1)
        return result

    # Возвращаем обертку функции
    return wrapper


def wait_until_window_change(poll_frequency: float = 0.1):
    """
    Декоратор, который ожидает пока окно не перестанет меняться.
    В обернутом методе должен быть аргумент
        decorator_args: dict
            timeout_window: общее время ожидания
            window_not_changing_period: период времени в течении которого окно не должно изменятся

    Аргументы:
        poll_frequency (float): Частота опроса содержимого окна на наличие изменений в секундах.
                               По умолчанию 0.1 секунды.

    Возвращает:
        function: Декорированная функция.
    """

    def func_decorator(func):
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            """
            Оберточная функция, которая инкапсулирует декорированную функцию
            с логикой обнаружения изменений окна.

            Аргументы:
                self: Экземпляр класса, к которому принадлежит
                      декорированный метод.
                *args: Произвольное число аргументов, переданных в
                       декорированный метод.
                **kwargs: Произвольное число именованных аргументов, переданных в
                          декорированный метод.

            Возвращает:
                bool: True, если содержимое окна изменяется в течении
                      заданного периода времени, иначе False.
            """

            # Инициализация
            func_result = False
            decorator_args = kwargs.get('decorator_args', {})
            timeout_window = decorator_args.get('timeout_window', 30)
            window_not_changing_period = decorator_args.get('window_not_changing_period', 10)
            # Запись начального времени
            start_time = time.time()
            # Вызов декорированной функции и сохранение результата
            func_result = func(self, *args,
                               **kwargs)

            # Обнаружение изменений экрана с экспоненциальной задержкой
            poll_interval = poll_frequency
            # Продолжаем до достижения тайм-аута
            while time.time() - start_time < timeout_window:
                # Запускаем новый период, в течение которого окно не изменяется
                window_not_changing_period_start_time = time.time()
                # Флаг для отслеживания того, изменилось ли окно за период
                window_not_changed = True

                while (time.time() - window_not_changing_period_start_time
                       < window_not_changing_period):
                    # Делаем снимок экрана и сохраняем его в памяти
                    image_bytes = self.driver.get_screenshot_as_png()
                    # Преобразуем в оттенки серого
                    image = Image.open(io.BytesIO(image_bytes)).convert('L')
                    # Обрезаем снимок до определенной области (лево, верх, право, низ)
                    box = (50, 50, 400, 400)
                    image = image.crop(box)
                    # Ждем указанный интервал между опросами
                    time.sleep(poll_interval)
                    new_image_bytes = self.driver.get_screenshot_as_png()
                    # Преобразуем в оттенки серого
                    new_image = Image.open(io.BytesIO(new_image_bytes)).convert('L')
                    new_image = new_image.crop(box)

                    # Проверяем, отличается ли сумма значений пикселей на двух изображениях
                    if np.sum(image) != np.sum(new_image):
                        # Содержимое окна изменилось
                        window_not_changed = False
                        break

                if window_not_changed:
                    self.logger.debug("Содержимое окна не изменялось в течение периода")
                    return func_result
                # Удваиваем время ожидания для каждого опроса
                poll_interval *= 2

            return func_result

        # Возвращаем обертку функции
        return wrapper

    # Возвращаем декоратор функций
    return func_decorator


def wait_for_window_change(poll_frequency: float = 0.5):
    """
    Декоратор, который ожидает изменения окна после выполнения метода.
    Если окно не изменилось - выполняет еще попытку.
    В обернутом методе должен быть аргумент
        decorator_args: dict
            timeout_window: int, время ожидания на попытку
            tries: int, количество попыток выполнения метода

    Аргументы:
        poll_frequency (float): Частота проверки окна на изменения.

    Возвращает:
        Декоратор функции.
    """

    def func_decorator(func):
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            """
            Оберточная функция, которая выполняет обнаружение изменения окна и
            выполнение декорированной функции.

            Аргументы:
                self: Экземпляр класса, содержащего декорированную функцию.
                *args: Переменное число аргументов.
                **kwargs: Произвольные именованные аргументы.

            Возвращает:
                Результат декорированной функции или False, если изменение окна не было обнаружено.
            """

            # Инициализация
            func_result = False
            decorator_args = kwargs.get('decorator_args', {})
            timeout_window = decorator_args.get('timeout_window', 10)
            tries = decorator_args.get('tries', 3)

            # Сделать снимок экрана и сохранить его в памяти в виде байтов
            image_bytes = self.driver.get_screenshot_as_png()
            # Открыть изображение из байтов и преобразовать его в оттенки серого
            image = Image.open(io.BytesIO(image_bytes)).convert(
                'L')

            # Обрезать снимок экрана до определенной области (лево, верх, право, низ)
            box = (50, 50, 400, 400)
            # Обрезать изображение на основе заданных координат области
            image = image.crop(box)

            # Попытаться обнаружить изменение экрана в течении tries попыток
            for _ in range(tries):
                # Записать текущее время начала попытки обнаружения
                start_time = time.time()
                # Выполнить декорированную функцию и сохранить результат
                func_result = func(self, *args,
                                   **kwargs)

                # Обнаружить изменение экрана с экспоненциальной задержкой
                poll_interval = poll_frequency
                # Проверить, находится ли прошедшее время в пределах заданного окна времени ожидания
                while time.time() - start_time < timeout_window:
                    # Приостановить выполнение на заданный интервал проверки
                    time.sleep(poll_interval)
                    # Сделать новый снимок экрана окна и получить данные изображения в виде байтов
                    new_image_bytes = self.driver.get_screenshot_as_png()
                    # Открыть новое изображение из байтов и преобразовать его в оттенки серого
                    new_image = Image.open(io.BytesIO(new_image_bytes)).convert(
                        'L')
                    # Обрезать новое изображение на основе заданных координат области
                    new_image = new_image.crop(
                        box)
                    # Сравнить суммы значений пикселей между исходным и новым изображениями
                    if not np.sum(image) == np.sum(
                            new_image):
                        # Записать сообщение о том, что произошло изменение экрана
                        self.logger.debug(
                            "Изменение экрана обнаружено")
                        # Вернуть True для обозначения обнаружения изменения экрана
                        return func_result
                        # Удвоить интервал проверки для следующей итерации
                        # (экспоненциальная задержка)
                    poll_interval *= 2

            return func_result

        # Возвращаем обертку функции
        return wrapper

    # Возвращаем декоратор функций
    return func_decorator


def wait_until_dom_change(poll_frequency: float = 0.1):
    """
    Декоратор, который ожидает пока DOM не перестанет меняться.
    В обернутом методе должен быть аргумент
        decorator_args: dict
            timeout_window: общее время ожидания
            window_not_changing_period: период времени в течении которого окно не должно изменятся

    Аргументы:
        poll_frequency (float): Частота опроса содержимого окна на наличие изменений в секундах.
                               По умолчанию 0.1 секунды.

    Возвращает:
        function: Декорированная функция.
    """

    def func_decorator(func):
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            """
            Оберточная функция, которая инкапсулирует декорированную функцию
            с логикой обнаружения изменений DOM.

            Аргументы:
                self: Экземпляр класса, к которому принадлежит
                      декорированный метод.
                *args: Произвольное число аргументов, переданных в
                       декорированный метод.
                **kwargs: Произвольное число именованных аргументов, переданных в
                          декорированный метод.

            Возвращает:
                bool: True, если содержимое окна изменяется в течении
                      заданного периода времени, иначе False.
            """

            # Инициализация
            func_result = False
            decorator_args = kwargs.get('decorator_args', {})
            timeout_window = decorator_args.get('timeout_window', 30)
            window_not_changing_period = decorator_args.get('window_not_changing_period', 10)
            # Запись начального времени
            start_time = time.time()
            # Вызов декорированной функции и сохранение результата
            func_result = func(self, *args,
                               **kwargs)

            # Обнаружение изменений экрана с экспоненциальной задержкой
            poll_interval = poll_frequency
            # Продолжаем до достижения тайм-аута
            while time.time() - start_time < timeout_window:
                # Запускаем новый период, в течение которого окно не изменяется
                window_not_changing_period_start_time = time.time()
                # Флаг для отслеживания того, изменилось ли окно за период
                window_not_changed = True

                while (time.time() - window_not_changing_period_start_time
                       < window_not_changing_period):
                    # Делаем снимок DOM
                    page_source = self.driver.page_source
                    # Ждем указанный интервал между опросами
                    time.sleep(poll_interval)
                    new_page_source = self.driver.page_source

                    # Проверяем, отличается ли сумма значений пикселей на двух изображениях
                    if page_source != new_page_source:
                        # Содержимое окна изменилось
                        window_not_changed = False
                        break
                if window_not_changed:
                    self.logger.debug("Содержимое DOM не изменялось в течение периода")
                    return func_result
                # Удваиваем время ожидания для каждого опроса
                poll_interval *= 2

            return func_result

        # Возвращаем обертку функции
        return wrapper

    # Возвращаем декоратор функций
    return func_decorator


def wait_for_dom_change(poll_frequency: float = 0.5):
    """
    Декоратор, который ожидает изменения DOM после выполнения метода.
    Если DOM не изменилось - выполняет еще попытку.
    В обернутом методе должен быть аргумент
        decorator_args: dict
            timeout_window: int, время ожидания на попытку
            tries: int, количество попыток выполнения метода

    Аргументы:
        poll_frequency (float): Частота проверки окна на изменения.

    Возвращает:
        Декоратор функции.
    """

    def func_decorator(func):
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            """
            Оберточная функция, которая выполняет обнаружение изменения окна и
            выполнение декорированной функции.

            Аргументы:
                self: Экземпляр класса, содержащего декорированную функцию.
                *args: Переменное число аргументов.
                **kwargs: Произвольные именованные аргументы.

            Возвращает:
                Результат декорированной функции или False, если изменение окна не было обнаружено.
            """

            # Инициализация
            func_result = False
            decorator_args = kwargs.get('decorator_args', {})
            timeout_window = decorator_args.get('timeout_window', 10)
            tries = decorator_args.get('tries', 3)

            # Сделать снимок DOM и сохранить его в памяти
            page_source = self.driver.page_source

            # Записать текущее время начала попытки обнаружения
            start_time = time.time()
            # Выполнить декорированную функцию и сохранить результат
            func_result = func(self, *args,
                               **kwargs)

            # Обнаружить изменение экрана с экспоненциальной задержкой
            poll_interval = poll_frequency
            # Проверить, находится ли прошедшее время в пределах заданного окна времени ожидания
            while time.time() - start_time < timeout_window:
                # Приостановить выполнение на заданный интервал проверки
                time.sleep(poll_interval)
                # Сделать новый снимок DOM
                new_page_source = self.driver.page_source
                # Сравнить
                if not page_source == new_page_source:
                    # Записать сообщение о том, что произошло изменение DOM
                    self.logger.debug(
                        "Изменение DOM обнаружено")
                    # Вернуть True для обозначения обнаружения изменения DOM
                    return func_result
                    # Удвоить интервал проверки для следующей итерации
                    # (экспоненциальная задержка)
                poll_interval *= 2

            return func_result

        # Возвращаем обертку функции
        return wrapper

    # Возвращаем декоратор функций
    return func_decorator


def time_it(func):
    """
    Замеряет время выполнения метода.
    Печатает результат замера.
    """

    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        execution_time = end_time - start_time
        print(f"Execution time of {func.__name__}: {execution_time:.2f} seconds")
        return result

    # Возвращаем обертку функции
    return wrapper


def step_info(my_str):
    """
    Декоратор, который перед вызовом метода вызывает logger.info и @allure.step,
    передавая в них строковую переменную, принятую в параметрах.

    Аргументы:
        my_str (str): Строковая переменная для использования в logger.info и @allure.step.

    Возвращает:
        function: Декоратор функций.

    Пример использования:
        @my_step_info("Мой шаг")
        def my_function():
            ...
    """

    # Определяем декоратор функций
    def func_decorator(func):
        # Создаем обертку функции, сохраняющую метаданные исходной функции
        @allure.step(my_str)
        def wrapper(self, *args, **kwargs):
            result = None
            # Логируем информацию перед вызовом метода
            self.logger.info(my_str)
            # Получаем скриншот до вызова метода
            screenshot = self.driver.get_screenshot_as_png()
            # Генерируем временную метку для имени скриншота
            timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
            # Устанавливаем имя скриншота до вызова метода
            screenshot_name_begin = f"screenshot_begin_{timestamp}.png"
            # Имя файла видеозаписи с временной меткой
            video_filename = f'screenrecord_{timestamp}.mp4'
            self.driver.start_recording_screen()
            try:
                # Выполняем исходную функцию
                result = func(self, *args, **kwargs)
            except Exception as error:
                # Если произошло исключение, прикрепляем скриншот до вызова метода к отчету
                allure.attach(screenshot,
                              name=screenshot_name_begin,
                              attachment_type=allure.attachment_type.PNG)
                # Получаем скриншот после вызова метода
                screenshot = self.driver.get_screenshot_as_png()
                # Генерируем временную метку для имени скриншота
                timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
                # Устанавливаем имя скриншота до вызова метода
                screenshot_name_end = f"screenshot_end_{timestamp}.png"
                # Если произошло исключение, прикрепляем скриншот после вызова метода к отчету
                allure.attach(screenshot,
                              name=screenshot_name_end,
                              attachment_type=allure.attachment_type.PNG)

                # Если произошло исключение, прикрепляем видеозапись выполнения метода к отчету
                allure.attach(base64.b64decode(self.driver.stop_recording_screen()),
                              name=video_filename,
                              attachment_type=allure.attachment_type.MP4)

                # Прикрепляем информацию об ошибке AssertionError к отчету
                allure.attach(str(error),
                              name=str(error),
                              attachment_type=allure.attachment_type.TEXT)

                # Выводим информацию в лог
                self.logger.error(f"{my_str} [не выполнено]")
                traceback_info = "".join(traceback.format_tb(sys.exc_info()[2]))
                error_msg = f"""Ошибка: {error},
                                        {args=},
                                        {kwargs=},
                            Traceback:
                            {traceback_info=}
                                """
                self.logger.error(error_msg)

                # В случае исключения помечаем тест провалившимся
                try:
                    pytest.fail(f"{func.__name__}({args}, {kwargs}), {error}")
                except Exception as e:
                    self.logger.error("Pytest не обнаружен")
                    raise

            # Логируем информацию после успешного выполнения метода
            self.logger.info(f"{my_str} [выполнено успешно]")
            # Возвращаем результат выполнения исходной функции
            return result

        # Возвращаем обертку функции
        return wrapper

    # Возвращаем декоратор функций
    return func_decorator


def screenshots():
    """
    В случае возникновения AssertionError в обернутом методе -
    прикрепляет к allure report скриншот до выполнения
    метода и после возникновения исключения, а также информацию об ошибке.
    В ином случае скриншот не прикрепляется.
    """

    # Определяем декоратор функций
    def func_decorator(func):
        # Создаем обертку функции, сохраняющую метаданные исходной функции
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            # Получаем скриншот до вызова метода
            screenshot = self.driver.get_screenshot_as_png()
            # Генерируем временную метку для имени скриншота
            timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
            # Устанавливаем имя скриншота до вызова метода
            screenshot_name_begin = f"screenshot_begin_{timestamp}.png"
            try:
                # Выполняем исходную функцию
                result = func(self, *args, **kwargs)
            except AssertionError as error:
                # Если произошло исключение, прикрепляем скриншот до вызова метода к отчету
                allure.attach(screenshot,
                              name=screenshot_name_begin,
                              attachment_type=allure.attachment_type.PNG)
                # Прикрепляем информацию об ошибке AssertionError к отчету
                allure.attach(str(error),
                              name="AssertionError",
                              attachment_type=allure.attachment_type.TEXT)
                # Рейзим исключение AssertionError с сохраненным traceback
                raise AssertionError(str(error)).with_traceback(sys.exc_info()[2])
            finally:
                # Получаем скриншот после вызова метода
                screenshot = self.driver.get_screenshot_as_png()
                # Обновляем временную метку для имени скриншота
                timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')
                # Устанавливаем имя скриншота после вызова метода
                screenshot_name_end = f"screenshot_end_{timestamp}.png"
                # Прикрепляем скриншот после вызова метода к отчету
                allure.attach(screenshot,
                              name=screenshot_name_end,
                              attachment_type=allure.attachment_type.PNG)
            # Возвращаем результат выполнения исходной функции
            return result

        # Возвращаем обертку функции
        return wrapper

    # Возвращаем декоратор функций
    return func_decorator


def log_debug():
    """
    Логирует начало и завершение обернутого метода
    """

    # Определяем декоратор функций
    def func_decorator(func):
        # Создаем обертку функции, сохраняющую метаданные исходной функции
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            # Получаем имя метода
            method_name = func.__name__
            # Логируем начало выполнения метода и переданные аргументы
            self.logger.debug(f"{method_name}() < {', '.join(map(str, args))}, "
                              f"{', '.join(f'{k}={v}' for k, v in kwargs.items())}")
            # Выполняем исходную функцию
            result = func(self, *args, **kwargs)
            # Если результат существует, логируем его
            if result:
                self.logger.debug(f"{method_name}() > {str(result)}")
            # Возвращаем результат выполнения исходной функции
            return result

        # Возвращаем обертку функции
        return wrapper

    # Возвращаем декоратор функций
    return func_decorator


def print_me():
    """
    Печатает начало и завершение обернутого метода
    """

    # Определяем декоратор функций
    def func_decorator(func):
        # Создаем обертку функции, сохраняющую метаданные исходной функции
        def wrapper(*args, **kwargs):
            # Получаем имя метода
            method_name = func.__name__
            # Печатаем начало выполнения метода и переданные аргументы
            print(f"{method_name}() < {', '.join(map(str, args))}, "
                  f"{', '.join(f'{k}={v}' for k, v in kwargs.items())}")
            # Выполняем исходную функцию
            result = func(*args, **kwargs)
            # Если результат существует, логируем его
            if result:
                print(f"{method_name}() > {str(result)}")
            # Возвращаем результат выполнения исходной функции
            return result

        # Возвращаем обертку функции
        return wrapper

    # Возвращаем декоратор функций
    return func_decorator
