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

__all__ = [
    "DocTestMatches",
]

import doctest
import re
from typing import Any

from ._impl import Matcher, Mismatch


def _indent(
    s: str,
    indent: int = 4,
    _pattern: re.Pattern[str] = re.compile("^(?!$)", re.MULTILINE),
) -> str:
    """Prepend non-empty lines in ``s`` with ``indent`` number of spaces"""
    return _pattern.sub(indent * " ", s)


class _NonManglingOutputChecker(doctest.OutputChecker):
    """Doctest checker that works with unicode rather than mangling strings

    This is needed because current Python versions have tried to fix string
    encoding related problems, but regressed the default behaviour with
    unicode inputs in the process.

    In Python 2.6 and 2.7 ``OutputChecker.output_difference`` is was changed
    to return a bytestring encoded as per ``sys.stdout.encoding``, or utf-8 if
    that can't be determined. Worse, that encoding process happens in the
    innocent looking `_indent` global function. Because the
    `DocTestMismatch.describe` result may well not be destined for printing to
    stdout, this is no good for us. To get a unicode return as before, the
    method is monkey patched if ``doctest._encoding`` exists.

    Python 3 has a different problem. For some reason both inputs are encoded
    to ascii with 'backslashreplace', making an escaped string matches its
    unescaped form. Overriding the offending ``OutputChecker._toAscii`` method
    is sufficient to revert this.
    """

    def _toAscii(self, s: str) -> str:
        """Return ``s`` unchanged rather than mangling it to ascii"""
        return s

    # Only do this overriding hackery if doctest has a broken _input function
    if getattr(doctest, "_encoding", None) is not None:
        from types import FunctionType as __F

        __f = doctest.OutputChecker.output_difference.__func__  # type: ignore[attr-defined]
        __g = dict(__f.__globals__)
        __g["_indent"] = _indent
        output_difference: Any = __F(__f.func_code, __g, "output_difference")
        del __F, __f, __g


class DocTestMatches(Matcher[str]):
    """See if a string matches a doctest example."""

    def __init__(self, example: str, flags: int = 0) -> None:
        """Create a DocTestMatches to match example.

        :param example: The example to match e.g. 'foo bar baz'
        :param flags: doctest comparison flags to match on. e.g.
            doctest.ELLIPSIS.
        """
        if not example.endswith("\n"):
            example += "\n"
        self.want = example  # required variable name by doctest.
        self.flags = flags
        self._checker = _NonManglingOutputChecker()

    def __str__(self) -> str:
        if self.flags:
            flagstr = f", flags={self.flags}"
        else:
            flagstr = ""
        return f"DocTestMatches({self.want!r}{flagstr})"

    def _with_nl(self, actual: str) -> str:
        result = self.want.__class__(actual)
        if not result.endswith("\n"):
            result += "\n"
        return result

    def match(self, actual: str) -> "DocTestMismatch | None":
        with_nl = self._with_nl(actual)
        if self._checker.check_output(self.want, with_nl, self.flags):
            return None
        return DocTestMismatch(self, with_nl)

    def _describe_difference(self, with_nl: str) -> str:
        result: str = self._checker.output_difference(self, with_nl, self.flags)
        return result


class DocTestMismatch(Mismatch):
    """Mismatch object for DocTestMatches."""

    def __init__(self, matcher: DocTestMatches, with_nl: str) -> None:
        self.matcher = matcher
        self.with_nl = with_nl

    def describe(self) -> str:
        return self.matcher._describe_difference(self.with_nl)
