import json
import urllib.parse
import uuid

from django.conf import settings
from django.test import TestCase, Client
from django.core.urlresolvers import reverse
from django.core import mail
from django.template.loader import get_template

from people.models import ORMAuthToken, ORMPerson, ORMConfirmationToken


class CreatePersonTestCase(TestCase):

    def test_creates_guest_person_and_returns_auth_token(self):
        CreatePersonTestCase._ScenarioMaker() \
                .given_a_client_secret_key() \
                .when_people_post_is_called_with_that_client_secret_key() \
                .then_response_status_should_be_201() \
                .then_response_body_should_be_an_auth_token() \
                .then_a_person_has_that_auth_token()

    def test_wrong_client_secret_key_returns_error(self):
        CreatePersonTestCase._ScenarioMaker() \
                .given_a_client_secret_key() \
                .when_people_post_is_called_with_other_client_secret_key() \
                .then_response_status_should_be_422() \
                .then_response_body_should_be_an_error() \


    class _ScenarioMaker:

        def __init__(self):
            self.orm_person = None
            self.orm_auth_token = None
            self.response = None
            self.client_secret_key = None

        def given_a_client_secret_key(self):
            self.client_secret_key = "scrt"
            settings.CLIENT_SECRET_KEY = self.client_secret_key
            return self

        def when_people_post_is_called_with_that_client_secret_key(self):
            client = Client()
            self.response = client.post(reverse('people'), {'client_secret_key': self.client_secret_key})
            return self

        def when_people_post_is_called_with_other_client_secret_key(self):
            client = Client()
            self.response = client.post(reverse('people'), {'client_secret_key': 'wrong_key'})
            return self

        def then_response_status_should_be_201(self):
            assert self.response.status_code == 201
            return self

        def then_response_status_should_be_422(self):
            assert self.response.status_code == 422
            return self

        def then_response_body_should_be_an_auth_token(self):
            body = json.loads(self.response.content)
            assert body['access_token'] is not None
            assert body['refresh_token'] is not None
            return self

        def then_response_body_should_be_an_error(self):
            body = json.loads(self.response.content)
            assert body == {
                    'error': {
                        'source': 'client_secret_key',
                        'code': 'invalid',
                        'message': 'Invalid client secret key'
                        }
                    }
            return self

        def then_a_person_has_that_auth_token(self):
            body = json.loads(self.response.content)
            orm_auth_token = ORMAuthToken.objects.get(access_token=body['access_token'],
                                                      refresh_token=body['refresh_token'])
            orm_person = ORMPerson.objects.get(id=orm_auth_token.person_id)
            assert orm_person is not None
            return self


