"""
Tests for ``allmydata.web.common``.

Ported to Python 3.
"""

import gc

from bs4 import (
    BeautifulSoup,
)
from hyperlink import (
    DecodedURL,
)

from testtools.matchers import (
    Equals,
    Contains,
    MatchesPredicate,
    AfterPreprocessing,
)
from testtools.twistedsupport import (
    failed,
    succeeded,
    has_no_result,
)

from twisted.python.failure import (
    Failure,
)
from twisted.internet.error import (
    ConnectionDone,
)
from twisted.internet.defer import (
    Deferred,
    fail,
)
from twisted.web.server import (
    NOT_DONE_YET,
)
from twisted.web.resource import (
    Resource,
)

from ...web.common import (
    render_exception,
)

from ..common import (
    SyncTestCase,
)
from ..common_web import (
    render,
)
from .common import (
    assert_soup_has_tag_with_attributes,
)

class StaticResource(Resource, object):
    """
    ``StaticResource`` is a resource that returns whatever Python object it is
    given from its render method.  This is useful for testing
    ``render_exception``\\ 's handling of different render results.
    """
    def __init__(self, response):
        Resource.__init__(self)
        self._response = response
        self._request = None

    @render_exception
    def render(self, request):
        self._request = request
        return self._response


class RenderExceptionTests(SyncTestCase):
    """
    Tests for ``render_exception`` (including the private helper ``_finish``).
    """
    def test_exception(self):
        """
        If the decorated method raises an exception then the exception is rendered
        into the response.
        """
        class R(Resource):
            @render_exception
            def render(self, request):
                raise Exception("synthetic exception")

        self.assertThat(
            render(R(), {}),
            succeeded(
                Contains(b"synthetic exception"),
            ),
        )

    def test_failure(self):
        """
        If the decorated method returns a ``Deferred`` that fires with a
        ``Failure`` then the exception the ``Failure`` wraps is rendered into
        the response.
        """
        resource = StaticResource(fail(Exception("synthetic exception")))
        self.assertThat(
            render(resource, {}),
            succeeded(
                Contains(b"synthetic exception"),
            ),
        )

    def test_resource(self):
        """
        If the decorated method returns an ``IResource`` provider then that
        resource is used to render the response.
        """
        resource = StaticResource(StaticResource(b"static result"))
        self.assertThat(
            render(resource, {}),
            succeeded(
                Equals(b"static result"),
            ),
        )

    def test_unicode(self):
        """
        If the decorated method returns a ``unicode`` string then that string is
        UTF-8 encoded and rendered into the response.
        """
        text = u"\N{SNOWMAN}"
        resource = StaticResource(text)
        self.assertThat(
            render(resource, {}),
            succeeded(
                Equals(text.encode("utf-8")),
            ),
        )

    def test_bytes(self):
        """
        If the decorated method returns a ``bytes`` string then that string is
        rendered into the response.
        """
        data = b"hello world"
        resource = StaticResource(data)
        self.assertThat(
            render(resource, {}),
            succeeded(
                Equals(data),
            ),
        )

    def test_decodedurl(self):
        """
        If the decorated method returns a ``DecodedURL`` then a redirect to that
        location is rendered into the response.
        """
        loc = u"http://example.invalid/foo?bar=baz"
        resource = StaticResource(DecodedURL.from_text(loc))
        self.assertThat(
            render(resource, {}),
            succeeded(
                MatchesPredicate(
                    lambda value: assert_soup_has_tag_with_attributes(
                        self,
                        BeautifulSoup(value, 'html5lib'),
                        "meta",
                        {"http-equiv": "refresh",
                         "content": "0;URL={}".format(loc),
                        },
                    )
                    # The assertion will raise if it has a problem, otherwise
                    # return None.  Turn the None into something
                    # MatchesPredicate recognizes as success.
                    or True,
                    "did not find meta refresh tag in %r",
                ),
            ),
        )

    def test_none(self):
        """
        If the decorated method returns ``None`` then the response is finished
        with no additional content.
        """
        self.assertThat(
            render(StaticResource(None), {}),
            succeeded(
                Equals(b""),
            ),
        )

    def test_not_done_yet(self):
        """
        If the decorated method returns ``NOT_DONE_YET`` then the resource is
        responsible for finishing the request itself.
        """
        the_request = []
        class R(Resource):
            @render_exception
            def render(self, request):
                the_request.append(request)
                return NOT_DONE_YET

        d = render(R(), {})

        self.assertThat(
            d,
            has_no_result(),
        )

        the_request[0].write(b"some content")
        the_request[0].finish()

        self.assertThat(
            d,
            succeeded(
                Equals(b"some content"),
            ),
        )

    def test_unknown(self):
        """
        If the decorated method returns something which is not explicitly
        supported, an internal server error is rendered into the response.
        """
        self.assertThat(
            render(StaticResource(object()), {}),
            succeeded(
                Equals(b"Internal Server Error"),
            ),
        )

    def test_disconnected(self):
        """
        If the transport is disconnected before the response is available, no
        ``RuntimeError`` is logged for finishing a disconnected request.
        """
        result = Deferred()
        resource = StaticResource(result)
        d = render(resource, {})

        resource._request.connectionLost(Failure(ConnectionDone()))
        result.callback(b"Some result")

        self.assertThat(
            d,
            failed(
                AfterPreprocessing(
                    lambda reason: reason.type,
                    Equals(ConnectionDone),
                ),
            ),
        )

        # Since we're not a trial TestCase we don't have flushLoggedErrors.
        # The next best thing is to make sure any dangling Deferreds have been
        # garbage collected and then let the generic trial logic for failing
        # tests with logged errors kick in.
        gc.collect()
