"""
The Docker Container "database" needs to run to execute this tests properly
"""
import datetime
import unittest

from app import db
from Models.UserModel import UserModel
from Models.ChannelModel import ChannelModel
from Models.ProjectModel import ProjectModel
from Models.MatchModel import MatchModel
from Models.LikeModel import LikeModel
from Models.DislikeModel import DislikeModel
from Rest.MatchesREST import MatchesREST
from Rest.Answer import MessageCodes
from Matching.PreferenceTyps import *


def prepare_db():
    db.session.commit()
    db.drop_all()
    db.create_all()
    db.session.commit()


def setup_one():
    """
    Three users, two projects, user one and two attend project one, user three attends all projects.
    Matriculation date of user one is closer to user two.
    Matriculation dates in chronological order: user_three, user_one, user_two
    User one has one interest, user two has two, user three has three.
    The interests are as shared as possible.
    """
    prepare_db()
    # create users
    user_one = UserModel.create("user_1", "randompassword", "randomemail1@email.de")
    user_two = UserModel.create("user_2", "randompassword", "randomemail2@email.de")
    user_three = UserModel.create("user_3", "randompassword", "randomemail3@email.de")
    # create personal information
    pi_one = user_one.personal_information
    pi_one.matriculation_date, pi_one.description, pi_one.interest_list = datetime.date(2020, 8, 29), "hello", [
        "Computer"]
    pi_two = user_two.personal_information
    pi_two.matriculation_date, pi_two.description, pi_two.interest_list = datetime.date(2021, 8, 29), "hello", [
        "Computer", "Sport"]
    pi_three = user_three.personal_information
    pi_three.matriculation_date, pi_three.description, pi_three.interest_list = datetime.date(2010, 8, 29), "hello", [
        "Computer", "Sport", "Sleeping"]
    db.session.commit()
    # create two channels
    channel_one, channel_two = ChannelModel(), ChannelModel()
    db.session.add(channel_one)
    db.session.add(channel_two)
    db.session.commit()
    # create two projects
    project_one = ProjectModel(name="A_proj_one", channel_id=channel_one.id, has_exam=True)
    project_two = ProjectModel(name="B_proj_two", channel_id=channel_two.id, has_exam=False)
    db.session.add(project_one)
    db.session.add(project_two)
    db.session.commit()
    # let users attend
    project_one.attendees.append(user_one)
    project_one.attendees.append(user_two)
    project_one.attendees.append(user_three)
    project_two.attendees.append(user_three)
    db.session.commit()

    return {'user_one': user_one, 'user_two': user_two, 'user_three': user_three,
            'project_one': project_one, 'project_two': project_two}


def setup_two():
    """
    Four users, user one has matched with user two and three.
    User one likes user four. User two dislikes user four.
    """
    prepare_db()
    # create users
    user_one = UserModel.create("user_1", "randompassword", "randomemail1@email.de")
    user_two = UserModel.create("user_2", "randompassword", "randomemail2@email.de")
    user_three = UserModel.create("user_3", "randompassword", "randomemail4@email.de")
    user_four = UserModel.create("user_4", "randompassword", "randomemail3@email.de")
    # create matches
    match_one_two = MatchModel()
    match_one_two.users = [user_one, user_two]
    match_one_three = MatchModel()
    match_one_three.users = [user_one, user_three]
    [db.session.add(m) for m in [match_one_two, match_one_three]]
    db.session.commit()
    # create likes, dislikes
    like_one_four = LikeModel(user=user_one, likes=user_four)
    dislike_two_four = DislikeModel(user=user_two, dislikes=user_four)
    [db.session.add(l) for l in [like_one_four, dislike_two_four]]
    db.session.commit()

    return {"user_one": user_one, "user_two": user_two, "user_three": user_three, "user_four": user_four}


