"""
Logger tests
============
"""

import logging
import os
import pathlib
import sys
import time

import pytest

# Used to determine which tests must be skipped
LOG_MODE = os.environ.get("KIVY_LOG_MODE", "KIVY")


@pytest.fixture
def file_handler():
    # restores handler to original state
    from kivy.config import Config

    log_dir = Config.get("kivy", "log_dir")
    log_maxfiles = Config.get("kivy", "log_maxfiles")

    try:
        yield None
    finally:
        Config.set("kivy", "log_dir", log_dir)
        Config.set("kivy", "log_maxfiles", log_maxfiles)


@pytest.mark.parametrize("n", [0, 1, 5])
def test_purge_logs(tmp_path, file_handler, n):
    from kivy.config import Config
    from kivy.logger import FileHandler

    Config.set("kivy", "log_dir", str(tmp_path))
    Config.set("kivy", "log_maxfiles", n)

    # create the default file first so it gets deleted so names match
    handler = FileHandler()
    handler._configure()
    open_file = pathlib.Path(handler.filename).name
    # wait a little so the timestamps are different for different files
    time.sleep(0.05)

    names = [f"log_{i}.txt" for i in range(n + 2)]
    for name in names:
        p = tmp_path / name
        p.write_text("some data")
        time.sleep(0.05)

    handler.purge_logs()

    # files that should have remained after purge
    expected_names = list(reversed(names))[:n]
    files = {f.name for f in tmp_path.iterdir()}
    if open_file in files:
        # one of the remaining files is the current open log, remove it
        files.remove(open_file)
        if len(expected_names) == len(files) + 1:
            # the open log may or may not have been counted in the remaining
            # files, remove one from expected to match removed open file
            expected_names = expected_names[:-1]

    assert set(expected_names) == files


@pytest.mark.xfail  # Issue #7986 not yet fixed.
def test_logger_history_size():
    from kivy.logger import Logger, LoggerHistory

    for x in range(200):
        Logger.info(x)
    assert len(LoggerHistory.history) == 100, "Wrong size: %s" % len(
        LoggerHistory.history
    )


@pytest.mark.skipif(
    LOG_MODE != "KIVY",
    reason="Requires KIVY_LOG_MODE==KIVY to run.",
)
def test_trace_level():
    """Kivy logger defines a custom level of Trace."""
    from kivy.logger import Logger, LOG_LEVELS, LoggerHistory
    import logging

    Logger.setLevel(9)

    # Try different ways to trigger a trace:
    Logger.trace("test: This is trace message 1")
    logging.log(logging.TRACE, "test: This is trace message 2")
    Logger.log(LOG_LEVELS["trace"], "test: This is trace message 3")
    # Not supported:
    # logging.trace('test: This is trace message 4')

    last_log_records = LoggerHistory.history[:3]
    assert all(log_record.levelno == 9 for log_record in last_log_records), [
        log_record.levelno for log_record in last_log_records
    ]


@pytest.mark.skipif(
    LOG_MODE == "PYTHON",
    reason="Requires KIVY_LOG_MODE!=PYTHON to run.",
)
def test_trace_level_has_level_name():
    from kivy.logger import Logger, LoggerHistory

    Logger.setLevel(9)
    Logger.trace("test: This is trace message 1")
    assert LoggerHistory.history[0].levelname == "TRACE"


def test_logging_does_not_deep_copy():
    # If the Logger does a deep copy of an uncopyable
    # data structure, it will fail. See issues #7585 and #7528.

    import threading
    from kivy.logger import Logger

    class UncopyableDatastructure:
        def __init__(self, name):
            self._lock = threading.Lock()
            self._name = name

        def __str__(self):
            return "UncopyableDatastructure(name=%r)" % self._name

    s = UncopyableDatastructure("Uncopyable")
    Logger.error("The value of s is %s", s)


def configured_string_logging(unique_code, formatter=None):
    """
    Helper function provides logger configured to write to log_output.
    """
    from io import StringIO

    log_output = StringIO()

    handler = logging.StreamHandler(stream=log_output)
    if formatter:
        handler.setFormatter(formatter)

    logger = logging.getLogger("tests.%s" % unique_code)

    # Do not escalate to root/Kivy loggers.
    logger.setLevel(9)  # Catch everything
    logger.propagate = False
    assert not logger.hasHandlers(), "Must use unique code between tests."

    logger.addHandler(handler)

    return logger, log_output


