import unittest
from chameleon.zpt.template import Macro

from pyramid.testing import DummyRequest
from pyramid.response import Response

from pyramid_views.views.base import View, TemplateView, RedirectView, MacroMixin
from pyramid_views.utils import ImproperlyConfigured

from . import views
from tests.base import BaseTest


class SimpleView(View):
    """
    A simple view with a docstring.
    """
    def get(self, request):
        return Response('This is a simple view')


class SimplePostView(SimpleView):
    post = SimpleView.get


class PostOnlyView(View):
    def post(self, request):
        return Response('This view only accepts POST')


class CustomizableView(SimpleView):
    parameter = {}


def decorator(view):
    view.is_decorated = True
    return view


class DecoratedDispatchView(SimpleView):

    @decorator
    def dispatch(self, request, *args, **kwargs):
        return super(DecoratedDispatchView, self).dispatch(request, *args, **kwargs)


class AboutTemplateView(TemplateView):
    def get(self, request):
        return self.render_to_response({})

    def get_template_names(self):
        return ['tests:templates/about.html']


class AboutTemplateAttributeView(TemplateView):
    template_name = 'tests:templates/about.html'

    def get(self, request):
        return self.render_to_response(context={})


class InstanceView(View):

    def get(self, request):
        return self


class ViewTest(BaseTest):
    def _assert_simple(self, response):
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.body, b'This is a simple view')

    def test_no_init_kwargs(self):
        """
        Test that a view can't be accidentally instantiated before deployment
        """
        try:
            SimpleView(key='value').as_view()
            self.fail('Should not be able to instantiate a view')
        except AttributeError:
            pass

    def test_no_init_args(self):
        """
        Test that a view can't be accidentally instantiated before deployment
        """
        try:
            SimpleView.as_view('value')
            self.fail('Should not be able to use non-keyword arguments instantiating a view')
        except TypeError:
            pass

    def test_pathological_http_method(self):
        """
        The edge case of a http request that spoofs an existing method name is caught.
        """
        self.assertEqual(SimpleView.as_view()(
            DummyRequest(path='/', method='DISPATCH')
        ).status_code, 405)

    def test_get_only(self):
        """
        Test a view which only allows GET doesn't allow other methods.
        """
        self._assert_simple(SimpleView.as_view()(
            DummyRequest(path='/', method='GET')
        ))
        self.assertEqual(SimpleView.as_view()(
            DummyRequest(path='/', method='POST')
        ).status_code, 405)
        self.assertEqual(SimpleView.as_view()(
            DummyRequest(path='/', method='FAKE')
        ).status_code, 405)

    def test_get_and_head(self):
        """
        Test a view which supplies a GET method also responds correctly to HEAD.
        """
        self._assert_simple(SimpleView.as_view()(
            DummyRequest(path='/', method='GET')
        ))
        response = SimpleView.as_view()(
            DummyRequest(path='/', method='HEAD')
        )
        self.assertEqual(response.status_code, 200)

    def test_head_no_get(self):
        """
        Test a view which supplies no GET method responds to HEAD with HTTP 405.
        """
        response = PostOnlyView.as_view()(
            DummyRequest(path='/', method='HEAD')
        )
        self.assertEqual(response.status_code, 405)

    def test_get_and_post(self):
        """
        Test a view which only allows both GET and POST.
        """
        self._assert_simple(SimplePostView.as_view()(
            DummyRequest(path='/', method='GET')
        ))
        self._assert_simple(SimplePostView.as_view()(
            DummyRequest(path='/', method='POST')
        ))
        self.assertEqual(SimplePostView.as_view()(
            DummyRequest(path='/', method='FAKE')
        ).status_code, 405)

    def test_invalid_keyword_argument(self):
        """
        Test that view arguments must be predefined on the class and can't
        be named like a HTTP method.
        """
        # Check each of the allowed method names
        for method in SimpleView.http_method_names:
            kwargs = dict(((method, "value"),))
            self.assertRaises(TypeError, SimpleView.as_view, **kwargs)

        # Check the case view argument is ok if predefined on the class...
        CustomizableView.as_view(parameter="value")
        # ...but raises errors otherwise.
        self.assertRaises(TypeError, CustomizableView.as_view, foobar="value")

    def test_calling_more_than_once(self):
        """
        Test a view can only be called once.
        """
        request = DummyRequest(path='/', method='GET')
        view = InstanceView.as_view()
        self.assertNotEqual(view(request), view(request))

    def test_class_attributes(self):
        """
        Test that the callable returned from as_view() has proper
        docstring, name and module.
        """
        self.assertEqual(SimpleView.__doc__, SimpleView.as_view().__doc__)
        self.assertEqual(SimpleView.__name__, SimpleView.as_view().__name__)
        self.assertEqual(SimpleView.__module__, SimpleView.as_view().__module__)

    def test_dispatch_decoration(self):
        """
        Test that attributes set by decorators on the dispatch method
        are also present on the closure.
        """
        self.assertTrue(DecoratedDispatchView.as_view().is_decorated)

    def test_options(self):
        """
        Test that views respond to HTTP OPTIONS requests with an Allow header
        appropriate for the methods implemented by the view class.
        """
        request = DummyRequest(path='/', method='OPTIONS')
        view = SimpleView.as_view()
        response = view(request)
        self.assertEqual(200, response.status_code)
        self.assertTrue(response.headers['Allow'])

    def test_options_for_get_view(self):
        """
        Test that a view implementing GET allows GET and HEAD.
        """
        request = DummyRequest(path='/', method='OPTIONS')
        view = SimpleView.as_view()
        response = view(request)
        self._assert_allows(response, 'GET', 'HEAD')

    def test_options_for_get_and_post_view(self):
        """
        Test that a view implementing GET and POST allows GET, HEAD, and POST.
        """
        request = DummyRequest(path='/', method='OPTIONS')
        view = SimplePostView.as_view()
        response = view(request)
        self._assert_allows(response, 'GET', 'HEAD', 'POST')

    def test_options_for_post_view(self):
        """
        Test that a view implementing POST allows POST.
        """
        request = DummyRequest(path='/', method='OPTIONS')
        view = PostOnlyView.as_view()
        response = view(request)
        self._assert_allows(response, 'POST')

    def _assert_allows(self, response, *expected_methods):
        "Assert allowed HTTP methods reported in the Allow response header"
        response_allows = set(response.headers['Allow'].split(', '))
        self.assertEqual(set(expected_methods + ('OPTIONS',)), response_allows)

    def test_args_kwargs_request_on_self(self):
        """
        Test a view only has args, kwargs & request once `as_view`
        has been called.
        """
        bare_view = InstanceView()
        view = InstanceView.as_view()(
            DummyRequest(path='/', method='GET')
        )
        for attribute in ('args', 'kwargs', 'request'):
            self.assertNotIn(attribute, dir(bare_view))
            self.assertIn(attribute, dir(view))

    def test_direct_instantiation(self):
        """
        It should be possible to use the view by directly instantiating it
        without going through .as_view() (#21564).
        """
        view = PostOnlyView()
        response = view.dispatch(
            DummyRequest(path='/', method='HEAD')
        )
        self.assertEqual(response.status_code, 405)


