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

"""
Tests for L{twisted.web.resource}.
"""

from twisted.trial.unittest import TestCase
from twisted.web.error import UnsupportedMethod
from twisted.web.http_headers import Headers
from twisted.web.iweb import IRequest
from twisted.web.resource import (
    FORBIDDEN,
    NOT_FOUND,
    Resource,
    _UnsafeErrorPage as ErrorPage,
    _UnsafeForbiddenResource as ForbiddenResource,
    _UnsafeNoResource as NoResource,
    getChildForRequest,
)
from twisted.web.test.requesthelper import DummyRequest


class ErrorPageTests(TestCase):
    """
    Tests for L{_UnafeErrorPage}, L{_UnsafeNoResource}, and
    L{_UnsafeForbiddenResource}.
    """

    def test_deprecatedErrorPage(self) -> None:
        """
        The public C{twisted.web.resource.ErrorPage} alias for the
        corresponding C{_Unsafe} class produces a deprecation warning when
        called.
        """
        _ = ErrorPage(123, "ono", "ono!")
        [warning] = self.flushWarnings()
        self.assertEqual(warning["category"], DeprecationWarning)
        self.assertIn("twisted.web.pages.errorPage", warning["message"])

    def test_deprecatedNoResource(self) -> None:
        """
        The public C{twisted.web.resource.NoResource} alias for the
        corresponding C{_Unsafe} class produces a deprecation warning when
        called.
        """
        _ = NoResource()
        [warning] = self.flushWarnings()
        self.assertEqual(warning["category"], DeprecationWarning)
        self.assertIn("twisted.web.pages.notFound", warning["message"])

    def test_deprecatedForbiddenResource(self) -> None:
        """
        The public C{twisted.web.resource.ForbiddenResource} alias for the
        corresponding C{_Unsafe} class produce a deprecation warning when
        called.
        """
        _ = ForbiddenResource()
        [warning] = self.flushWarnings()
        self.assertEqual(warning["category"], DeprecationWarning)
        self.assertIn("twisted.web.pages.forbidden", warning["message"])

    def test_getChild(self) -> None:
        """
        The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
        called on.
        """
        page = ErrorPage(321, "foo", "bar")
        self.assertIdentical(page.getChild(b"name", object()), page)

    def _pageRenderingTest(
        self, page: Resource, code: int, brief: str, detail: str
    ) -> None:
        request = DummyRequest([b""])
        template = (
            "\n"
            "<html>\n"
            "  <head><title>%s - %s</title></head>\n"
            "  <body>\n"
            "    <h1>%s</h1>\n"
            "    <p>%s</p>\n"
            "  </body>\n"
            "</html>\n"
        )
        expected = template % (code, brief, brief, detail)
        self.assertEqual(page.render(request), expected.encode("utf-8"))
        self.assertEqual(request.responseCode, code)
        self.assertEqual(
            request.responseHeaders,
            Headers({b"content-type": [b"text/html; charset=utf-8"]}),
        )

    def test_errorPageRendering(self) -> None:
        """
        L{ErrorPage.render} returns a C{bytes} describing the error defined by
        the response code and message passed to L{ErrorPage.__init__}.  It also
        uses that response code to set the response code on the L{Request}
        passed in.
        """
        code = 321
        brief = "brief description text"
        detail = "much longer text might go here"
        page = ErrorPage(code, brief, detail)
        self._pageRenderingTest(page, code, brief, detail)

    def test_noResourceRendering(self) -> None:
        """
        L{NoResource} sets the HTTP I{NOT FOUND} code.
        """
        detail = "long message"
        page = NoResource(detail)
        self._pageRenderingTest(page, NOT_FOUND, "No Such Resource", detail)

    def test_forbiddenResourceRendering(self) -> None:
        """
        L{ForbiddenResource} sets the HTTP I{FORBIDDEN} code.
        """
        detail = "longer message"
        page = ForbiddenResource(detail)
        self._pageRenderingTest(page, FORBIDDEN, "Forbidden Resource", detail)


class DynamicChild(Resource):
    """
    A L{Resource} to be created on the fly by L{DynamicChildren}.
    """

    def __init__(self, path: bytes, request: IRequest) -> None:
        Resource.__init__(self)
        self.path = path
        self.request = request


class DynamicChildren(Resource):
    """
    A L{Resource} with dynamic children.
    """

    def getChild(self, path: bytes, request: IRequest) -> DynamicChild:
        return DynamicChild(path, request)


class BytesReturnedRenderable(Resource):
    """
    A L{Resource} with minimal capabilities to render a response.
    """

    def __init__(self, response: bytes) -> None:
        """
        @param response: A C{bytes} object giving the value to return from
            C{render_GET}.
        """
        Resource.__init__(self)
        self._response = response

    def render_GET(self, request: object) -> bytes:
        """
        Render a response to a I{GET} request by returning a short byte string
        to be written by the server.
        """
        return self._response


