import ast
import json
import logging
import os
import py
import re
from collections.abc import Callable
from functools import wraps
from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar

import allure
import pytest
from selenium.common.exceptions import WebDriverException


P = ParamSpec("P")
R = TypeVar("R")
TCls = TypeVar("TCls", bound=type)

log = logging.getLogger(__name__)

MAX_SYMBOLS = 510
TABS_FORMAT = " " * 20


def repr_console_errors(console_errors: Any, tabs: str = TABS_FORMAT) -> str:
    """Format console errors for logging."""
    return f"\n{tabs}".join(
        f"[CONSOLE ERROR]: {error}" for error in set(console_errors)
    )


def repr_args(*args: Any, **kwargs: Any) -> str:
    """Format function arguments for logging."""
    return "{args}{mark}{kwargs}".format(
        args=", ".join([str(x) for x in args]) if args else "",
        kwargs=", ".join(f"{k}={v}" for k, v in kwargs.items())
        if kwargs
        else "",
        mark=", " if args and kwargs else "",
    )


def find_console_browser_errors(driver: Any) -> set[str] | None:
    """Return browser console errors, skipping ignored hosts if set."""
    try:
        browser_errors = [
            x["message"]
            for x in list(
                filter(
                    lambda x: x["level"] == "SEVERE",
                    driver.get_log("browser"),
                )
            )
        ]
    except WebDriverException as e:
        log.warning(f"Exception when reading Browser Console log: {str(e)}")
        return None
    else:
        is_skipped_hosts = os.environ.get("SKIPPED_HOSTS")
        if is_skipped_hosts and browser_errors:
            for host in ast.literal_eval(is_skipped_hosts):
                browser_errors = list(
                    filter(lambda x: host not in x, browser_errors)
                )
                if not browser_errors:
                    break
        return set(browser_errors)


def logger_for_element_methods[**P, R](fn: Callable[P, R]) -> Callable[P, R]:
    """Decorator for logging element method calls and console errors."""

    @wraps(fn)
    def wrapper(self: Any, *args: P.args, **kwargs: P.kwargs) -> R:
        res = fn(self, *args, **kwargs)
        console_errors = find_console_browser_errors(self.driver)
        is_check = os.environ.get("CHECK_CONSOLE")
        if console_errors:
            log.warning(
                "Browser console js error found:\n"
                f"{TABS_FORMAT}{repr_console_errors(console_errors)}\n"
                f"{TABS_FORMAT}Url: {self.driver.current_url}\n"
                f"{TABS_FORMAT}Action: {self.__class__.__name__}"
                f"({self.by}={self.locator})"
                f".{fn.__name__}({repr_args(*args, **kwargs)})"
            )

            with allure.step("[Console Error] ⚠️"):
                for err in console_errors:
                    split_type = "\\n" if "\\n" in err else "\n"
                    allure.attach(
                        name="js error",
                        body=json.dumps(err.strip().split(split_type), indent=4),
                        attachment_type=allure.attachment_type.JSON,
                    )

            error_tmpl = (
                f"Url: {self.driver.current_url}\n"
                f"Action: {self.__class__.__name__}({self.by}={self.locator})"
                f".{fn.__name__}({repr_args(*args, **kwargs)})\n"
            )
            if is_check and hasattr(self.driver, "console_errors"):
                for err in set(console_errors):
                    self.driver.console_errors.append(
                        f"[CONSOLE ERROR]: {err}\n{error_tmpl}"
                    )
        return res

    return wrapper


if TYPE_CHECKING:

    def add_logger_to_base_element_classes[TCls: type](cls: TCls) -> TCls:
        """No-op for type checkers, зберігає видимість методів у IDE."""
        return cls

else:

    def add_logger_to_base_element_classes[TCls: type](cls: TCls) -> TCls:
        """Decorate all public methods of a class with console logging."""
        for name, method in cls.__dict__.items():
            log.info(f"{name}, {method}")
            if (
                not name.startswith("_")
                and callable(method)
                and name != "lookup"
            ):
                setattr(cls, name, logger_for_element_methods(method))
        return cls


