import datetime
import json
import re
from unittest.mock import patch

from django.test import Client
from django.test.client import RequestFactory
from django.test.utils import override_settings
from django.utils import timezone

try:
    from django.urls import reverse
except ImportError:
    from django.core.urlresolvers import reverse

import pytest
from django.contrib.auth.models import AnonymousUser
from django.core import mail
from freezegun import freeze_time

from invitations.adapters import BaseInvitationsAdapter, get_invitations_adapter
from invitations.app_settings import app_settings
from invitations.forms import InviteForm
from invitations.utils import get_invitation_model
from invitations.views import AcceptInvite, SendJSONInvite

Invitation = get_invitation_model()


class TestInvitationModel:
    @freeze_time("2015-07-30 12:00:06")
    def test_create_invitation(self, invitation_a):
        assert invitation_a.email == "email@example.com"
        assert invitation_a.key
        assert invitation_a.accepted is False
        assert invitation_a.created == timezone.now()

    def test_invitation_key_expiry(self, invitation_a):
        invitation_a.sent = timezone.now() - datetime.timedelta(
            days=app_settings.INVITATION_EXPIRY,
            minutes=1,
        )
        assert invitation_a.key_expired() is True

        invitation_a.sent = timezone.now() - datetime.timedelta(
            days=app_settings.INVITATION_EXPIRY,
            minutes=-1,
        )
        assert invitation_a.key_expired() is False


class TestInvitationsAdapter:
    def test_fetch_adapter(self):
        adapter = get_invitations_adapter()
        assert isinstance(adapter, BaseInvitationsAdapter)

    def test_email_subject_prefix_settings_with_site(self):
        adapter = get_invitations_adapter()
        result = adapter.format_email_subject("Bar", context={"site_name": "Foo.com"})
        assert result == "[Foo.com] Bar"

    @override_settings(INVITATIONS_EMAIL_SUBJECT_PREFIX="")
    def test_email_subject_prefix_settings_with_custom_override(self):
        adapter = get_invitations_adapter()
        result = adapter.format_email_subject("Bar", context={"site_name": "Foo.com"})
        assert result == "Bar"


class TestInvitationsSendView:
    client = Client()

    @pytest.mark.django_db
    def test_auth(self):
        response = self.client.post(
            reverse("invitations:send-invite"),
            {"email": "valid@example.com"},
            follow=True,
        )

        assert response.status_code == 404

    @pytest.mark.parametrize(
        "email, error",
        [
            ("invalid@example", "Enter a valid email address"),
            ("invited@example.com", "This e-mail address has already been"),
            ("flobble@example.com", "An active user is"),
        ],
    )
    def test_invalid_form_submissions(self, user_a, user_b, invitation_b, email, error):
        self.client.login(username="flibble", password="password")
        resp = self.client.post(reverse("invitations:send-invite"), {"email": email})

        form = resp.context_data["form"]
        assert error in form.errors["email"][0]

    @freeze_time("2015-07-30 12:00:06")
    def test_valid_form_submission(self, user_a):
        self.client.login(username="flibble", password="password")
        resp = self.client.post(
            reverse("invitations:send-invite"),
            {"email": "email@example.com"},
        )
        invitation = Invitation.objects.get(email="email@example.com")

        assert resp.status_code == 200
        assert "success_message" in resp.context_data.keys()

        assert invitation.sent == timezone.now()
        assert len(mail.outbox) == 1
        assert mail.outbox[0].to[0] == "email@example.com"
        assert "Invitation to join example.com" in mail.outbox[0].subject
        url = re.search(r"(?P<url>/invitations/[^\s]+)", mail.outbox[0].body).group(
            "url",
        )
        assert url == reverse(
            app_settings.CONFIRMATION_URL_NAME,
            kwargs={"key": invitation.key},
        )

    @override_settings(INVITATION_MODEL="ExampleSwappableInvitation")
    @freeze_time("2015-07-30 12:00:06")
    def test_valid_form_submission_with_swapped_model(self, user_a):
        self.client.login(username="flibble", password="password")
        resp = self.client.post(
            reverse("invitations:send-invite"),
            {"email": "email@example.com"},
        )
        invitation = Invitation.objects.get(email="email@example.com")

        assert resp.status_code == 200
        assert "success_message" in resp.context_data.keys()

        assert invitation.sent == timezone.now()
        assert len(mail.outbox) == 1
        assert mail.outbox[0].to[0] == "email@example.com"
        assert "Invitation to join example.com" in mail.outbox[0].subject
        url = re.search(r"(?P<url>/invitations/[^\s]+)", mail.outbox[0].body).group(
            "url",
        )
        assert url == reverse(
            app_settings.CONFIRMATION_URL_NAME,
            kwargs={"key": invitation.key},
        )


