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

"""
HTTP errors.
"""


import re
import sys
import traceback

from twisted.python.compat import nativeString
from twisted.trial import unittest
from twisted.web import error
from twisted.web.template import Tag


class CodeToMessageTests(unittest.TestCase):
    """
    L{_codeToMessages} inverts L{_responses.RESPONSES}
    """

    def test_validCode(self):
        m = error._codeToMessage(b"302")
        self.assertEqual(m, b"Found")

    def test_invalidCode(self):
        m = error._codeToMessage(b"987")
        self.assertEqual(m, None)

    def test_nonintegerCode(self):
        m = error._codeToMessage(b"InvalidCode")
        self.assertEqual(m, None)


class ErrorTests(unittest.TestCase):
    """
    Tests for how L{Error} attributes are initialized.
    """

    def test_noMessageValidStatus(self):
        """
        If no C{message} argument is passed to the L{Error} constructor and the
        C{code} argument is a valid HTTP status code, C{message} is set to the
        HTTP reason phrase for C{code}.
        """
        e = error.Error(b"200")
        self.assertEqual(e.message, b"OK")
        self.assertEqual(str(e), "200 OK")

    def test_noMessageForStatus(self):
        """
        If no C{message} argument is passed to the L{Error} constructor and
        C{code} isn't a known HTTP status code, C{message} stays L{None}.
        """
        e = error.Error(b"999")
        self.assertEqual(e.message, None)
        self.assertEqual(str(e), "999")

    def test_invalidStatus(self):
        """
        If C{code} isn't plausibly an HTTP status code (i.e., composed of
        digits) it is rejected with L{ValueError}.
        """
        with self.assertRaises(ValueError):
            error.Error(b"InvalidStatus")

    def test_messageExists(self):
        """
        If a C{message} argument is passed to the L{Error} constructor, the
        C{message} isn't affected by the value of C{status}.
        """
        e = error.Error(b"200", b"My own message")
        self.assertEqual(e.message, b"My own message")
        self.assertEqual(str(e), "200 My own message")

    def test_str(self):
        """
        C{str()} on an L{Error} returns the code and message it was
        instantiated with.
        """
        # Bytestring status
        e = error.Error(b"200", b"OK")
        self.assertEqual(str(e), "200 OK")

        # int status
        e = error.Error(200, b"OK")
        self.assertEqual(str(e), "200 OK")


class PageRedirectTests(unittest.TestCase):
    """
    Tests for how L{PageRedirect} attributes are initialized.
    """

    def test_noMessageValidStatus(self):
        """
        If no C{message} argument is passed to the L{PageRedirect} constructor
        and the C{code} argument is a valid HTTP status code, C{code} is mapped
        to a descriptive string to which C{message} is assigned.
        """
        e = error.PageRedirect(b"200", location=b"/foo")
        self.assertEqual(e.message, b"OK to /foo")

    def test_noMessageValidStatusNoLocation(self):
        """
        If no C{message} argument is passed to the L{PageRedirect} constructor
        and C{location} is also empty and the C{code} argument is a valid HTTP
        status code, C{code} is mapped to a descriptive string to which
        C{message} is assigned without trying to include an empty location.
        """
        e = error.PageRedirect(b"200")
        self.assertEqual(e.message, b"OK")

    def test_noMessageInvalidStatusLocationExists(self):
        """
        If no C{message} argument is passed to the L{PageRedirect} constructor
        and C{code} isn't a valid HTTP status code, C{message} stays L{None}.
        """
        e = error.PageRedirect(b"999", location=b"/foo")
        self.assertEqual(e.message, None)

    def test_messageExistsLocationExists(self):
        """
        If a C{message} argument is passed to the L{PageRedirect} constructor,
        the C{message} isn't affected by the value of C{status}.
        """
        e = error.PageRedirect(b"200", b"My own message", location=b"/foo")
        self.assertEqual(e.message, b"My own message to /foo")

    def test_messageExistsNoLocation(self):
        """
        If a C{message} argument is passed to the L{PageRedirect} constructor
        and no location is provided, C{message} doesn't try to include the
        empty location.
        """
        e = error.PageRedirect(b"200", b"My own message")
        self.assertEqual(e.message, b"My own message")


