from bs4 import BeautifulSoup
from django.conf import settings
from django.core.cache import cache
from django.db.models import Max
from django.urls import reverse
from django.utils.timezone import localdate, localtime, now
from model_bakery import baker

from club.forms import ClubMemberForm
from club.models import Membership
from club.tests.base import TestClub
from core.baker_recipes import subscriber_user
from core.models import AnonymousUser, User


class TestMembershipQuerySet(TestClub):
    def test_ongoing(self):
        """Test that the ongoing queryset method returns the memberships that
        are not ended.
        """
        current_members = list(self.club.members.ongoing().order_by("id"))
        expected = [
            self.simple_board_member.memberships.get(club=self.club),
            self.president.memberships.get(club=self.club),
            self.richard.memberships.get(club=self.club),
        ]
        expected.sort(key=lambda i: i.id)
        assert current_members == expected

    def test_ongoing_with_membership_ending_today(self):
        """Test that a membership ending the present day is considered as ended."""
        today = localdate()
        self.richard.memberships.filter(club=self.club).update(end_date=today)
        current_members = list(self.club.members.ongoing().order_by("id"))
        expected = [
            self.simple_board_member.memberships.get(club=self.club),
            self.president.memberships.get(club=self.club),
        ]
        expected.sort(key=lambda i: i.id)
        assert current_members == expected

    def test_board(self):
        """Test that the board queryset method returns the memberships
        of user in the club board.
        """
        board_members = list(self.club.members.board().order_by("id"))
        expected = [
            self.simple_board_member.memberships.get(club=self.club),
            self.president.memberships.get(club=self.club),
            # sli is no more member, but he was in the board
            self.sli.memberships.get(club=self.club),
        ]
        expected.sort(key=lambda i: i.id)
        assert board_members == expected

    def test_ongoing_board(self):
        """Test that combining ongoing and board returns users
        who are currently board members of the club.
        """
        members = list(self.club.members.ongoing().board().order_by("id"))
        expected = [
            self.simple_board_member.memberships.get(club=self.club),
            self.president.memberships.get(club=self.club),
        ]
        expected.sort(key=lambda i: i.id)
        assert members == expected

    def test_update_invalidate_cache(self):
        """Test that the `update` queryset method properly invalidate cache."""
        mem_skia = self.simple_board_member.memberships.get(club=self.club)
        cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
        self.simple_board_member.memberships.update(end_date=localtime(now()).date())
        assert (
            cache.get(f"membership_{mem_skia.club_id}_{mem_skia.user_id}")
            == "not_member"
        )

        mem_richard = self.richard.memberships.get(club=self.club)
        cache.set(
            f"membership_{mem_richard.club_id}_{mem_richard.user_id}", mem_richard
        )
        self.richard.memberships.update(role=5)
        new_mem = self.richard.memberships.get(club=self.club)
        assert new_mem != "not_member"
        assert new_mem.role == 5

    def test_update_change_club_groups(self):
        """Test that `update` set the user groups accordingly."""
        user = baker.make(User)
        membership = baker.make(Membership, end_date=None, user=user, role=5)
        members_group = membership.club.members_group
        board_group = membership.club.board_group
        assert user.groups.contains(members_group)
        assert user.groups.contains(board_group)

        user.memberships.update(role=1)  # from board to simple member
        assert user.groups.contains(members_group)
        assert not user.groups.contains(board_group)

        user.memberships.update(role=5)  # from member to board
        assert user.groups.contains(members_group)
        assert user.groups.contains(board_group)

        user.memberships.update(end_date=localdate())  # end the membership
        assert not user.groups.contains(members_group)
        assert not user.groups.contains(board_group)

    def test_delete_invalidate_cache(self):
        """Test that the `delete` queryset properly invalidate cache."""
        mem_skia = self.simple_board_member.memberships.get(club=self.club)
        mem_comptable = self.president.memberships.get(club=self.club)
        cache.set(f"membership_{mem_skia.club_id}_{mem_skia.user_id}", mem_skia)
        cache.set(
            f"membership_{mem_comptable.club_id}_{mem_comptable.user_id}", mem_comptable
        )

        # should delete the subscriptions of simple_board_member and president
        self.club.members.ongoing().board().delete()

        for membership in (mem_skia, mem_comptable):
            cached_mem = cache.get(
                f"membership_{membership.club_id}_{membership.user_id}"
            )
            assert cached_mem == "not_member"

    def test_delete_remove_from_groups(self):
        """Test that `delete` removes from club groups"""
        user = baker.make(User)
        memberships = baker.make(Membership, role=iter([1, 5]), user=user, _quantity=2)
        club_groups = {
            memberships[0].club.members_group,
            memberships[1].club.members_group,
            memberships[1].club.board_group,
        }
        assert set(user.groups.all()).issuperset(club_groups)
        user.memberships.all().delete()
        assert set(user.groups.all()).isdisjoint(club_groups)


