import csv
import io
from datetime import timedelta

from django.contrib.auth import get_user_model
from django.test import TestCase
from django.utils import timezone

from account.models import RoleChoices
from prompt.models import Conversation, Message
from task.models import Task

from .models import Award, Rating, Submission, SubmissionAward

User = get_user_model()


def _make_user(username, role=RoleChoices.NORMAL, classname=""):
    user = User.objects.create_user(username=username, password="pw")
    user.role = role
    user.classname = classname
    user.save(update_fields=["role", "classname"])
    return user


def _make_task(
    title="Test Challenge",
    task_type="challenge",
    display=1,
    is_public=True,
):
    return Task.objects.create(
        title=title,
        task_type=task_type,
        display=display,
        content="",
        is_public=is_public,
    )


class SubmissionPromptChainTest(TestCase):
    def setUp(self):
        self.viewer = _make_user("viewer")
        self.author = _make_user("author")
        self.task = _make_task()

        viewer_conv = Conversation.objects.create(user=self.viewer, task=self.task)
        Message.objects.create(
            conversation=viewer_conv,
            role="user",
            content="viewer prompt",
        )
        Message.objects.create(
            conversation=viewer_conv,
            role="assistant",
            content="viewer answer",
            code_html="<p>viewer</p>",
        )

        author_conv = Conversation.objects.create(user=self.author, task=self.task)
        Message.objects.create(
            conversation=author_conv,
            role="user",
            content="author prompt",
        )
        self.submission = Submission.objects.create(
            user=self.author,
            task=self.task,
            html="<button>author</button>",
            css="button { color: red; }",
            js="",
        )
        Message.objects.create(
            conversation=author_conv,
            role="assistant",
            content="author answer",
            code_html="<button>author</button>",
            code_css="button { color: red; }",
            code_js="",
            submission=self.submission,
        )

    def test_normal_user_can_view_prompt_chain_for_another_users_submission(self):
        self.client.force_login(self.viewer)

        resp = self.client.get(f"/api/submission/{self.submission.id}/prompt-chain")

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(len(data), 1)
        self.assertEqual(data[0]["question"], "author prompt")
        self.assertEqual(data[0]["html"], "<button>author</button>")