class InfiniteRedirectionTests(unittest.TestCase):
    """
    Tests for how L{InfiniteRedirection} attributes are initialized.
    """

    def test_noMessageValidStatus(self):
        """
        If no C{message} argument is passed to the L{InfiniteRedirection}
        constructor and the C{code} argument is a valid HTTP status code,
        C{code} is mapped to a descriptive string to which C{message} is
        assigned.
        """
        e = error.InfiniteRedirection(b"200", location=b"/foo")
        self.assertEqual(e.message, b"OK to /foo")

    def test_noMessageValidStatusNoLocation(self):
        """
        If no C{message} argument is passed to the L{InfiniteRedirection}
        constructor and C{location} is also empty and the C{code} argument is a
        valid HTTP status code, C{code} is mapped to a descriptive string to
        which C{message} is assigned without trying to include an empty
        location.
        """
        e = error.InfiniteRedirection(b"200")
        self.assertEqual(e.message, b"OK")

    def test_noMessageInvalidStatusLocationExists(self):
        """
        If no C{message} argument is passed to the L{InfiniteRedirection}
        constructor and C{code} isn't a valid HTTP status code, C{message} stays
        L{None}.
        """
        e = error.InfiniteRedirection(b"999", location=b"/foo")
        self.assertEqual(e.message, None)
        self.assertEqual(str(e), "999")

    def test_messageExistsLocationExists(self):
        """
        If a C{message} argument is passed to the L{InfiniteRedirection}
        constructor, the C{message} isn't affected by the value of C{status}.
        """
        e = error.InfiniteRedirection(b"200", b"My own message", location=b"/foo")
        self.assertEqual(e.message, b"My own message to /foo")

    def test_messageExistsNoLocation(self):
        """
        If a C{message} argument is passed to the L{InfiniteRedirection}
        constructor and no location is provided, C{message} doesn't try to
        include the empty location.
        """
        e = error.InfiniteRedirection(b"200", b"My own message")
        self.assertEqual(e.message, b"My own message")


class RedirectWithNoLocationTests(unittest.TestCase):
    """
    L{RedirectWithNoLocation} is a subclass of L{Error} which sets
    a custom message in the constructor.
    """

    def test_validMessage(self):
        """
        When C{code}, C{message}, and C{uri} are passed to the
        L{RedirectWithNoLocation} constructor, the C{message} and C{uri}
        attributes are set, respectively.
        """
        e = error.RedirectWithNoLocation(b"302", b"REDIRECT", b"https://example.com")
        self.assertEqual(e.message, b"REDIRECT to https://example.com")
        self.assertEqual(e.uri, b"https://example.com")


class MissingRenderMethodTests(unittest.TestCase):
    """
    Tests for how L{MissingRenderMethod} exceptions are initialized and
    displayed.
    """

    def test_constructor(self):
        """
        Given C{element} and C{renderName} arguments, the
        L{MissingRenderMethod} constructor assigns the values to the
        corresponding attributes.
        """
        elt = object()
        e = error.MissingRenderMethod(elt, "renderThing")
        self.assertIs(e.element, elt)
        self.assertIs(e.renderName, "renderThing")

    def test_repr(self):
        """
        A L{MissingRenderMethod} is represented using a custom string
        containing the element's representation and the method name.
        """
        elt = object()
        e = error.MissingRenderMethod(elt, "renderThing")
        self.assertEqual(
            repr(e),
            ("'MissingRenderMethod': " "%r had no render method named 'renderThing'")
            % elt,
        )


class MissingTemplateLoaderTests(unittest.TestCase):
    """
    Tests for how L{MissingTemplateLoader} exceptions are initialized and
    displayed.
    """

    def test_constructor(self):
        """
        Given an C{element} argument, the L{MissingTemplateLoader} constructor
        assigns the value to the corresponding attribute.
        """
        elt = object()
        e = error.MissingTemplateLoader(elt)
        self.assertIs(e.element, elt)

    def test_repr(self):
        """
        A L{MissingTemplateLoader} is represented using a custom string
        containing the element's representation and the method name.
        """
        elt = object()
        e = error.MissingTemplateLoader(elt)
        self.assertEqual(repr(e), "'MissingTemplateLoader': %r had no loader" % elt)