class TestMembership(TestClub):
    def assert_membership_started_today(self, user: User, role: int):
        """Assert that the given membership is active and started today."""
        membership = user.memberships.ongoing().filter(club=self.club).first()
        assert membership is not None
        assert localtime(now()).date() == membership.start_date
        assert membership.end_date is None
        assert membership.role == role
        assert membership.club.get_membership_for(user) == membership
        assert user.is_in_group(pk=self.club.members_group_id)
        assert user.is_in_group(pk=self.club.board_group_id)

    def assert_membership_ended_today(self, user: User):
        """Assert that the given user have a membership which ended today."""
        today = localtime(now()).date()
        assert user.memberships.filter(club=self.club, end_date=today).exists()
        assert self.club.get_membership_for(user) is None

    def test_access_unauthorized(self):
        """Test that users who never subscribed and anonymous users
        cannot see the page.
        """
        response = self.client.post(self.members_url)
        assert response.status_code == 403

        self.client.force_login(self.public)
        response = self.client.post(self.members_url)
        assert response.status_code == 403

    def test_display(self):
        """Test that a GET request return a page where the requested
        information are displayed.
        """
        self.client.force_login(self.simple_board_member)
        response = self.client.get(self.members_url)
        assert response.status_code == 200
        soup = BeautifulSoup(response.text, "lxml")
        table = soup.find("table", id="club_members_table")
        assert [r.text for r in table.find("thead").find_all("td")] == [
            "Utilisateur",
            "Rôle",
            "Description",
            "Depuis",
            "Marquer comme ancien",
        ]
        rows = table.find("tbody").find_all("tr")
        memberships = self.club.members.ongoing().order_by("-role")
        for row, membership in zip(
            rows, memberships.select_related("user"), strict=False
        ):
            user = membership.user
            user_url = reverse("core:user_profile", args=[user.id])
            cols = row.find_all("td")
            user_link = cols[0].find("a")
            assert user_link.attrs["href"] == user_url
            assert user_link.text == user.get_display_name()
            assert cols[1].text == settings.SITH_CLUB_ROLES[membership.role]
            assert cols[2].text == membership.description
            assert cols[3].text == str(membership.start_date)

            if membership.role <= 3:  # 3 is the role of simple_board_member
                form_input = cols[4].find("input")
                expected_attrs = {
                    "type": "checkbox",
                    "name": "users_old",
                    "value": str(user.id),
                }
                assert form_input.attrs.items() >= expected_attrs.items()
            else:
                assert cols[4].find_all() == []

    def test_root_add_one_club_member(self):
        """Test that root users can add members to clubs, one at a time."""
        self.client.force_login(self.root)
        response = self.client.post(
            self.members_url,
            {"users": [self.subscriber.id], "role": 3},
        )
        self.assertRedirects(response, self.members_url)
        self.subscriber.refresh_from_db()
        self.assert_membership_started_today(self.subscriber, role=3)

    def test_root_add_multiple_club_member(self):
        """Test that root users can add multiple members at once to clubs."""
        self.client.force_login(self.root)
        response = self.client.post(
            self.members_url,
            {
                "users": (self.subscriber.id, self.krophil.id),
                "role": 3,
            },
        )
        self.assertRedirects(response, self.members_url)
        self.subscriber.refresh_from_db()
        self.assert_membership_started_today(self.subscriber, role=3)
        self.assert_membership_started_today(self.krophil, role=3)

    def test_add_unauthorized_members(self):
        """Test that users who are not currently subscribed
        cannot be members of clubs.
        """
        for user in self.public, self.old_subscriber:
            form = ClubMemberForm(
                data={"users": [user.id], "role": 1},
                request_user=self.root,
                club=self.club,
            )

            assert not form.is_valid()
            assert form.errors == {
                "users": [
                    "L'utilisateur doit être cotisant pour faire partie d'un club"
                ]
            }

    def test_add_members_already_members(self):
        """Test that users who are already members of a club
        cannot be added again to this club.
        """
        self.client.force_login(self.root)
        current_membership = self.simple_board_member.memberships.ongoing().get(
            club=self.club
        )
        nb_memberships = self.simple_board_member.memberships.count()
        self.client.post(
            self.members_url,
            {"users": self.simple_board_member.id, "role": current_membership.role + 1},
        )
        self.simple_board_member.refresh_from_db()
        assert nb_memberships == self.simple_board_member.memberships.count()
        new_membership = self.simple_board_member.memberships.ongoing().get(
            club=self.club
        )
        assert current_membership == new_membership
        assert self.club.get_membership_for(self.simple_board_member) == new_membership

    def test_add_not_existing_users(self):
        """Test that not existing users cannot be added in clubs.
        If one user in the request is invalid, no membership creation at all
        can take place.
        """
        nb_memberships = self.club.members.count()
        max_id = User.objects.aggregate(id=Max("id"))["id"]
        for members in [max_id + 1], [max_id + 1, self.subscriber.id]:
            form = ClubMemberForm(
                data={"users": members, "role": 1},
                request_user=self.root,
                club=self.club,
            )
            assert not form.is_valid()
            assert form.errors == {
                "users": [
                    "Sélectionnez un choix valide. "
                    f"{max_id + 1} n\u2019en fait pas partie."
                ]
            }
        self.club.refresh_from_db()
        assert self.club.members.count() == nb_memberships

    def test_president_add_members(self):
        """Test that the president of the club can add members."""
        president = self.club.members.get(role=10).user
        nb_club_membership = self.club.members.count()
        nb_subscriber_memberships = self.subscriber.memberships.count()
        self.client.force_login(president)
        response = self.client.post(
            self.members_url,
            {"users": self.subscriber.id, "role": 9},
        )
        self.assertRedirects(response, self.members_url)
        self.club.refresh_from_db()
        self.subscriber.refresh_from_db()
        assert self.club.members.count() == nb_club_membership + 1
        assert self.subscriber.memberships.count() == nb_subscriber_memberships + 1
        self.assert_membership_started_today(self.subscriber, role=9)

    def test_add_member_greater_role(self):
        """Test that a member of the club member cannot create
        a membership with a greater role than its own.
        """
        form = ClubMemberForm(
            data={"users": [self.subscriber.id], "role": 10},
            request_user=self.simple_board_member,
            club=self.club,
        )
        nb_memberships = self.club.members.count()

        assert not form.is_valid()
        assert form.errors == {
            "__all__": ["Vous n'avez pas la permission de faire cela"]
        }
        self.club.refresh_from_db()
        assert nb_memberships == self.club.members.count()
        assert not self.subscriber.memberships.filter(club=self.club).exists()

    def test_add_member_without_role(self):
        """Test that trying to add members without specifying their role fails."""
        self.client.force_login(self.root)
        form = ClubMemberForm(
            data={"users": [self.subscriber.id]},
            request_user=self.simple_board_member,
            club=self.club,
        )

        assert not form.is_valid()
        assert form.errors == {"role": ["Vous devez choisir un rôle"]}

    def test_end_membership_self(self):
        """Test that a member can end its own membership."""
        self.client.force_login(self.simple_board_member)
        self.client.post(
            self.members_url,
            {"users_old": self.simple_board_member.id},
        )
        self.simple_board_member.refresh_from_db()
        self.assert_membership_ended_today(self.simple_board_member)

    def test_end_membership_lower_role(self):
        """Test that board members of the club can end memberships
        of users with lower roles.
        """
        # remainder : simple_board_member has role 3, president has role 10, richard has role 1
        self.client.force_login(self.simple_board_member)
        response = self.client.post(
            self.members_url,
            {"users_old": self.richard.id},
        )
        self.assertRedirects(response, self.members_url)
        self.club.refresh_from_db()
        self.assert_membership_ended_today(self.richard)

    def test_end_membership_higher_role(self):
        """Test that board members of the club cannot end memberships
        of users with higher roles.
        """
        membership = self.president.memberships.filter(club=self.club).first()
        self.client.force_login(self.simple_board_member)
        self.client.post(
            self.members_url,
            {"users_old": self.president.id},
        )
        self.club.refresh_from_db()
        new_membership = self.club.get_membership_for(self.president)
        assert new_membership is not None
        assert new_membership == membership

        membership = self.president.memberships.filter(club=self.club).first()
        assert membership.end_date is None

    def test_end_membership_as_main_club_board(self):
        """Test that board members of the main club can end the membership
        of anyone.
        """
        # make subscriber a board member
        subscriber = subscriber_user.make()
        Membership.objects.create(club=self.ae, user=subscriber, role=3)

        nb_memberships = self.club.members.ongoing().count()
        self.client.force_login(subscriber)
        response = self.client.post(
            self.members_url,
            {"users_old": self.president.id},
        )
        self.assertRedirects(response, self.members_url)
        self.assert_membership_ended_today(self.president)
        assert self.club.members.ongoing().count() == nb_memberships - 1

    def test_end_membership_as_root(self):
        """Test that root users can end the membership of anyone."""
        nb_memberships = self.club.members.ongoing().count()
        self.client.force_login(self.root)
        response = self.client.post(
            self.members_url,
            {"users_old": [self.president.id]},
        )
        self.assertRedirects(response, self.members_url)
        self.assert_membership_ended_today(self.president)
        assert self.club.members.ongoing().count() == nb_memberships - 1

    def test_end_membership_as_foreigner(self):
        """Test that users who are not in this club cannot end its memberships."""
        nb_memberships = self.club.members.count()
        membership = self.richard.memberships.filter(club=self.club).first()
        self.client.force_login(self.subscriber)
        self.client.post(
            self.members_url,
            {"users_old": [self.richard.id]},
        )
        # nothing should have changed
        new_mem = self.club.get_membership_for(self.richard)
        assert self.club.members.count() == nb_memberships
        assert membership == new_mem

    def test_remove_from_club_group(self):
        """Test that when a membership ends, the user is removed from club groups."""
        user = baker.make(User)
        baker.make(Membership, user=user, club=self.club, end_date=None, role=3)
        assert user.groups.contains(self.club.members_group)
        assert user.groups.contains(self.club.board_group)
        user.memberships.update(end_date=localdate())
        assert not user.groups.contains(self.club.members_group)
        assert not user.groups.contains(self.club.board_group)

    def test_add_to_club_group(self):
        """Test that when a membership begins, the user is added to the club group."""
        assert not self.subscriber.groups.contains(self.club.members_group)
        assert not self.subscriber.groups.contains(self.club.board_group)
        baker.make(Membership, club=self.club, user=self.subscriber, role=3)
        assert self.subscriber.groups.contains(self.club.members_group)
        assert self.subscriber.groups.contains(self.club.board_group)

    def test_change_position_in_club(self):
        """Test that when moving from board to members, club group change"""
        membership = baker.make(
            Membership, club=self.club, user=self.subscriber, role=3
        )
        assert self.subscriber.groups.contains(self.club.members_group)
        assert self.subscriber.groups.contains(self.club.board_group)
        membership.role = 1
        membership.save()
        assert self.subscriber.groups.contains(self.club.members_group)
        assert not self.subscriber.groups.contains(self.club.board_group)

    def test_club_owner(self):
        """Test that a club is owned only by board members of the main club."""
        anonymous = AnonymousUser()
        assert not self.club.is_owned_by(anonymous)
        assert not self.club.is_owned_by(self.subscriber)

        # make sli a board member
        self.sli.memberships.all().delete()
        Membership(club=self.ae, user=self.sli, role=3).save()
        assert self.club.is_owned_by(self.sli)

    def test_change_club_name(self):
        """Test that changing the club name doesn't break things."""
        members_group = self.club.members_group
        board_group = self.club.board_group
        initial_members = set(members_group.users.values_list("id", flat=True))
        initial_board = set(board_group.users.values_list("id", flat=True))
        self.club.name = "something else"
        self.club.save()
        self.club.refresh_from_db()

        # The names should have changed, but not the ids nor the group members
        assert self.club.members_group.name == "something else - Membres"
        assert self.club.board_group.name == "something else - Bureau"
        assert self.club.members_group.id == members_group.id
        assert self.club.board_group.id == board_group.id
        new_members = set(self.club.members_group.users.values_list("id", flat=True))
        new_board = set(self.club.board_group.users.values_list("id", flat=True))
        assert new_members == initial_members
        assert new_board == initial_board