from django.test import LiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec

from core.models import UserAccount, Submission, FeedbackComment
from functional_tests.util import (login, create_browser, reset_browser_after_test,
                                   subscriptions_loaded_cond)
from util import factory_boys as fact


class UntestedParent:
    class TestFeedbackCreationGeneric(LiveServerTestCase):
        browser: webdriver.Firefox = None
        username = None
        password = None
        role = None

        @classmethod
        def setUpClass(cls):
            super().setUpClass()
            cls.browser = create_browser()

        @classmethod
        def tearDownClass(cls):
            super().tearDownClass()
            cls.browser.quit()

        def setUp(self):
            self.sub_type = fact.SubmissionTypeFactory.create()
            fact.SubmissionFactory.create_batch(2, type=self.sub_type)

        def tearDown(self):
            reset_browser_after_test(self.browser, self.live_server_url)

        def _login(self):
            login(self.browser, self.live_server_url, self.username, self.password)

        def _reconstruct_submission_code(self):
            sub_table = self.browser.find_element_by_class_name('submission-table')
            lines = sub_table.find_elements_by_tag_name('tr')
            line_no_code_pairs = [
                (line.get_attribute('id'),
                 # call get_attribute here to get non normalized text
                 # https://github.com/SeleniumHQ/selenium/issues/2608
                 line.find_element_by_class_name('code-cell-content').get_attribute('textContent'))
                for line in lines
            ]
            line_no_code_pairs.sort(key=lambda x: x[0])  # sort by ids
            code_lines = list(zip(*line_no_code_pairs))[1]
            return '\n'.join(code_lines)

        # stage can be 'initial', 'validate', or 'conflict'
        def _go_to_subscription(self, stage='initial', sub_type=None):
            tasks = self.browser.find_element_by_name('subscription-list')
            WebDriverWait(self.browser, 10).until(subscriptions_loaded_cond(tasks))
            tab = tasks.find_element_by_xpath(f'//*[contains(text(), "{stage}")]')
            tab.click()
            sub_type = sub_type if sub_type is not None else self.sub_type

            sub_type_xpath = f'//*[contains(text(), "{sub_type.name}") ' \
                f'and not(contains(@class, "inactive"))]'
            WebDriverWait(self.browser, 10).until(
                ec.element_to_be_clickable((By.XPATH, sub_type_xpath)),
                message="SubmissionType not clickable"
            )
            sub_type_el = tasks.find_element_by_xpath(sub_type_xpath)
            sub_type_el.click()
            WebDriverWait(self.browser, 10).until(ec.url_contains('subscription'))

        def write_comments_on_lines(self, line_comment_tuples):
            """ line_comment_tuples is an iterable containing tuples of
                (line_no, comment) where the line number starts at 1
            """

            sub_table = self.browser.find_element_by_class_name('submission-table')
            lines = sub_table.find_elements_by_tag_name('tr')

            for (line_no, comment) in line_comment_tuples:
                line = lines[line_no-1]
                line.find_element_by_tag_name('button').click()
                textarea = line.find_element_by_tag_name('textarea')
                textarea.send_keys(comment)
                line.find_element_by_id('submit-comment').click()

        def test_student_text_is_correctly_displayed(self):
            self._login()
            self._go_to_subscription()
            code = self._reconstruct_submission_code()
            # query db for Submission with seen code, throws if not present and test fails
            Submission.objects.get(text=code)

        def test_submission_type_is_correctly_displayed(self):
            self._login()
            self._go_to_subscription()
            sub_type_el = self.browser.find_element_by_id('submission-type')
            title = sub_type_el.find_element_by_class_name('title')
            self.assertEqual(
                f'{self.sub_type.name} - Full score: {self.sub_type.full_score}',
                title.text
            )
            solution = sub_type_el.find_element_by_class_name('solution-code')
            self.assertEqual(self.sub_type.solution, solution.get_attribute('textContent'))
            description = sub_type_el.find_element_by_class_name('type-description')
            html_el_in_desc = description.find_element_by_tag_name('h1')
            self.assertEqual('This', html_el_in_desc.text)

        def test_test_output_is_displayed(self):
            # create a test for every submission
            test = None
            for submission in Submission.objects.all():
                test = fact.TestFactory.create(submission=submission, annotation='This is a test')
            self._login()
            self._go_to_subscription()
            tests = self.browser.find_element_by_id('submission-tests')
            name_label = tests.find_element_by_name('test-name-label')
            name_label.click()
            self.assertIn(test.name, name_label.text)
            self.assertIn(test.label, name_label.text)
            test_output = tests.find_element_by_name('test-output')
            WebDriverWait(self.browser, 10).until(ec.visibility_of(test_output))
            self.assertEqual(test.annotation, test_output.text)

        def wait_until_code_changes(self, code):
            def condition(*args):
                try:
                    # code might change during the call resulting in the exception
                    new_code = self._reconstruct_submission_code()
                except StaleElementReferenceException:
                    return False
                return code != new_code
            return condition

        def test_can_give_max_score(self):
            self._login()
            self._go_to_subscription()
            code = self._reconstruct_submission_code()
            self.browser.find_element_by_id('score-full').click()
            submit_btn = self.browser.find_element_by_id('submit-feedback')
            submit_btn.click()
            WebDriverWait(self.browser, 10).until(
                self.wait_until_code_changes(code)
            )
            submission_for_code = Submission.objects.get(text=code)
            self.assertEqual(self.sub_type.full_score, submission_for_code.feedback.score)

        def test_zero_score_without_warning_gives_error(self):
            self._login()
            self._go_to_subscription()
            self.browser.find_element_by_id('score-zero').click()
            submit_btn = self.browser.find_element_by_id('submit-feedback')
            submit_btn.click()
            notification = self.browser.find_element_by_class_name('notification-content')
            self.assertIn('comment', notification.text)

        def test_can_give_zero_score(self):
            self._login()
            self._go_to_subscription()
            code = self._reconstruct_submission_code()
            self.browser.find_element_by_id('score-zero').click()
            self.write_comments_on_lines([(0, 'A comment')])
            self.browser.find_element_by_id('submit-feedback').click()
            WebDriverWait(self.browser, 10).until(self.wait_until_code_changes(code))
            submission_for_code = Submission.objects.get(text=code)
            self.assertEqual(0, submission_for_code.feedback.score)

        def test_can_give_comments_and_decreased_score(self):
            self._login()
            self._go_to_subscription()
            code = self._reconstruct_submission_code()

            # give half full score
            score_input = self.browser.find_element_by_id('score-input')
            score_input.send_keys(self.sub_type.full_score // 2)

            # give feedback on first and last line of submission
            comment_text = 'This is feedback'
            self.write_comments_on_lines([
                (1, comment_text), (0, comment_text)  # 0 corresponds to the last line
            ])

            submit_btn = self.browser.find_element_by_id('submit-feedback')
            submit_btn.click()
            WebDriverWait(self.browser, 10).until(
                self.wait_until_code_changes(code)
            )
            submission_for_code = Submission.objects.get(text=code)
            self.assertEqual(self.sub_type.full_score // 2, submission_for_code.feedback.score)
            self.assertEqual(2, submission_for_code.feedback.feedback_lines.count())
            fst_comment = FeedbackComment.objects.get(
                of_feedback=submission_for_code.feedback,
                of_line=1
            )
            self.assertEqual(comment_text, fst_comment.text)
            last_line_of_sub = len(submission_for_code.text.split('\n'))
            snd_comment = FeedbackComment.objects.get(
                of_feedback=submission_for_code.feedback,
                of_line=last_line_of_sub
            )
            self.assertEqual(comment_text, snd_comment.text)

        def test_can_skip_submission(self):
            self._login()
            self._go_to_subscription()
            code = self._reconstruct_submission_code()
            self.browser.find_element_by_id('skip-submission').click()
            WebDriverWait(self.browser, 10).until(self.wait_until_code_changes(code))

        def test_can_validate_submission(self):
            self._login()
            self._go_to_subscription()

            def correct():
                code = self._reconstruct_submission_code()
                self.write_comments_on_lines([(0, 'A comment by me')])
                self.browser.find_element_by_id('score-zero').click()
                self.browser.find_element_by_id('submit-feedback').click()
                return code
            code = correct()
            WebDriverWait(self.browser, 10).until(self.wait_until_code_changes(code))
            correct()

            sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
            WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))

            reset_browser_after_test(self.browser, self.live_server_url)  # logs out user

            user_snd = 'tutor_snd'
            password = 'p'
            fact.UserAccountFactory(username=user_snd, password=password)

            login(self.browser, self.live_server_url, user_snd, password)
            self._go_to_subscription(stage='validate')
            self.write_comments_on_lines([(0, 'I disagree'), (1, 'Full points!')])
            code_final = self._reconstruct_submission_code()
            self.browser.find_element_by_id('score-full').click()
            self.browser.find_element_by_id('submit-feedback').click()

            WebDriverWait(self.browser, 10).until(self.wait_until_code_changes(code_final))
            code_non_final = self._reconstruct_submission_code()
            self.browser.find_element_by_class_name('final-checkbox').click()
            self.browser.find_element_by_id('submit-feedback').click()

            sub_url = 'subscription/' + str(self.sub_type.pk) + '/ended'
            WebDriverWait(self.browser, 10).until(ec.url_contains(sub_url))

            reset_browser_after_test(self.browser, self.live_server_url)

            user_rev = 'rev'
            password = 'p'
            role = UserAccount.REVIEWER
            fact.UserAccountFactory(username=user_rev, password=password, role=role)
            login(self.browser, self.live_server_url, user_rev, password)

            self._go_to_subscription('conflict')
            code = self._reconstruct_submission_code()
            self.assertEqual(code, code_non_final)

            submission_for_code = Submission.objects.get(text=code_final)
            self.assertEqual(self.sub_type.full_score, submission_for_code.feedback.score)
            self.assertEqual(3, submission_for_code.feedback.feedback_lines.count())

            submission_for_code = Submission.objects.get(text=code_non_final)
            self.assertEqual(0, submission_for_code.feedback.score)
            self.assertEqual(1, submission_for_code.feedback.feedback_lines.count())


class TestFeedbackCreationTutor(UntestedParent.TestFeedbackCreationGeneric):
    def setUp(self):
        super().setUp()
        self.username = 'tutor'
        self.password = 'p'
        self.role = UserAccount.TUTOR
        fact.UserAccountFactory(
            username=self.username,
            password=self.password,
            role=self.role
        )