@pytest.mark.django_db
class TestInvitationsAcceptView:
    client = Client()

    def test_accept_invite_get_is_404(self, settings, invitation_b):
        settings.INVITATIONS_CONFIRM_INVITE_ON_GET = False
        resp = self.client.get(
            reverse(
                app_settings.CONFIRMATION_URL_NAME,
                kwargs={"key": invitation_b.key},
            ),
            follow=True,
        )
        assert resp.status_code == 404

    @pytest.mark.parametrize(
        "method",
        [
            ("get"),
            ("post"),
        ],
    )
    def test_accept_invite_invalid_key(self, method):
        client_with_method = getattr(self.client, method)
        resp = client_with_method(
            reverse(app_settings.CONFIRMATION_URL_NAME, kwargs={"key": "invalidKey"}),
            follow=True,
        )
        assert resp.status_code == 410

    @pytest.mark.parametrize(
        "method",
        [
            ("get"),
            ("post"),
        ],
    )
    def test_accept_invite_invalid_key_error_disabled(self, settings, method):
        settings.INVITATIONS_GONE_ON_ACCEPT_ERROR = False
        settings.INVITATIONS_LOGIN_REDIRECT = "/login-url/"
        client_with_method = getattr(self.client, method)
        resp = client_with_method(
            reverse(app_settings.CONFIRMATION_URL_NAME, kwargs={"key": "invalidKey"}),
            follow=True,
        )
        assert resp.request["PATH_INFO"] == "/login-url/"

    @pytest.mark.parametrize(
        "method",
        [
            ("get"),
            ("post"),
        ],
    )
    def test_accept_invite_accepted_key(self, accepted_invitation, method):
        client_with_method = getattr(self.client, method)
        resp = client_with_method(
            reverse(
                app_settings.CONFIRMATION_URL_NAME,
                kwargs={"key": accepted_invitation.key},
            ),
            follow=True,
        )
        assert resp.status_code == 410

    @pytest.mark.parametrize(
        "method",
        [
            ("get"),
            ("post"),
        ],
    )
    def test_accept_invite_accepted_key_error_disabled(
        self,
        settings,
        accepted_invitation,
        method,
    ):
        settings.INVITATIONS_GONE_ON_ACCEPT_ERROR = False
        settings.INVITATIONS_LOGIN_REDIRECT = "/login-url/"
        client_with_method = getattr(self.client, method)
        resp = client_with_method(
            reverse(
                app_settings.CONFIRMATION_URL_NAME,
                kwargs={"key": accepted_invitation.key},
            ),
            follow=True,
        )
        assert resp.request["PATH_INFO"] == "/login-url/"

    @pytest.mark.parametrize(
        "method",
        [
            ("get"),
            ("post"),
        ],
    )
    def test_accept_invite_expired_key(
        self,
        settings,
        sent_invitation_by_user_a,
        method,
    ):
        settings.INVITATIONS_INVITATION_EXPIRY = 0
        client_with_method = getattr(self.client, method)
        resp = client_with_method(
            reverse(
                app_settings.CONFIRMATION_URL_NAME,
                kwargs={"key": sent_invitation_by_user_a.key},
            ),
            follow=True,
        )
        assert resp.status_code == 410

    @pytest.mark.parametrize(
        "method",
        [
            ("get"),
            ("post"),
        ],
    )
    def test_accept_invite_expired_key_error_disabled(
        self,
        sent_invitation_by_user_a,
        method,
        settings,
    ):
        settings.INVITATIONS_INVITATION_EXPIRY = 0
        settings.INVITATIONS_GONE_ON_ACCEPT_ERROR = False
        settings.INVITATIONS_SIGNUP_REDIRECT = "/signup-url/"
        client_with_method = getattr(self.client, method)
        resp = client_with_method(
            reverse(
                app_settings.CONFIRMATION_URL_NAME,
                kwargs={"key": sent_invitation_by_user_a.key},
            ),
            follow=True,
        )
        assert resp.request["PATH_INFO"] == "/signup-url/"

    @pytest.mark.parametrize(
        "method",
        [
            ("get"),
            ("post"),
        ],
    )
    def test_accept_invite(self, settings, sent_invitation_by_user_a, user_a, method):
        settings.INVITATIONS_SIGNUP_REDIRECT = "/non-existent-url/"
        client_with_method = getattr(self.client, method)
        resp = client_with_method(
            reverse(
                app_settings.CONFIRMATION_URL_NAME,
                kwargs={"key": sent_invitation_by_user_a.key},
            ),
            follow=True,
        )
        invite = Invitation.objects.get(email="email@example.com")
        assert invite.accepted is True
        assert invite.inviter == user_a
        assert resp.request["PATH_INFO"] == "/non-existent-url/"

    def test_signup_redirect(self, settings, sent_invitation_by_user_a):
        settings.INVITATIONS_SIGNUP_REDIRECT = "/non-existent-url/"
        resp = self.client.post(
            reverse(
                app_settings.CONFIRMATION_URL_NAME,
                kwargs={"key": sent_invitation_by_user_a.key},
            ),
            follow=True,
        )
        invite = Invitation.objects.get(email="email@example.com")
        assert invite.accepted is True
        assert resp.request["PATH_INFO"] == "/non-existent-url/"


