"""Tests for the FoldIt module"""
import json
import logging
from functools import partial

from django.test import TestCase
from django.test.client import RequestFactory
from django.core.urlresolvers import reverse

from foldit.views import foldit_ops, verify_code
from foldit.models import PuzzleComplete, Score
from student.models import unique_id_for_user, CourseEnrollment
from student.tests.factories import UserFactory

from datetime import datetime, timedelta
from pytz import UTC
from opaque_keys.edx.locations import SlashSeparatedCourseKey

log = logging.getLogger(__name__)


class FolditTestCase(TestCase):
    """Tests for various responses of the FoldIt module"""
    def setUp(self):
        super(FolditTestCase, self).setUp()

        self.factory = RequestFactory()
        self.url = reverse('foldit_ops')

        self.course_id = SlashSeparatedCourseKey('course', 'id', '1')
        self.course_id2 = SlashSeparatedCourseKey('course', 'id', '2')

        self.user = UserFactory.create()
        self.user2 = UserFactory.create()

        CourseEnrollment.enroll(self.user, self.course_id)
        CourseEnrollment.enroll(self.user2, self.course_id2)

        now = datetime.now(UTC)
        self.tomorrow = now + timedelta(days=1)
        self.yesterday = now - timedelta(days=1)

    def make_request(self, post_data, user=None):
        """Makes a request to foldit_ops with the given post data and user (if specified)"""
        request = self.factory.post(self.url, post_data)
        request.user = self.user if not user else user
        return request

    def make_puzzle_score_request(self, puzzle_ids, best_scores, user=None):
        """
        Given lists of puzzle_ids and best_scores (must have same length), make a
        SetPlayerPuzzleScores request and return the response.
        """
        if not isinstance(best_scores, list):
            best_scores = [best_scores]
        if not isinstance(puzzle_ids, list):
            puzzle_ids = [puzzle_ids]
        user = self.user if not user else user

        def score_dict(puzzle_id, best_score):
            """Returns a valid json-parsable score dict"""
            return {"PuzzleID": puzzle_id,
                    "ScoreType": "score",
                    "BestScore": best_score,
                    # current scores don't actually matter
                    "CurrentScore": best_score + 0.01,
                    "ScoreVersion": 23}
        scores = [score_dict(pid, bs) for pid, bs in zip(puzzle_ids, best_scores)]
        scores_str = json.dumps(scores)

        verify = {"Verify": verify_code(user.email, scores_str),
                  "VerifyMethod": "FoldItVerify"}
        data = {'SetPlayerPuzzleScoresVerify': json.dumps(verify),
                'SetPlayerPuzzleScores': scores_str}

        request = self.make_request(data, user)

        response = foldit_ops(request)
        self.assertEqual(response.status_code, 200)
        return response

    def test_SetPlayerPuzzleScores(self):  # pylint: disable=invalid-name

        puzzle_id = 994391
        best_score = 0.078034
        response = self.make_puzzle_score_request(puzzle_id, [best_score])

        self.assertEqual(response.content, json.dumps(
            [{"OperationID": "SetPlayerPuzzleScores",
              "Value": [{
                  "PuzzleID": puzzle_id,
                  "Status": "Success"}]}]))

        # There should now be a score in the db.
        top_10 = Score.get_tops_n(10, puzzle_id)
        self.assertEqual(len(top_10), 1)
        self.assertEqual(top_10[0]['score'], Score.display_score(best_score))

    def test_SetPlayerPuzzleScores_many(self):  # pylint: disable=invalid-name

        response = self.make_puzzle_score_request([1, 2], [0.078034, 0.080000])

        self.assertEqual(response.content, json.dumps(
            [{
                "OperationID": "SetPlayerPuzzleScores",
                "Value": [
                    {
                        "PuzzleID": 1,
                        "Status": "Success"
                    }, {
                        "PuzzleID": 2,
                        "Status": "Success"
                    }
                ]
            }]
        ))

    def test_SetPlayerPuzzleScores_multiple(self):  # pylint: disable=invalid-name
        """
        Check that multiple posts with the same id are handled properly
        (keep latest for each user, have multiple users work properly)
        """
        orig_score = 0.07
        puzzle_id = '1'
        self.make_puzzle_score_request([puzzle_id], [orig_score])

        # There should now be a score in the db.
        top_10 = Score.get_tops_n(10, puzzle_id)
        self.assertEqual(len(top_10), 1)
        self.assertEqual(top_10[0]['score'], Score.display_score(orig_score))

        # Reporting a better score should overwrite
        better_score = 0.06
        self.make_puzzle_score_request([1], [better_score])

        top_10 = Score.get_tops_n(10, puzzle_id)
        self.assertEqual(len(top_10), 1)

        # Floats always get in the way, so do almostequal
        self.assertAlmostEqual(
            top_10[0]['score'],
            Score.display_score(better_score),
            delta=0.5
        )

        # reporting a worse score shouldn't
        worse_score = 0.065
        self.make_puzzle_score_request([1], [worse_score])

        top_10 = Score.get_tops_n(10, puzzle_id)
        self.assertEqual(len(top_10), 1)
        # should still be the better score
        self.assertAlmostEqual(
            top_10[0]['score'],
            Score.display_score(better_score),
            delta=0.5
        )

    def test_SetPlayerPuzzleScores_multiple_courses(self):  # pylint: disable=invalid-name
        puzzle_id = "1"

        player1_score = 0.05
        player2_score = 0.06

        course_list_1 = [self.course_id]
        course_list_2 = [self.course_id2]

        self.make_puzzle_score_request(puzzle_id, player1_score, self.user)

        course_1_top_10 = Score.get_tops_n(10, puzzle_id, course_list_1)
        course_2_top_10 = Score.get_tops_n(10, puzzle_id, course_list_2)
        total_top_10 = Score.get_tops_n(10, puzzle_id)

        #  player1 should now be in the top 10 of course 1 and not in course 2
        self.assertEqual(len(course_1_top_10), 1)
        self.assertEqual(len(course_2_top_10), 0)
        self.assertEqual(len(total_top_10), 1)

        self.make_puzzle_score_request(puzzle_id, player2_score, self.user2)

        course_2_top_10 = Score.get_tops_n(10, puzzle_id, course_list_2)
        total_top_10 = Score.get_tops_n(10, puzzle_id)

        #  player2 should now be in the top 10 of course 2 and not in course 1
        self.assertEqual(len(course_1_top_10), 1)
        self.assertEqual(len(course_2_top_10), 1)
        self.assertEqual(len(total_top_10), 2)

    def test_SetPlayerPuzzleScores_many_players(self):  # pylint: disable=invalid-name
        """
        Check that when we send scores from multiple users, the correct order
        of scores is displayed. Note that, before being processed by
        display_score, lower scores are better.
        """
        puzzle_id = ['1']
        player1_score = 0.08
        player2_score = 0.02
        self.make_puzzle_score_request(puzzle_id, player1_score, self.user)

        # There should now be a score in the db.
        top_10 = Score.get_tops_n(10, puzzle_id)
        self.assertEqual(len(top_10), 1)
        self.assertEqual(top_10[0]['score'], Score.display_score(player1_score))

        self.make_puzzle_score_request(puzzle_id, player2_score, self.user2)

        # There should now be two scores in the db
        top_10 = Score.get_tops_n(10, puzzle_id)
        self.assertEqual(len(top_10), 2)

        # Top score should be player2_score. Second should be player1_score
        self.assertAlmostEqual(
            top_10[0]['score'],
            Score.display_score(player2_score),
            delta=0.5
        )
        self.assertAlmostEqual(
            top_10[1]['score'],
            Score.display_score(player1_score),
            delta=0.5
        )

        # Top score user should be self.user2.username
        self.assertEqual(top_10[0]['username'], self.user2.username)

    def test_SetPlayerPuzzleScores_error(self):  # pylint: disable=invalid-name

        scores = [{
            "PuzzleID": 994391,
            "ScoreType": "score",
            "BestScore": 0.078034,
            "CurrentScore": 0.080035,
            "ScoreVersion": 23
        }]
        validation_str = json.dumps(scores)

        verify = {
            "Verify": verify_code(self.user.email, validation_str),
            "VerifyMethod": "FoldItVerify"
        }

        # change the real string -- should get an error
        scores[0]['ScoreVersion'] = 22
        scores_str = json.dumps(scores)

        data = {
            'SetPlayerPuzzleScoresVerify': json.dumps(verify),
            'SetPlayerPuzzleScores': scores_str
        }

        request = self.make_request(data)

        response = foldit_ops(request)
        self.assertEqual(response.status_code, 200)

        self.assertEqual(response.content,
                         json.dumps([{
                             "OperationID": "SetPlayerPuzzleScores",
                             "Success": "false",
                             "ErrorString": "Verification failed",
                             "ErrorCode": "VerifyFailed"}]))

    def make_puzzles_complete_request(self, puzzles):
        """
        Make a puzzles complete request, given an array of
        puzzles.  E.g.

        [ {"PuzzleID": 13, "Set": 1, "SubSet": 2},
          {"PuzzleID": 53524, "Set": 1, "SubSet": 1} ]
        """
        puzzles_str = json.dumps(puzzles)

        verify = {
            "Verify": verify_code(self.user.email, puzzles_str),
            "VerifyMethod": "FoldItVerify"
        }

        data = {
            'SetPuzzlesCompleteVerify': json.dumps(verify),
            'SetPuzzlesComplete': puzzles_str
        }

        request = self.make_request(data)

        response = foldit_ops(request)
        self.assertEqual(response.status_code, 200)
        return response

    @staticmethod
    def set_puzzle_complete_response(values):
        """Returns a json response of a Puzzle Complete message"""
        return json.dumps([{"OperationID": "SetPuzzlesComplete",
                            "Value": values}])

    def test_SetPlayerPuzzlesComplete(self):  # pylint: disable=invalid-name

        puzzles = [
            {"PuzzleID": 13, "Set": 1, "SubSet": 2},
            {"PuzzleID": 53524, "Set": 1, "SubSet": 1}
        ]

        response = self.make_puzzles_complete_request(puzzles)

        self.assertEqual(response.content,
                         self.set_puzzle_complete_response([13, 53524]))

    def test_SetPlayerPuzzlesComplete_multiple(self):  # pylint: disable=invalid-name
        """Check that state is stored properly"""

        puzzles = [
            {"PuzzleID": 13, "Set": 1, "SubSet": 2},
            {"PuzzleID": 53524, "Set": 1, "SubSet": 1}
        ]

        response = self.make_puzzles_complete_request(puzzles)

        self.assertEqual(response.content,
                         self.set_puzzle_complete_response([13, 53524]))

        puzzles = [
            {"PuzzleID": 14, "Set": 1, "SubSet": 3},
            {"PuzzleID": 15, "Set": 1, "SubSet": 1}
        ]

        response = self.make_puzzles_complete_request(puzzles)

        self.assertEqual(
            response.content,
            self.set_puzzle_complete_response([13, 14, 15, 53524])
        )

    def test_SetPlayerPuzzlesComplete_level_complete(self):  # pylint: disable=invalid-name
        """Check that the level complete function works"""

        puzzles = [
            {"PuzzleID": 13, "Set": 1, "SubSet": 2},
            {"PuzzleID": 53524, "Set": 1, "SubSet": 1}
        ]

        response = self.make_puzzles_complete_request(puzzles)

        self.assertEqual(response.content,
                         self.set_puzzle_complete_response([13, 53524]))

        puzzles = [
            {"PuzzleID": 14, "Set": 1, "SubSet": 3},
            {"PuzzleID": 15, "Set": 1, "SubSet": 1}
        ]

        response = self.make_puzzles_complete_request(puzzles)

        self.assertEqual(response.content,
                         self.set_puzzle_complete_response([13, 14, 15, 53524]))

        is_complete = partial(
            PuzzleComplete.is_level_complete, unique_id_for_user(self.user))

        self.assertTrue(is_complete(1, 1))
        self.assertTrue(is_complete(1, 3))
        self.assertTrue(is_complete(1, 2))
        self.assertFalse(is_complete(4, 5))

        puzzles = [{"PuzzleID": 74, "Set": 4, "SubSet": 5}]

        response = self.make_puzzles_complete_request(puzzles)

        self.assertTrue(is_complete(4, 5))

        # Now check due dates

        self.assertTrue(is_complete(1, 1, due=self.tomorrow))
        self.assertFalse(is_complete(1, 1, due=self.yesterday))

    def test_SetPlayerPuzzlesComplete_error(self):  # pylint: disable=invalid-name

        puzzles = [
            {"PuzzleID": 13, "Set": 1, "SubSet": 2},
            {"PuzzleID": 53524, "Set": 1, "SubSet": 1}
        ]

        puzzles_str = json.dumps(puzzles)

        verify = {
            "Verify": verify_code(self.user.email, puzzles_str + "x"),
            "VerifyMethod": "FoldItVerify"
        }

        data = {
            'SetPuzzlesCompleteVerify': json.dumps(verify),
            'SetPuzzlesComplete': puzzles_str
        }

        request = self.make_request(data)

        response = foldit_ops(request)
        self.assertEqual(response.status_code, 200)

        self.assertEqual(response.content,
                         json.dumps([{
                             "OperationID": "SetPuzzlesComplete",
                             "Success": "false",
                             "ErrorString": "Verification failed",
                             "ErrorCode": "VerifyFailed"}]))