class FlattenerErrorTests(unittest.TestCase):
    """
    Tests for L{FlattenerError}.
    """

    def makeFlattenerError(self, roots=[]):
        try:
            raise RuntimeError("oh noes")
        except Exception as e:
            tb = traceback.extract_tb(sys.exc_info()[2])
            return error.FlattenerError(e, roots, tb)

    def fakeFormatRoot(self, obj):
        return "R(%s)" % obj

    def test_constructor(self):
        """
        Given C{exception}, C{roots}, and C{traceback} arguments, the
        L{FlattenerError} constructor assigns the roots to the C{_roots}
        attribute.
        """
        e = self.makeFlattenerError(roots=["a", "b"])
        self.assertEqual(e._roots, ["a", "b"])

    def test_str(self):
        """
        The string form of a L{FlattenerError} is identical to its
        representation.
        """
        e = self.makeFlattenerError()
        self.assertEqual(str(e), repr(e))

    def test_reprWithRootsAndWithTraceback(self):
        """
        The representation of a L{FlattenerError} initialized with roots and a
        traceback contains a formatted representation of those roots (using
        C{_formatRoot}) and a formatted traceback.
        """
        e = self.makeFlattenerError(["a", "b"])
        e._formatRoot = self.fakeFormatRoot
        self.assertTrue(
            re.match(
                "Exception while flattening:\n"
                "  R\\(a\\)\n"
                "  R\\(b\\)\n"
                '  File "[^"]*", line [0-9]*, in makeFlattenerError\n'
                '    raise RuntimeError\\("oh noes"\\)\n'
                "RuntimeError: oh noes\n$",
                repr(e),
                re.M | re.S,
            ),
            repr(e),
        )

    def test_reprWithoutRootsAndWithTraceback(self):
        """
        The representation of a L{FlattenerError} initialized without roots but
        with a traceback contains a formatted traceback but no roots.
        """
        e = self.makeFlattenerError([])
        self.assertTrue(
            re.match(
                "Exception while flattening:\n"
                '  File "[^"]*", line [0-9]*, in makeFlattenerError\n'
                '    raise RuntimeError\\("oh noes"\\)\n'
                "RuntimeError: oh noes\n$",
                repr(e),
                re.M | re.S,
            ),
            repr(e),
        )

    def test_reprWithoutRootsAndWithoutTraceback(self):
        """
        The representation of a L{FlattenerError} initialized without roots but
        with a traceback contains a formatted traceback but no roots.
        """
        e = error.FlattenerError(RuntimeError("oh noes"), [], None)
        self.assertTrue(
            re.match(
                "Exception while flattening:\n" "RuntimeError: oh noes\n$",
                repr(e),
                re.M | re.S,
            ),
            repr(e),
        )

    def test_formatRootShortUnicodeString(self):
        """
        The C{_formatRoot} method formats a short unicode string using the
        built-in repr.
        """
        e = self.makeFlattenerError()
        self.assertEqual(e._formatRoot(nativeString("abcd")), repr("abcd"))

    def test_formatRootLongUnicodeString(self):
        """
        The C{_formatRoot} method formats a long unicode string using the
        built-in repr with an ellipsis.
        """
        e = self.makeFlattenerError()
        longString = nativeString("abcde-" * 20)
        self.assertEqual(
            e._formatRoot(longString),
            repr("abcde-abcde-abcde-ab<...>e-abcde-abcde-abcde-"),
        )

    def test_formatRootShortByteString(self):
        """
        The C{_formatRoot} method formats a short byte string using the
        built-in repr.
        """
        e = self.makeFlattenerError()
        self.assertEqual(e._formatRoot(b"abcd"), repr(b"abcd"))

    def test_formatRootLongByteString(self):
        """
        The C{_formatRoot} method formats a long byte string using the
        built-in repr with an ellipsis.
        """
        e = self.makeFlattenerError()
        longString = b"abcde-" * 20
        self.assertEqual(
            e._formatRoot(longString),
            repr(b"abcde-abcde-abcde-ab<...>e-abcde-abcde-abcde-"),
        )

    def test_formatRootTagNoFilename(self):
        """
        The C{_formatRoot} method formats a C{Tag} with no filename information
        as 'Tag <tagName>'.
        """
        e = self.makeFlattenerError()
        self.assertEqual(e._formatRoot(Tag("a-tag")), "Tag <a-tag>")

    def test_formatRootTagWithFilename(self):
        """
        The C{_formatRoot} method formats a C{Tag} with filename information
        using the filename, line, column, and tag information
        """
        e = self.makeFlattenerError()
        t = Tag("a-tag", filename="tpl.py", lineNumber=10, columnNumber=20)
        self.assertEqual(
            e._formatRoot(t), 'File "tpl.py", line 10, column 20, in "a-tag"'
        )

    def test_string(self):
        """
        If a L{FlattenerError} is created with a string root, up to around 40
        bytes from that string are included in the string representation of the
        exception.
        """
        self.assertEqual(
            str(error.FlattenerError(RuntimeError("reason"), ["abc123xyz"], [])),
            "Exception while flattening:\n" "  'abc123xyz'\n" "RuntimeError: reason\n",
        )
        self.assertEqual(
            str(error.FlattenerError(RuntimeError("reason"), ["0123456789" * 10], [])),
            "Exception while flattening:\n"
            "  '01234567890123456789"
            "<...>01234567890123456789'\n"  # TODO: re-add 0
            "RuntimeError: reason\n",
        )

    def test_unicode(self):
        """
        If a L{FlattenerError} is created with a unicode root, up to around 40
        characters from that string are included in the string representation
        of the exception.
        """
        self.assertEqual(
            str(
                error.FlattenerError(RuntimeError("reason"), ["abc\N{SNOWMAN}xyz"], [])
            ),
            "Exception while flattening:\n"
            "  'abc\\u2603xyz'\n"  # Codepoint for SNOWMAN
            "RuntimeError: reason\n",
        )
        self.assertEqual(
            str(
                error.FlattenerError(
                    RuntimeError("reason"), ["01234567\N{SNOWMAN}9" * 10], []
                )
            ),
            "Exception while flattening:\n"
            "  '01234567\\u2603901234567\\u26039"
            "<...>01234567\\u2603901234567"
            "\\u26039'\n"
            "RuntimeError: reason\n",
        )


class UnsupportedMethodTests(unittest.SynchronousTestCase):
    """
    Tests for L{UnsupportedMethod}.
    """

    def test_str(self):
        """
        The C{__str__} for L{UnsupportedMethod} makes it clear that what it
        shows is a list of the supported methods, not the method that was
        unsupported.
        """
        e = error.UnsupportedMethod([b"HEAD", b"PATCH"])
        self.assertEqual(
            str(e),
            "Expected one of [b'HEAD', b'PATCH']",
        )
