from datetime import timedelta
from unittest import mock

from django.contrib.auth import get_user_model
from django.core.management import call_command
from django.db import IntegrityError
from django.test import TestCase

from social_django.models import (
    AbstractUserSocialAuth,
    Association,
    Code,
    DjangoStorage,
    Nonce,
    Partial,
    UserSocialAuth,
)


class TestSocialAuthUser(TestCase):
    def test_user_relationship_none(self):
        """Accessing User.social_user outside of the pipeline doesn't work"""
        User = get_user_model()
        user = User._default_manager.create_user(username="randomtester")
        with self.assertRaises(AttributeError):
            user.social_user

    def test_user_existing_relationship(self):
        """Accessing User.social_user outside of the pipeline doesn't work"""
        User = get_user_model()
        user = User._default_manager.create_user(username="randomtester")
        UserSocialAuth.objects.create(user=user, provider="my-provider", uid="1234")
        with self.assertRaises(AttributeError):
            user.social_user

    def test_get_social_auth(self):
        User = get_user_model()
        user = User._default_manager.create_user(username="randomtester")
        user_social = UserSocialAuth.objects.create(
            user=user, provider="my-provider", uid="1234"
        )
        other = UserSocialAuth.get_social_auth("my-provider", "1234")
        self.assertEqual(other, user_social)

    def test_get_social_auth_none(self):
        other = UserSocialAuth.get_social_auth("my-provider", "1234")
        self.assertIsNone(other)

    def test_cleanup(self):
        Code.objects.create(email="first@example.com")
        Code.objects.create(email="second@example.com")
        code = Code.objects.create(email="expire@example.com")
        code.timestamp -= timedelta(days=30)
        code.save()

        Partial.objects.create()
        partial = Partial.objects.create()
        partial.timestamp -= timedelta(days=30)
        partial.save()

        call_command("clearsocial")

        self.assertEqual(2, Code.objects.count())
        self.assertEqual(1, Partial.objects.count())