class TemplateViewTest(BaseTest):
    urls = 'generic_views.urls'

    def _assert_about(self, response):
        self.assertEqual(response.status_code, 200)
        self.assertIn('<h1>About</h1>', response.body)

    def test_get(self):
        """
        Test a view that simply renders a template on GET
        """
        self._assert_about(AboutTemplateView.as_view()(
            DummyRequest(path='/about/', method='GET')
        ))

    def test_head(self):
        """
        Test a TemplateView responds correctly to HEAD
        """
        response = AboutTemplateView.as_view()(
            DummyRequest(path='/about/', method='HEAD')
        )
        self.assertEqual(response.status_code, 200)

    def test_get_template_attribute(self):
        """
        Test a view that renders a template on GET with the template name as
        an attribute on the class.
        """
        self._assert_about(AboutTemplateAttributeView.as_view()(
            DummyRequest(path='/about/', method='GET')
        ))

    def test_get_generic_template(self):
        """
        Test a completely generic view that renders a template on GET
        with the template name as an argument at instantiation.
        """
        self._assert_about(TemplateView.as_view(template_name='tests:templates/about.html')(
            DummyRequest(path='/about/', method='GET')
        ))

    def test_template_name_required(self):
        """
        A template view must provide a template name
        """
        view = TemplateView.as_view()
        self.assertRaises(ImproperlyConfigured, view, DummyRequest(path='/', method='GET'))

    def test_content_type(self):
        view = TemplateView.as_view(template_name='tests:templates/robots.txt', content_type='text/plain')
        response = view(DummyRequest(path='/', method='GET'))
        self.assertEqual(response.headers['Content-Type'], 'text/plain; charset=UTF-8')


