from http import HTTPStatus
import json
from typing import Callable

from bs4 import BeautifulSoup
import pytest

from generic.models import CidUser, CidUserGroup, UserUserGroup, ExamBase
from generic.views import ExamViews
from django.contrib.auth.models import User, Group
from django.urls import reverse
from rich.pretty import pprint


def AssertNotFound(client, url):
    """Helper to quickly test for urls that should return a 404 (NOT FOUND) error"""
    response = client.get(url)
    assert (
        response.status_code == HTTPStatus.NOT_FOUND
    ), f"Response content: {response.content}"


def create_cid_user_and_groups(db, marker=None):
    group1 = CidUserGroup.objects.create(name="Group1")
    group2 = CidUserGroup.objects.create(name="Group2")

    cid_user_1000 = CidUser.objects.create(cid=1000, passcode="ABCD", group=group1)
    cid_user_1001 = CidUser.objects.create(cid=1001, passcode="EFGH", group=group1)
    cid_user_2001 = CidUser.objects.create(cid=2001, passcode="IJKL", group=group2)

    assert cid_user_1000.cid == 1000
    assert cid_user_1001.cid == 1001

    # Create UserUsers
    user1 = User.objects.create_user(
        username="user1", email="test@email.com", password="test1234"
    )
    user2 = User.objects.create_user(
        username="user2", email="test2@email.com", password="test1234"
    )
    usergroup1 = UserUserGroup.objects.create(name="Group1")
    usergroup1.users.add(user1)
    usergroup2 = UserUserGroup.objects.create(name="Group2")
    usergroup2.users.add(user2)

    #if marker is not None:
    #    marker_group = Group.objects.create(name=f"{marker}_marker")
    #    #marker_group = Group.objects.create(name=f"{marker}_marker")

    marker = User.objects.create_user(
        username="marker", email="test@marker.com", password="test1234"
    )


def create_exam(db, ExamViews: ExamViews):
    exam: ExamBase = ExamViews.Exam.objects.create(
        name="Cid Exam", exam_mode=True, active=True
    )

    cidgroup1: CidUserGroup = CidUserGroup.objects.get(name="Group1")
    usergroup1: UserUserGroup = UserUserGroup.objects.get(name="Group1")

    exam.cid_user_groups.add(cidgroup1)

    exam.valid_cid_users.add(*cidgroup1.ciduser_set.all())

    # exam.valid_user_users.add(user1)
    exam.valid_user_users.add(*usergroup1.users.all())

    assert exam

    return exam


