# -*- coding: utf-8 -*-
from contextlib import nested

import django
from django.conf import settings
from django.db import connections, reset_queries
from django.test.utils import override_settings
from django.utils import translation
from django.utils.functional import lazy

import jinja2
import multidb
from mock import patch
from nose import SkipTest
from nose.tools import eq_
from test_utils import trans_eq, TestCase

from testapp.models import TranslatedModel, UntranslatedModel, FancyModel
from translations import widgets
from translations.query import order_by_translation
from translations.models import (LinkifiedTranslation, PurifiedTranslation,
                                 Translation, TranslationSequence)


def ids(qs):
    return [o.id for o in qs]


class TranslationFixturelessTestCase(TestCase):
    "We want to be able to rollback stuff."

    def test_whitespace(self):
        t = Translation(localized_string='     khaaaaaan!    ', id=999)
        t.save()
        eq_('khaaaaaan!', t.localized_string)


class TranslationSequenceTestCase(TestCase):
    """
    Make sure automatic translation sequence generation works
    as expected.
    """

    def test_empty_translations_seq(self):
        """Make sure we can handle an empty translation sequence table."""
        TranslationSequence.objects.all().delete()
        newtrans = Translation.new('abc', 'en-us')
        newtrans.save()
        assert newtrans.id > 0, (
            'Empty translation table should still generate an ID.')

    def test_single_translation_sequence(self):
        """Make sure we only ever have one translation sequence."""
        TranslationSequence.objects.all().delete()
        eq_(TranslationSequence.objects.count(), 0)
        for i in range(5):
            newtrans = Translation.new(str(i), 'en-us')
            newtrans.save()
            eq_(TranslationSequence.objects.count(), 1)

    def test_translation_sequence_increases(self):
        """Make sure translation sequence increases monotonically."""
        newtrans1 = Translation.new('abc', 'en-us')
        newtrans1.save()
        newtrans2 = Translation.new('def', 'de')
        newtrans2.save()
        assert newtrans2.pk > newtrans1.pk, (
            'Translation sequence needs to keep increasing.')


