# SPDX-License-Identifier: MIT OR Apache-2.0
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the MIT License.  See the LICENSE file in the root of this
# repository for complete details.

import pickle
import sys

from io import StringIO
from unittest import mock

import pytest

import structlog

from structlog import dev


class TestPad:
    def test_normal(self):
        """
        If chars are missing, adequate number of " " are added.
        """
        assert 100 == len(dev._pad("test", 100))

    def test_negative(self):
        """
        If string is already too long, don't do anything.
        """
        assert len("test") == len(dev._pad("test", 2))


@pytest.fixture(name="cr", scope="session")
def _cr():
    return dev.ConsoleRenderer(
        colors=dev._has_colors, exception_formatter=dev.plain_traceback
    )


@pytest.fixture(name="styles", scope="session")
def _styles(cr):
    return cr._styles


@pytest.fixture(name="padded", scope="session")
def _padded(styles):
    return styles.bright + dev._pad("test", dev._EVENT_WIDTH) + styles.reset


class TestConsoleRenderer:
    @pytest.mark.skipif(dev.colorama, reason="Colorama must be missing.")
    @pytest.mark.skipif(
        not dev._IS_WINDOWS, reason="Must be running on Windows."
    )
    def test_missing_colorama(self):
        """
        ConsoleRenderer(colors=True) raises SystemError on initialization if
        Colorama is missing and _IS_WINDOWS is True.
        """
        with pytest.raises(SystemError) as e:
            dev.ConsoleRenderer(colors=True)

        assert (
            "ConsoleRenderer with `colors=True` requires the Colorama package "
            "installed."
        ) in e.value.args[0]

    def test_plain(self, cr, padded):
        """
        Works with a plain event_dict with only the event.
        """
        rv = cr(None, None, {"event": "test"})

        assert padded == rv

    def test_timestamp(self, cr, styles, padded):
        """
        Timestamps get prepended.
        """
        rv = cr(None, None, {"event": "test", "timestamp": 42})

        assert (styles.timestamp + "42" + styles.reset + " " + padded) == rv

    def test_event_stringified(self, cr, padded):
        """
        Event is cast to string.
        """
        not_a_string = Exception("test")

        rv = cr(None, None, {"event": not_a_string})

        assert padded == rv

    def test_event_renamed(self):
        """
        The main event key can be renamed.
        """
        cr = dev.ConsoleRenderer(colors=False, event_key="msg")

        assert "new event name                 event='something custom'" == cr(
            None, None, {"msg": "new event name", "event": "something custom"}
        )

    def test_timestamp_renamed(self):
        """
        The timestamp key can be renamed.
        """
        cr = dev.ConsoleRenderer(colors=False, timestamp_key="ts")

        assert (
            "2023-09-07 le event"
            == cr(
                None,
                None,
                {"ts": "2023-09-07", "event": "le event"},
            ).rstrip()
        )

    def test_event_key_property(self):
        """
        The event_key property can be set and retrieved.
        """
        cr = dev.ConsoleRenderer(colors=False)

        assert cr.event_key == "event"

        cr.event_key = "msg"

        assert cr.event_key == "msg"
        assert "new event name                 event='something custom'" == cr(
            None,
            None,
            {"msg": "new event name", "event": "something custom"},
        )

    def test_timestamp_key_property(self):
        """
        The timestamp_key property can be set and retrieved.
        """
        cr = dev.ConsoleRenderer(colors=False)

        assert cr.timestamp_key == "timestamp"

        cr.timestamp_key = "ts"

        assert cr.timestamp_key == "ts"
        assert (
            "2023-09-07 le event"
            == cr(
                None,
                None,
                {"ts": "2023-09-07", "event": "le event"},
            ).rstrip()
        )

    def test_level(self, cr, styles, padded):
        """
        Levels are rendered aligned, in square brackets, and color-coded.
        """
        rv = cr(
            None, None, {"event": "test", "level": "critical", "foo": "bar"}
        )

        assert (
            "["
            + dev.RED
            + styles.bright
            + dev._pad("critical", cr._longest_level)
            + styles.reset
            + "] "
            + padded
            + " "
            + styles.kv_key
            + "foo"
            + styles.reset
            + "="
            + styles.kv_value
            + "bar"
            + styles.reset
        ) == rv

    def test_init_accepts_overriding_levels(self, styles, padded):
        """
        Stdlib levels are rendered aligned, in brackets, and color coded.
        """
        my_styles = dev.ConsoleRenderer.get_default_level_styles(
            colors=dev._has_colors
        )
        my_styles["MY_OH_MY"] = my_styles["critical"]
        cr = dev.ConsoleRenderer(
            colors=dev._has_colors, level_styles=my_styles
        )

        # this would blow up if the level_styles override failed
        rv = cr(
            None, None, {"event": "test", "level": "MY_OH_MY", "foo": "bar"}
        )

        assert (
            "["
            + dev.RED
            + styles.bright
            + dev._pad("MY_OH_MY", cr._longest_level)
            + styles.reset
            + "] "
            + padded
            + " "
            + styles.kv_key
            + "foo"
            + styles.reset
            + "="
            + styles.kv_value
            + "bar"
            + styles.reset
        ) == rv

    def test_returns_colorful_styles_when_colors_true(self):
        """
        When colors=True, returns _colorful_styles instance.
        """
        styles = dev.ConsoleRenderer.get_default_column_styles(colors=True)

        assert styles is dev._colorful_styles

    def test_returns_plain_styles_when_colors_false(self):
        """
        When colors=False, returns _plain_styles instance.
        """
        styles = dev.ConsoleRenderer.get_default_column_styles(colors=False)

        assert styles is dev._plain_styles
        assert styles.reset == ""
        assert styles.bright == ""
        assert styles.level_critical == ""
        assert styles.kv_key == ""
        assert styles.kv_value == ""

    @pytest.mark.skipif(
        not dev._IS_WINDOWS or dev.colorama is not None,
        reason="Only relevant on Windows without colorama",
    )
    def test_raises_system_error_on_windows_without_colorama(self):
        """
        On Windows without colorama, raises SystemError when colors=True.
        """
        with pytest.raises(SystemError, match="requires the colorama package"):
            dev.ConsoleRenderer.get_default_column_styles(colors=True)

    @pytest.mark.skipif(
        not dev._IS_WINDOWS or dev.colorama is None,
        reason="Only relevant on Windows with colorama",
    )
    def test_initializes_colorama_on_windows_with_force_colors(self):
        """
        On Windows with colorama, force_colors=True reinitializes colorama.
        """
        with mock.patch.object(
            dev.colorama, "init"
        ) as mock_init, mock.patch.object(
            dev.colorama, "deinit"
        ) as mock_deinit:
            styles = dev.ConsoleRenderer.get_default_column_styles(
                colors=True, force_colors=True
            )

            assert styles is dev._colorful_styles
            mock_deinit.assert_called_once()
            mock_init.assert_called_once_with(strip=False)

    def test_logger_name(self, cr, styles, padded):
        """
        Logger names are appended after the event.
        """
        rv = cr(None, None, {"event": "test", "logger": "some_module"})

        assert (
            padded
            + " ["
            + styles.reset
            + styles.bright
            + dev.BLUE
            + "some_module"
            + styles.reset
            + "]"
            + styles.reset
        ) == rv

    def test_logger_name_name(self, cr, padded, styles):
        """
        It's possible to set the logger name using a "logger_name" key.
        """
        assert (
            padded
            + " ["
            + styles.reset
            + styles.bright
            + dev.BLUE
            + "yolo"
            + styles.reset
            + "]"
            + styles.reset
        ) == cr(None, None, {"event": "test", "logger_name": "yolo"})

    def test_key_values(self, cr, styles, padded):
        """
        Key-value pairs go sorted alphabetically to the end.
        """
        rv = cr(None, None, {"event": "test", "key": "value", "foo": "bar"})

        assert (
            padded
            + " "
            + styles.kv_key
            + "foo"
            + styles.reset
            + "="
            + styles.kv_value
            + "bar"
            + styles.reset
            + " "
            + styles.kv_key
            + "key"
            + styles.reset
            + "="
            + styles.kv_value
            + "value"
            + styles.reset
        ) == rv

    def test_key_values_unsorted(self, styles, padded):
        """
        Key-value pairs go in original order to the end.
        """
        cr = dev.ConsoleRenderer(sort_keys=False)

        rv = cr(
            None,
            None,
            {"event": "test", "key": "value", "foo": "bar"},
        )

        assert (
            padded
            + " "
            + styles.kv_key
            + "key"
            + styles.reset
            + "="
            + styles.kv_value
            + "value"
            + styles.reset
            + " "
            + styles.kv_key
            + "foo"
            + styles.reset
            + "="
            + styles.kv_value
            + "bar"
            + styles.reset
        ) == rv

    @pytest.mark.parametrize("wrap", [True, False])
    def test_exception_rendered(
        self, cr, recwarn, wrap, styles, padded, monkeypatch
    ):
        """
        Exceptions are rendered after a new line if they are already rendered
        in the event dict.

        A warning is emitted if exception printing is "customized".
        """
        exc = "Traceback:\nFake traceback...\nFakeError: yolo"

        # Wrap the formatter to provoke the warning.
        if wrap:
            monkeypatch.setattr(
                cr,
                "_exception_formatter",
                lambda s, ei: dev.plain_traceback(s, ei),
            )

        rv = cr(None, None, {"event": "test", "exception": exc})

        assert (f"{padded}\n" + exc) == rv

        if wrap:
            (w,) = recwarn.list
            assert (
                "Remove `format_exc_info` from your processor chain "
                "if you want pretty exceptions.",
            ) == w.message.args

    def test_stack_info(self, cr, styles, padded):
        """
        Stack traces are rendered after a new line.
        """
        stack = "fake stack"
        rv = cr(None, None, {"event": "test", "stack": stack})

        assert (f"{padded}\n" + stack) == rv

    def test_exc_info_tuple(self, cr, styles, padded):
        """
        If exc_info is a tuple, it is used.
        """

        try:
            0 / 0
        except ZeroDivisionError:
            ei = sys.exc_info()

        rv = cr(None, None, {"event": "test", "exc_info": ei})

        exc = dev._format_exception(ei)

        assert (f"{padded}\n" + exc) == rv

    def test_exc_info_bool(self, cr, styles, padded):
        """
        If exc_info is True, it is obtained using sys.exc_info().
        """

        try:
            0 / 0
        except ZeroDivisionError:
            ei = sys.exc_info()
            rv = cr(None, None, {"event": "test", "exc_info": True})

        exc = dev._format_exception(ei)

        assert (f"{padded}\n" + exc) == rv

    def test_exc_info_exception(self, cr, styles, padded):
        """
        If exc_info is an exception, it is used by converting to a tuple.
        """

        try:
            0 / 0
        except ZeroDivisionError as e:
            ei = e

        rv = cr(None, None, {"event": "test", "exc_info": ei})

        exc = dev._format_exception((ei.__class__, ei, ei.__traceback__))

        assert (f"{padded}\n" + exc) == rv

    def test_pad_event_to_param(self, styles):
        """
        `pad_event_to` parameter works.
        """
        rv = dev.ConsoleRenderer(42, dev._has_colors)(
            None, None, {"event": "test", "foo": "bar"}
        )

        assert (
            styles.bright
            + dev._pad("test", 42)
            + styles.reset
            + " "
            + styles.kv_key
            + "foo"
            + styles.reset
            + "="
            + styles.kv_value
            + "bar"
            + styles.reset
        ) == rv

    @pytest.mark.parametrize("explicit_ei", ["tuple", "exception", False])
    def test_everything(self, cr, styles, padded, explicit_ei):
        """
        Put all cases together.
        """
        if explicit_ei:
            try:
                0 / 0
            except ZeroDivisionError as e:
                if explicit_ei == "tuple":
                    ei = sys.exc_info()
                elif explicit_ei == "exception":
                    ei = e
                else:
                    raise ValueError from None
        else:
            ei = True

        stack = "fake stack trace"
        ed = {
            "event": "test",
            "exc_info": ei,
            "key": "value",
            "foo": "bar",
            "timestamp": "13:13",
            "logger": "some_module",
            "level": "error",
            "stack": stack,
        }

        if explicit_ei:
            rv = cr(None, None, ed)
        else:
            try:
                0 / 0
            except ZeroDivisionError:
                rv = cr(None, None, ed)
                ei = sys.exc_info()

        if isinstance(ei, BaseException):
            ei = (ei.__class__, ei, ei.__traceback__)

        exc = dev._format_exception(ei)

        assert (
            styles.timestamp
            + "13:13"
            + styles.reset
            + " ["
            + styles.level_error
            + styles.bright
            + dev._pad("error", cr._longest_level)
            + styles.reset
            + "] "
            + padded
            + " ["
            + styles.reset
            + styles.bright
            + dev.BLUE
            + "some_module"
            + styles.reset
            + "]"
            + styles.reset
            + " "
            + styles.kv_key
            + "foo"
            + styles.reset
            + "="
            + styles.kv_value
            + "bar"
            + styles.reset
            + " "
            + styles.kv_key
            + "key"
            + styles.reset
            + "="
            + styles.kv_value
            + "value"
            + styles.reset
            + "\n"
            + stack
            + "\n\n"
            + "=" * 79
            + "\n"
            + "\n"
            + exc
        ) == rv

    def test_colorama_colors_false(self):
        """
        If colors is False, don't use colors or styles ever.
        """
        plain_cr = dev.ConsoleRenderer(colors=False)

        rv = plain_cr(
            None, None, {"event": "event", "level": "info", "foo": "bar"}
        )

        assert dev._plain_styles is plain_cr._styles
        assert "[info     ] event                          foo=bar" == rv

    def test_colorama_force_colors(self, styles, padded):
        """
        If force_colors is True, use colors even if the destination is non-tty.
        """
        cr = dev.ConsoleRenderer(
            colors=dev._has_colors, force_colors=dev._has_colors
        )

        rv = cr(
            None, None, {"event": "test", "level": "critical", "foo": "bar"}
        )

        assert (
            "["
            + dev.RED
            + styles.bright
            + dev._pad("critical", cr._longest_level)
            + styles.reset
            + "] "
            + padded
            + " "
            + styles.kv_key
            + "foo"
            + styles.reset
            + "="
            + styles.kv_value
            + "bar"
            + styles.reset
        ) == rv

        assert not dev._has_colors or dev._colorful_styles is cr._styles

    @pytest.mark.parametrize("rns", [True, False])
    def test_repr_native_str(self, rns):
        """
        repr_native_str=False doesn't repr on native strings.  "event" is
        never repr'ed.
        """
        rv = dev.ConsoleRenderer(colors=False, repr_native_str=rns)(
            None, None, {"event": "哈", "key": 42, "key2": "哈"}
        )

        cnt = rv.count("哈")

        assert 2 == cnt

    @pytest.mark.parametrize("repr_native_str", [True, False])
    @pytest.mark.parametrize("force_colors", [True, False])
    @pytest.mark.parametrize("proto", range(pickle.HIGHEST_PROTOCOL + 1))
    def test_pickle(self, repr_native_str, force_colors, proto):
        """
        ConsoleRenderer can be pickled and unpickled.
        """
        r = dev.ConsoleRenderer(
            repr_native_str=repr_native_str, force_colors=force_colors
        )

        assert r(None, None, {"event": "foo"}) == pickle.loads(
            pickle.dumps(r, proto)
        )(None, None, {"event": "foo"})

    def test_no_exception(self):
        """
        If there is no exception, don't blow up.
        """
        r = dev.ConsoleRenderer(colors=False)

        assert (
            "hi" == r(None, None, {"event": "hi", "exc_info": None}).rstrip()
        )

    def test_columns_warns_about_meaningless_arguments(self, recwarn):
        """
        If columns is set, a warning is emitted for all ignored arguments.
        """
        dev.ConsoleRenderer(
            columns=[dev.Column("", lambda k, v: "")],
            pad_event_to=42,
            colors=not dev._has_colors,
            force_colors=True,
            repr_native_str=True,
            level_styles=dev._plain_styles,
            event_key="not event",
            timestamp_key="not timestamp",
        )

        assert {
            f"The `{arg}` argument is ignored when passing `columns`."
            for arg in (
                "pad_event_to",
                "colors",
                "force_colors",
                "repr_native_str",
                "level_styles",
                "event_key",
                "timestamp_key",
            )
        } == {str(w.message) for w in recwarn.list}

    def test_detects_default_column(self):
        """
        The default renderer is detected and removed from the columns list.
        """
        fake_formatter = object()
        llcf = dev.Column("log_level", dev.LogLevelColumnFormatter(None, ""))

        cr = dev.ConsoleRenderer(
            columns=[dev.Column("", fake_formatter), llcf]
        )

        assert fake_formatter is cr._default_column_formatter
        assert [llcf] == cr._columns

    def test_enforces_presence_of_exactly_one_default_formatter(self):
        """
        If there is no, or more than one, default formatter, raise ValueError.
        """
        with pytest.raises(
            ValueError,
            match="Must pass a default column formatter",
        ):
            dev.ConsoleRenderer(columns=[])

        with pytest.raises(
            ValueError,
            match="Only one default column formatter allowed",
        ):
            dev.ConsoleRenderer(
                columns=[
                    dev.Column("", lambda k, v: ""),
                    dev.Column("", lambda k, v: ""),
                ]
            )

    def test_does_not_modify_styles(self):
        """
        Instantiating ConsoleRenderer should not modify the styles passed in.

        Ref #643
        """
        styles = {"info": "something"}
        copy = styles.copy()

        dev.ConsoleRenderer(level_styles=styles)

        assert copy == styles

    def test_exception_formatter_property(self, cr):
        """
        The exception formatter can be set and retrieved without
        re-instantiating ConsoleRenderer.
        """
        sentinel = object()

        cr.exception_formatter = sentinel

        assert sentinel is cr.exception_formatter
        assert sentinel is cr._exception_formatter

    def test_sort_keys_property(self, cr):
        """
        The sort_keys setting can be set and retrieved without re-instantiating
        ConsoleRenderer.
        """
        assert cr.sort_keys is True
        assert cr._sort_keys is True

        cr.sort_keys = False

        assert cr.sort_keys is False
        assert cr._sort_keys is False

        cr.sort_keys = True

        assert cr.sort_keys is True
        assert cr._sort_keys is True

    def test_columns_property(self, cr):
        """
        The columns property can be set and retrieved without re-instantiating
        ConsoleRenderer.

        The property also fakes the default column formatter.
        """
        cols = [dev.Column("", lambda k, v: "")]

        cr.columns = cols

        assert cols == cr.columns
        assert [] == cr._columns
        assert cols[0].formatter == cr._default_column_formatter