class ExamTester:
    def __init__(self, db, client, app_name, ExamViews) -> None:
        self.db = db
        self.client = client
        self.app_name = app_name

        create_cid_user_and_groups(db)
        self.exam = exam = create_exam(db, ExamViews)

        cidgroup1: CidUserGroup = CidUserGroup.objects.get(name="Group1")

        self.answers_per_question = 1
        match app_name:
            case "physics":
                assert exam in cidgroup1.physics_cid_user_groups.all()
                self.answers_per_question = 5
            case "sbas":
                assert exam in cidgroup1.sba_cid_user_groups.all()

        cid_user_1001 = CidUser.objects.get(cid=1001)
        cid_user_1000 = CidUser.objects.get(cid=1000)
        self.cid_user_2001 = cid_user_2001 = CidUser.objects.get(cid=2001)

        user1 = User.objects.get(username="user1")
        user2 = User.objects.get(username="user2")

        self.all_users = [cid_user_1000, cid_user_1001, cid_user_2001, user1, user2]

        self.cid_users = [cid_user_1000, cid_user_1001, cid_user_2001]

        self.invalid_users = [cid_user_2001, user2]

        self.current_user = None
        self.generated_questions = []

    def load_user(self, user):
        self.client.logout()

        self.current_user = user

        if isinstance(self.current_user, CidUser):
            self.testing_cid_user = True
        else:
            self.testing_cid_user = False

            self.client.force_login(user)

    def create_questions(self, create_question_func: Callable):
        self.create_question_func = create_question_func

        self.generated_questions = [
            create_question_func(self.exam),
            create_question_func(self.exam),
            create_question_func(self.exam),
        ]

    def add_question(self):
        self.generated_questions.append(self.create_question_func(self.exam))

    def check_active_exams(self, n):
        active_exams = self.client.get(reverse(f"{self.app_name}:active_exams"))
        assert active_exams.status_code == 200
        assert (
            len(json.loads(active_exams.content)["exams"]) == n
        ), f"Current user: {self.current_user}"

    def check_onsite_start_page(self):
        start_page_res = self.client.get(
            reverse(f"{self.app_name}:exam_start", kwargs={"pk": self.exam.pk})
        )

        # Check user access to the start page
        assert start_page_res.status_code == HTTPStatus.OK
        start_page_soup = BeautifulSoup(start_page_res.content, "html.parser")
        if (
            self.current_user in self.cid_users
            or self.current_user in self.invalid_users
        ):
            assert "Enter your CID and passcode" in str(start_page_soup)
        else:
            assert "Start Exam" in str(start_page_soup)

    def check_take_page(self):
        for n, question in enumerate(self.generated_questions):
            if self.testing_cid_user:
                take_url = reverse(
                    f"{self.app_name}:exam_take",
                    kwargs={
                        "pk": self.exam.pk,
                        "sk": n,
                        "cid": self.current_user.cid,
                        "passcode": self.current_user.passcode,
                    },
                )
            else:
                take_url = reverse(
                    f"{self.app_name}:exam_take_user",
                    kwargs={
                        "pk": self.exam.pk,
                        "sk": n,
                    },
                )

            cid_take_res = self.client.get(take_url)

            # Check that users that have not been added cannot access the take pages
            if self.current_user in self.invalid_users:
                assert cid_take_res.status_code == HTTPStatus.NOT_FOUND
                continue
            else:
                assert (
                    cid_take_res.status_code == HTTPStatus.OK
                ), f"Current user: {self.current_user}"

            cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")


            # Check correct user is displayed
            if self.testing_cid_user:
                display_user = str(self.current_user.cid)
            else:
                display_user = self.current_user.username
            assert display_user in cid_take_soup.find("span", {"id": "user-id"}).string

            # Check review mode alert not visible
            assert cid_take_soup.find("div", {"class": "review-mode-alert"}) is None

            # The main take page loads the question controls via HTMX/JS into
            # the fragment URL. Request that fragment and run button/answer
            # checks against it (server-rendered), not the main page which is
            # just a container.
            if self.testing_cid_user:
                frag_url = reverse(
                    f"{self.app_name}:exam_take_fragment",
                    kwargs={
                        "pk": self.exam.pk,
                        "sk": n,
                        "cid": self.current_user.cid,
                        "passcode": self.current_user.passcode,
                    },
                )
            else:
                frag_url = reverse(
                    f"{self.app_name}:exam_take_fragment_user",
                    kwargs={
                        "pk": self.exam.pk,
                        "sk": n,
                    },
                )

            frag_res = self.client.get(frag_url)

            # If the user is invalid they may not be able to load the fragment
            if self.current_user in self.invalid_users:
                assert frag_res.status_code == HTTPStatus.NOT_FOUND
                continue
            else:
                assert frag_res.status_code == HTTPStatus.OK

            frag_soup = BeautifulSoup(frag_res.content, "html.parser")

            # Check that relevant buttons are being displayed in the fragment
            assert frag_soup.find("button", {"name": "finish"}) is not None

            if n < len(self.generated_questions) - 1:
                assert frag_soup.find("button", {"name": "next"}) is not None

            if n > 0:
                assert frag_soup.find("button", {"name": "previous"}) is not None

            # Check that stem text is displayed (main page header is also kept in
            # sync by fragment JS, but verify here that fragment contains stem)
            assert (
                frag_soup.find("div", {"class": "question-stem-fragment"}).string.strip()
                == question.stem
            )

            match self.app_name:
                case "physics":
                    assert (
                        frag_soup.find("label", {"for": "id_a"}).string
                        == question.a
                    )
                    assert (
                        frag_soup.find("label", {"for": "id_b"}).string
                        == question.b
                    )
                    assert (
                        frag_soup.find("label", {"for": "id_c"}).string
                        == question.c
                    )
                    assert (
                        frag_soup.find("label", {"for": "id_d"}).string
                        == question.d
                    )
                    assert (
                        frag_soup.find("label", {"for": "id_e"}).string
                        == question.e
                    )
                case "sbas":
                    assert (
                        frag_soup.find("ul", {"class": "sba-answer-list"})
                        is not None
                    )
                    assert (
                        len(
                            frag_soup.find(
                                "ul", {"class": "sba-answer-list"}
                            ).find_all("li")
                        )
                        == 5
                    )

            # As we haven't submitted any answers they should all be blank
            assert (
                frag_soup.find("input", {"type": "checkbox", "checked": True})
                is None
            )

    def check_take_overview(self, answered, total):
        if self.testing_cid_user:
            take_overview_url = reverse(
                f"{self.app_name}:exam_take_overview",
                kwargs={
                    "pk": self.exam.pk,
                    "cid": self.current_user.cid,
                    "passcode": self.current_user.passcode,
                },
            )
        else:
            take_overview_url = reverse(
                f"{self.app_name}:exam_take_overview_user",
                kwargs={
                    "pk": self.exam.pk,
                },
            )

        cid_take_overview_res = self.client.get(take_overview_url)

        # Check that users that have not been added cannot access the take pages
        if self.current_user in self.invalid_users:
            assert cid_take_overview_res.status_code == HTTPStatus.NOT_FOUND
        else:
            assert cid_take_overview_res.status_code == HTTPStatus.OK

            cid_take_overview_soup = BeautifulSoup(
                cid_take_overview_res.content, "html.parser"
            )
            assert (
                f"{answered} out of 3 questions answered"
                in cid_take_overview_soup.find(
                    "div", attrs={"class": "overview-text"}
                ).text
            )

            # Different apps render unanswered markers differently:
            # - `sbas` uses <button class="unanswered"> nodes
            # - `physics` uses anchor tiles with a title attribute for unanswered
            if self.app_name == "sbas":
                count_unanswered = len(
                    cid_take_overview_soup.find_all(
                        "button", attrs={"class": "unanswered"}
                    )
                )
            else:
                # Count overview tiles that include the "not answered" title
                overview_tiles = cid_take_overview_soup.find_all(
                    class_="overview-question-btn"
                )
                count_unanswered = sum(1 for t in overview_tiles if t.has_attr("title"))

            assert count_unanswered == total - answered

    def check_cid_scores_page(self):
        if self.testing_cid_user:
            # Get overview scores page (invalid passcode)
            AssertNotFound(
                self.client,
                reverse(
                    f"cid_scores",
                    kwargs={"cid": self.current_user.cid, "passcode": "AAAA"},
                ),
            )

            cid_scores_url = reverse(
                f"cid_scores",
                kwargs={
                    "cid": self.current_user.cid,
                    "passcode": self.current_user.passcode,
                },
            )
        else:
            cid_scores_url = reverse(f"user_scores")

        # Get overview scores page
        cid_scores_res = self.client.get(cid_scores_url)

        assert cid_scores_res.status_code == HTTPStatus.OK

        cid_scores_soup = BeautifulSoup(cid_scores_res.content, "html.parser")

        if self.current_user in self.invalid_users:
            # page should load but we shouldn't be able to find the exam
            with pytest.raises(AttributeError):
                search_exam = (
                    cid_scores_soup.find("div", {"id": "exam-assigned"})
                    .find("ul", {"class": self.exam.app_name.lower()})
                    .find_all("li", attrs={"data-exam-id": self.exam.pk})
                )
        else:
            search_exam = (
                cid_scores_soup.find("div", {"id": "exam-assigned"})
                .find("ul", {"class": self.exam.app_name.lower()})
                .find("li", attrs={"data-exam-id": self.exam.pk})
            )
            assert search_exam
            assert str(self.exam) in str(search_exam)
            assert len(search_exam.find("button", {"class": "start-button"})) > 0
            # assert "Active" in assigned_exams # check it is active

            invalid_exam = cid_scores_soup.find_all(
                "li", attrs={"data-exam-id": self.exam.pk + 5}
            )
            assert not invalid_exam  # Check we can't find an invalid exam

            # Check that we have a results link
            results_exam = (
                cid_scores_soup.find("div", {"id": "exam-results"})
                .find("ul", {"class": self.exam.app_name})
                .find_all("li", attrs={"data-exam-id": self.exam.pk})
            )
            assert results_exam
            assert str(self.exam) in str(results_exam)
            assert str(self.exam) + " test" not in str(results_exam)
            assert "Results Published" not in str(
                results_exam
            )  # It should not be published yet

    def check_cid_scores_user_page(self):
        if self.testing_cid_user:
            AssertNotFound(
                self.client,
                reverse(
                    f"{self.exam.app_name}:exam_scores_cid_user",
                    kwargs={
                        "pk": self.exam.pk,
                        "cid": self.current_user.cid,
                        "passcode": "AAAA",
                    },
                ),
            )
            AssertNotFound(
                self.client,
                reverse(
                    f"{self.exam.app_name}:exam_scores_cid_user",
                    kwargs={
                        "pk": self.exam.pk,
                        "cid": self.cid_user_2001.cid,
                        "passcode": self.cid_user_2001.passcode,
                    },
                ),
            )

            exam_scores_url = reverse(
                f"{self.exam.app_name}:exam_scores_cid_user",
                kwargs={
                    "pk": self.exam.pk,
                    "cid": self.current_user.cid,
                    "passcode": self.current_user.passcode,
                },
            )
        else:
            exam_scores_url = reverse(
                f"{self.exam.app_name}:exam_scores_user",
                kwargs={"pk": self.exam.pk},
            )

        self.exam_scores_url = exam_scores_url

        # Load the exam results page
        cid_exam_scores_res = self.client.get(exam_scores_url)

        if self.current_user in self.invalid_users:
            assert cid_exam_scores_res.status_code == HTTPStatus.NOT_FOUND
            return
        else:
            assert cid_exam_scores_res.status_code == HTTPStatus.OK

        cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")

        # Check that we have an alert that exams are not published
        alert = cid_exam_scores_soup.find("div", {"class": "alert"})
        assert "Results are not currently published." in str(alert)

        # Find any element with the submitted-user-answer class (div or span)
        submitted_answers = cid_exam_scores_soup.find_all(
            class_="submitted-user-answer"
        )

        # Check that we are showing enough questions / answers
        match self.app_name:
            case "physics":
                # Physics renders per-part answers inside an ordered list
                # within each question block. Count li elements inside those
                # lists to get the total number of parts shown.
                ols = cid_exam_scores_soup.find_all("ol", class_="mb-0")
                answer_li_count = sum(len(ol.find_all("li")) for ol in ols)
                assert answer_li_count == 5 * len(self.generated_questions)
                assert len(submitted_answers) == 5 * len(self.generated_questions)
                for ans in submitted_answers:
                    assert ans.text.strip() == "True"

            case "sbas":
                answer_lis = cid_exam_scores_soup.find_all(
                    "li", {"class": "user-answer-li"}
                )
                assert len(answer_lis) == len(self.generated_questions)
                assert len(submitted_answers) == len(self.generated_questions)

                for ans in submitted_answers:
                    assert ans.text.strip() == "a"

        # Check no correct answers are shown
        assert not cid_exam_scores_soup.find_all("span", {"class": "correct-answer"})

        # Add another question to the exam
        self.add_question()

        # Load the exam results page (again)
        cid_exam_scores_res = self.client.get(exam_scores_url)
        cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")

        submitted_answers = cid_exam_scores_soup.find_all(class_="submitted-user-answer")
        match self.app_name:
            case "physics":
                ols = cid_exam_scores_soup.find_all("ol", class_="mb-0")
                answer_li_count = sum(len(ol.find_all("li")) for ol in ols)
                assert answer_li_count == 5 * len(self.generated_questions)
                assert len(submitted_answers) == 5 * len(self.generated_questions)

                for ans in submitted_answers[:-5]:
                    assert ans.text.strip() == "True"

                # The new question will not have any answers
                for ans in submitted_answers[-5:]:
                    assert ans.text.strip() == "Not answered"

            case "sbas":
                answer_lis = cid_exam_scores_soup.find_all(
                    "li", {"class": "user-answer-li"}
                )
                assert len(answer_lis) == len(self.generated_questions)
                assert len(submitted_answers) == len(self.generated_questions)

                print(submitted_answers)

                for ans in submitted_answers[:3]:
                    print("test1", ans)
                    assert ans.text.strip() == "a"

                # The new question will not have any answers
                for ans in submitted_answers[3:]:
                    print("test2", ans)
                    assert ans.text.strip() == "Not answered"

        # Publish the results!
        self.exam.publish_results = True
        self.exam.save()

        # Load the exam results page (again)
        cid_exam_scores_res = self.client.get(exam_scores_url)
        cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")

        assert (
            cid_exam_scores_soup.find("div", {"class": "alert"}) is None
        )  # Check our warning banner is gone

        # Check that we are displaying the correct answers (correctly)
        # Different apps render correct answers differently. For physics the
        # template uses a div containing the text "Correct answer: ...".
        if self.app_name == "physics":
            correct_answers = [
                t for t in cid_exam_scores_soup.find_all(string=True) if "Correct answer:" in t
            ]
        else:
            correct_answers = cid_exam_scores_soup.find_all(
                "span", {"class": "correct-answer"}
            )

        assert len(correct_answers) == self.answers_per_question * len(
            self.generated_questions
        )

        n = 0
        for i, question in enumerate(self.generated_questions):
            match self.app_name:
                case "physics":
                    for ans in question.get_answers():
                        assert correct_answers[n].text.strip() == f"Correct answer: {ans}"
                        n = n + 1
                case "sbas":
                    ans = question.get_correct_answer()
                    assert correct_answers[i].text.strip() == f"Correct answer: {ans}"

        submitted_answers = cid_exam_scores_soup.find_all(class_="submitted-user-answer")
        assert len(submitted_answers) == self.answers_per_question * len(
            self.generated_questions
        )

        print(submitted_answers)

        for ans in submitted_answers[:-self.answers_per_question]:
            print("------", ans.text.strip())
            match self.app_name:
                case "physics":
                    assert ans.text.strip() == "True"
                case "sbas":
                    assert ans.text.strip() == "a"

        # The new question will not have any answers
        for ans in submitted_answers[-self.answers_per_question:]:
            assert ans.text.strip() == "Not answered"

        match self.app_name:
            case "physics":
                assert (
                    cid_exam_scores_soup.find("div", {"class": "score-overview"})
                    .find("span", {"id": "total-score"})
                    .string
                    == "9"
                )
                assert (
                    cid_exam_scores_soup.find("div", {"class": "score-overview"})
                    .find("span", {"id": "max-score"})
                    .string
                    == "20"
                )
            case "sbas":
                assert (
                    cid_exam_scores_soup.find("div", {"class": "score-overview"})
                    .find("span", {"id": "total-score"})
                    .string
                    == "3"
                )
                assert (
                    cid_exam_scores_soup.find("div", {"class": "score-overview"})
                    .find("span", {"id": "max-score"})
                    .string
                    == "4"
                )

    def check_cid_scores_user_page2(self):
        cid_exam_scores_res = self.client.get(self.exam_scores_url)

        cid_exam_scores_soup = BeautifulSoup(cid_exam_scores_res.content, "html.parser")

        # As we have answered correctly the new score should be increased by 1 * answer_count
        match self.app_name:
            case "physics":
                assert (
                    cid_exam_scores_soup.find("div", {"class": "score-overview"})
                    .find("span", {"id": "total-score"})
                    .string
                    == "14"
                ), f"Invalid answer count, Current user: {self.current_user}"
            case "sbas":
                assert (
                    cid_exam_scores_soup.find("div", {"class": "score-overview"})
                    .find("span", {"id": "total-score"})
                    .string
                    == "4"
                ), f"Invalid answer count, Current user: {self.current_user}"

        # Check what happens when exam is not active
        self.exam.active = False
        self.exam.save()

        start_page_res = self.client.get(
            reverse(f"{self.app_name}:exam_start", kwargs={"pk": self.exam.pk})
        )
        start_page_soup = BeautifulSoup(start_page_res.content, "html.parser")

        assert "Exam is currently inactive" in start_page_soup.text

        if self.testing_cid_user:
            cid_take_url = reverse(
                f"{self.app_name}:exam_take",
                kwargs={
                    "pk": self.exam.pk,
                    "sk": 0,
                    "cid": self.current_user.cid,
                    "passcode": self.current_user.passcode,
                },
            )
        else:
            cid_take_url = reverse(
                f"{self.app_name}:exam_take_user",
                kwargs={
                    "pk": self.exam.pk,
                    "sk": 0,
                },
            )
        cid_take_res = self.client.get(cid_take_url)
        cid_take_soup = BeautifulSoup(cid_take_res.content, "html.parser")
        print(cid_take_soup.text)
        assert "Exam is currently inactive" in cid_take_soup.text

        # Delete the last question, from both the list
        q = self.generated_questions.pop()
        # and the database
        q.delete()