class ShowcaseManagementApiTest(TestCase):
    def setUp(self):
        self.admin = _make_user("admin")
        self.admin.role = RoleChoices.ADMIN
        self.admin.save(update_fields=["role"])
        self.student = _make_user("student")
        self.task = _make_task()
        self.award = Award.objects.create(name="最佳视觉", sort_order=10)
        self.submission = Submission.objects.create(
            user=self.student,
            task=self.task,
            html="<main>work</main>",
            css="main { color: red; }",
            js="",
            score=4.5,
            view_count=8,
        )

    def test_normal_user_cannot_access_management_api(self):
        self.client.force_login(self.student)

        resp = self.client.get("/api/submission/showcase/manage/awards")
        lookup_resp = self.client.get(
            f"/api/submission/showcase/manage/submissions/{self.submission.id}"
        )

        self.assertIn(resp.status_code, (302, 403))
        self.assertIn(lookup_resp.status_code, (302, 403))

    def test_admin_can_find_submission_by_id_for_showcase_management(self):
        self.client.force_login(self.admin)

        resp = self.client.get(
            f"/api/submission/showcase/manage/submissions/{self.submission.id}"
        )

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["submission_id"], str(self.submission.id))
        self.assertEqual(data["username"], "student")
        self.assertEqual(data["task_title"], "Test Challenge")
        self.assertEqual(data["task_display"], 1)
        self.assertEqual(data["score"], 4.5)
        self.assertEqual(data["view_count"], 8)
        self.assertFalse(data["has_prompt_chain"])
        self.assertNotIn("html", data)

    def test_admin_can_create_and_update_award(self):
        self.client.force_login(self.admin)

        create_resp = self.client.post(
            "/api/submission/showcase/manage/awards",
            data={
                "name": "最佳互动",
                "description": "交互完整",
                "sort_order": 3,
                "is_active": True,
                "item_ordering": "score",
            },
            content_type="application/json",
        )
        self.assertEqual(create_resp.status_code, 200)
        created = create_resp.json()
        self.assertEqual(created["name"], "最佳互动")
        self.assertEqual(created["item_count"], 0)

        update_resp = self.client.put(
            f"/api/submission/showcase/manage/awards/{created['id']}",
            data={
                "name": "最佳交互",
                "description": "操作体验完整",
                "sort_order": 1,
                "is_active": False,
                "item_ordering": "view_count",
            },
            content_type="application/json",
        )
        self.assertEqual(update_resp.status_code, 200)
        updated = update_resp.json()
        self.assertEqual(updated["name"], "最佳交互")
        self.assertEqual(updated["description"], "操作体验完整")
        self.assertEqual(updated["sort_order"], 1)
        self.assertFalse(updated["is_active"])
        self.assertEqual(updated["item_ordering"], "view_count")

    def test_admin_cannot_add_same_submission_twice(self):
        self.client.force_login(self.admin)
        payload = {"submission_id": str(self.submission.id), "sort_order": 2}

        first_resp = self.client.post(
            f"/api/submission/showcase/manage/awards/{self.award.id}/items",
            data=payload,
            content_type="application/json",
        )
        self.assertEqual(first_resp.status_code, 200)
        self.assertEqual(first_resp.json()["submission_id"], str(self.submission.id))

        duplicate_resp = self.client.post(
            f"/api/submission/showcase/manage/awards/{self.award.id}/items",
            data=payload,
            content_type="application/json",
        )

        self.assertEqual(duplicate_resp.status_code, 400)
        self.assertEqual(
            SubmissionAward.objects.filter(
                award=self.award,
                submission=self.submission,
            ).count(),
            1,
        )

    def test_public_showcase_hides_removed_or_inactive_items(self):
        self.client.force_login(self.admin)
        add_resp = self.client.post(
            f"/api/submission/showcase/manage/awards/{self.award.id}/items",
            data={"submission_id": str(self.submission.id), "sort_order": 0},
            content_type="application/json",
        )
        item_id = add_resp.json()["id"]

        self.client.force_login(self.student)
        visible_resp = self.client.get("/api/submission/showcase/")
        self.assertEqual(visible_resp.status_code, 200)
        self.assertEqual(len(visible_resp.json()), 1)
        detail_resp = self.client.get(
            f"/api/submission/showcase/{self.submission.id}/"
        )
        self.assertEqual(detail_resp.status_code, 200)

        self.client.force_login(self.admin)
        delete_resp = self.client.delete(
            f"/api/submission/showcase/manage/items/{item_id}"
        )
        self.assertEqual(delete_resp.status_code, 200)

        self.client.force_login(self.student)
        removed_resp = self.client.get("/api/submission/showcase/")
        self.assertEqual(removed_resp.status_code, 200)
        self.assertEqual(removed_resp.json(), [])
        removed_detail_resp = self.client.get(
            f"/api/submission/showcase/{self.submission.id}/"
        )
        self.assertEqual(removed_detail_resp.status_code, 404)

        self.client.force_login(self.admin)
        self.client.post(
            f"/api/submission/showcase/manage/awards/{self.award.id}/items",
            data={"submission_id": str(self.submission.id), "sort_order": 0},
            content_type="application/json",
        )
        deactivate_resp = self.client.put(
            f"/api/submission/showcase/manage/awards/{self.award.id}",
            data={
                "name": self.award.name,
                "description": self.award.description,
                "sort_order": self.award.sort_order,
                "is_active": False,
                "item_ordering": self.award.item_ordering,
            },
            content_type="application/json",
        )
        self.assertEqual(deactivate_resp.status_code, 200)

        self.client.force_login(self.student)
        inactive_resp = self.client.get("/api/submission/showcase/")
        self.assertEqual(inactive_resp.status_code, 200)
        self.assertEqual(inactive_resp.json(), [])
        inactive_detail_resp = self.client.get(
            f"/api/submission/showcase/{self.submission.id}/"
        )
        self.assertEqual(inactive_detail_resp.status_code, 404)