class TestSetExcInfo:
    def test_wrong_name(self):
        """
        Do nothing if name is not exception.
        """
        assert {} == dev.set_exc_info(None, "foo", {})

    @pytest.mark.parametrize("ei", [False, None, ()])
    def test_already_set(self, ei):
        """
        Do nothing if exc_info is already set.
        """
        assert {"exc_info": ei} == dev.set_exc_info(
            None, "foo", {"exc_info": ei}
        )

    def test_set_it(self):
        """
        Set exc_info to True if its not set and if the method name is
        exception.
        """
        assert {"exc_info": True} == dev.set_exc_info(None, "exception", {})


@pytest.mark.skipif(dev.rich is not None, reason="Needs missing Rich.")
def test_rich_traceback_formatter_no_rich():
    """
    Trying to use RichTracebackFormatter without Rich should raise an helpful
    error.
    """
    with pytest.raises(
        ModuleNotFoundError,
        match="RichTracebackFormatter requires Rich to be installed",
    ):
        dev.rich_traceback(StringIO(), sys.exc_info())


@pytest.mark.skipif(dev.rich is None, reason="Needs Rich.")
class TestRichTracebackFormatter:
    def test_default(self):
        """
        If Rich is present, it's the default.
        """
        assert dev.default_exception_formatter is dev.rich_traceback

    def test_does_not_blow_up(self, sio):
        """
        We trust Rich to do the right thing, so we just exercise the function
        and check the first new line that we add manually is present.
        """
        try:
            0 / 0
        except ZeroDivisionError:
            dev.rich_traceback(sio, sys.exc_info())

        assert sio.getvalue().startswith("\n")

    def test_width_minus_one(self, sio):
        """
        If width is -1, it raises a DeprecationWarning and is replaced by None to let `rich` handle it.
        """
        rtf = dev.RichTracebackFormatter(width=-1)

        with pytest.deprecated_call():
            try:
                0 / 0
            except ZeroDivisionError:
                rtf(sio, sys.exc_info())

        assert rtf.width is None

    @pytest.mark.parametrize("code_width_support", [True, False])
    def test_code_width_support(self, sio, code_width_support):
        """
        If rich does not support code_width, it should not fail
        """
        from rich.traceback import Trace

        tb = mock.Mock(
            spec=[
                attr
                for attr in dir(dev.Traceback(Trace([])))
                if (code_width_support or attr != "code_width")
            ]
        )
        tb.__rich_console__.return_value = "for Python 3.8 compatibility"

        with mock.patch.object(
            dev.Traceback, "from_exception", return_value=tb
        ) as factory:
            try:
                0 / 0
            except ZeroDivisionError:
                dev.rich_traceback(sio, sys.exc_info())

        assert "code_width" not in factory.call_args.kwargs

        if code_width_support:
            assert tb.code_width == 88


