from collections import deque

import django
from django.test.html import parse_html, Element
from django.test.utils import override_settings
from django.urls import reverse
from django.utils import encoding, translation
from django.test import TestCase
from django.contrib import auth
from .models import Article, Category
from parler.appsettings import PARLER_LANGUAGES


class TestMixin:

    def setUp(self):
        cat = Category()
        cat.name = "Cheese"
        cat.save()
        self.cat_id = cat.id

        art = Article()
        art.set_current_language('en')
        art.title = "Cheese omelet"
        art.slug = "cheese-omelet"
        art.content = "This is the wonderful recipe of a cheese omelet."
        art.set_current_language('fr')
        art.title = "Omelette du fromage"
        art.slug = "omelette-du-fromage"
        art.content = "Voilà la recette de l'omelette au fromage"
        art.category = cat

        art.save()

        self.art_id = art.id

    def assertInContent(self, member, resp, msg=None):
        return super().assertIn(member, encoding.smart_text(resp.content), msg)

    def assertNotInContent(self, member, resp, msg=None):
        return super().assertNotIn(member, encoding.smart_text(resp.content), msg)

    def assertHTMLInContent(self, html_tag, resp):
        find_html = parse_html(html_tag)
        if find_html.children:
            raise ValueError("Can only look for single tags")
        tag_name = find_html.name
        find_attrs = dict(find_html.attributes)

        html = parse_html(encoding.smart_text(resp.content))
        queue = deque()
        queue.extend(html.children)
        while queue:
            node = queue.popleft()
            if isinstance(node, Element):
                if node.name == tag_name and _is_dict_subset(find_attrs, dict(node.attributes)):
                    return

                if node.children:
                    queue.extend(node.children)

        raise AssertionError("Element <{html_tag}> not found in {html}".format(
            html_tag=html_tag,
            html=html,
        ))


def _is_dict_subset(d1, d2):
    # This is compatible with any version of Python, and doesn't care about dict ordering.
    return all(key in d2 and d2[key] == d1[key] for key in d1)


class ArticleTestCase(TestMixin, TestCase):

    @override_settings(ROOT_URLCONF='example.urls')
    def test_home(self):
        resp = self.client.get('/', follow=True)
        self.assertRedirects(resp, '/en/')
        self.assertTemplateUsed(resp, 'article/list.html')
        self.assertNotInContent("/en/cheese-omelet", resp)

        # now published
        Article.objects.filter(id=self.art_id).update(published=True)

        resp = self.client.get(reverse('article-list'))  # == /en/
        self.assertInContent("/en/cheese-omelet", resp)

    @override_settings(ROOT_URLCONF='example.urls')
    def test_view_article(self):
        resp = self.client.get(reverse('article-details', kwargs={'slug': 'cheese-omelet'}))
        self.assertEqual(404, resp.status_code)

        # now published
        Article.objects.filter(id=self.art_id).update(published=True)

        resp = self.client.get(reverse('article-details', kwargs={'slug': 'cheese-omelet'}))
        self.assertTemplateUsed(resp, 'article/details.html')
        self.assertInContent("This is the wonderful recipe of a cheese omelet.", resp)