class RedirectViewTest(BaseTest):

    def test_no_url(self):
        "Without any configuration, returns HTTP 410 GONE"
        response = RedirectView.as_view()(
            DummyRequest(path='/foo/', method='GET')
        )
        self.assertEqual(response.status_code, 410)

    def test_permanent_redirect(self):
        "Default is a permanent redirect"
        response = RedirectView.as_view(url='/bar/')(
            DummyRequest(path='/foo/', method='GET')
        )
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/')

    def test_temporary_redirect(self):
        "Permanent redirects are an option"
        response = RedirectView.as_view(url='/bar/', permanent=False)(
            DummyRequest(path='/foo/', method='GET')
        )
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.location, '/bar/')

    def test_include_args(self):
        "GET arguments can be included in the redirected URL"
        response = RedirectView.as_view(url='/bar/')(
            DummyRequest(path='/foo/', method='GET')
        )
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/')

        response = RedirectView.as_view(url='/bar/', query_string=True)(
            DummyRequest(path='/foo/', method='GET', query_string='pork=spam')
        )
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/?pork=spam')

    def test_include_urlencoded_args(self):
        "GET arguments can be URL-encoded when included in the redirected URL"
        response = RedirectView.as_view(url='/bar/', query_string=True)(
            DummyRequest(path='/foo/', method='GET', query_string='unicode=%E2%9C%93')
        )
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/?unicode=%E2%9C%93')

    def test_parameter_substitution(self):
        "Redirection URLs can be parameterized"
        response = RedirectView.as_view(url='/bar/%(object_id)d/')(
            DummyRequest(path='/foo/42/', method='GET'),
            object_id=42
        )
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/42/')

    def test_named_url_pattern(self):
        "Named pattern parameter should reverse to the matching pattern"
        self.config.add_route('artist_detail', 'detail/artist/{pk}', view=RedirectView.as_view())
        response = RedirectView.as_view(pattern_name='artist_detail')(
            DummyRequest(path='/foo/', method='GET'),
            pk=1
        )

        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.headers['Location'], 'http://example.com/detail/artist/1')

    def test_named_url_pattern_using_args(self):
        self.config.add_route('artist_detail', 'detail/artist/*traverse', view=RedirectView.as_view())
        response = RedirectView.as_view(pattern_name='artist_detail')(DummyRequest(path='/foo/', method='GET'), traverse=(1,2))
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.headers['Location'], 'http://example.com/detail/artist/1/2')

    def test_wrong_named_url_pattern(self):
        "A wrong pattern name returns 410 GONE"
        response = RedirectView.as_view(pattern_name='wrong.pattern_name')(
            DummyRequest(path='/foo/', method='GET')
        )
        self.assertEqual(response.status_code, 410)

    def test_redirect_POST(self):
        "Default is a permanent redirect"
        response = RedirectView.as_view(url='/bar/')(DummyRequest(path='/foo/', method='POST'))
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/')

    def test_redirect_HEAD(self):
        "Default is a permanent redirect"
        response = RedirectView.as_view(url='/bar/')(DummyRequest(path='/foo/', method='HEAD'))
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/')

    def test_redirect_OPTIONS(self):
        "Default is a permanent redirect"
        response = RedirectView.as_view(url='/bar/')(DummyRequest(path='/foo/', method='OPTIONS'))
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/')

    def test_redirect_PUT(self):
        "Default is a permanent redirect"
        response = RedirectView.as_view(url='/bar/')(DummyRequest(path='/foo/', method='PUT'))
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/')

    def test_redirect_PATCH(self):
        "Default is a permanent redirect"
        response = RedirectView.as_view(url='/bar/')(DummyRequest(path='/foo/', method='PATCH'))
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/')

    def test_redirect_DELETE(self):
        "Default is a permanent redirect"
        response = RedirectView.as_view(url='/bar/')(DummyRequest(path='/foo/', method='DELETE'))
        self.assertEqual(response.status_code, 301)
        self.assertEqual(response.location, '/bar/')

    def test_direct_instantiation(self):
        """
        It should be possible to use the view without going through .as_view()
        (#21564).
        """
        view = RedirectView()
        response = view.dispatch(
            DummyRequest(path='/foo/', method='HEAD')
        )
        self.assertEqual(response.status_code, 410)