@pytest.mark.skipif(
    dev.better_exceptions is None, reason="Needs better-exceptions."
)
class TestBetterTraceback:
    def test_default(self):
        """
        If better-exceptions is present and Rich is NOT present, it's the
        default.
        """
        assert (
            dev.rich is not None
            or dev.default_exception_formatter is dev.better_traceback
        )

    def test_does_not_blow_up(self):
        """
        We trust better-exceptions to do the right thing, so we just exercise
        the function.
        """
        sio = StringIO()
        try:
            0 / 0
        except ZeroDivisionError:
            dev.better_traceback(sio, sys.exc_info())

        assert sio.getvalue().startswith("\n")


class TestLogLevelColumnFormatter:
    def test_no_style(self):
        """
        No level_styles means no control characters and no padding.
        """
        assert "[critical]" == dev.LogLevelColumnFormatter(None, "foo")(
            "", "critical"
        )


class TestGetActiveConsoleRenderer:
    def test_ok(self):
        """
        If there's an active ConsoleRenderer, it's returned.
        """
        assert (
            structlog.get_config()["processors"][-1]
            is dev.ConsoleRenderer.get_active()
        )

    def test_no_console_renderer(self):
        """
        If no ConsoleRenderer is configured, raise
        NoConsoleRendererConfiguredError.
        """
        structlog.configure(processors=[])

        with pytest.raises(
            structlog.exceptions.NoConsoleRendererConfiguredError
        ):
            dev.ConsoleRenderer.get_active()

    def test_multiple_console_renderers(self):
        """
        If multiple ConsoleRenderers are configured, raise
        MultipleConsoleRenderersConfiguredError because it's most likely a bug.
        """
        structlog.configure(
            processors=[dev.ConsoleRenderer(), dev.ConsoleRenderer()]
        )

        with pytest.raises(
            structlog.exceptions.MultipleConsoleRenderersConfiguredError
        ):
            dev.ConsoleRenderer.get_active()