def logger_for_loading_page(fn: Callable[..., Any]) -> Callable[..., Any]:
    """Decorator for logging page loading with console error checks."""

    @wraps(fn)
    def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
        res = fn(self, *args, **kwargs)
        if os.environ.get("CHECK_CONSOLE"):
            console_errors = find_console_browser_errors(self.driver)
            if console_errors:
                log.warning(
                    "Browser console js error found:\n"
                    f"{TABS_FORMAT}{repr_console_errors(console_errors)}\n"
                    f"{TABS_FORMAT}Url: {self.driver.current_url}\n"
                    f"{TABS_FORMAT}Action: wait for page loaded ..."
                )
                if hasattr(self.driver, "console_errors"):
                    for err in set(console_errors):
                        self.driver.console_errors.append(
                            f"[CONSOLE ERROR]: {err}\n"
                            f"Url: {self.driver.current_url}\n"
                            "Action: wait for page loaded ...\n"
                        )
        return res

    return wrapper


IGNORE_LOG_MESSAGES = [
    "InsecureRequestWarning: Unverified HTTPS request is being made. "
    "Adding certificate verification is strongly advised.",
    "Starting new HTTP connection",
    "Starting new HTTPS connection",
]


class LoggerFilter(logging.Filter):
    """Filter for ignoring specific log messages."""

    def filter(self, record: logging.LogRecord) -> bool:  # noqa A003
        if hasattr(record, "msg"):
            is_ignore = any(
                set(filter(lambda x: x in record.msg, IGNORE_LOG_MESSAGES))
            )
            if is_ignore:
                return False
        return record.levelno > 10


class Logger:
    """Logger integration with pytest runtime."""

    def pytest_runtest_setup(self, item: Any) -> None:
        """Attach custom log handler at test setup."""
        item.capturelog_handler = LoggerHandler()
        item.capturelog_handler.setFormatter(
            logging.Formatter(
                "%(asctime)-12s%(levelname)-8s%(message)s\n", "%H:%M:%S"
            )
        )
        root_logger = logging.getLogger()
        item.capturelog_handler.addFilter(LoggerFilter())
        root_logger.addHandler(item.capturelog_handler)
        root_logger.setLevel(logging.NOTSET)

    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_makereport(self, item: Any, call: Any) -> Any:
        """Hook to capture and attach logs to pytest reports."""
        outcome = yield
        report = outcome.get_result()
        if hasattr(item, "capturelog_handler"):
            if call.when == "teardown":
                root_logger = logging.getLogger()
                root_logger.removeHandler(item.capturelog_handler)
            if not report.passed:
                longrepr = getattr(report, "longrepr", None)
                if hasattr(longrepr, "addsection"):
                    captured_log = item.capturelog_handler.stream.getvalue()
                    if captured_log:
                        longrepr.sections.insert(
                            len(longrepr.sections),
                            ("Captured log", f"\n{captured_log}", "-"),
                        )
                        if item.config.getoption("--allure-report"):
                            allure.attach(
                                captured_log,
                                name="CAPTURED LOG",
                                attachment_type=allure.attachment_type.TEXT,
                            )
            if call.when == "teardown":
                item.capturelog_handler.close()
                del item.capturelog_handler


class LoggerHandler(logging.StreamHandler):
    """Custom log handler to capture logs into pytest."""

    def __init__(self) -> None:
        logging.StreamHandler.__init__(self)
        self.stream = py.io.TextIO()
        self.records: list[logging.LogRecord] = []

    def close(self) -> None:
        """Close the handler and underlying stream."""
        logging.StreamHandler.close(self)
        self.stream.close()


def request_logging(request: Any, url: str, method: str, **kwargs: Any) -> None:
    """Log outgoing requests with response details."""
    content = str(request.data)
    warning_message = (
        "[Python request] STATUS CODE: {status_code}, LINK: {link}\n"
        "{tabs}METHOD: {method}\n"
        "{tabs}BODY: {body}\n"
        "{tabs}RESPONSE CONTENT: "
        "{content}{ellipsis} ({length} symbols)\n".format(
            status_code=request.status,
            method=method,
            link=url,
            body=kwargs.get("body"),
            content=re.sub(r"\s+", " ", content)[:MAX_SYMBOLS],
            ellipsis=" ..." if len(content) > MAX_SYMBOLS else "",
            length=len(request.data),
            tabs=" " * 12,
        )
    )
    if 400 <= request.status < 500:
        log.warning(warning_message)
    elif request.status >= 500:
        log.error(warning_message)
    else:
        log.info(f"[Python request] STATUS CODE: {request.status}, LINK: {url}")