class TestInvitationSignals:
    client = Client()

    @patch("invitations.signals.invite_url_sent.send")
    def test_invite_url_sent_triggered_correctly(
        self,
        mock_signal,
        sent_invitation_by_user_a,
        user_a,
    ):
        invite_url = reverse(
            app_settings.CONFIRMATION_URL_NAME,
            args=[sent_invitation_by_user_a.key],
        )
        request = RequestFactory().get("/")
        invite_url = request.build_absolute_uri(invite_url)

        sent_invitation_by_user_a.send_invitation(request)

        assert mock_signal.called
        assert mock_signal.call_count == 1

        mock_signal.assert_called_with(
            instance=sent_invitation_by_user_a,
            invite_url_sent=invite_url,
            inviter=user_a,
            sender=Invitation,
        )

    @override_settings(INVITATIONS_SIGNUP_REDIRECT="/non-existent-url/")
    @patch("invitations.signals.invite_accepted.send")
    def test_invite_invite_accepted_triggered_correctly(
        self,
        mock_signal,
        sent_invitation_by_user_a,
    ):
        request = RequestFactory().get("/")
        sent_invitation_by_user_a.send_invitation(request)

        self.client.post(
            reverse(
                app_settings.CONFIRMATION_URL_NAME,
                kwargs={"key": sent_invitation_by_user_a.key},
            ),
            follow=True,
        )
        assert mock_signal.called
        assert mock_signal.call_count == 1

        assert mock_signal.call_args[1]["email"] == "email@example.com"
        assert mock_signal.call_args[1]["sender"] == AcceptInvite


class TestInvitationsForm:
    @pytest.mark.parametrize(
        "email, form_validity, errors",
        [
            ("bogger@example.com", True, None),
            ("accepted@example.com", False, "has already accepted an invite"),
            ("pending@example.com", False, "has already been invited"),
            ("flobble@example.com", False, "active user is using this"),
        ],
    )
    def test_form(
        self,
        email,
        form_validity,
        errors,
        accepted_invitation,
        pending_invitation,
        user_b,
    ):
        form = InviteForm(data={"email": email})
        if errors:
            assert errors in str(form.errors)
        else:
            assert form.errors == {}
        assert form.is_valid() is form_validity


