# This file contents is copied whole sale from pytest. The license for pytest:

# The MIT License (MIT)
#
# Copyright (c) 2004-2020 Holger Krekel and others
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
# of the Software, and to permit persons to whom the Software is furnished to do
# so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from io import StringIO
from typing import List
import logging
from contextlib import contextmanager
import re

_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")


def _remove_ansi_escape_sequences(text):
    return _ANSI_ESCAPE_SEQ.sub("", text)


@contextmanager
def catching_logs(handler, formatter=None, level=None):
    """Context manager that prepares the whole logging machinery properly."""
    root_logger = logging.getLogger()

    if formatter is not None:
        handler.setFormatter(formatter)
    if level is not None:
        handler.setLevel(level)

    # Adding the same handler twice would confuse logging system.
    # Just don't do that.
    add_new_handler = handler not in root_logger.handlers

    if add_new_handler:
        root_logger.addHandler(handler)
    if level is not None:
        orig_level = root_logger.level
        root_logger.setLevel(min(orig_level, level))
    try:
        yield handler
    finally:
        if level is not None:
            root_logger.setLevel(orig_level)
        if add_new_handler:
            root_logger.removeHandler(handler)


class LogCaptureHandler(logging.StreamHandler):
    """A logging handler that stores log records and the log text."""

    def __init__(self) -> None:
        """Creates a new log handler."""
        logging.StreamHandler.__init__(self, StringIO())
        self.records = []  # type: List[logging.LogRecord]

    def emit(self, record: logging.LogRecord) -> None:
        """Keep the log records in a list in addition to the log text."""
        self.records.append(record)
        logging.StreamHandler.emit(self, record)

    def reset(self) -> None:
        self.records = []
        self.stream = StringIO()


class LogCaptureFixture:
    """Provides access and control of log capturing."""

    def __init__(self, item) -> None:
        """Creates a new funcarg."""
        self._item = item
        # dict of log name -> log level
        self._initial_log_levels = {}  # type: Dict[str, int]

    def _finalize(self) -> None:
        """Finalizes the fixture.

        This restores the log levels changed by :meth:`set_level`.
        """
        # restore log levels
        for logger_name, level in self._initial_log_levels.items():
            logger = logging.getLogger(logger_name)
            logger.setLevel(level)

    @property
    def handler(self) -> LogCaptureHandler:
        """
        :rtype: LogCaptureHandler
        """
        return self._item.catch_log_handler  # type: ignore[no-any-return]  # noqa: F723

    def get_records(self, when: str) -> List[logging.LogRecord]:
        """
        Get the logging records for one of the possible test phases.

        :param str when:
            Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".

        :rtype: List[logging.LogRecord]
        :return: the list of captured records at the given stage

        .. versionadded:: 3.4
        """
        handler = self._item.catch_log_handlers.get(when)
        if handler:
            return handler.records  # type: ignore[no-any-return]  # noqa: F723
        else:
            return []

    @property
    def text(self):
        """Returns the formatted log text."""
        return _remove_ansi_escape_sequences(self.handler.stream.getvalue())

    @property
    def records(self):
        """Returns the list of log records."""
        return self.handler.records

    @property
    def record_tuples(self):
        """Returns a list of a stripped down version of log records intended
        for use in assertion comparison.

        The format of the tuple is:

            (logger_name, log_level, message)
        """
        return [(r.name, r.levelno, r.getMessage()) for r in self.records]

    @property
    def messages(self):
        """Returns a list of format-interpolated log messages.

        Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
        are all interpolated.
        Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
        levels, timestamps, etc, making exact comparisons more reliable.

        Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
        to the logging functions) is not included, as this is added by the formatter in the handler.

        .. versionadded:: 3.7
        """
        return [r.getMessage() for r in self.records]

    def clear(self):
        """Reset the list of log records and the captured log text."""
        self.handler.reset()

    def set_level(self, level, logger=None):
        """Sets the level for capturing of logs. The level will be restored to its previous value at the end of
        the test.

        :param int level: the logger to level.
        :param str logger: the logger to update the level. If not given, the root logger level is updated.

        .. versionchanged:: 3.4
            The levels of the loggers changed by this function will be restored to their initial values at the
            end of the test.
        """
        logger_name = logger
        logger = logging.getLogger(logger_name)
        # save the original log-level to restore it during teardown
        self._initial_log_levels.setdefault(logger_name, logger.level)
        logger.setLevel(level)

    @contextmanager
    def at_level(self, level, logger=None):
        """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
        level is restored to its original value.

        :param int level: the logger to level.
        :param str logger: the logger to update the level. If not given, the root logger level is updated.
        """
        logger = logging.getLogger(logger)
        orig_level = logger.level
        logger.setLevel(level)
        try:
            yield
        finally:
            logger.setLevel(orig_level)
