from __future__ import absolute_import

from datetime import datetime
from operator import attrgetter

from django.test import TestCase

from .models import (Person, Group, Membership, CustomMembership,
    PersonSelfRefM2M, Friendship)


class M2mThroughTests(TestCase):
    def setUp(self):
        self.bob = Person.objects.create(name='Bob')
        self.jim = Person.objects.create(name='Jim')
        self.jane = Person.objects.create(name='Jane')
        self.rock = Group.objects.create(name='Rock')
        self.roll = Group.objects.create(name='Roll')

    def test_m2m_through(self):
        # We start out by making sure that the Group 'rock' has no members.
        self.assertQuerysetEqual(
            self.rock.members.all(),
            []
        )
        # To make Jim a member of Group Rock, simply create a Membership object.
        m1 = Membership.objects.create(person=self.jim, group=self.rock)
        # We can do the same for Jane and Rock.
        m2 = Membership.objects.create(person=self.jane, group=self.rock)
        # Let's check to make sure that it worked.  Jane and Jim should be members of Rock.
        self.assertQuerysetEqual(
            self.rock.members.all(), [
                'Jane',
                'Jim'
            ],
            attrgetter("name")
        )
        # Now we can add a bunch more Membership objects to test with.
        m3 = Membership.objects.create(person=self.bob, group=self.roll)
        m4 = Membership.objects.create(person=self.jim, group=self.roll)
        m5 = Membership.objects.create(person=self.jane, group=self.roll)
        # We can get Jim's Group membership as with any ForeignKey.
        self.assertQuerysetEqual(
            self.jim.group_set.all(), [
                'Rock',
                'Roll'
            ],
            attrgetter("name")
        )
        # Querying the intermediary model works like normal.
        self.assertEqual(
            repr(Membership.objects.get(person=self.jane, group=self.rock)),
            '<Membership: Jane is a member of Rock>'
        )
        # It's not only get that works. Filter works like normal as well.
        self.assertQuerysetEqual(
            Membership.objects.filter(person=self.jim), [
                '<Membership: Jim is a member of Rock>',
                '<Membership: Jim is a member of Roll>'
            ]
        )
        self.rock.members.clear()
        # Now there will be no members of Rock.
        self.assertQuerysetEqual(
            self.rock.members.all(),
            []
        )



    def test_forward_descriptors(self):
        # Due to complications with adding via an intermediary model,
        # the add method is not provided.
        self.assertRaises(AttributeError, lambda: self.rock.members.add(self.bob))
        # Create is also disabled as it suffers from the same problems as add.
        self.assertRaises(AttributeError, lambda: self.rock.members.create(name='Anne'))
        # Remove has similar complications, and is not provided either.
        self.assertRaises(AttributeError, lambda: self.rock.members.remove(self.jim))

        m1 = Membership.objects.create(person=self.jim, group=self.rock)
        m2 = Membership.objects.create(person=self.jane, group=self.rock)

        # Here we back up the list of all members of Rock.
        backup = list(self.rock.members.all())
        # ...and we verify that it has worked.
        self.assertEqual(
            [p.name for p in backup],
            ['Jane', 'Jim']
        )
        # The clear function should still work.
        self.rock.members.clear()
        # Now there will be no members of Rock.
        self.assertQuerysetEqual(
            self.rock.members.all(),
            []
        )

        # Assignment should not work with models specifying a through model for many of
        # the same reasons as adding.
        self.assertRaises(AttributeError, setattr, self.rock, "members", backup)
        # Let's re-save those instances that we've cleared.
        m1.save()
        m2.save()
        # Verifying that those instances were re-saved successfully.
        self.assertQuerysetEqual(
            self.rock.members.all(),[
                'Jane',
                'Jim'
            ],
            attrgetter("name")
        )

    def test_reverse_descriptors(self):
        # Due to complications with adding via an intermediary model,
        # the add method is not provided.
        self.assertRaises(AttributeError, lambda: self.bob.group_set.add(self.rock))
        # Create is also disabled as it suffers from the same problems as add.
        self.assertRaises(AttributeError, lambda: self.bob.group_set.create(name="funk"))
        # Remove has similar complications, and is not provided either.
        self.assertRaises(AttributeError, lambda: self.jim.group_set.remove(self.rock))

        m1 = Membership.objects.create(person=self.jim, group=self.rock)
        m2 = Membership.objects.create(person=self.jim, group=self.roll)

        # Here we back up the list of all of Jim's groups.
        backup = list(self.jim.group_set.all())
        self.assertEqual(
            [g.name for g in backup],
            ['Rock', 'Roll']
        )
        # The clear function should still work.
        self.jim.group_set.clear()
        # Now Jim will be in no groups.
        self.assertQuerysetEqual(
            self.jim.group_set.all(),
            []
        )
        # Assignment should not work with models specifying a through model for many of
        # the same reasons as adding.
        self.assertRaises(AttributeError, setattr, self.jim, "group_set", backup)
        # Let's re-save those instances that we've cleared.

        m1.save()
        m2.save()
        # Verifying that those instances were re-saved successfully.
        self.assertQuerysetEqual(
            self.jim.group_set.all(),[
                'Rock',
                'Roll'
            ],
            attrgetter("name")
        )

    def test_custom_tests(self):
        # Let's see if we can query through our second relationship.
        self.assertQuerysetEqual(
            self.rock.custom_members.all(),
            []
        )
        # We can query in the opposite direction as well.
        self.assertQuerysetEqual(
            self.bob.custom.all(),
            []
        )

        cm1 = CustomMembership.objects.create(person=self.bob, group=self.rock)
        cm2 = CustomMembership.objects.create(person=self.jim, group=self.rock)

        # If we get the number of people in Rock, it should be both Bob and Jim.
        self.assertQuerysetEqual(
            self.rock.custom_members.all(),[
                'Bob',
                'Jim'
            ],
            attrgetter("name")
        )
        # Bob should only be in one custom group.
        self.assertQuerysetEqual(
            self.bob.custom.all(),[
                'Rock'
            ],
            attrgetter("name")
        )
        # Let's make sure our new descriptors don't conflict with the FK related_name.
        self.assertQuerysetEqual(
            self.bob.custom_person_related_name.all(),[
                '<CustomMembership: Bob is a member of Rock>'
            ]
        )

    def test_self_referential_tests(self):
        # Let's first create a person who has no friends.
        tony = PersonSelfRefM2M.objects.create(name="Tony")
        self.assertQuerysetEqual(
            tony.friends.all(),
            []
        )

        chris = PersonSelfRefM2M.objects.create(name="Chris")
        f = Friendship.objects.create(first=tony, second=chris, date_friended=datetime.now())

        # Tony should now show that Chris is his friend.
        self.assertQuerysetEqual(
            tony.friends.all(),[
                'Chris'
            ],
            attrgetter("name")
        )
        # But we haven't established that Chris is Tony's Friend.
        self.assertQuerysetEqual(
            chris.friends.all(),
            []
        )
        f2 = Friendship.objects.create(first=chris, second=tony, date_friended=datetime.now())

        # Having added Chris as a friend, let's make sure that his friend set reflects
        # that addition.
        self.assertQuerysetEqual(
            chris.friends.all(),[
                'Tony'
            ],
            attrgetter("name")
        )

        # Chris gets mad and wants to get rid of all of his friends.
        chris.friends.clear()
        # Now he should not have any more friends.
        self.assertQuerysetEqual(
            chris.friends.all(),
            []
        )
        # Since this isn't a symmetrical relation, Tony's friend link still exists.
        self.assertQuerysetEqual(
            tony.friends.all(),[
                'Chris'
            ],
            attrgetter("name")
        )

    def test_query_tests(self):
        m1 = Membership.objects.create(person=self.jim, group=self.rock)
        m2 = Membership.objects.create(person=self.jane, group=self.rock)
        m3 = Membership.objects.create(person=self.bob, group=self.roll)
        m4 = Membership.objects.create(person=self.jim, group=self.roll)
        m5 = Membership.objects.create(person=self.jane, group=self.roll)

        m2.invite_reason = "She was just awesome."
        m2.date_joined = datetime(2006, 1, 1)
        m2.save()
        m3.date_joined = datetime(2004, 1, 1)
        m3.save()
        m5.date_joined = datetime(2004, 1, 1)
        m5.save()

        # We can query for the related model by using its attribute name (members, in
        # this case).
        self.assertQuerysetEqual(
            Group.objects.filter(members__name='Bob'),[
                'Roll'
            ],
            attrgetter("name")
        )

        # To query through the intermediary model, we specify its model name.
        # In this case, membership.
        self.assertQuerysetEqual(
            Group.objects.filter(membership__invite_reason="She was just awesome."),[
                'Rock'
            ],
            attrgetter("name")
        )

        # If we want to query in the reverse direction by the related model, use its
        # model name (group, in this case).
        self.assertQuerysetEqual(
            Person.objects.filter(group__name="Rock"),[
                'Jane',
                'Jim'
            ],
            attrgetter("name")
        )

        cm1 = CustomMembership.objects.create(person=self.bob, group=self.rock)
        cm2 = CustomMembership.objects.create(person=self.jim, group=self.rock)
        # If the m2m field has specified a related_name, using that will work.
        self.assertQuerysetEqual(
            Person.objects.filter(custom__name="Rock"),[
                'Bob',
                'Jim'
            ],
            attrgetter("name")
        )

        # To query through the intermediary model in the reverse direction, we again
        # specify its model name (membership, in this case).
        self.assertQuerysetEqual(
            Person.objects.filter(membership__invite_reason="She was just awesome."),[
                'Jane'
            ],
            attrgetter("name")
        )

        # Let's see all of the groups that Jane joined after 1 Jan 2005:
        self.assertQuerysetEqual(
            Group.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__person=self.jane),[
                'Rock'
            ],
            attrgetter("name")
        )

        # Queries also work in the reverse direction: Now let's see all of the people
        # that have joined Rock since 1 Jan 2005:
        self.assertQuerysetEqual(
            Person.objects.filter(membership__date_joined__gt=datetime(2005, 1, 1), membership__group=self.rock),[
                'Jane',
                'Jim'
            ],
            attrgetter("name")
        )

        # Conceivably, queries through membership could return correct, but non-unique
        # querysets.  To demonstrate this, we query for all people who have joined a
        # group after 2004:
        self.assertQuerysetEqual(
            Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)),[
                'Jane',
                'Jim',
                'Jim'
            ],
            attrgetter("name")
        )

        # Jim showed up twice, because he joined two groups ('Rock', and 'Roll'):
        self.assertEqual(
            [(m.person.name, m.group.name) for m in Membership.objects.filter(date_joined__gt=datetime(2004, 1, 1))],
            [('Jane', 'Rock'), ('Jim', 'Rock'), ('Jim', 'Roll')]
        )
        # QuerySet's distinct() method can correct this problem.
        self.assertQuerysetEqual(
            Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1)).distinct(),[
                'Jane',
                'Jim'
            ],
            attrgetter("name")
        )