class TranslationTestCase(TestCase):
    fixtures = ['testapp/test_models.json']

    def setUp(self):
        super(TranslationTestCase, self).setUp()
        self.redirect_url = settings.REDIRECT_URL
        self.redirect_secret_key = settings.REDIRECT_SECRET_KEY
        settings.REDIRECT_URL = None
        settings.REDIRECT_SECRET_KEY = 'sekrit'
        translation.activate('en-US')

    def tearDown(self):
        super(TranslationTestCase, self).tearDown()
        settings.REDIRECT_URL = self.redirect_url
        settings.REDIRECT_SECRET_KEY = self.redirect_secret_key

    def test_meta_translated_fields(self):
        assert not hasattr(UntranslatedModel._meta, 'translated_fields')

        eq_(set(TranslatedModel._meta.translated_fields),
            set([TranslatedModel._meta.get_field('no_locale'),
                 TranslatedModel._meta.get_field('name'),
                 TranslatedModel._meta.get_field('description')]))

        eq_(set(FancyModel._meta.translated_fields),
            set([FancyModel._meta.get_field('purified'),
                 FancyModel._meta.get_field('linkified')]))

    def test_fetch_translations(self):
        """Basic check of fetching translations in the current locale."""
        o = TranslatedModel.objects.get(id=1)
        trans_eq(o.name, 'some name', 'en-US')
        trans_eq(o.description, 'some description', 'en-US')

    def test_fetch_no_translations(self):
        """Make sure models with no translations aren't harmed."""
        o = UntranslatedModel.objects.get(id=1)
        eq_(o.number, 17)

    def test_fetch_translation_de_locale(self):
        """Check that locale fallbacks work."""
        try:
            translation.activate('de')
            o = TranslatedModel.objects.get(id=1)
            trans_eq(o.name, 'German!! (unst unst)', 'de')
            trans_eq(o.description, 'some description', 'en-US')
        finally:
            translation.deactivate()

    def test_create_translation(self):
        o = TranslatedModel.objects.create(name='english name')
        get_model = lambda: TranslatedModel.objects.get(id=o.id)
        trans_eq(o.name, 'english name', 'en-US')
        eq_(o.description, None)

        # Make sure the translation id is stored on the model, not the autoid.
        eq_(o.name.id, o.name_id)

        # Check that a different locale creates a new row with the same id.
        translation.activate('de')
        german = get_model()
        trans_eq(o.name, 'english name', 'en-US')

        german.name = u'Gemütlichkeit name'
        german.description = u'clöüserw description'
        german.save()

        trans_eq(german.name, u'Gemütlichkeit name', 'de')
        trans_eq(german.description, u'clöüserw description', 'de')

        # ids should be the same, autoids are different.
        eq_(o.name.id, german.name.id)
        assert o.name.autoid != german.name.autoid

        # Check that de finds the right translation.
        fresh_german = get_model()
        trans_eq(fresh_german.name, u'Gemütlichkeit name', 'de')
        trans_eq(fresh_german.description, u'clöüserw description', 'de')

        # Check that en-US has the right translations.
        translation.deactivate()
        english = get_model()
        trans_eq(english.name, 'english name', 'en-US')
        english.debug = True
        eq_(english.description, None)

        english.description = 'english description'
        english.save()

        fresh_english = get_model()
        trans_eq(fresh_english.description, 'english description', 'en-US')
        eq_(fresh_english.description.id, fresh_german.description.id)

    def test_update_translation(self):
        o = TranslatedModel.objects.get(id=1)
        translation_id = o.name.autoid

        o.name = 'new name'
        o.save()

        o = TranslatedModel.objects.get(id=1)
        trans_eq(o.name, 'new name', 'en-US')
        # Make sure it was an update, not an insert.
        eq_(o.name.autoid, translation_id)

    def test_create_with_dict(self):
        # Set translations with a dict.
        strings = {'en-US': 'right language', 'de': 'wrong language'}
        o = TranslatedModel.objects.create(name=strings)

        # Make sure we get the English text since we're in en-US.
        trans_eq(o.name, 'right language', 'en-US')

        # Check that de was set.
        translation.activate('de')
        o = TranslatedModel.objects.get(id=o.id)
        trans_eq(o.name, 'wrong language', 'de')

        # We're in de scope, so we should see the de text.
        de = TranslatedModel.objects.create(name=strings)
        trans_eq(o.name, 'wrong language', 'de')

        # Make sure en-US was still set.
        translation.deactivate()
        o = TranslatedModel.objects.get(id=de.id)
        trans_eq(o.name, 'right language', 'en-US')

    def test_update_with_dict(self):
        # There's existing en-US and de strings.
        strings = {'de': None, 'fr': 'oui'}
        get_model = lambda: TranslatedModel.objects.get(id=1)

        # Don't try checking that the model's name value is en-US.  It will be
        # one of the other locales, but we don't know which one.  You just set
        # the name to a dict, deal with it.
        m = get_model()
        m.name = strings
        m.save()

        # en-US was not touched.
        trans_eq(get_model().name, 'some name', 'en-US')

        # de was updated to NULL, so it falls back to en-US.
        translation.activate('de')
        trans_eq(get_model().name, 'some name', 'en-US')

        # fr was added.
        translation.activate('fr')
        trans_eq(get_model().name, 'oui', 'fr')

    def test_dict_with_hidden_locale(self):
        with self.settings(HIDDEN_LANGUAGES=('xxx',)):
            o = TranslatedModel.objects.get(id=1)
            o.name = {'en-US': 'active language', 'xxx': 'hidden language',
                      'de': 'another language'}
            o.save()
        ts = Translation.objects.filter(id=o.name_id)
        eq_(sorted(ts.values_list('locale', flat=True)),
            ['de', 'en-US', 'xxx'])

    def test_dict_bad_locale(self):
        m = TranslatedModel.objects.get(id=1)
        m.name = {'de': 'oof', 'xxx': 'bam', 'es': 'si'}
        m.save()

        ts = Translation.objects.filter(id=m.name_id)
        eq_(sorted(ts.values_list('locale', flat=True)),
            ['de', 'en-US', 'es'])

    def test_sorting(self):
        """Test translation comparisons in Python code."""
        b = Translation.new('bbbb', 'de')
        a = Translation.new('aaaa', 'de')
        c = Translation.new('cccc', 'de')
        eq_(sorted([c, a, b]), [a, b, c])

    def test_sorting_en(self):
        q = TranslatedModel.objects.all()
        expected = [4, 1, 3]

        eq_(ids(order_by_translation(q, 'name')), expected)
        eq_(ids(order_by_translation(q, '-name')), list(reversed(expected)))

    def test_sorting_mixed(self):
        translation.activate('de')
        q = TranslatedModel.objects.all()
        expected = [1, 4, 3]

        eq_(ids(order_by_translation(q, 'name')), expected)
        eq_(ids(order_by_translation(q, '-name')), list(reversed(expected)))

    def test_sorting_by_field(self):
        field = TranslatedModel._meta.get_field('default_locale')
        TranslatedModel.get_fallback = classmethod(lambda cls: field)

        translation.activate('de')
        q = TranslatedModel.objects.all()
        expected = [3, 1, 4]

        eq_(ids(order_by_translation(q, 'name')), expected)
        eq_(ids(order_by_translation(q, '-name')), list(reversed(expected)))

        del TranslatedModel.get_fallback

    def test_new_purified_field(self):
        # This is not a full test of the html sanitizing.  We expect the
        # underlying bleach library to have full tests.
        s = '<a id=xx href="http://xxx.com">yay</a> <i>http://yyy.com</i>'
        m = FancyModel.objects.create(purified=s)
        eq_(m.purified.localized_string_clean,
            '<a href="http://xxx.com" rel="nofollow">yay</a> '
            '<i><a href="http://yyy.com" rel="nofollow">'
            'http://yyy.com</a></i>')
        eq_(m.purified.localized_string, s)

    def test_new_linkified_field(self):
        s = '<a id=xx href="http://xxx.com">yay</a> <i>http://yyy.com</i>'
        m = FancyModel.objects.create(linkified=s)
        eq_(m.linkified.localized_string_clean,
            '<a href="http://xxx.com" rel="nofollow">yay</a> '
            '&lt;i&gt;<a href="http://yyy.com" rel="nofollow">'
            'http://yyy.com</a>&lt;/i&gt;')
        eq_(m.linkified.localized_string, s)

    def test_update_purified_field(self):
        m = FancyModel.objects.get(id=1)
        s = '<a id=xx href="http://xxx.com">yay</a> <i>http://yyy.com</i>'
        m.purified = s
        m.save()
        eq_(m.purified.localized_string_clean,
            '<a href="http://xxx.com" rel="nofollow">yay</a> '
            '<i><a href="http://yyy.com" rel="nofollow">'
            'http://yyy.com</a></i>')
        eq_(m.purified.localized_string, s)

    def test_update_linkified_field(self):
        m = FancyModel.objects.get(id=1)
        s = '<a id=xx href="http://xxx.com">yay</a> <i>http://yyy.com</i>'
        m.linkified = s
        m.save()
        eq_(m.linkified.localized_string_clean,
            '<a href="http://xxx.com" rel="nofollow">yay</a> '
            '&lt;i&gt;<a href="http://yyy.com" rel="nofollow">'
            'http://yyy.com</a>&lt;/i&gt;')
        eq_(m.linkified.localized_string, s)

    def test_purified_field_str(self):
        m = FancyModel.objects.get(id=1)
        eq_(u'%s' % m.purified,
            '<i>x</i> '
            '<a href="http://yyy.com" rel="nofollow">http://yyy.com</a>')

    def test_linkified_field_str(self):
        m = FancyModel.objects.get(id=1)
        eq_(u'%s' % m.linkified,
            '&lt;i&gt;x&lt;/i&gt; '
            '<a href="http://yyy.com" rel="nofollow">http://yyy.com</a>')

    def test_purifed_linkified_fields_in_template(self):
        m = FancyModel.objects.get(id=1)
        env = jinja2.Environment()
        t = env.from_string('{{ m.purified }}=={{ m.linkified }}')
        s = t.render(m=m)
        eq_(s, u'%s==%s' % (m.purified.localized_string_clean,
                            m.linkified.localized_string_clean))

    def test_outgoing_url(self):
        """
        Make sure linkified field is properly bounced off our outgoing URL
        redirector.
        """
        settings.REDIRECT_URL = 'http://example.com/'

        s = 'I like http://example.org/awesomepage.html .'
        m = FancyModel.objects.create(linkified=s)
        eq_(m.linkified.localized_string_clean,
            'I like <a href="http://example.com/'
            '40979175e3ef6d7a9081085f3b99f2f05447b22ba790130517dd62b7ee59ef94/'
            'http%3A//example.org/'
            'awesomepage.html" rel="nofollow">http://example.org/awesomepage'
            '.html</a> .')
        eq_(m.linkified.localized_string, s)

    def test_require_locale(self):
        obj = TranslatedModel.objects.get(id=1)
        eq_(unicode(obj.no_locale), 'blammo')
        eq_(obj.no_locale.locale, 'en-US')

        # Switch the translation to a locale we wouldn't pick up by default.
        obj.no_locale.locale = 'fr'
        obj.no_locale.save()

        obj = TranslatedModel.objects.get(id=1)
        eq_(unicode(obj.no_locale), 'blammo')
        eq_(obj.no_locale.locale, 'fr')

    def test_delete_set_null(self):
        """
        Test that deleting a translation sets the corresponding FK to NULL,
        if it was the only translation for this field.
        """
        obj = TranslatedModel.objects.get(id=1)
        trans_id = obj.description.id
        eq_(Translation.objects.filter(id=trans_id).count(), 1)

        obj.description.delete()

        obj = TranslatedModel.objects.no_cache().get(id=1)
        eq_(obj.description_id, None)
        eq_(obj.description, None)
        eq_(Translation.objects.no_cache().filter(id=trans_id).exists(), False)

    @patch.object(TranslatedModel, 'get_fallback', create=True)
    def test_delete_keep_other_translations(self, get_fallback):
        # To make sure both translations for the name are used, set the
        # fallback to the second locale, which is 'de'.
        get_fallback.return_value = 'de'

        obj = TranslatedModel.objects.get(id=1)

        orig_name_id = obj.name.id
        eq_(obj.name.locale.lower(), 'en-us')
        eq_(Translation.objects.filter(id=orig_name_id).count(), 2)

        obj.name.delete()

        obj = TranslatedModel.objects.no_cache().get(id=1)
        eq_(Translation.objects.no_cache().filter(id=orig_name_id).count(), 1)

        # We shouldn't have set name_id to None.
        eq_(obj.name_id, orig_name_id)

        # We should find a Translation.
        eq_(obj.name.id, orig_name_id)
        eq_(obj.name.locale, 'de')


