# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
Test cases for L{twisted.logger._filter}.
"""

from __future__ import annotations

from collections.abc import Iterable
from typing import cast

from zope.interface import implementer
from zope.interface.exceptions import BrokenMethodImplementation
from zope.interface.verify import verifyObject

from constantly import NamedConstant

from twisted.trial import unittest
from .._filter import (
    FilteringLogObserver,
    ILogFilterPredicate,
    LogLevelFilterPredicate,
    PredicateResult,
)
from .._interfaces import ILogObserver, LogEvent
from .._levels import InvalidLogLevelError, LogLevel
from .._observer import LogPublisher, bitbucketLogObserver


class FilteringLogObserverTests(unittest.TestCase):
    """
    Tests for L{FilteringLogObserver}.
    """

    def test_interface(self) -> None:
        """
        L{FilteringLogObserver} is an L{ILogObserver}.
        """
        observer = FilteringLogObserver(cast(ILogObserver, lambda e: None), ())
        try:
            verifyObject(ILogObserver, observer)
        except BrokenMethodImplementation as e:
            self.fail(e)

    def filterWith(
        self, filters: Iterable[str], other: bool = False
    ) -> list[int] | tuple[list[int], list[int]]:
        """
        Apply a set of pre-defined filters on a known set of events and return
        the filtered list of event numbers.

        The pre-defined events are four events with a C{count} attribute set to
        C{0}, C{1}, C{2}, and C{3}.

        @param filters: names of the filters to apply.
            Options are:
                - C{"twoMinus"} (count <=2),
                - C{"twoPlus"} (count >= 2),
                - C{"notTwo"} (count != 2),
                - C{"no"} (False).
        @param other: Whether to return a list of filtered events as well.

        @return: event numbers or 2-tuple of lists of event numbers.
        """
        events: list[LogEvent] = [
            dict(count=0),
            dict(count=1),
            dict(count=2),
            dict(count=3),
        ]

        class Filters:
            @staticmethod
            def twoMinus(event: LogEvent) -> NamedConstant:
                """
                count <= 2

                @param event: an event

                @return: L{PredicateResult.yes} if C{event["count"] <= 2},
                    otherwise L{PredicateResult.maybe}.
                """
                if event["count"] <= 2:
                    return PredicateResult.yes
                return PredicateResult.maybe

            @staticmethod
            def twoPlus(event: LogEvent) -> NamedConstant:
                """
                count >= 2

                @param event: an event

                @return: L{PredicateResult.yes} if C{event["count"] >= 2},
                    otherwise L{PredicateResult.maybe}.
                """
                if event["count"] >= 2:
                    return PredicateResult.yes
                return PredicateResult.maybe

            @staticmethod
            def notTwo(event: LogEvent) -> NamedConstant:
                """
                count != 2

                @param event: an event

                @return: L{PredicateResult.yes} if C{event["count"] != 2},
                    otherwise L{PredicateResult.maybe}.
                """
                if event["count"] == 2:
                    return PredicateResult.no
                return PredicateResult.maybe

            @staticmethod
            def no(event: LogEvent) -> NamedConstant:
                """
                No way, man.

                @param event: an event

                @return: L{PredicateResult.no}
                """
                return PredicateResult.no

            @staticmethod
            def bogus(event: LogEvent) -> NamedConstant:
                """
                Bogus result.

                @param event: an event

                @return: something other than a valid predicate result.
                """
                return None

        predicates = (getattr(Filters, f) for f in filters)
        eventsSeen: list[LogEvent] = []
        eventsNotSeen: list[LogEvent] = []
        trackingObserver = cast(ILogObserver, eventsSeen.append)

        if other:
            negativeObserver = cast(ILogObserver, eventsNotSeen.append)
        else:
            negativeObserver = bitbucketLogObserver

        filteringObserver = FilteringLogObserver(
            trackingObserver, predicates, negativeObserver
        )

        for e in events:
            filteringObserver(e)

        if other:
            return (
                [cast(int, e["count"]) for e in eventsSeen],
                [cast(int, e["count"]) for e in eventsNotSeen],
            )
        else:
            return [cast(int, e["count"]) for e in eventsSeen]

    def test_shouldLogEventNoFilters(self) -> None:
        """
        No filters: all events come through.
        """
        self.assertEqual(self.filterWith([]), [0, 1, 2, 3])

    def test_shouldLogEventNoFilter(self) -> None:
        """
        Filter with negative predicate result.
        """
        self.assertEqual(self.filterWith(["notTwo"]), [0, 1, 3])

    def test_shouldLogEventOtherObserver(self) -> None:
        """
        Filtered results get sent to the other observer, if passed.
        """
        self.assertEqual(self.filterWith(["notTwo"], True), ([0, 1, 3], [2]))

    def test_shouldLogEventYesFilter(self) -> None:
        """
        Filter with positive predicate result.
        """
        self.assertEqual(self.filterWith(["twoPlus"]), [0, 1, 2, 3])

    def test_shouldLogEventYesNoFilter(self) -> None:
        """
        Series of filters with positive and negative predicate results.
        """
        self.assertEqual(self.filterWith(["twoPlus", "no"]), [2, 3])

    def test_shouldLogEventYesYesNoFilter(self) -> None:
        """
        Series of filters with positive, positive and negative predicate
        results.
        """
        self.assertEqual(self.filterWith(["twoPlus", "twoMinus", "no"]), [0, 1, 2, 3])

    def test_shouldLogEventBadPredicateResult(self) -> None:
        """
        Filter with invalid predicate result.
        """
        self.assertRaises(TypeError, self.filterWith, ["bogus"])

    def test_call(self) -> None:
        """
        Test filtering results from each predicate type.
        """
        e: LogEvent = dict(obj=object())

        def callWithPredicateResult(result: NamedConstant) -> list[LogEvent]:
            seen: list[LogEvent] = []
            observer = FilteringLogObserver(
                cast(ILogObserver, lambda e: seen.append(e)),
                (cast(ILogFilterPredicate, lambda e: result),),
            )
            observer(e)
            return seen

        self.assertIn(e, callWithPredicateResult(PredicateResult.yes))
        self.assertIn(e, callWithPredicateResult(PredicateResult.maybe))
        self.assertNotIn(e, callWithPredicateResult(PredicateResult.no))

    def test_trace(self) -> None:
        """
        Tracing keeps track of forwarding through the filtering observer.
        """
        event: LogEvent = dict(log_trace=[])

        oYes = cast(ILogObserver, lambda e: None)
        oNo = cast(ILogObserver, lambda e: None)

        @implementer(ILogObserver)
        def testObserver(e: LogEvent) -> None:
            self.assertIs(e, event)
            self.assertEqual(
                event["log_trace"],
                [
                    (publisher, yesFilter),
                    (yesFilter, oYes),
                    (publisher, noFilter),
                    # ... noFilter doesn't call oNo
                    (publisher, oTest),
                ],
            )

        oTest = testObserver

        yesFilter = FilteringLogObserver(
            oYes, (cast(ILogFilterPredicate, lambda e: PredicateResult.yes),)
        )
        noFilter = FilteringLogObserver(
            oNo, (cast(ILogFilterPredicate, lambda e: PredicateResult.no),)
        )

        publisher = LogPublisher(yesFilter, noFilter, testObserver)
        publisher(event)


class LogLevelFilterPredicateTests(unittest.TestCase):
    """
    Tests for L{LogLevelFilterPredicate}.
    """

    def test_defaultLogLevel(self) -> None:
        """
        Default log level is used.
        """
        predicate = LogLevelFilterPredicate()

        # Test using both "" and None as default namespace, because None was the
        # documented default value in the past.

        for default in ("", cast(str, None)):
            self.assertEqual(
                predicate.logLevelForNamespace(default), predicate.defaultLogLevel
            )
            self.assertEqual(
                predicate.logLevelForNamespace("rocker.cool.namespace"),
                predicate.defaultLogLevel,
            )

    def test_setLogLevel(self) -> None:
        """
        Setting and retrieving log levels.
        """
        predicate = LogLevelFilterPredicate()

        # Test using both "" and None as default namespace, because None was the
        # documented default value in the past.

        for default in ("", cast(str, None)):
            predicate.setLogLevelForNamespace(default, LogLevel.error)
            predicate.setLogLevelForNamespace("twext.web2", LogLevel.debug)
            predicate.setLogLevelForNamespace("twext.web2.dav", LogLevel.warn)

            self.assertEqual(predicate.logLevelForNamespace(""), LogLevel.error)
            self.assertEqual(
                predicate.logLevelForNamespace(cast(str, None)), LogLevel.error
            )
            self.assertEqual(predicate.logLevelForNamespace("twisted"), LogLevel.error)
            self.assertEqual(
                predicate.logLevelForNamespace("twext.web2"), LogLevel.debug
            )
            self.assertEqual(
                predicate.logLevelForNamespace("twext.web2.dav"), LogLevel.warn
            )
            self.assertEqual(
                predicate.logLevelForNamespace("twext.web2.dav.test"), LogLevel.warn
            )
            self.assertEqual(
                predicate.logLevelForNamespace("twext.web2.dav.test1.test2"),
                LogLevel.warn,
            )

    def test_setInvalidLogLevel(self) -> None:
        """
        Can't pass invalid log levels to C{setLogLevelForNamespace()}.
        """
        predicate = LogLevelFilterPredicate()

        self.assertRaises(
            InvalidLogLevelError,
            predicate.setLogLevelForNamespace,
            "twext.web2",
            object(),
        )

        # Level must be a constant, not the name of a constant
        self.assertRaises(
            InvalidLogLevelError,
            predicate.setLogLevelForNamespace,
            "twext.web2",
            "debug",
        )

    def test_clearLogLevels(self) -> None:
        """
        Clearing log levels.
        """
        predicate = LogLevelFilterPredicate()

        predicate.setLogLevelForNamespace("twext.web2", LogLevel.debug)
        predicate.setLogLevelForNamespace("twext.web2.dav", LogLevel.error)

        predicate.clearLogLevels()

        self.assertEqual(
            predicate.logLevelForNamespace("twisted"), predicate.defaultLogLevel
        )
        self.assertEqual(
            predicate.logLevelForNamespace("twext.web2"), predicate.defaultLogLevel
        )
        self.assertEqual(
            predicate.logLevelForNamespace("twext.web2.dav"), predicate.defaultLogLevel
        )
        self.assertEqual(
            predicate.logLevelForNamespace("twext.web2.dav.test"),
            predicate.defaultLogLevel,
        )
        self.assertEqual(
            predicate.logLevelForNamespace("twext.web2.dav.test1.test2"),
            predicate.defaultLogLevel,
        )

    def test_filtering(self) -> None:
        """
        Events are filtered based on log level/namespace.
        """
        predicate = LogLevelFilterPredicate()

        predicate.setLogLevelForNamespace("", LogLevel.error)
        predicate.setLogLevelForNamespace("twext.web2", LogLevel.debug)
        predicate.setLogLevelForNamespace("twext.web2.dav", LogLevel.warn)

        def checkPredicate(
            namespace: str, level: NamedConstant, expectedResult: NamedConstant
        ) -> None:
            event: LogEvent = dict(log_namespace=namespace, log_level=level)
            self.assertEqual(expectedResult, predicate(event))

        checkPredicate("", LogLevel.debug, PredicateResult.no)
        checkPredicate(cast(str, None), LogLevel.debug, PredicateResult.no)
        checkPredicate("", LogLevel.error, PredicateResult.no)
        checkPredicate(cast(str, None), LogLevel.error, PredicateResult.no)

        checkPredicate("twext.web2", LogLevel.debug, PredicateResult.maybe)
        checkPredicate("twext.web2", LogLevel.error, PredicateResult.maybe)

        checkPredicate("twext.web2.dav", LogLevel.debug, PredicateResult.no)
        checkPredicate("twext.web2.dav", LogLevel.error, PredicateResult.maybe)

        checkPredicate("", LogLevel.critical, PredicateResult.no)
        checkPredicate(cast(str, None), LogLevel.critical, PredicateResult.no)
        checkPredicate("twext.web2", None, PredicateResult.no)