class AdminArticleTestCase(TestMixin, TestCase):

    credentials = {
        'username': 'admin',
        'password': 'password'
    }

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.user, _ = auth.models.User.objects.get_or_create(is_superuser=True, is_staff=True, username=cls.credentials['username'])
        cls.user.set_password(cls.credentials['password'])
        cls.user.save()

    def test_admin_list(self):
        self.client.login(**self.credentials)
        resp = self.client.get(reverse('admin:article_article_changelist'))
        self.assertEqual(200, resp.status_code)
        self.assertTemplateUsed(resp, 'admin/change_list.html')

    def test_admin_add(self):
        self.client.login(**self.credentials)

        # careful, running tests from the example app with
        # python example/manage.py test article
        # will fail, because languages declared in the settings are
        # different, and not in the same order.
        self.assertEqual('nl', PARLER_LANGUAGES.get_first_language())

        resp = self.client.get(reverse('admin:article_article_add'))
        self.assertEqual(200, resp.status_code)
        self.assertIn('<h1>Add Article (Dutch)</h1>', encoding.smart_text(resp.content))

        translation.activate('fr')
        resp = self.client.get(reverse('admin:article_article_add'))
        self.assertEqual(200, resp.status_code)

        if django.VERSION >= (3, 0):
            self.assertInContent('<h1>Ajout de Article (Hollandais)</h1>', resp)
        else:
            self.assertInContent('<h1>Ajout Article (Hollandais)</h1>', resp)

        translation.activate('en')

        resp = self.client.get(reverse('admin:article_article_add'), {"language": "nl"})
        self.assertEqual(200, resp.status_code)
        self.assertInContent('<h1>Add Article (Dutch)</h1>', resp)

    def test_admin_add_post(self):
        self.client.login(**self.credentials)
        resp = self.client.post(
            reverse('admin:article_article_add'),
            {
                'title': "my article",
                'slug': "my-article",
                'content': "my super content",
            },
            follow=True
        )

        self.assertRedirects(resp, reverse('admin:article_article_changelist'))
        self.assertEqual(1, Article.objects.filter(translations__slug='my-article').count())

    def test_admin_change(self):
        self.client.login(**self.credentials)

        # careful, running tests from the example app with
        # python example/manage.py test article
        # will fail, because languages declared in the settings are
        # different, and not in the same order.
        self.assertEqual('nl', PARLER_LANGUAGES.get_first_language())

        translation.activate('en')
        resp = self.client.get(reverse('admin:article_article_change', args=[self.art_id]))
        self.assertEqual(200, resp.status_code)
        self.assertInContent('<h1>Change Article (Dutch)</h1>', resp)

        resp = self.client.get(reverse('admin:article_article_change', args=[self.art_id]), {"language": "en"})
        self.assertEqual(200, resp.status_code)
        self.assertInContent('<h1>Change Article (English)</h1>', resp)
        self.assertHTMLInContent('<input name="title" type="text" value="Cheese omelet">', resp)

        translation.activate('fr')
        resp = self.client.get(reverse('admin:article_article_change', args=[self.art_id]), {"language": "en"})
        self.assertEqual(200, resp.status_code)
        self.assertInContent('<h1>Modification de Article (Anglais)</h1>', resp)
        self.assertHTMLInContent('<input name="title" type="text" value="Cheese omelet">', resp)

        translation.activate('en')

        resp = self.client.get(reverse('admin:article_article_change', args=[self.art_id]), {"language": "nl"})
        self.assertEqual(200, resp.status_code)
        self.assertInContent('<h1>Change Article (Dutch)</h1>', resp)
        self.assertHTMLInContent('<input name="title" type="text">', resp)

    def test_admin_change_category(self):
        self.client.login(**self.credentials)
        resp = self.client.get(reverse('admin:article_category_change', args=[self.cat_id]))
        self.assertEqual(200, resp.status_code)

        self.client.login(**self.credentials)
        resp = self.client.get(reverse('admin:article_stackedcategory_change', args=[self.cat_id]))
        self.assertEqual(200, resp.status_code)

        self.client.login(**self.credentials)
        resp = self.client.get(reverse('admin:article_tabularcategory_change', args=[self.cat_id]))
        self.assertEqual(200, resp.status_code)

    def test_admin_delete_translation(self):
        self.client.login(**self.credentials)
        # delete confirmation
        resp = self.client.get(
            reverse('admin:article_article_delete_translation', args=[self.art_id, 'en']),
        )
        self.assertTemplateUsed(resp, 'admin/delete_confirmation.html')

        # we can go to the pagein nl even if there is no translation in that language
        translation.activate('nl')
        resp = self.client.get(
            reverse('admin:article_article_delete_translation', args=[self.art_id, 'en']),
        )
        self.assertTemplateUsed(resp, 'admin/delete_confirmation.html')
        translation.activate('en')

        # delete confirmed
        resp = self.client.post(
            reverse('admin:article_article_delete_translation', args=[self.art_id, 'en']),
            {"post": "yes"}
        )
        self.assertRedirects(resp, reverse('admin:article_article_change', args=(self.art_id,)))
        self.assertEqual(0, Article.objects.filter(translations__slug='cheese-omelet').count())

        # try to delete something that is not there
        resp = self.client.post(
            reverse('admin:article_article_delete_translation', args=[self.art_id, 'en']),
            {"post": "yes"}
        )
        self.assertEqual(404, resp.status_code)

        # try to delete the only remaining translation
        translation.activate('fr')
        resp = self.client.post(
            reverse('admin:article_article_delete_translation', args=[self.art_id, 'fr']),
            {"post": "yes"}
        )
        self.assertEqual(200, resp.status_code)
        self.assertTemplateUsed(resp, 'admin/parler/deletion_not_allowed.html')

    def test_admin_delete_translation_unavailable(self):
        """
        To be fixed : when trying to delete the last language when a translation
        in the current language does not exist, parler fails with exception:
            Article does not have a translation for the current language!
        """
        self.client.login(**self.credentials)
        # delete confirmed
        resp = self.client.post(
            reverse('admin:article_article_delete_translation', args=[self.art_id, 'en']),
            {"post": "yes"}
        )

        # now try to delete the last translation, but the active language is english, and there is no translation in this language
        resp = self.client.post(
            reverse('admin:article_article_delete_translation', args=[self.art_id, 'fr']),
            {"post": "yes"}
        )
        self.assertEqual(200, resp.status_code)
        self.assertTemplateUsed(resp, 'admin/parler/deletion_not_allowed.html')

    def test_admin_delete(self):

        self.client.login(**self.credentials)
        resp = self.client.post(
            reverse('admin:article_article_changelist'),
            {
                'action': 'delete_selected',
                'select_across': 0,
                'index': 0,
                '_selected_action': self.art_id,
            }
        )
        self.assertEqual(200, resp.status_code)
        self.assertTemplateUsed(resp, 'admin/delete_selected_confirmation.html')

        # confirmed deleteion
        resp = self.client.post(
            reverse('admin:article_article_changelist'),
            {
                'action': 'delete_selected',
                'post': 'yes',
                '_selected_action': self.art_id,
            },
            follow=True
        )
        self.assertRedirects(resp, reverse('admin:article_article_changelist'))
        self.assertEqual(0, Article.objects.count())