class GradebookApiTest(TestCase):
    def setUp(self):
        self.super = _make_user("grade-super", role=RoleChoices.SUPER)
        self.admin = _make_user("grade-admin", role=RoleChoices.ADMIN)
        self.normal = _make_user("grade-normal", classname="blocked")

    def _student(self, username, classname="10A"):
        return _make_user(username, classname=classname)

    def _submit(self, user, task, score, created=None):
        submission = Submission.objects.create(
            user=user,
            task=task,
            score=score,
            html="",
            css="",
            js="",
        )
        if created is not None:
            Submission.objects.filter(pk=submission.pk).update(created=created)
            submission.refresh_from_db()
        return submission

    def test_gradebook_requires_classname(self):
        self.client.force_login(self.super)

        resp = self.client.get("/api/submission/gradebook/")

        self.assertEqual(resp.status_code, 400)
        self.assertEqual(resp.json()["detail"], "请选择班级")

    def test_normal_user_cannot_access_gradebook(self):
        self.client.force_login(self.normal)

        resp = self.client.get("/api/submission/gradebook/?classname=10A")
        export_resp = self.client.get(
            "/api/submission/gradebook/export/?classname=10A"
        )

        self.assertIn(resp.status_code, (302, 403))
        self.assertIn(export_resp.status_code, (302, 403))

    def test_admin_user_cannot_access_gradebook(self):
        self.client.force_login(self.admin)

        resp = self.client.get("/api/submission/gradebook/?classname=10A")
        export_resp = self.client.get(
            "/api/submission/gradebook/export/?classname=10A"
        )

        self.assertIn(resp.status_code, (302, 403))
        self.assertIn(export_resp.status_code, (302, 403))

    def test_admin_user_is_counted_in_class_gradebook(self):
        self._student("alice")
        admin_in_class = _make_user(
            "grade-admin-2", role=RoleChoices.ADMIN, classname="10A"
        )
        task = _make_task(title="Admin Visible", task_type="challenge", display=9)
        self._submit(admin_in_class, task, 5.0)
        self.client.force_login(self.super)

        resp = self.client.get("/api/submission/gradebook/?classname=10A")

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["student_count"], 2)
        usernames = [row["username"] for row in data["rows"]]
        self.assertIn("grade-admin-2", usernames)

    def test_coverage_includes_tutorial_and_challenge_without_public_requirement(self):
        students = [
            self._student("alice"),
            self._student("bob"),
            self._student("carol"),
            self._student("dave"),
        ]
        tutorial = _make_task(
            title="Intro",
            task_type="tutorial",
            display=1,
            is_public=False,
        )
        challenge = _make_task(
            title="Challenge One",
            task_type="challenge",
            display=1,
            is_public=True,
        )
        low_coverage = _make_task(
            title="Optional",
            task_type="challenge",
            display=2,
            is_public=True,
        )
        self._submit(students[0], tutorial, 4.0)
        self._submit(students[1], tutorial, 5.0)
        self._submit(students[0], challenge, 3.0)
        self._submit(students[1], challenge, 4.0)
        self._submit(students[0], low_coverage, 5.0)
        self.client.force_login(self.super)

        resp = self.client.get("/api/submission/gradebook/?classname=10A")

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["student_count"], 4)
        self.assertEqual(data["coverage_threshold_count"], 2)
        self.assertEqual(data["included_task_count"], 2)
        self.assertEqual(
            [task["id"] for task in data["tasks"]],
            [tutorial.id, challenge.id],
        )
        self.assertTrue(all(task["included"] for task in data["tasks"]))

        alice = next(row for row in data["rows"] if row["username"] == "alice")
        carol = next(row for row in data["rows"] if row["username"] == "carol")
        self.assertEqual(alice["tutorial_total"], 4.0)
        self.assertEqual(alice["challenge_total"], 3.0)
        self.assertEqual(alice["total_score"], 7.0)
        self.assertEqual(alice["average_score"], 3.5)
        self.assertEqual(alice["submitted_task_count"], 2)
        self.assertEqual(alice["missing_task_count"], 0)
        self.assertEqual(carol["total_score"], 0.0)
        self.assertEqual(carol["submitted_task_count"], 0)
        self.assertEqual(carol["missing_task_count"], 2)

        include_all_resp = self.client.get(
            "/api/submission/gradebook/?classname=10A&include_all_tasks=true"
        )
        include_all = include_all_resp.json()
        optional = next(
            task for task in include_all["tasks"] if task["id"] == low_coverage.id
        )
        alice_all = next(
            row for row in include_all["rows"] if row["username"] == "alice"
        )
        self.assertFalse(optional["included"])
        self.assertTrue(alice_all["scores"][str(low_coverage.id)]["submitted"])
        self.assertEqual(alice_all["scores"][str(low_coverage.id)]["score"], 5.0)
        self.assertEqual(alice_all["total_score"], 7.0)
        self.assertEqual(alice_all["submitted_task_count"], 2)

    def test_best_submission_uses_highest_score_and_latest_equal_score_link(self):
        alice = self._student("alice")
        bob = self._student("bob")
        task = _make_task(title="Best Score", task_type="tutorial", display=3)
        older = timezone.now() - timedelta(days=2)
        newer = timezone.now() - timedelta(days=1)
        self._submit(alice, task, 2.0)
        old_best = self._submit(alice, task, 4.5, created=older)
        new_best = self._submit(alice, task, 4.5, created=newer)
        self._submit(bob, task, 3.0)
        self.client.force_login(self.super)

        resp = self.client.get("/api/submission/gradebook/?classname=10A")

        self.assertEqual(resp.status_code, 200)
        alice_row = next(
            row for row in resp.json()["rows"] if row["username"] == "alice"
        )
        cell = alice_row["scores"][str(task.id)]
        self.assertEqual(cell["score"], 4.5)
        self.assertEqual(cell["submission_id"], str(new_best.id))
        self.assertNotEqual(cell["submission_id"], str(old_best.id))

    def test_task_type_and_username_filters_keep_full_class_rank(self):
        alice = self._student("alice")
        bob = self._student("bob")
        tutorial = _make_task(title="Tutorial", task_type="tutorial", display=1)
        challenge = _make_task(title="Challenge", task_type="challenge", display=1)
        self._submit(alice, tutorial, 1.0)
        self._submit(bob, tutorial, 5.0)
        self._submit(alice, challenge, 5.0)
        self._submit(bob, challenge, 1.0)
        self.client.force_login(self.super)

        resp = self.client.get(
            "/api/submission/gradebook/?classname=10A&task_type=tutorial&username=alice"
        )

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual([task["task_type"] for task in data["tasks"]], ["tutorial"])
        self.assertEqual(len(data["rows"]), 1)
        self.assertEqual(data["rows"][0]["username"], "alice")
        self.assertEqual(data["rows"][0]["rank"], 2)

    def test_missing_class_returns_empty_table_with_class_options(self):
        self._student("alice")
        self.client.force_login(self.super)

        resp = self.client.get("/api/submission/gradebook/?classname=missing")

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertIn("10A", data["classes"])
        self.assertEqual(data["student_count"], 0)
        self.assertEqual(data["coverage_threshold_count"], 0)
        self.assertEqual(data["tasks"], [])
        self.assertEqual(data["rows"], [])

    def test_no_included_tasks_still_returns_student_rows(self):
        students = [
            self._student("alice"),
            self._student("bob"),
            self._student("carol"),
        ]
        optional = _make_task(title="Low Coverage", task_type="challenge", display=8)
        self._submit(students[0], optional, 5.0)
        self.client.force_login(self.super)

        resp = self.client.get("/api/submission/gradebook/?classname=10A")

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["student_count"], 3)
        self.assertEqual(data["coverage_threshold_count"], 2)
        self.assertEqual(data["tasks"], [])
        self.assertEqual(data["included_task_count"], 0)
        self.assertEqual(len(data["rows"]), 3)
        alice = next(row for row in data["rows"] if row["username"] == "alice")
        self.assertEqual(alice["total_score"], 0.0)
        self.assertIsNone(alice["average_score"])
        self.assertEqual(alice["submitted_task_count"], 0)
        self.assertEqual(alice["missing_task_count"], 0)

        include_all_resp = self.client.get(
            "/api/submission/gradebook/?classname=10A&include_all_tasks=true"
        )
        include_all = include_all_resp.json()
        self.assertEqual(include_all["task_count"], 1)
        self.assertFalse(include_all["tasks"][0]["included"])
        alice_all = next(
            row for row in include_all["rows"] if row["username"] == "alice"
        )
        self.assertTrue(alice_all["scores"][str(optional.id)]["submitted"])
        self.assertEqual(alice_all["total_score"], 0.0)

    def test_grade_boundaries_use_ceil_thresholds(self):
        task = _make_task(title="Boundary", task_type="challenge", display=7)
        for i in range(1, 21):
            student = self._student(f"s{i:02d}", classname="10B")
            self._submit(student, task, float(21 - i))
        self.client.force_login(self.super)

        resp = self.client.get("/api/submission/gradebook/?classname=10B")

        self.assertEqual(resp.status_code, 200)
        rows_by_name = {row["username"]: row for row in resp.json()["rows"]}
        self.assertEqual(rows_by_name["s01"]["grade"], "A")
        self.assertEqual(rows_by_name["s06"]["grade"], "A")
        self.assertEqual(rows_by_name["s07"]["grade"], "B")
        self.assertEqual(rows_by_name["s14"]["grade"], "B")
        self.assertEqual(rows_by_name["s15"]["grade"], "C")
        self.assertEqual(rows_by_name["s18"]["grade"], "C")
        self.assertEqual(rows_by_name["s19"]["grade"], "D")
        self.assertEqual(rows_by_name["s20"]["grade"], "E")

    def test_export_csv_matches_current_filters(self):
        alice = self._student("alice")
        bob = self._student("bob")
        tutorial = _make_task(title="Intro", task_type="tutorial", display=1)
        challenge = _make_task(title="Challenge", task_type="challenge", display=1)
        self._submit(alice, tutorial, 4.0)
        self._submit(bob, tutorial, 2.0)
        self._submit(alice, challenge, 5.0)
        self.client.force_login(self.super)

        resp = self.client.get(
            "/api/submission/gradebook/export/?classname=10A&task_type=tutorial"
        )

        self.assertEqual(resp.status_code, 200)
        self.assertEqual(resp["Content-Type"], "text/csv; charset=utf-8")
        self.assertIn("attachment;", resp["Content-Disposition"])
        rows = list(csv.reader(io.StringIO(resp.content.decode("utf-8-sig"))))
        self.assertEqual(
            rows[0],
            [
                "排名",
                "等级",
                "考核等级",
                "考核分数",
                "用户名",
                "班级",
                "教程1-Intro",
                "教程合计",
                "挑战合计",
                "总分",
                "平均分",
                "已提交任务数",
                "未提交任务数",
            ],
        )
        self.assertEqual(rows[1][4], "alice")
        self.assertEqual(rows[1][6], "4")
        self.assertEqual(rows[1][9], "4")