def setup_user_with_preferences():
    """
    One users. With matching preferences: project: same, age_group: higher, interests: different
    """
    user_with_preferences = UserModel.create("user_with_preferences", "randompassword", "preferences@email.de")
    user_with_preferences.matching_preferences.proj_pref = ProjectPreference.SAME
    user_with_preferences.matching_preferences.age_group_pref = AgeGroupPreference.HIGHER
    # TODO substitute DONT_CARE with meaningful interests preference
    user_with_preferences.matching_preferences.interests_pref = InterestsPreferences.DONT_CARE
    # add personal information
    pi = user_with_preferences.personal_information
    pi.matriculation_date, pi.description, pi.interest_list = datetime.date(2011, 8, 29), "hello", ["Computer"]
    db.session.add(user_with_preferences)
    db.session.commit()
    return user_with_preferences


class TestMatchesRest(unittest.TestCase):
    def test_setup_one(self):
        setup = setup_one()
        user_one = setup['user_one']
        project_one = setup['project_one']
        project_two = setup['project_two']

        user_one_after = UserModel.query.get(user_one.id)
        self.assertEqual(user_one_after.personal_information.description, "hello")
        project_one_after = ProjectModel.query.get(project_one.id)
        user_one_after = UserModel.query.get(user_one.id)
        self.assertTrue(user_one_after in project_one_after.attendees)
        self.assertTrue(project_one_after in user_one_after.personal_information.projects)
        project_two_after = ProjectModel.query.get(project_two.id)
        self.assertTrue(user_one_after not in project_two_after.attendees)
        self.assertTrue(project_two_after not in user_one_after.personal_information.projects)

    def test_projects_matching(self):
        # SETUP
        setup = setup_one()
        user_one = setup['user_one']
        user_two = setup['user_two']

        # ACTION
        match_config = {"type": "Projects"}
        project_matching_result = MatchesREST.get_matches(user_one.id, 1, match_config)

        # RESULT
        # right message code
        self.assertEqual(MessageCodes.WORKED.value, project_matching_result.message)
        # one user is returned, it is user_two
        self.assertEqual(1, len(project_matching_result.result))
        result_user = project_matching_result.result[0]
        self.assertEqual(user_two, UserModel.query.get(result_user.id))
        self.assertEqual(user_two.username, result_user.display_name)
        self.assertFalse(result_user.profile_pic_id)

    def test_default_preference_matching_no_custom(self):
        # SETUP
        setup = setup_one()
        user_one = setup['user_one']

        # ACTION
        match_config = {"type": "Preference"}
        project_matching_result = MatchesREST.get_matches(user_one.id, 1, match_config)

        # RESULT
        # right message code
        self.assertEqual(project_matching_result.message, MessageCodes.WORKED.value)
        # one user is returned
        self.assertEqual(len(project_matching_result.result), 1)

    def test_default_preference_matching_custom(self):
        # SETUP
        setup = setup_one()
        user_one = setup['user_one']

        # ACTION
        match_config = {"type": "Preference", "custom": {
            "ProjectPreference": ProjectPreference.DONT_CARE,
            "AgeGroupPreference": AgeGroupPreference.DONT_CARE,
            "InterestsPreferences": InterestsPreferences.DONT_CARE}
                        }
        project_matching_result = MatchesREST.get_matches(user_one.id, 1, match_config)

        # RESULT
        # right message code
        self.assertEqual(project_matching_result.message, MessageCodes.WORKED.value)
        # one user is returned
        self.assertEqual(len(project_matching_result.result), 1)

    def test_custom_preference_matching_projects(self):
        # SETUP
        setup = setup_one()
        user_one = setup['user_one']
        user_two = setup['user_two']
        user_three = setup['user_three']

        # ACTION
        match_config_same = {"type": "Preference", "custom": {"ProjectPreference": ProjectPreference.SAME}}
        match_config_different = {"type": "Preference", "custom": {"ProjectPreference": ProjectPreference.DIFFERENT}}
        project_matching_result_same = MatchesREST.get_matches(user_one.id, 1, match_config_same)
        project_matching_result_different = MatchesREST.get_matches(user_one.id, 1, match_config_different)

        # RESULT
        # right message code
        self.assertEqual(project_matching_result_same.message, MessageCodes.WORKED.value)
        self.assertEqual(project_matching_result_different.message, MessageCodes.WORKED.value)
        # both return one user
        self.assertEqual(len(project_matching_result_same.result), 1)
        self.assertEqual(len(project_matching_result_different.result), 1)
        # same returns user two, different returns user three
        self.assertEqual(project_matching_result_same.result[0].id, user_two.id)
        self.assertEqual(project_matching_result_different.result[0].id, user_three.id)

    def test_custom_preferences_matching_age_group(self):
        # SETUP
        setup = setup_one()
        user_one = setup['user_one']
        user_two = setup['user_two']
        user_three = setup['user_three']

        # ACTION
        match_config_same = {"type": "Preference", "custom": {"AgeGroupPreference": AgeGroupPreference.SAME}}
        match_config_lower = {"type": "Preference", "custom": {"AgeGroupPreference": AgeGroupPreference.LOWER}}
        match_config_higher = {"type": "Preference", "custom": {"AgeGroupPreference": AgeGroupPreference.HIGHER}}
        age_matching_result_same = MatchesREST.get_matches(user_one.id, 1, match_config_same)
        age_matching_result_lower = MatchesREST.get_matches(user_one.id, 1, match_config_lower)
        age_matching_result_higher = MatchesREST.get_matches(user_one.id, 1, match_config_higher)

        # RESULT
        # right message code
        self.assertEqual(age_matching_result_same.message, MessageCodes.WORKED.value)
        self.assertEqual(age_matching_result_lower.message, MessageCodes.WORKED.value)
        self.assertEqual(age_matching_result_higher.message, MessageCodes.WORKED.value)
        # one user is returned by each
        self.assertEqual(len(age_matching_result_same.result), 1)
        self.assertEqual(len(age_matching_result_lower.result), 1)
        self.assertEqual(len(age_matching_result_higher.result), 1)
        # user two is returned for same and higher
        self.assertEqual(age_matching_result_same.result[0].id, user_two.id)
        self.assertEqual(age_matching_result_higher.result[0].id, user_two.id)
        # user three is returned for lower
        self.assertEqual(age_matching_result_lower.result[0].id, user_three.id)

    def test_custom_preferences_matching_interests(self):
        pass

    def test_complex_user_matching_preferences(self):
        # SETUP
        setup = setup_one()
        user_two = setup['user_two']
        project_one = setup['project_one']
        preference_user = setup_user_with_preferences()
        project_one.attendees.append(preference_user)
        db.session.commit()

        # ACTION
        match_config = {"type": "Preference"}
        complex_matching_result = MatchesREST.get_matches(preference_user.id, 1, match_config)

        # RESULT
        # right message code
        self.assertEqual(complex_matching_result.message, MessageCodes.WORKED.value)
        # one user is returned
        self.assertEqual(len(complex_matching_result.result), 1)
        # user two is returned
        self.assertEqual(complex_matching_result.result[0].id, user_two.id)

    def test_get_confirmed_matches(self):
        # SETUP
        setup = setup_two()
        user_one = setup['user_one']
        user_two = setup['user_two']
        user_three = setup['user_three']
        user_four = setup['user_four']

        # ACTION
        confirmed_matches_result = MatchesREST.get_confirmed_matches(user_one.id)

        # RESULT
        # right message code
        self.assertEqual(confirmed_matches_result.message, MessageCodes.WORKED.value)
        # two users are returned
        returned_users = confirmed_matches_result.result
        self.assertEqual(len(returned_users), 2)
        # the two users are user two and three
        returned_ids = [user.id for user in returned_users]
        self.assertTrue(user_two.id in returned_ids)
        self.assertTrue(user_three.id in returned_ids)
        # user four is not returned
        self.assertFalse(user_four.id in returned_ids)

    def test_like_user(self):
        # SETUP
        setup = setup_two()
        user_three = setup['user_three']
        user_four = setup['user_four']

        # ACTION
        like_user_result = MatchesREST.post_like_user(user_three.id, user_four.id)

        # RESULT
        # right message code
        self.assertEqual(like_user_result.message, MessageCodes.WORKED.value)
        # like exists
        self.assertTrue(LikeModel.query.filter_by(user_id=user_three.id, likes_id=user_four.id).first())

    def test_like_already_liked_user(self):
        # SETUP
        setup = setup_two()
        user_one = setup['user_one']
        user_four = setup['user_four']

        # ACTION
        like_user_already_liked_result = MatchesREST.post_like_user(user_one.id, user_four.id)

        # RESULT
        # right message code
        self.assertEqual(like_user_already_liked_result.message, MessageCodes.MATCHING_ALREADY_LIKED.value)
        # like still exists
        self.assertTrue(LikeModel.query.filter_by(user_id=user_one.id, likes_id=user_four.id).first())

    def test_like_already_matched_user(self):
        # SETUP
        setup = setup_two()
        user_one = setup['user_one']
        user_two = setup['user_two']

        # ACTION
        like_user_already_matched_result = MatchesREST.post_like_user(user_one.id, user_two.id)

        # RESULT
        # right message code
        self.assertEqual(like_user_already_matched_result.message, MessageCodes.MATCHING_ALREADY_MATCHED.value)
        # no like exists
        self.assertFalse(LikeModel.query.filter_by(user_id=user_one.id, likes_id=user_two.id).first())

    def test_like_already_disliked_user(self):
        # SETUP
        setup = setup_two()
        user_two = setup['user_two']
        user_four = setup['user_four']

        # ACTION
        like_user_already_disliked_result = MatchesREST.post_like_user(user_two.id, user_four.id)

        # RESULT
        # right message code
        self.assertEqual(like_user_already_disliked_result.message, MessageCodes.MATCHING_DISLIKE_REVERTED.value)
        # like exists
        self.assertTrue(LikeModel.query.filter_by(user_id=user_two.id, likes_id=user_four.id).first())
        # dislike doesn't exist anymore
        self.assertFalse(DislikeModel.query.filter_by(user_id=user_four.id, dislikes_id=user_four.id).first())

    def test_dislike_user(self):
        # SETUP
        setup = setup_two()
        user_three = setup['user_three']
        user_four = setup['user_four']

        # ACTION
        dislike_user_result = MatchesREST.post_dislike(user_four.id, user_three.id)

        # RESULT
        # right message code
        self.assertEqual(dislike_user_result.message, MessageCodes.WORKED.value)
        # dislike exists
        self.assertTrue(DislikeModel.query.filter_by(user_id=user_four.id, dislikes_id=user_three.id).first())

    def test_dislike_already_disliked_user(self):
        # SETUP
        setup = setup_two()
        user_two = setup['user_two']
        user_four = setup['user_four']

        # ACTION
        dislike_user_already_disliked_result = MatchesREST.post_dislike(user_two.id, user_four.id)

        # RESULT
        # right message code
        self.assertEqual(dislike_user_already_disliked_result.message, MessageCodes.MATCHING_ALREADY_DISLIKED.value)
        # dislike exists
        self.assertTrue(DislikeModel.query.filter_by(user_id=user_two.id, dislikes_id=user_four.id).first())

    def test_dislike_already_liked_user(self):
        # SETUP
        setup = setup_two()
        user_one = setup['user_one']
        user_four = setup['user_four']

        # ACTION
        dislike_user_already_liked_result = MatchesREST.post_dislike(user_one.id, user_four.id)

        # RESULT
        # right message code
        self.assertEqual(dislike_user_already_liked_result.message, MessageCodes.MATCHING_LIKE_REVERTED.value)
        # dislike still
        self.assertTrue(DislikeModel.query.filter_by(user_id=user_one.id, dislikes_id=user_four.id).first())
        # like doesn't exist anymore
        self.assertFalse(LikeModel.query.filter_by(user_id=user_one.id, likes_id=user_four.id).first())

    def test_dislike_already_matched_user(self):
        # SETUP
        setup = setup_two()
        user_one = setup['user_one']
        user_two = setup['user_two']

        # ACTION
        dislike_user_already_matched_result = MatchesREST.post_dislike(user_one.id, user_two.id)

        # RESULT
        # right message code
        self.assertEqual(dislike_user_already_matched_result.message, MessageCodes.MATCHING_ALREADY_MATCHED.value)
        # no dislike exists
        self.assertFalse(DislikeModel.query.filter_by(user_id=user_one.id, dislikes_id=user_two.id).first())

    def test_match_suggest_already_matched_user(self):
        # SETUP
        setup = setup_two()
        user_one = setup['user_one']
        user_two = setup['user_two']

        # ACTION
        match_config_pref = {"type": "Preference"}
        get_match_suggestions_preference_result = MatchesREST.get_matches(user_one.id, 10, match_config_pref)

        # RESULT
        # right message code
        self.assertEqual(MessageCodes.WORKED.value, get_match_suggestions_preference_result.message)
        # user two is not in results (because match confirmed already)
        self.assertTrue(user_two not in get_match_suggestions_preference_result.result)

    def test_match_suggest_not_same_project_project_matcher(self):
        # SETUP
        setup = setup_two()
        user_one = setup['user_one']

        # ACTION
        match_config_proj = {"type": "Projects"}
        get_match_suggestions_projects_result = MatchesREST.get_matches(user_one.id, 10, match_config_proj)

        # RESULT
        # right message code
        self.assertEqual(MessageCodes.WORKED.value, get_match_suggestions_projects_result.message)
        # not suggestions are returned
        self.assertFalse(get_match_suggestions_projects_result.result)

    def test_single_project_matching(self):
        # SETUP
        setup = setup_one()
        user_one = setup['user_one']
        user_two = setup['user_two']
        project_one = setup['project_one']
        # set preference of user one
        user_one.matching_preferences.age_group_pref = AgeGroupPreference.SAME
        db.session.add(user_one)
        db.session.commit()

        # ACTION
        match_config = {"type": "SingleProject", "custom": {"project_id": str(project_one.id)}}
        single_project_matching_result = MatchesREST.get_matches(user_one.id, 1, match_config)

        # RESULT
        # right message code
        self.assertEqual(MessageCodes.WORKED.value, single_project_matching_result.message)
        # one user is returned, it is user_two
        self.assertEqual(1, len(single_project_matching_result.result))
        result_user = single_project_matching_result.result[0]
        self.assertEqual(user_two, UserModel.query.get(result_user.id))
        self.assertEqual(user_two.username, result_user.display_name)
        self.assertFalse(result_user.profile_pic_id)

    def test_matchable_flag_other(self):
        # SETUP
        setup = setup_one()
        user_one = setup["user_one"]
        # make user two unmatchable
        user_two = setup["user_two"]
        user_two.moderation_flags.can_match = False
        db.session.add(user_two)
        db.session.commit()

        # ACTION
        match_config = {"type": "Preference"}
        get_matches_result = MatchesREST.get_matches(user_one.id, 2, match_config)

        # RESULT
        # right message code
        self.assertEqual(MessageCodes.WORKED.value, get_matches_result.message)
        # only one match returned
        self.assertEqual(1, len(get_matches_result.result))
        # not matchable user not in results
        self.assertTrue(user_two not in get_matches_result.result)

    def test_matchable_flag_self(self):
        # SETUP
        setup = setup_one()
        user_one = setup["user_one"]
        # make user one unmatchable
        user_one.moderation_flags.can_match = False
        db.session.add(user_one)
        db.session.commit()

        # ACTION
        match_config = {"type": "Preference"}
        get_matches_result = MatchesREST.get_matches(user_one.id, 2, match_config)

        # RESULT
        # right message code
        self.assertEqual(MessageCodes.WORKED.value, get_matches_result.message)
        # no matches are returned
        self.assertEqual(0, len(get_matches_result.result))