class TranslationMultiDbTests(TestCase):
    fixtures = ['testapp/test_models.json']

    def setUp(self):
        super(TranslationMultiDbTests, self).setUp()
        translation.activate('en-US')

    def tearDown(self):
        self.cleanup_fake_connections()
        super(TranslationMultiDbTests, self).tearDown()

    @property
    def mocked_dbs(self):
        return {
            'default': settings.DATABASES['default'],
            'slave-1': settings.DATABASES['default'].copy(),
            'slave-2': settings.DATABASES['default'].copy(),
        }

    def cleanup_fake_connections(self):
        with patch.object(django.db.connections, 'databases', self.mocked_dbs):
            for key in ('default', 'slave-1', 'slave-2'):
                connections[key].close()

    @override_settings(DEBUG=True)
    def test_translations_queries(self):
        # Make sure we are in a clean environnement.
        reset_queries()
        TranslatedModel.objects.get(pk=1)
        eq_(len(connections['default'].queries), 3)

    @override_settings(DEBUG=True)
    def test_translations_reading_from_multiple_db(self):
        with patch.object(django.db.connections, 'databases', self.mocked_dbs):
            # Make sure we are in a clean environnement.
            reset_queries()

            with patch('multidb.get_slave', lambda: 'slave-2'):
                TranslatedModel.objects.get(pk=1)
                eq_(len(connections['default'].queries), 0)
                eq_(len(connections['slave-1'].queries), 0)
                eq_(len(connections['slave-2'].queries), 3)

    @override_settings(DEBUG=True)
    def test_translations_reading_from_multiple_db_using(self):
        raise SkipTest('Will need a django-queryset-transform patch to work')
        with patch.object(django.db.connections, 'databases', self.mocked_dbs):
            # Make sure we are in a clean environnement.
            reset_queries()

            with patch('multidb.get_slave', lambda: 'slave-2'):
                TranslatedModel.objects.using('slave-1').get(pk=1)
                eq_(len(connections['default'].queries), 0)
                eq_(len(connections['slave-1'].queries), 3)
                eq_(len(connections['slave-2'].queries), 0)

    @override_settings(DEBUG=True)
    def test_translations_reading_from_multiple_db_pinning(self):
        with patch.object(django.db.connections, 'databases', self.mocked_dbs):
            # Make sure we are in a clean environnement.
            reset_queries()

            with nested(patch('multidb.get_slave', lambda: 'slave-2'),
                        multidb.pinning.use_master):
                TranslatedModel.objects.get(pk=1)
                eq_(len(connections['default'].queries), 3)
                eq_(len(connections['slave-1'].queries), 0)
                eq_(len(connections['slave-2'].queries), 0)