class TestUserSocialAuth(TestCase):
    def setUp(self):
        self.user_model = get_user_model()
        self.user = self.user_model._default_manager.create_user(
            username="randomtester", email="user@example.com"
        )
        self.usa = UserSocialAuth.objects.create(
            user=self.user, provider="my-provider", uid="1234"
        )

    def test_changed(self):
        self.user.email = eml = "test@example.com"
        UserSocialAuth.changed(user=self.user)
        db_eml = self.user_model._default_manager.get(username=self.user.username).email
        self.assertEqual(db_eml, eml)

    def test_set_extra_data(self):
        self.usa.set_extra_data({"a": "b"})
        self.usa.refresh_from_db()
        db_data = UserSocialAuth.objects.get(id=self.usa.id).extra_data
        self.assertEqual(db_data, {"a": "b"})

    def test_disconnect(self):
        m = mock.Mock()
        UserSocialAuth.disconnect(m)
        self.assertListEqual(m.method_calls, [mock.call.delete()])

    def test_username_field(self):
        self.assertEqual(UserSocialAuth.username_field(), "username")
        with mock.patch(
            "social_django.models.UserSocialAuth.user_model",
            return_value=mock.Mock(USERNAME_FIELD="test"),
        ):
            self.assertEqual(UserSocialAuth.username_field(), "test")

    def test_user_exists(self):
        self.assertTrue(UserSocialAuth.user_exists(username=self.user.username))
        self.assertFalse(UserSocialAuth.user_exists(username="test"))

    def test_get_username(self):
        self.assertEqual(UserSocialAuth.get_username(self.user), self.user.username)

    def test_create_user(self):
        # Catch integrity error and find existing user
        UserSocialAuth.create_user(username=self.user.username)

    def test_create_user_reraise(self):
        with self.assertRaises(IntegrityError):
            UserSocialAuth.create_user(username=self.user.username, email=None)

    @mock.patch(
        "social_django.models.UserSocialAuth.username_field", return_value="email"
    )
    @mock.patch(
        "django.contrib.auth.models.UserManager.create_user", side_effect=IntegrityError
    )
    def test_create_user_custom_username(self, *args):
        UserSocialAuth.create_user(username=self.user.email)

    @mock.patch("social_django.storage.transaction", spec=[])
    def test_create_user_without_transaction_atomic(self, *args):
        UserSocialAuth.create_user(username="test")
        self.assertTrue(
            self.user_model._default_manager.filter(username="test").exists()
        )

    def test_get_user(self):
        self.assertEqual(UserSocialAuth.get_user(pk=self.user.pk), self.user)
        self.assertIsNone(UserSocialAuth.get_user(pk=123))

    def test_get_users_by_email(self):
        qs = UserSocialAuth.get_users_by_email(email=self.user.email)
        self.assertEqual(qs.count(), 1)

    def test_get_social_auth(self):
        usa = self.usa
        # Model
        self.assertEqual(
            UserSocialAuth.get_social_auth(provider=usa.provider, uid=usa.uid), usa
        )
        self.assertIsNone(UserSocialAuth.get_social_auth(provider="a", uid=1))

        # Mixin
        self.assertEqual(
            super(AbstractUserSocialAuth, usa).get_social_auth(
                provider=usa.provider, uid=usa.uid
            ),
            usa,
        )
        self.assertIsNone(
            super(AbstractUserSocialAuth, usa).get_social_auth(provider="a", uid=1)
        )

        # Manager
        self.assertEqual(
            UserSocialAuth.objects.get_social_auth(provider=usa.provider, uid=usa.uid),
            usa,
        )
        self.assertIsNone(UserSocialAuth.objects.get_social_auth(provider="a", uid=1))

    def test_get_social_auth_for_user(self):
        qs = UserSocialAuth.get_social_auth_for_user(
            user=self.user, provider=self.usa.provider, id=self.usa.id
        )
        self.assertEqual(qs.count(), 1)

    def test_create_social_auth(self):
        usa = UserSocialAuth.create_social_auth(user=self.user, provider="test", uid=1)
        self.assertEqual(usa.uid, "1")
        self.assertEqual(str(usa), str(self.user))

    @mock.patch("social_django.storage.transaction", spec=[])
    def test_create_social_auth_without_transaction_atomic(self, *args):
        with self.assertRaises(IntegrityError):
            UserSocialAuth.create_social_auth(
                user=self.user, provider=self.usa.provider, uid=self.usa.uid
            )

    def test_username_max_length(self):
        self.assertEqual(UserSocialAuth.username_max_length(), 150)


class TestNonce(TestCase):
    def test_use(self):
        self.assertEqual(Nonce.objects.count(), 0)
        self.assertTrue(Nonce.use(server_url="/", timestamp=1, salt="1"))
        self.assertFalse(Nonce.use(server_url="/", timestamp=1, salt="1"))
        self.assertEqual(Nonce.objects.count(), 1)


class TestAssociation(TestCase):
    def test_store_get_remove(self):
        Association.store(
            server_url="/",
            association=mock.Mock(
                handle="a", secret=b"b", issued=1, lifetime=2, assoc_type="c"
            ),
        )

        qs = Association.get(handle="a")
        self.assertEqual(qs.count(), 1)
        self.assertEqual(qs[0].secret, "Yg==\n")

        Association.remove(ids_to_delete=[qs.first().id])
        self.assertEqual(Association.objects.count(), 0)


class TestCode(TestCase):
    def test_get_code(self):
        code1 = Code.objects.create(email="test@example.com", code="abc")
        code2 = Code.get_code(code="abc")
        self.assertEqual(code1, code2)
        self.assertIsNone(Code.get_code(code="xyz"))


class TestPartial(TestCase):
    def test_load_destroy(self):
        p = Partial.objects.create(token="x", backend="y", data={})
        self.assertEqual(Partial.load(token="x"), p)
        self.assertIsNone(Partial.load(token="y"))

        Partial.destroy(token="x")
        self.assertEqual(Partial.objects.count(), 0)


class TestDjangoStorage(TestCase):
    def test_is_integrity_error(self):
        self.assertTrue(DjangoStorage.is_integrity_error(IntegrityError()))