class ModifyPersonTestCase(TestCase):

    def test_modify_person_username_and_email(self):
        ModifyPersonTestCase._ScenarioMaker() \
                .given_a_guest_person_in_db_with_auth_token() \
                .given_a_confirmation_token_for_that_person() \
                .given_another_confirmation_token_for_that_person() \
                .given_a_username() \
                .given_an_email() \
                .when_that_person_call_patch_people_me_with_that_params() \
                .then_response_status_should_be_200() \
                .then_response_body_should_be_person_info() \
                .then_person_should_be_updated_and_marked_as_registered() \
                .then_old_confirmation_tokens_should_be_deleted() \
                .then_ask_confirmation_email_should_be_sent()

    def test_wrong_client_secret_key_returns_error(self):
        ModifyPersonTestCase._ScenarioMaker() \
                .given_a_registered_and_confirmed_person() \
                .given_a_username() \
                .given_an_email() \
                .when_that_person_call_patch_people_me_with_that_params() \
                .then_response_status_should_be_409() \
                .then_response_body_should_be_conflict_error() \
                .then_person_not_should_be_updated() \
                .then_ask_confirmation_email_should_not_be_sent()

    def test_already_used_mail_returns_error(self):
        ModifyPersonTestCase._ScenarioMaker() \
                .given_a_guest_person_in_db_with_auth_token() \
                .given_an_email() \
                .given_a_username() \
                .given_another_confirmed_person_with_that_email() \
                .when_that_person_call_patch_people_me_with_that_params() \
                .then_response_body_should_be_422_with_invalid_email() \
                .then_person_not_should_be_updated() \
                .then_ask_confirmation_email_should_not_be_sent()

    class _ScenarioMaker:

        def __init__(self):
            self.orm_person = None
            self.orm_confirmation_token = None
            self.orm_confirmation_token_2 = None
            self.username = None
            self.email = None
            self.response = None

        def given_a_guest_person_in_db_with_auth_token(self):
            self.orm_person = ORMPerson.objects.create()
            self.orm_auth_token = ORMAuthToken.objects.create(person_id=self.orm_person.id)
            return self

        def given_a_registered_and_confirmed_person(self):
            self.orm_person = ORMPerson.objects.create(username='u', email='e@m.c',
                                                       is_registered=True, is_email_confirmed=True)
            self.orm_auth_token = ORMAuthToken.objects.create(person_id=self.orm_person.id)
            return self

        def given_a_confirmation_token_for_that_person(self):
            self.orm_confirmation_token = ORMConfirmationToken.objects.create(person_id=self.orm_person.id)
            return self

        def given_another_confirmation_token_for_that_person(self):
            self.orm_confirmation_token_2 = ORMConfirmationToken.objects.create(person_id=self.orm_person.id)
            return self

        def given_another_confirmed_person_with_that_email(self):
            ORMPerson.objects.create(username='oo', email=self.email, is_registered=True, is_email_confirmed=True)
            return self

        def given_a_username(self):
            self.username = 'usr.nm'
            return self

        def given_an_email(self):
            self.email = 'usr@m.c'
            return self

        def when_that_person_call_patch_people_me_with_that_params(self):
            client = Client()
            auth_headers = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.orm_auth_token.access_token), }
            self.response = client.patch(reverse('person'),
                                         urllib.parse.urlencode({'username': self.username, 'email': self.email}),
                                         content_type='application/x-www-form-urlencoded',
                                         **auth_headers)
            return self

        def then_response_status_should_be_200(self):
            assert self.response.status_code == 200
            return self

        def then_response_body_should_be_person_info(self):
            body = json.loads(self.response.content)
            assert body['username'] == self.username
            assert body['email'] == self.email
            assert body['is_registered'] is True
            assert body['is_email_confirmed'] is False
            return self

        def then_person_should_be_updated_and_marked_as_registered(self):
            orm_updated_person = ORMPerson.objects.get(id=self.orm_person.id)
            assert orm_updated_person.username == self.username
            assert orm_updated_person.email == self.email
            assert orm_updated_person.is_registered is True
            assert orm_updated_person.is_email_confirmed is False
            return self

        def then_old_confirmation_tokens_should_be_deleted(self):
            assert not ORMConfirmationToken.objects.filter(token=self.orm_confirmation_token.token).exists()
            assert not ORMConfirmationToken.objects.filter(token=self.orm_confirmation_token_2.token).exists()
            return self

        def then_ask_confirmation_email_should_be_sent(self):
            assert mail.outbox[0].subject == 'Abidria account confirmation'
            confirmation_token = ORMConfirmationToken.objects.get(person_id=self.orm_person.id).token
            confirmation_reverse_url = self.response.wsgi_request.build_absolute_uri(reverse('email-confirmation'))
            confirmation_url = "{}?token={}".format(confirmation_reverse_url, confirmation_token)
            context_params = {'username': self.username, 'confirmation_url': confirmation_url}
            plain_text_message = get_template('ask_confirmation_email.txt').render(context_params)
            html_message = get_template('ask_confirmation_email.html').render(context_params)
            assert mail.outbox[0].body == plain_text_message
            assert mail.outbox[0].from_email == settings.EMAIL_HOST_USER
            assert mail.outbox[0].to == [self.email, ]
            assert mail.outbox[0].alternatives[0][0] == html_message
            return self

        def then_response_status_should_be_409(self):
            assert self.response.status_code == 409
            return self

        def then_response_body_should_be_conflict_error(self):
            body = json.loads(self.response.content)
            assert body == {
                    'error': {
                        'source': 'person',
                        'code': 'already_registered',
                        'message': 'Person already registered'
                        }
                    }
            return self

        def then_response_body_should_be_422_with_invalid_email(self):
            body = json.loads(self.response.content)
            assert body == {
                    'error': {
                        'source': 'email',
                        'code': 'not_allowed',
                        'message': 'Email not allowed'
                        }
                    }
            return self

        def then_person_not_should_be_updated(self):
            orm_updated_person = ORMPerson.objects.get(id=self.orm_person.id)
            assert orm_updated_person.username != self.username
            assert orm_updated_person.email != self.email
            assert orm_updated_person.is_registered == self.orm_person.is_registered
            assert orm_updated_person.is_email_confirmed == self.orm_person.is_email_confirmed
            return self

        def then_ask_confirmation_email_should_not_be_sent(self):
            assert len(mail.outbox) == 0
            return self