@pytest.mark.django_db
class TestInvitationsManager:
    def test_managers(
        self,
        sent_invitation_by_user_a,
        accepted_invitation,
        expired_invitation,
        invitation_b,
    ):
        valid = Invitation.objects.all_valid().values_list("email", flat=True)
        expired = Invitation.objects.all_expired().values_list("email", flat=True)
        expected_valid = ["email@example.com", "invited@example.com"]
        expected_expired = ["accepted@example.com", "expired@example.com"]

        assert sorted(valid) == sorted(expected_valid)
        assert sorted(expired) == sorted(expected_expired)

    def test_delete_all(self):
        valid = Invitation.objects.all_valid().values_list("email", flat=True)
        Invitation.objects.delete_expired_confirmations()
        remaining_invites = Invitation.objects.all().values_list("email", flat=True)
        assert sorted(valid) == sorted(remaining_invites)


class TestInvitationsJSON:
    client = Client()

    @pytest.mark.parametrize(
        "data, expected, status_code",
        [
            (
                ["accepted@example.com"],
                {
                    "valid": [],
                    "invalid": [{"accepted@example.com": "already accepted"}],
                },
                400,
            ),
            (
                ["xample.com"],
                {"valid": [], "invalid": [{"xample.com": "invalid email"}]},
                400,
            ),
            ("xample.com", {"valid": [], "invalid": []}, 400),
            (
                ["pending@example.com"],
                {"valid": [], "invalid": [{"pending@example.com": "pending invite"}]},
                400,
            ),
            (
                ["flobble@example.com"],
                {
                    "valid": [],
                    "invalid": [{"flobble@example.com": "user registered email"}],
                },
                400,
            ),
            (
                ["example@example.com"],
                {"valid": [{"example@example.com": "invited"}], "invalid": []},
                201,
            ),
        ],
    )
    def test_post(
        self,
        settings,
        data,
        expected,
        status_code,
        user_a,
        accepted_invitation,
        pending_invitation,
        user_b,
    ):
        settings.INVITATIONS_ALLOW_JSON_INVITES = True
        self.client.login(username="flibble", password="password")
        response = self.client.post(
            reverse("invitations:send-json-invite"),
            data=json.dumps(data),
            content_type="application/json",
        )

        assert response.status_code == status_code
        assert json.loads(response.content.decode()) == expected

    def test_json_setting(self, user_a):
        self.client.login(username="flibble", password="password")
        response = self.client.post(
            reverse("invitations:send-json-invite"),
            data=json.dumps(["example@example.com"]),
            content_type="application/json",
        )

        assert response.status_code == 404

    @override_settings(INVITATIONS_ALLOW_JSON_INVITES=True)
    def test_anonymous_get(self):
        request = RequestFactory().get(
            reverse("invitations:send-json-invite"),
            content_type="application/json",
        )
        request.user = AnonymousUser()
        response = SendJSONInvite.as_view()(request)

        assert response.status_code == 302

    def test_authenticated_get(self, settings, user_a):
        settings.INVITATIONS_ALLOW_JSON_INVITES = True
        request = RequestFactory().get(
            reverse("invitations:send-json-invite"),
            content_type="application/json",
        )
        request.user = user_a
        response = SendJSONInvite.as_view()(request)

        assert response.status_code == 405


class TestInvitationsAdmin:
    client = Client()

    def test_admin_form_add(self, super_user):
        self.client.login(username="flibble", password="password")
        response = self.client.post(
            reverse("admin:invitations_invitation_add"),
            {"email": "valid@example.com", "inviter": super_user.id},
            follow=True,
        )
        invite = Invitation.objects.get(email="valid@example.com")

        assert response.status_code == 200
        assert invite.sent
        assert invite.inviter == super_user

    def test_admin_form_change(self, super_user, invitation_b):
        self.client.login(username="flibble", password="password")
        response = self.client.get(
            reverse("admin:invitations_invitation_change", args=(invitation_b.id,)),
            follow=True,
        )

        assert response.status_code == 200
        fields = list(response.context_data["adminform"].form.fields.keys())
        expected_fields = ["accepted", "key", "sent", "inviter", "email", "created"]
        assert fields == expected_fields
