import datetime
import json
import warnings

from django.core import mail
from django.core.signing import BadSignature
from django.contrib.auth.models import User
from django.test import TestCase, Client, override_settings
from freezegun import freeze_time
import pytz

from contribmap.models import Contrib
from contribmap.forms import PublicContribForm
from contribmap.tokens import ContribTokenManager, URLTokenManager


class APITestClient(Client):
    def json_get(self, *args, **kwargs):
        """ Annotate the response with a .data containing parsed JSON
        """
        response = super().get(*args, **kwargs)
        response.data = json.loads(response.content.decode('utf-8'))
        return response


class APITestCase(TestCase):
    def setUp(self):
        super().setUp()
        self.client = APITestClient()


class TestContrib(TestCase):
    def test_comma_separatedcharfield(self):
        co = Contrib(name='foo', orientations=['SO', 'NE'],
                     contrib_type=Contrib.CONTRIB_CONNECT,
                     latitude=0.5, longitude=0.5,
)
        co.save()
        self.assertEqual(
            Contrib.objects.get(name='foo').orientations,
            ['SO', 'NE'])
        co.orientations = ['S']
        co.save()


class TestContribPrivacy(TestCase):
    def test_always_private_field(self):
        c = Contrib.objects.create(
            name='John',
            phone='010101010101',
            contrib_type=Contrib.CONTRIB_CONNECT,
            latitude=0.5,
            longitude=0.5,
        )
        self.assertEqual(c.get_public_field('phone'), None)

    def test_public_field(self):
        c = Contrib.objects.create(
            name='John',
            phone='010101010101',
            contrib_type=Contrib.CONTRIB_CONNECT,
            privacy_name=True,
            latitude=0.5,
            longitude=0.5,
        )
        self.assertEqual(c.get_public_field('name'), 'John')

    def test_public_callable_field(self):
        c = Contrib.objects.create(
            name='John',
            phone='010101010101',
            orientations=['N'],
            contrib_type=Contrib.CONTRIB_CONNECT,
            privacy_name=True,
            latitude=0.5,
            longitude=0.5,
        )
        self.assertEqual(c.get_public_field('angles'), [[-23, 22]])

    def test_private_field(self):
        c = Contrib.objects.create(
            name='John',
            phone='010101010101',
            contrib_type=Contrib.CONTRIB_CONNECT,
            latitude=0.5,
            longitude=0.5,
        )
        self.assertEqual(c.privacy_name, False)
        self.assertEqual(c.get_public_field('name'), None)


class TestViews(APITestCase):
    def mk_contrib_post_data(self, *args, **kwargs):
        post_data = {
            'roof': True,
            'privacy_place_details': True,
            'privacy_coordinates': True,
            'phone': '0202020202',
            'orientations': ('N', 'NO', 'O', 'SO', 'S', 'SE', 'E', 'NE'),
            'orientation': 'all',
            'name': 'JohnCleese',
            'longitude': -1.553621,
            'latitude': 47.218371,
            'floor_total': '2',
            'floor': 1,
            'email': 'coucou@example.com',
            'contrib_type': 'connect',
            'connect_local': 'on',
        }
        post_data.update(kwargs)
        return post_data

    def test_public_json(self):
        response = self.client.json_get('/map/public.json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data['features']), 0)

        Contrib.objects.create(
            name='John',
            phone='010101010101',
            contrib_type=Contrib.CONTRIB_CONNECT,
            privacy_coordinates=True,
            latitude=0.5,
            longitude=0.5,
        )
        response = self.client.json_get('/map/public.json')
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data['features']), 1)

    def test_private_json(self):
        self.client.force_login(
            User.objects.create(username='foo', is_staff=False))

        response = self.client.get('/map/private.json')
        self.assertEqual(response.status_code, 403)

    def test_private_json_staff(self):
        self.client.force_login(
            User.objects.create(username='foo', is_staff=True))
        response = self.client.get('/map/private.json')
        self.assertEqual(response.status_code, 200)

    @override_settings(NOTIFICATION_EMAILS=['foo@example.com'])
    def test_add_contrib_sends_moderator_email(self):
        post_data = self.mk_contrib_post_data({'name': 'JohnCleese'})
        del post_data['email']

        response = self.client.post('/map/contribute', post_data)
        self.assertEqual(response.status_code, 302)

        self.assertEqual(len(mail.outbox), 1)
        self.assertIn('JohnCleese', mail.outbox[0].subject)
        self.assertIn('JohnCleese', mail.outbox[0].body)
        self.assertEqual(mail.outbox[0].recipients(), ['foo@example.com'])

    def test_add_contrib_sends_no_author_email(self):
        # Send no email if author did not mentioned an email
        post_data = self.mk_contrib_post_data()
        del post_data['email']

        response = self.client.post('/map/contribute', post_data)
        self.assertEqual(response.status_code, 302)
        self.assertEqual(len(mail.outbox), 0)

    def test_add_contrib_sends_author_email(self):
        # Send no email if author did not mentioned an email
        response = self.client.post(
            '/map/contribute',
            self.mk_contrib_post_data(email='author@example.com'))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(len(mail.outbox), 1)