def test_colonsplittinglogrecord_with_colon():
    from kivy.logger import ColonSplittingLogRecord

    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.DEBUG,
        pathname="test.py",
        lineno=1,
        msg="Part1: Part2: Part 3",
        args=("args",),
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    # Just making sure we know what it looks like before.
    assert (
        str(originallogrecord)
        == '<LogRecord: kivy.test, 10, test.py, 1, "Part1: Part2: Part 3">'
    )
    shimmedlogrecord = ColonSplittingLogRecord(originallogrecord)
    assert (
        str(shimmedlogrecord) == "<LogRecord: kivy.test, 10, test.py, 1, "
        '"[Part1       ] Part2: Part 3">'
    )


def test_colonsplittinglogrecord_without_colon():
    from kivy.logger import ColonSplittingLogRecord

    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.DEBUG,
        pathname="test.py",
        lineno=1,
        msg="Part1 Part2 Part 3",
        args=("args",),
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    shimmedlogrecord = ColonSplittingLogRecord(originallogrecord)
    # No colons means no change.
    assert str(originallogrecord) == str(shimmedlogrecord)

    # Try a non-string (Issue #7984)
    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.DEBUG,
        pathname="test.py",
        lineno=1,
        msg=1,
        args=None,
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    shimmedlogrecord = ColonSplittingLogRecord(originallogrecord)
    # No colons means no change.
    assert str(originallogrecord) == str(shimmedlogrecord)


def test_uncoloredlogrecord_without_markup():
    from kivy.logger import UncoloredLogRecord

    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.DEBUG,
        pathname="test.py",
        lineno=1,
        msg="Part1: Part2 Part 3",
        args=("args",),
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    shimmedlogrecord = UncoloredLogRecord(originallogrecord)
    # No markup means no change.
    assert str(originallogrecord) == str(shimmedlogrecord)

    # Try a non-string (Issue #7984)
    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.DEBUG,
        pathname="test.py",
        lineno=1,
        msg=1,
        args=None,
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    shimmedlogrecord = UncoloredLogRecord(originallogrecord)
    assert str(originallogrecord) == str(shimmedlogrecord)


def test_uncoloredlogrecord_with_markup():
    from kivy.logger import UncoloredLogRecord

    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.DEBUG,
        pathname="test.py",
        lineno=1,
        msg="Part1: $BOLDPart2$RESET Part 3",
        args=("args",),
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    shimmedlogrecord = UncoloredLogRecord(originallogrecord)
    # No markup means no change.
    assert (
        str(shimmedlogrecord)
        == '<LogRecord: kivy.test, 10, test.py, 1, "Part1: Part2 Part 3">'
    )


def test_coloredlogrecord_without_markup():
    from kivy.logger import ColoredLogRecord

    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.DEBUG,
        pathname="test.py",
        lineno=1,
        msg="Part1: Part2 Part 3",
        args=("args",),
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    shimmedlogrecord = ColoredLogRecord(originallogrecord)
    # The str() looks the same, because it doesn't include levelname.
    assert str(originallogrecord) == str(shimmedlogrecord)
    # But there is a change in the levelname
    assert originallogrecord.levelname != shimmedlogrecord.levelname
    assert shimmedlogrecord.levelname == "\x1b[1;36mDEBUG\x1b[0m"

    # Try a non-string (Issue #7984)
    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.DEBUG,
        pathname="test.py",
        lineno=1,
        msg=1,
        args=None,
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    shimmedlogrecord = ColoredLogRecord(originallogrecord)
    assert str(originallogrecord) == str(shimmedlogrecord)


def test_coloredlogrecord_with_markup():
    from kivy.logger import ColoredLogRecord

    originallogrecord = logging.LogRecord(
        name="kivy.test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Part1: $BOLDPart2$RESET Part 3",
        args=("args",),
        exc_info=None,
        func="test_colon_splitting",
        sinfo=None,
    )
    shimmedlogrecord = ColoredLogRecord(originallogrecord)
    # Bolding has been added to message.
    assert (
        str(shimmedlogrecord) == "<LogRecord: kivy.test, 20, test.py, 1, "
        '"Part1: \x1b[1mPart2\x1b[0m Part 3">'
    )
    # And there is a change in the levelname
    assert originallogrecord.levelname != shimmedlogrecord.levelname
    assert shimmedlogrecord.levelname == "\x1b[1;32mINFO\x1b[0m"


def test_kivyformatter_colon_no_color():
    from kivy.logger import KivyFormatter

    formatter = KivyFormatter("[%(levelname)-7s] %(message)s", use_color=False)
    logger, log_output = configured_string_logging("1", formatter)
    logger.info("Fancy: $BOLDmess$RESETage")
    assert log_output.getvalue() == "[INFO   ] [Fancy       ] message\n"