class RandomForRatingApiTest(TestCase):
    def setUp(self):
        self.viewer = _make_user("viewer")
        self.author = _make_user("author")
        self.task = _make_task()

    def _submission(self, user, html="<div>work</div>"):
        return Submission.objects.create(
            user=user, task=self.task, html=html, css="", js=""
        )

    def test_excludes_own_and_already_rated_submissions(self):
        self._submission(self.viewer)  # 自己的提交，应排除
        rated = self._submission(self.author)
        Rating.objects.create(user=self.viewer, submission=rated, score=4)
        eligible = self._submission(self.author)

        self.client.force_login(self.viewer)
        resp = self.client.get("/api/submission/random-for-rating/")

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertIsNotNone(data)
        self.assertEqual(data["submission_id"], str(eligible.id))
        self.assertEqual(data["username"], "author")
        self.assertEqual(data["task_title"], self.task.title)
        self.assertEqual(data["task_display"], self.task.display)
        self.assertEqual(data["task_type"], "challenge")

    def test_prefers_submission_with_fewer_than_five_ratings(self):
        raters = [_make_user(f"rater{i}") for i in range(5)]
        many_ratings = self._submission(self.author, html="<div>many</div>")
        for rater in raters:
            Rating.objects.create(user=rater, submission=many_ratings, score=3)
        few_ratings = self._submission(self.author, html="<div>few</div>")
        for rater in raters[:2]:
            Rating.objects.create(user=rater, submission=few_ratings, score=3)

        self.client.force_login(self.viewer)
        resp = self.client.get("/api/submission/random-for-rating/")

        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["submission_id"], str(few_ratings.id))

    def test_exclude_id_param_excludes_given_submission(self):
        only = self._submission(self.author)

        self.client.force_login(self.viewer)
        resp = self.client.get(
            f"/api/submission/random-for-rating/?exclude_id={only.id}"
        )

        self.assertEqual(resp.status_code, 200)
        self.assertIsNone(resp.json())

    def test_normal_user_reaching_daily_cap_returns_none(self):
        self._submission(self.author, html="<div>extra</div>")
        for i in range(30):
            target = self._submission(self.author, html=f"<div>{i}</div>")
            Rating.objects.create(user=self.viewer, submission=target, score=3)

        self.client.force_login(self.viewer)
        resp = self.client.get("/api/submission/random-for-rating/")

        self.assertEqual(resp.status_code, 200)
        self.assertIsNone(resp.json())