class TestManageView(APITestCase):
    def setUp(self):
        self.contrib = Contrib.objects.create(
            name='John',
            phone='010101010101',
            contrib_type=Contrib.CONTRIB_CONNECT,
            privacy_coordinates=True,
            latitude=0.5,
            longitude=0.5,
        )
        self.token = ContribTokenManager().mk_token(self.contrib)

    def test_manage_with_token(self):
        # No token
        response = self.client.get('/map/manage/{}'.format(self.contrib.pk))
        self.assertEqual(response.status_code, 403)

        # Garbage token
        response = self.client.get(
            '/map/manage/{}?token=burp'.format(self.contrib.pk))
        self.assertEqual(response.status_code, 403)

        # Valid token, but for another contrib
        contrib2 = Contrib.objects.create(
            name='John2',
            phone='010101010101',
            contrib_type=Contrib.CONTRIB_CONNECT,
            privacy_coordinates=True,
            latitude=0.5,
            longitude=0.5,
        )
        token2 = ContribTokenManager().mk_token(contrib2)

        response = self.client.get('/map/manage/{}?token={}'.format(
            self.contrib.pk, token2))
        self.assertEqual(response.status_code, 403)

        # Normal legitimate access case
        response = self.client.get(
            '/map/manage/{}?token={}'.format(self.contrib.pk, self.token))
        self.assertEqual(response.status_code, 200)

        # Deleted contrib
        Contrib.objects.all().delete()
        response = self.client.get(
            '/map/manage/{}?token={}'.format(self.contrib.pk, self.token))
        self.assertEqual(response.status_code, 404)

    def test_delete(self):
        response = self.client.post(
            '/map/manage/{}?token={}'.format(self.contrib.pk, self.token),
            {'action': 'delete'})
        self.assertEqual(response.status_code, 302)
        self.assertFalse(Contrib.objects.filter(pk=self.contrib.pk).exists())

    def test_renew(self):
        self.contrib.date = datetime.datetime(2009, 10, 10, tzinfo=pytz.utc)
        self.contrib.expiration_date = datetime.datetime(2010, 10, 10, tzinfo=pytz.utc)
        self.contrib.save()

        with freeze_time('12-12-2100', tz_offset=0):
            response = self.client.post(
                '/map/manage/{}?token={}'.format(self.contrib.pk, self.token),
                {'action': 'renew'})
        self.assertEqual(response.status_code, 200)
        self.contrib = Contrib.objects.get(pk=self.contrib.pk)  # refresh
        self.assertEqual(
            self.contrib.expiration_date.date(),
            datetime.date(2101, 12, 12))


class TestForms(TestCase):
    valid_data = {
        'roof': True,
        'privacy_place_details': True,
        'privacy_coordinates': True,
        'orientations': ['N'],
        'orientation': 'all',
        'name': 'JohnCleese',
        'longitude': -1.553621,
        'email': 'foo@example.com',
        'phone': '0202020202',
        'latitude': 47.218371,
        'floor_total': '2',
        'floor': 1,
        'contrib_type': 'connect',
        'connect_local': 'on',
    }

    def test_contact_validation(self):
        no_contact, phone_contact, email_contact, both_contact = [
            self.valid_data.copy() for i in range(4)]

        del phone_contact['email']
        del email_contact['phone']
        del no_contact['phone']
        del no_contact['email']

        both_contact.update(phone_contact)
        both_contact.update(email_contact)

        self.assertFalse(PublicContribForm(no_contact).is_valid())
        self.assertTrue(PublicContribForm(phone_contact).is_valid())
        self.assertTrue(PublicContribForm(email_contact).is_valid())
        self.assertTrue(PublicContribForm(both_contact).is_valid())

    def test_floors_validation(self):
        invalid_floors = self.valid_data.copy()
        invalid_floors['floor'] = 2
        invalid_floors['floor_total'] = 1

        self.assertFalse(PublicContribForm(invalid_floors).is_valid())
        self.assertTrue(PublicContribForm(self.valid_data).is_valid())

        invalid_floors['floor'] = None
        invalid_floors['floor_total'] = None
        self.assertTrue(PublicContribForm(invalid_floors).is_valid())

    def test_share_fields_validation(self):
        data = self.valid_data.copy()
        data['contrib_type'] = 'share'

        self.assertFalse(PublicContribForm(data).is_valid())
        data['access_type'] = 'cable'
        self.assertTrue(PublicContribForm(data).is_valid())

    @override_settings(NOTIFICATION_EMAILS=['foo@example.com'])
    def test_add_contrib_like_a_robot(self):
        robot_data = self.valid_data.copy()
        robot_data['human_field'] = 'should contain no value'
        response = self.client.post('/map/contribute', robot_data)
        self.assertEqual(response.status_code, 403)
        self.assertEqual(len(mail.outbox), 0)


class TestDataImport(TestCase):
    fixtures = ['bottle_data.yaml']

    @classmethod
    def setUpClass(cls, *args, **kwargs):
        # Silence the warnings about naive datetimes contained in the yaml.
        with warnings.catch_warnings():  # Scope warn catch to this block
            warnings.simplefilter('ignore', RuntimeWarning)
            return super().setUpClass(*args, **kwargs)

    def test_re_save(self):
        for contrib in Contrib.objects.all():
            contrib.full_clean()
            contrib.save()


class URLTokenManagerTests(TestCase):
    def test_sign_unsign_ok(self):
        input_data = {'foo': 12}
        at = URLTokenManager().sign(input_data)
        output_data = URLTokenManager().unsign(at)
        self.assertEqual(input_data, output_data)

    def test_sign_unsign_wrong_sig(self):
        with self.assertRaises(BadSignature):
            URLTokenManager().unsign(
                b'eyJmb28iOiAxfTpvUFZ1Q3FsSldtQ2htMXJBMmx5VFV0ZWxDLWM')


class ContribTokenManagerTests(TestCase):
    def test_sign_unsign_ok(self):
        Contrib.objects.create(
            name='John2',
            phone='010101020101',
            contrib_type=Contrib.CONTRIB_CONNECT,
            privacy_coordinates=True,
            latitude=0.1,
            longitude=0.12,
        )

        contrib = Contrib.objects.all().first()

        manager = ContribTokenManager()
        token = manager.mk_token(contrib)

        self.assertEqual(
            manager.get_instance_if_allowed(token, contrib.pk),
            contrib)