def test_kivyformatter_colon_color():
    from kivy.logger import KivyFormatter

    formatter = KivyFormatter("[%(levelname)-18s] %(message)s", use_color=True)

    logger, log_output = configured_string_logging("2", formatter)
    logger.info("Fancy: $BOLDmess$RESETage")
    assert (
        log_output.getvalue()
        == "[\x1b[1;32mINFO\x1b[0m   ] [Fancy       ] \x1b[1mmess\x1b[0mage\n"
    )


@pytest.mark.logmodepython
@pytest.mark.logmodemixed
@pytest.mark.skipif(
    LOG_MODE == "KIVY",
    reason="Requires KIVY_LOG_MODE!=KIVY to run.",
)
def test_kivy_log_mode_marker_on():
    """
    This is a test of the pytest markers.
    This should only be invoked if the environment variable is appropriately set
    (before pytest is run).

    Also, tests that kivy.logger paid attention to the environment variable
    """
    assert logging.root.level != 0, "Root logger was modified"


@pytest.mark.skipif(
    LOG_MODE != "KIVY",
    reason="Requires KIVY_LOG_MODE==KIVY to run.",
)
def test_kivy_log_mode_marker_off():
    """
    This is a test of the pytest markers.
    This should only be invoked if the environment variable is properly set
    (before pytest is run).

    Also, tests that kivy.logger paid attention to the environment variable
    """
    assert logging.root.level == 0, "Root logger was not modified"


# These tests are an overly-dramatic simulation of a third-party library's
# logging infrastructure.
#
# A handler is attached to the root-logger, which takes action when a
# critical message is logged.


class NuclearReactorMonitoringHandler(logging.Handler):
    NUCLEAR_REACTOR_STATUS = "Nominal"

    def __init__(self):
        super().__init__(level=logging.CRITICAL)

    def emit(self, log_record):
        # The fact that this was called means a critical log record
        # was created.
        sys.stderr.write("Please proceed immediately to the nearest exit.\n")
        sys.stderr.flush()
        self.NUCLEAR_REACTOR_STATUS = "Evacuated"


def simulate_evacuation():
    logging.getLogger().addHandler(logging.StreamHandler())

    handler = NuclearReactorMonitoringHandler()
    logging.getLogger().addHandler(handler)

    nuclear_core_logger = logging.getLogger("powerstation.core")
    nuclear_core_logger.info("Core temperature nominal")
    assert handler.NUCLEAR_REACTOR_STATUS == "Nominal"
    nuclear_core_logger.critical("Radioactive gas leak")
    assert handler.NUCLEAR_REACTOR_STATUS == "Evacuated"


@pytest.mark.logmodepython
@pytest.mark.logmodemixed
def test_third_party_handlers_work():
    # This should work under any mode.
    simulate_evacuation()


def are_regular_logs_handled():
    from kivy.logger import LoggerHistory

    LoggerHistory.clear_history()
    logging.getLogger("test").info(1)
    return bool(LoggerHistory.history)


def are_kivy_logger_logs_handled():
    from kivy.logger import LoggerHistory, Logger

    LoggerHistory.clear_history()
    Logger.info(1)
    return bool(LoggerHistory.history)


def is_stderr_output_handled():
    from kivy.logger import LoggerHistory

    LoggerHistory.clear_history()
    sys.stderr.write("Test output to stderr\n")
    sys.stderr.flush()
    return bool(LoggerHistory.history)


@pytest.mark.skipif(
    LOG_MODE != "KIVY",
    reason="Requires KIVY_LOG_MODE==KIVY to run.",
)
def test_kivy_mode_handlers():
    assert are_regular_logs_handled()
    assert are_kivy_logger_logs_handled()
    # This line doesn't work because of pytest's handling of stderr.
    # Runs fine as stand-alone code.
    # assert is_stderr_output_handled()


@pytest.mark.logmodepython
@pytest.mark.skipif(
    LOG_MODE != "PYTHON",
    reason="Requires KIVY_LOG_MODE==PYTHON to run.",
)
def test_python_mode_handlers():
    assert not are_regular_logs_handled()
    assert not are_kivy_logger_logs_handled()
    assert not is_stderr_output_handled()


@pytest.mark.logmodemixed
@pytest.mark.skipif(
    LOG_MODE != "MIXED",
    reason="Requires KIVY_LOG_MODE==MIXED to run.",
)
def test_mixed_mode_handlers():
    assert not are_regular_logs_handled()
    assert are_kivy_logger_logs_handled()
    assert not is_stderr_output_handled()