class GetContextDataTest(unittest.TestCase):

    def test_get_context_data_super(self):
        test_view = views.CustomContextView()
        context = test_view.get_context_data(kwarg_test='kwarg_value')

        # the test_name key is inserted by the test classes parent
        self.assertTrue('test_name' in context)
        self.assertEqual(context['kwarg_test'], 'kwarg_value')
        self.assertEqual(context['custom_key'], 'custom_value')

        # test that kwarg overrides values assigned higher up
        context = test_view.get_context_data(test_name='test_value')
        self.assertEqual(context['test_name'], 'test_value')

    def test_object_at_custom_name_in_context_data(self):
        # Checks 'pony' key presence in dict returned by get_context_date
        test_view = views.CustomSingleObjectView()
        test_view.context_object_name = 'pony'
        context = test_view.get_context_data()
        self.assertEqual(context['pony'], test_view.object)

    def test_object_in_get_context_data(self):
        # Checks 'object' key presence in dict returned by get_context_date #20234
        test_view = views.CustomSingleObjectView()
        context = test_view.get_context_data()
        self.assertEqual(context['object'], test_view.object)


class UseMultipleObjectMixinTest(unittest.TestCase):
    # rf = RequestFactory()

    def test_use_query_from_view(self):
        test_view = views.CustomMultipleObjectMixinView()
        test_view.get(DummyRequest(path='/', method='POST'))
        # Don't pass query as argument
        context = test_view.get_context_data()
        self.assertEqual(context['object_list'], test_view.query)

    def test_overwrite_query(self):
        test_view = views.CustomMultipleObjectMixinView()
        test_view.get(DummyRequest(path='/', method='POST'))
        query = [{'name': 'Lennon'}, {'name': 'Ono'}]
        self.assertNotEqual(test_view.query, query)
        # Overwrite the view's query with query from kwarg
        context = test_view.get_context_data(object_list=query)
        self.assertEqual(context['object_list'], query)


class SingleObjectTemplateResponseMixinTest(unittest.TestCase):

    def test_template_mixin_without_template(self):
        """
        We want to makes sure that if you use a template mixin, but forget the
        template, it still tells you it's ImproperlyConfigured instead of
        TemplateDoesNotExist.
        """
        view = views.TemplateResponseWithoutTemplate()
        self.assertRaises(ImproperlyConfigured, view.get_template_names)


class MacroMixinTest(BaseTest):

    def test_get_macros(self):
        class MyMacroMixin(MacroMixin):
            macro_names = {
                'forms': 'pyramid_views:macros/forms.pt'
            }
        macros = MyMacroMixin().get_macros()
        self.assertIn('forms', macros)
        self.assertIn('form', macros['forms'].names)
        self.assertIsInstance(macros['forms']['form'], Macro)