class PostEmailConfirmationTestCase(TestCase):

    def test_post_email_confirmations_confirms_person_email(self):
        PostEmailConfirmationTestCase.ScenarioMaker() \
                .given_an_unconfirmed_registered_person() \
                .given_an_auth_token_for_that_person() \
                .given_a_confirmation_token_for_that_person() \
                .when_post_email_confirmation() \
                .then_response_should_be_200_and_person() \
                .then_person_should_have_is_email_confirmed_true() \
                .then_confirmation_token_should_be_removed()

    def test_post_email_confirmation_with_invalid_token_returns_error(self):
        PostEmailConfirmationTestCase.ScenarioMaker() \
                .given_an_unconfirmed_registered_person() \
                .given_an_auth_token_for_that_person() \
                .given_a_fake_confirmation_token() \
                .when_post_email_confirmation() \
                .then_response_code_should_be_422() \
                .then_response_body_should_be_invalide_token_error() \
                .then_person_should_have_is_email_confirmed_false()

    class ScenarioMaker:

        def __init__(self):
            self.orm_person = None
            self.orm_auth_token = None
            self.orm_confirmation_token = None
            self.result = None

        def given_an_unconfirmed_registered_person(self):
            self.orm_person = ORMPerson.objects.create(is_registered=True, username='usr',
                                                       email='e@m.c', is_email_confirmed=False)
            return self

        def given_an_auth_token_for_that_person(self):
            self.orm_auth_token = ORMAuthToken.objects.create(person_id=self.orm_person.id)
            return self

        def given_a_confirmation_token_for_that_person(self):
            self.orm_confirmation_token = ORMConfirmationToken.objects.create(person_id=self.orm_person.id)
            self.confirmation_token = self.orm_confirmation_token.token
            return self

        def given_a_fake_confirmation_token(self):
            self.confirmation_token = uuid.uuid4()
            return self

        def when_post_email_confirmation(self):
            client = Client()
            auth_headers = {'HTTP_AUTHORIZATION': 'Token {}'.format(self.orm_auth_token.access_token), }
            self.response = client.post(reverse('email-confirmation'),
                                        urllib.parse.urlencode({'confirmation_token': self.confirmation_token}),
                                        content_type='application/x-www-form-urlencoded',
                                        **auth_headers)
            return self

        def then_response_should_be_200_and_person(self):
            self.response.status_code == 204
            body = json.loads(self.response.content)
            assert body == {
                    'is_registered': True,
                    'username': 'usr',
                    'email': 'e@m.c',
                    'is_email_confirmed': True
                    }
            return self

        def then_person_should_have_is_email_confirmed_true(self):
            assert ORMPerson.objects.get(id=self.orm_person.id).is_email_confirmed is True
            return self

        def then_confirmation_token_should_be_removed(self):
            assert not ORMConfirmationToken.objects.filter(token=self.orm_confirmation_token.token).exists()
            return self

        def then_response_code_should_be_422(self):
            assert self.response.status_code == 422
            return self

        def then_response_body_should_be_invalide_token_error(self):
            body = json.loads(self.response.content)
            assert body == {
                    'error': {
                        'source': 'confirmation_token',
                        'code': 'invalid',
                        'message': 'Invalid confirmation token'
                        }
                    }
            return self

        def then_person_should_have_is_email_confirmed_false(self):
            assert ORMPerson.objects.get(id=self.orm_person.id).is_email_confirmed is False
            return self