class ImplicitAllowedMethods(Resource):
    """
    A L{Resource} which implicitly defines its allowed methods by defining
    renderers to handle them.
    """

    def render_GET(self, request: object) -> None:
        pass

    def render_PUT(self, request: object) -> None:
        pass


class ResourceTests(TestCase):
    """
    Tests for L{Resource}.
    """

    def test_staticChildren(self) -> None:
        """
        L{Resource.putChild} adds a I{static} child to the resource.  That child
        is returned from any call to L{Resource.getChildWithDefault} for the
        child's path.
        """
        resource = Resource()
        child = Resource()
        sibling = Resource()
        resource.putChild(b"foo", child)
        resource.putChild(b"bar", sibling)
        self.assertIdentical(
            child, resource.getChildWithDefault(b"foo", DummyRequest([]))
        )

    def test_dynamicChildren(self) -> None:
        """
        L{Resource.getChildWithDefault} delegates to L{Resource.getChild} when
        the requested path is not associated with any static child.
        """
        path = b"foo"
        request = DummyRequest([])
        resource = DynamicChildren()
        child = resource.getChildWithDefault(path, request)
        self.assertIsInstance(child, DynamicChild)
        self.assertEqual(child.path, path)
        self.assertIdentical(child.request, request)

    def test_staticChildPathType(self) -> None:
        """
        Test that passing the wrong type to putChild results in a warning,
        and a failure in Python 3
        """
        resource = Resource()
        child = Resource()
        sibling = Resource()
        self.assertRaises(TypeError, resource.putChild, "foo", child)
        self.assertRaises(TypeError, resource.putChild, None, sibling)

    def test_defaultHEAD(self) -> None:
        """
        When not otherwise overridden, L{Resource.render} treats a I{HEAD}
        request as if it were a I{GET} request.
        """
        expected = b"insert response here"
        request = DummyRequest([])
        request.method = b"HEAD"
        resource = BytesReturnedRenderable(expected)
        self.assertEqual(expected, resource.render(request))

    def test_explicitAllowedMethods(self) -> None:
        """
        The L{UnsupportedMethod} raised by L{Resource.render} for an unsupported
        request method has a C{allowedMethods} attribute set to the value of the
        C{allowedMethods} attribute of the L{Resource}, if it has one.
        """
        expected = [b"GET", b"HEAD", b"PUT"]
        resource = Resource()
        resource.allowedMethods = expected
        request = DummyRequest([])
        request.method = b"FICTIONAL"
        exc = self.assertRaises(UnsupportedMethod, resource.render, request)
        self.assertEqual(set(expected), set(exc.allowedMethods))

    def test_implicitAllowedMethods(self) -> None:
        """
        The L{UnsupportedMethod} raised by L{Resource.render} for an unsupported
        request method has a C{allowedMethods} attribute set to a list of the
        methods supported by the L{Resource}, as determined by the
        I{render_}-prefixed methods which it defines, if C{allowedMethods} is
        not explicitly defined by the L{Resource}.
        """
        expected = {b"GET", b"HEAD", b"PUT"}
        resource = ImplicitAllowedMethods()
        request = DummyRequest([])
        request.method = b"FICTIONAL"
        exc = self.assertRaises(UnsupportedMethod, resource.render, request)
        self.assertEqual(expected, set(exc.allowedMethods))


class GetChildForRequestTests(TestCase):
    """
    Tests for L{getChildForRequest}.
    """

    def test_exhaustedPostPath(self) -> None:
        """
        L{getChildForRequest} returns whatever resource has been reached by the
        time the request's C{postpath} is empty.
        """
        request = DummyRequest([])
        resource = Resource()
        result = getChildForRequest(resource, request)
        self.assertIdentical(resource, result)

    def test_leafResource(self) -> None:
        """
        L{getChildForRequest} returns the first resource it encounters with a
        C{isLeaf} attribute set to C{True}.
        """
        request = DummyRequest([b"foo", b"bar"])
        resource = Resource()
        resource.isLeaf = True
        result = getChildForRequest(resource, request)
        self.assertIdentical(resource, result)

    def test_postPathToPrePath(self) -> None:
        """
        As path segments from the request are traversed, they are taken from
        C{postpath} and put into C{prepath}.
        """
        request = DummyRequest([b"foo", b"bar"])
        root = Resource()
        child = Resource()
        child.isLeaf = True
        root.putChild(b"foo", child)
        self.assertIdentical(child, getChildForRequest(root, request))
        self.assertEqual(request.prepath, [b"foo"])
        self.assertEqual(request.postpath, [b"bar"])
