# Copyright (c) 2009-2016 testtools developers. See LICENSE for details.

__all__ = ["IsDeprecated", "WarningMessage", "Warnings"]

import warnings
from collections.abc import Callable
from typing import Any

from ._basic import Is
from ._const import Always
from ._datastructures import MatchesListwise, MatchesStructure
from ._higherorder import (
    AfterPreprocessing,
    Annotate,
)
from ._impl import Matcher, Mismatch


def WarningMessage(
    category_type: type[Warning],
    message: "Matcher[Any] | None" = None,
    filename: "Matcher[Any] | None" = None,
    lineno: "Matcher[Any] | None" = None,
    line: "Matcher[Any] | None" = None,
) -> "MatchesStructure[warnings.WarningMessage]":
    r"""Create a matcher that will match `warnings.WarningMessage`\s.

    For example, to match captured `DeprecationWarning`s with a message about
    some ``foo`` being replaced with ``bar``:

    .. code-block:: python

       WarningMessage(DeprecationWarning,
                      message=MatchesAll(
                          Contains('foo is deprecated'),
                          Contains('use bar instead')))

    :param type category_type: A warning type, for example `DeprecationWarning`.
    :param message_matcher: A matcher object that will be evaluated against
        warning's message.
    :param filename_matcher: A matcher object that will be evaluated against
        the warning's filename.
    :param lineno_matcher: A matcher object that will be evaluated against the
        warning's line number.
    :param line_matcher: A matcher object that will be evaluated against the
        warning's line of source code.
    """
    category_matcher = Is(category_type)
    message_matcher: Matcher[Any] = message or Always()
    filename_matcher: Matcher[Any] = filename or Always()
    lineno_matcher: Matcher[Any] = lineno or Always()
    line_matcher: Matcher[Any] = line or Always()
    return MatchesStructure(
        category=Annotate("Warning's category type does not match", category_matcher),
        message=Annotate(
            "Warning's message does not match", AfterPreprocessing(str, message_matcher)
        ),
        filename=Annotate("Warning's filname does not match", filename_matcher),
        lineno=Annotate("Warning's line number does not match", lineno_matcher),
        line=Annotate("Warning's source line does not match", line_matcher),
    )


class Warnings(Matcher[Callable[[], Any]]):
    """Match if the matchee produces warnings."""

    def __init__(self, warnings_matcher: "Matcher[Any] | None" = None) -> None:
        """Create a Warnings matcher.

        :param warnings_matcher: Optional validator for the warnings emitted by
        matchee. If no warnings_matcher is supplied then the simple fact that
        at least one warning is emitted is considered enough to match on.
        """
        self.warnings_matcher = warnings_matcher

    def match(self, matchee: Callable[[], Any]) -> Mismatch | None:
        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")
            # Handle staticmethod objects by extracting the underlying function
            if isinstance(matchee, staticmethod):
                matchee = matchee.__func__
            matchee()
            if self.warnings_matcher is not None:
                return self.warnings_matcher.match(w)
            elif not w:
                return Mismatch("Expected at least one warning, got none")
        return None

    def __str__(self) -> str:
        return f"Warnings({self.warnings_matcher!s})"


def IsDeprecated(message: "Matcher[Any]") -> Warnings:
    """Make a matcher that checks that a callable produces exactly one
    `DeprecationWarning`.

    :param message: Matcher for the warning message.
    """
    return Warnings(
        MatchesListwise(
            [WarningMessage(category_type=DeprecationWarning, message=message)]
        )
    )