class TestConsoleRendererProperties:
    def test_level_styles_roundtrip(self):
        """
        The level_styles property can be set and retrieved.
        """
        cr = dev.ConsoleRenderer(colors=True)
        custom = {"info": "X", "error": "Y"}

        cr.level_styles = custom

        assert cr.level_styles is custom
        assert cr._level_styles is custom

    @pytest.mark.parametrize("colors", [True, False])
    def test_set_level_styles_none_resets_to_defaults(self, colors):
        """
        Setting level_styles to None resets to defaults.
        """
        cr = dev.ConsoleRenderer(colors=colors)
        cr.level_styles = {"info": "X"}

        cr.level_styles = None

        assert (
            dev.ConsoleRenderer.get_default_level_styles(colors=colors)
            == cr._level_styles
        )

    def test_roundtrip_pad_level(self):
        """
        The pad_level property can be set and retrieved.
        """
        cr = dev.ConsoleRenderer(pad_level=True)

        assert cr.pad_level is True
        assert cr._pad_level is True

        cr.pad_level = False

        assert cr.pad_level is False
        assert cr._pad_level is False

        cr.pad_level = True

        assert cr.pad_level is True
        assert cr._pad_level is True

    def test_roundtrip_pad_event_to(self):
        """
        The pad_event_to property can be set and retrieved.
        """
        cr = dev.ConsoleRenderer()

        assert cr.pad_event_to == dev._EVENT_WIDTH
        assert cr._pad_event_to == dev._EVENT_WIDTH

        cr.pad_event_to = 50

        assert cr.pad_event_to == 50
        assert cr._pad_event_to == 50

        cr.pad_event_to = 20

        assert cr.pad_event_to == 20
        assert cr._pad_event_to == 20

    def test_repr_native_str_property(self, cr):
        """
        The repr_native_str property can be set and retrieved, and affects formatting.
        """
        cr = dev.ConsoleRenderer(colors=False, repr_native_str=False)

        assert False is cr.repr_native_str
        assert "event                          key=plain" == cr(
            None, None, {"event": "event", "key": "plain"}
        )

        cr.repr_native_str = True

        assert "event                          key='plain'" == cr(
            None, None, {"event": "event", "key": "plain"}
        )

    def test_pad_event_deprecation_warning(self, recwarn):
        """
        Using pad_event argument raises a deprecation warning.
        """
        dev.ConsoleRenderer(pad_event=42)

        (w,) = recwarn.list
        assert (
            "The `pad_event` argument is deprecated. Use `pad_event_to` instead."
        ) == w.message.args[0]
        assert w.category is DeprecationWarning

    def test_pad_event_to_param_raises_value_error(self):
        """
        Using pad_event_to and pad_event raises a ValueError.
        """
        with pytest.raises(ValueError):  # noqa: PT011
            dev.ConsoleRenderer(pad_event_to=42, pad_event=42)

    def test_same_value_resets_level_styles(self, cr):
        """
        Setting colors to the same value resets the level styles to the
        defaults.
        """
        val = cr.colors
        cr._level_styles = {"info": "X", "error": "Y"}

        cr.colors = cr.colors

        assert val is cr.colors
        assert (
            cr._level_styles
            == dev.ConsoleRenderer.get_default_level_styles(colors=val)
        )

    @pytest.mark.skipif(
        dev._IS_WINDOWS and dev.colorama is None,
        reason="Toggling colors=True requires colorama on Windows",
    )
    def test_toggle_colors_updates_styles_and_levels(self):
        """
        Toggling colors updates the styles and level styles to colorful styles.
        """
        cr = dev.ConsoleRenderer(colors=False)

        assert cr.colors is False
        assert cr._colors is False
        assert cr._styles is dev._plain_styles
        assert (
            cr._level_styles
            == dev.ConsoleRenderer.get_default_level_styles(colors=False)
        )

        cr.colors = True

        assert cr.colors is True
        assert cr._colors is True
        assert cr._styles is dev._colorful_styles
        assert (
            cr._level_styles
            == dev.ConsoleRenderer.get_default_level_styles(colors=True)
        )

    @pytest.mark.skipif(
        dev._IS_WINDOWS and dev.colorama is None,
        reason="Toggling colors=True requires colorama on Windows",
    )
    def test_toggle_colors_resets_custom_level_styles(self):
        """
        Toggling colors resets the level styles to the defaults for the new
        color setting.
        """
        custom = {"info": "X", "error": "Y"}
        cr = dev.ConsoleRenderer(colors=False, level_styles=custom)

        assert custom == cr._level_styles

        cr.colors = True
        assert (
            dev.ConsoleRenderer.get_default_level_styles(colors=True)
            == cr._level_styles
        )

        # And switching back follows defaults for the new setting again
        cr.colors = False
        assert (
            dev.ConsoleRenderer.get_default_level_styles(colors=False)
            == cr._level_styles
        )

    def test_same_force_colors_value_resets_level_styles(self, cr):
        """
        Setting force_colors to the same value resets the level styles to the
        defaults.
        """
        val = cr.force_colors

        cr._level_styles = {"info": "X", "error": "Y"}

        cr.force_colors = cr.force_colors

        assert val is cr.force_colors
        assert (
            cr._level_styles
            == dev.ConsoleRenderer.get_default_level_styles(colors=cr.colors)
        )

    def test_toggle_force_colors_updates_styles_and_levels(self):
        """
        Setting force_colors to the same value resets the level styles to the
        defaults.
        """
        cr = dev.ConsoleRenderer(colors=True, force_colors=False)

        assert cr.force_colors is False
        assert cr._force_colors is False
        assert cr._styles is dev.ConsoleRenderer.get_default_column_styles(
            colors=True, force_colors=False
        )
        assert (
            cr._level_styles
            == dev.ConsoleRenderer.get_default_level_styles(colors=True)
        )

        cr.force_colors = True

        assert cr.force_colors is True
        assert cr._force_colors is True
        assert cr._styles is dev.ConsoleRenderer.get_default_column_styles(
            colors=True, force_colors=True
        )
        assert (
            cr._level_styles
            == dev.ConsoleRenderer.get_default_level_styles(colors=True)
        )

    def test_toggle_force_colors_resets_custom_level_styles(self):
        """
        Toggling force_colors resets the level styles to the defaults for the
        new force_colors setting.
        """
        custom = {"info": "X", "error": "Y"}
        cr = dev.ConsoleRenderer(colors=True, level_styles=custom)

        assert custom == cr._level_styles

        cr.force_colors = True
        assert (
            dev.ConsoleRenderer.get_default_level_styles(colors=True)
            == cr._level_styles
        )

        cr.force_colors = False
        assert (
            dev.ConsoleRenderer.get_default_level_styles(colors=True)
            == cr._level_styles
        )