def test_translation_bool():
    t = lambda s: Translation(localized_string=s)

    assert bool(t('text')) is True
    assert bool(t(' ')) is False
    assert bool(t('')) is False
    assert bool(t(None)) is False


def test_translation_unicode():
    t = lambda s: Translation(localized_string=s)

    eq_(unicode(t('hello')), 'hello')
    eq_(unicode(t(None)), '')


def test_widget_value_from_datadict():
    data = {'f_en-US': 'woo', 'f_de': 'herr', 'f_fr_delete': ''}
    actual = widgets.TransMulti().value_from_datadict(data, [], 'f')
    expected = {'en-US': 'woo', 'de': 'herr', 'fr': None}
    eq_(actual, expected)


def test_purified_translation_html():
    """__html__() should return a string."""
    s = u'<b>heyhey</b>'
    x = PurifiedTranslation(localized_string=s)
    assert isinstance(x.__html__(), unicode)
    eq_(x.__html__(), s)


def test_comparison_with_lazy():
    x = Translation(localized_string='xxx')
    lazy_u = lazy(lambda x: x, unicode)
    x == lazy_u('xxx')
    lazy_u('xxx') == x


def test_cache_key():
    # Test that we are not taking the db into account when building our
    # cache keys for django-cache-machine. See bug 928881.
    eq_(Translation._cache_key(1, 'default'),
        Translation._cache_key(1, 'slave'))

    # Test that we are using the same cache no matter what Translation class
    # we use.
    eq_(PurifiedTranslation._cache_key(1, 'default'),
        Translation._cache_key(1, 'default'))
    eq_(LinkifiedTranslation._cache_key(1, 'default'),
        Translation._cache_key(1, 'default'))
